feat: consolidate local-first mnote web runtime

This commit is contained in:
lix-2026
2026-05-28 22:01:44 +08:00
parent 7354807ee9
commit 39b9a0183a
154 changed files with 13591 additions and 12728 deletions
@@ -141,7 +141,7 @@ import {
};
const pageAggregateUrlFromDescriptor = (descriptor) => {
const url = new URL(`/api/page-aggregate/${encodeURIComponent(descriptor.documentId)}`, window.location.origin);
url.searchParams.set('sourceKind', descriptor.sourceKind || 'convex_workspace');
url.searchParams.set('sourceKind', descriptor.sourceKind || 'local_folder');
if (descriptor.workspaceId) url.searchParams.set('workspaceId', descriptor.workspaceId);
if (descriptor.rootUri) url.searchParams.set('rootUri', descriptor.rootUri);
return url;
@@ -151,10 +151,10 @@ import {
documentId: aggregate?.identity?.documentId || aggregate?.identity?.document_id || descriptor.documentId,
workspaceId: aggregate?.identity?.workspaceId || aggregate?.identity?.workspace_id || descriptor.workspaceId || '',
paneRole,
sourceKind: descriptor.sourceKind || 'convex_workspace',
sourceKind: descriptor.sourceKind || 'local_folder',
rootUri: descriptor.rootUri || '',
pageAggregateScriptId: paneRole === 'secondary' ? '__MNOTE_SECONDARY_PAGE_AGGREGATE__' : '__MNOTE_PAGE_AGGREGATE__',
saveEndpoint: (descriptor.sourceKind || 'convex_workspace') === 'local_folder'
saveEndpoint: (descriptor.sourceKind || 'local_folder') === 'local_folder'
? '/api/page-body/write'
: '/api/documents/save',
titleEndpoint: '/api/documents/title',
@@ -225,6 +225,48 @@ import {
if (pageTab instanceof HTMLElement) return String(pageTab.getAttribute('data-document-id') || '').trim();
return '';
};
const currentWebShellSourceKind = () => {
try {
return currentUrl().searchParams.get('sourceKind') || '';
} catch (_) {
return '';
}
};
const currentWebShellRootUri = () => {
try {
return currentUrl().searchParams.get('rootUri') || '';
} catch (_) {
return '';
}
};
const restoreResourceTabInputFromUrl = () => {
const url = currentUrl();
const raw = String(url.searchParams.get('resourceTab') || '').trim();
if (!raw) return null;
const objectIdentity = raw.includes('::') ? raw.slice(raw.indexOf('::') + 2) : raw;
if (!objectIdentity.startsWith('resource:file:')) return null;
const rootUri = currentWebShellRootUri();
if (!rootUri) return null;
const rest = objectIdentity.slice('resource:file:'.length);
const prefix = `${rootUri}:`;
if (!rest.startsWith(prefix)) return null;
const path = rest.slice(prefix.length).replace(/^\/+/, '');
if (!path) return null;
const title = path.split('/').filter(Boolean).pop() || path;
return {
objectIdentity,
documentId: currentWebShellDocumentId(),
workspaceId: currentWebShellWorkspaceId(),
sourceKind: currentWebShellSourceKind() || 'local_folder',
rootUri,
path,
assetId: `local:asset:${path}`,
title,
fileName: title,
openTarget: 'active-tab',
};
};
let nextViewId = 1;
const mindmapHost = createMindmapHostRuntime({
@@ -267,12 +309,18 @@ import {
const markSessionExternalConflict = (...args) => documentSessions.markSessionExternalConflict(...args);
const queueSessionSave = (...args) => documentSessions.queueSessionSave(...args);
const refreshSessionFromExternalChange = (...args) => documentSessions.refreshSessionFromExternalChange(...args);
const ensureLocalFolderEventChannel = (...args) => documentSessions.ensureLocalFolderEventChannel(...args);
const sessionMatchesDocumentWorkspace = (...args) => documentSessions.sessionMatchesDocumentWorkspace(...args);
const getOrCreateDocumentSession = (...args) => documentSessions.getOrCreateDocumentSession(...args);
const updatePaneChrome = (runtimeDescriptor) => {
const pane = runtimeDescriptor.root.closest('[data-document-pane="true"]');
if (!(pane instanceof HTMLElement)) return;
if (runtimeDescriptor.paneRole === 'primary') {
document.querySelectorAll('[data-mnote-navigation-page-placeholder][data-pane-role="primary"]').forEach((node) => {
if (node instanceof HTMLElement) node.hidden = true;
});
}
const aggregate = runtimeDescriptor.aggregate || {};
const bootstrap = runtimeDescriptor.bootstrap || {};
const title = aggregate?.head?.title || '无标题';
@@ -366,9 +414,94 @@ import {
return payload.result;
};
const ensureLazyPrimaryPaneRoot = () => {
let root = document.querySelector(`${ROOT_SELECTOR}[data-pane-role="primary"]`);
if (root instanceof HTMLElement) return root;
const pagePanel = document.querySelector('[data-mnote-page-tab-panel][data-pane-role="primary"]');
if (!(pagePanel instanceof HTMLElement)) return null;
const pane = document.createElement('section');
pane.className = 'document-pane';
pane.setAttribute('data-document-pane', 'true');
pane.setAttribute('data-pane-role', 'primary');
pane.setAttribute('data-pane-document-id', '');
pane.setAttribute('data-pane-workspace-id', currentWebShellWorkspaceId());
pane.setAttribute('data-pane-visible', 'false');
pane.hidden = true;
const shell = document.createElement('main');
shell.className = 'document-shell';
shell.setAttribute('data-editor-host', 'leptos_tiptap_island');
shell.setAttribute('data-document-id', '');
shell.setAttribute('data-workspace-id', currentWebShellWorkspaceId());
shell.setAttribute('data-pane-role', 'primary');
shell.setAttribute('data-page-wide-layout', 'false');
shell.setAttribute('data-page-small-text', 'false');
shell.setAttribute('data-layout-density', 'normal');
shell.setAttribute('data-page-font', 'default');
shell.setAttribute('data-page-show-heading-numbers', 'false');
const header = document.createElement('header');
header.className = 'document-shell-header';
header.setAttribute('data-page-title-hidden', 'true');
header.hidden = true;
const titleHeading = document.createElement('h1');
titleHeading.className = 'document-title-heading';
const titleInput = document.createElement('textarea');
titleInput.id = 'mnote-page-title-input';
titleInput.className = 'document-title-input';
titleInput.setAttribute('aria-label', '页面标题');
titleInput.setAttribute('data-page-title-input', 'true');
titleInput.setAttribute('data-document-id', '');
titleInput.setAttribute('data-workspace-id', currentWebShellWorkspaceId());
titleInput.setAttribute('data-pane-role', 'primary');
titleInput.setAttribute('data-title-endpoint', '/api/documents/title');
titleInput.rows = 1;
titleHeading.appendChild(titleInput);
const meta = document.createElement('div');
meta.className = 'document-shell-meta';
meta.setAttribute('aria-label', '页面元信息');
const currentTitle = document.createElement('span');
currentTitle.setAttribute('data-page-title-current', 'true');
meta.appendChild(currentTitle);
header.append(titleHeading, meta);
const aggregateMarker = document.createElement('section');
aggregateMarker.setAttribute('data-page-aggregate-snapshot', 'mnote.page_aggregate.v1');
const subtree = document.createElement('section');
subtree.setAttribute('data-testid', 'mnote-page-subtree');
subtree.setAttribute('data-page-tree-source', 'page_aggregate.tree.pageSubtree');
subtree.setAttribute('data-page-subtree-present', 'false');
subtree.setAttribute('data-pane-role', 'primary');
const island = document.createElement('section');
island.id = 'mnote-editor-island';
island.setAttribute('data-editor-host', 'leptos_tiptap_island');
island.setAttribute('data-pane-role', 'primary');
root = document.createElement('div');
root.id = 'mnote-leptos-tiptap-island-editor-root';
root.setAttribute('data-testid', 'mnote-leptos-tiptap-island-editor-root');
root.setAttribute('data-editor-host-kind', 'leptos_tiptap_island');
root.setAttribute('data-runtime-editor-status', 'booting');
root.setAttribute('data-pane-role', 'primary');
const observability = document.createElement('div');
observability.className = 'sr-only';
observability.setAttribute('data-editor-host-observability', 'rust-web-inline-island');
observability.setAttribute('data-editor-host-active', 'leptos_tiptap_island');
observability.setAttribute('data-editor-host-requested', 'leptos_tiptap_island');
observability.setAttribute('data-editor-host-status', 'booting');
observability.setAttribute('data-pane-role', 'primary');
island.append(root, observability);
shell.append(header, aggregateMarker, subtree, island);
pane.appendChild(shell);
pagePanel.appendChild(pane);
return root;
};
const replacePaneDocument = async (paneRole, descriptor, options = {}) => {
mindmapHost.unmountMindmapPane(paneRole);
const root = document.querySelector(`${ROOT_SELECTOR}[data-pane-role="${paneRole}"]`);
const root = paneRole === 'primary'
? ensureLazyPrimaryPaneRoot()
: document.querySelector(`${ROOT_SELECTOR}[data-pane-role="${paneRole}"]`);
const observability = document.querySelector(`[data-editor-host-observability][data-pane-role="${paneRole}"]`);
if (!(root instanceof HTMLElement)) throw new Error(`pane_root_missing_${paneRole}`);
const runtimePromise = loadRuntime();
@@ -428,14 +561,14 @@ import {
return {
documentId,
workspaceId: explicit.workspaceId || url.searchParams.get('workspaceId') || '',
sourceKind: explicit.sourceKind || url.searchParams.get('secondarySourceKind') || url.searchParams.get('sourceKind') || 'convex_workspace',
sourceKind: explicit.sourceKind || url.searchParams.get('secondarySourceKind') || url.searchParams.get('sourceKind') || 'local_folder',
rootUri: explicit.rootUri || url.searchParams.get('secondaryRootUri') || url.searchParams.get('rootUri') || '',
};
}
return {
documentId,
workspaceId: explicit.workspaceId || url.searchParams.get('workspaceId') || '',
sourceKind: explicit.sourceKind || url.searchParams.get('sourceKind') || 'convex_workspace',
sourceKind: explicit.sourceKind || url.searchParams.get('sourceKind') || 'local_folder',
rootUri: explicit.rootUri || url.searchParams.get('rootUri') || '',
};
};
@@ -529,6 +662,11 @@ import {
broadcastSessionContent(session, view);
}
if ((pendingExternalChange || recentExternalChange) && (session.dirty || recentLocalInput)) {
if (session.sourceKind === 'local_folder' && session.sessionKind !== 'resource') {
session.externalChangePending = true;
if (session.dirty) queueSessionSave(session);
return;
}
markSessionExternalConflict(session, externalConflictMessage);
return;
}
@@ -629,6 +767,9 @@ import {
} else {
setStatus(runtimeDescriptor, 'ready');
}
if (session.pageBodySource) runtimeDescriptor.root.setAttribute('data-mnote-page-body-source', session.pageBodySource);
if (session.projectionSource) runtimeDescriptor.root.setAttribute('data-mnote-projection-source', session.projectionSource);
if (session.blockProjectionVersion) runtimeDescriptor.root.setAttribute('data-mnote-block-projection-version', session.blockProjectionVersion);
enhanceEditorAttachmentLinksSoon();
};
view.onError = (event) => {
@@ -674,6 +815,7 @@ import {
resourceTabs = createResourceTabRuntime({
loadRuntime,
createEditorViewBinding,
ensureLocalFolderEventChannel,
unmountEditorViewBinding,
setStatus,
documentSessionRegistry,
@@ -828,7 +970,16 @@ import {
}, { once: true });
if (paneRuntimes.length) {
Promise.all(paneRuntimes.map((paneRuntime) => mountPane(paneRuntime))).catch((error) => {
Promise.all(paneRuntimes.map((paneRuntime) => mountPane(paneRuntime))).then(() => {
const restoreInput = restoreResourceTabInputFromUrl();
if (restoreInput) {
return openResourceInActiveTab(restoreInput).catch((error) => {
console.warn('mnote resource tab URL 恢复失败', error);
return false;
});
}
return false;
}).catch((error) => {
console.error('mnote multi-pane editor mount failed', error);
});
}