chore: land tree view-state, vault, Pi module split, and repo hygiene
Persist PageTree expand state via control-plane view-state and align chevron/DOM with restored expansion; keep Sidex-style shallow page-tree scan and drop the unused recursive scanner that only added cargo noise. Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi into a module package, and retire Hermes/ACP/OpenHub recycle + root harness evidence from the index while gitignoring recycle and local diag dumps. Archive superseded design/bugs docs under old/, point architecture at ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor regressions so the working tree can stay clean.
This commit is contained in:
@@ -28,7 +28,10 @@ import {
|
||||
(() => {
|
||||
const PANES_BOOTSTRAP_ID = '__MNOTE_DOCUMENT_PANES_BOOTSTRAP__';
|
||||
const ROOT_SELECTOR = '[data-testid="mnote-leptos-tiptap-island-editor-root"]';
|
||||
// 历史 spike 前缀仍是 island runtime 的默认派发;稳定前缀 `mnote:tiptap-island`
|
||||
// 由 island 双发(架构收口 21 Phase 3),host 优先监听 spike 事件保持兼容。
|
||||
const EVENT_PREFIX = 'mnote:leptos-tiptap-spike';
|
||||
const STABLE_EVENT_PREFIX = 'mnote:tiptap-island';
|
||||
const CHANGE_EVENT = `${EVENT_PREFIX}:change`;
|
||||
const SAVE_EVENT = `${EVENT_PREFIX}:save-request`;
|
||||
const READY_EVENT = `${EVENT_PREFIX}:ready`;
|
||||
@@ -50,6 +53,72 @@ import {
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
const refreshBreadcrumb = (documentId, title, workspaceId) => {
|
||||
const host = document.querySelector('[data-breadcrumb-pages="true"]');
|
||||
if (!(host instanceof HTMLElement)) return;
|
||||
const rows = Array.from(document.querySelectorAll(
|
||||
'.wolai-page-row[data-node-id], .tree-row[data-shell-mode="page"][data-node-id]',
|
||||
));
|
||||
const byId = new Map();
|
||||
rows.forEach((row) => {
|
||||
const id = String(row.getAttribute('data-node-id') || row.getAttribute('data-document-id') || '').trim();
|
||||
if (!id || byId.has(id)) return;
|
||||
const titleNode = row.querySelector('.wolai-row-title, .tree-link-title');
|
||||
byId.set(id, {
|
||||
id,
|
||||
title: String(titleNode?.textContent || '').trim() || '无标题',
|
||||
parentId: String(row.getAttribute('data-parent-id') || '').trim(),
|
||||
href: row instanceof HTMLAnchorElement ? row.href : '',
|
||||
});
|
||||
});
|
||||
const chain = [];
|
||||
const seen = new Set();
|
||||
let cursor = String(documentId || '').trim();
|
||||
while (cursor && !seen.has(cursor) && chain.length < 64) {
|
||||
const item = byId.get(cursor);
|
||||
if (!item) break;
|
||||
seen.add(cursor);
|
||||
chain.push(item);
|
||||
cursor = item.parentId;
|
||||
}
|
||||
chain.reverse();
|
||||
if (!chain.length) return;
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
if (workspaceId) query.set('workspaceId', workspaceId);
|
||||
const children = [];
|
||||
chain.forEach((item, index) => {
|
||||
if (index > 0) {
|
||||
const separator = document.createElement('span');
|
||||
separator.className = 'wolai-breadcrumb-separator';
|
||||
separator.setAttribute('aria-hidden', 'true');
|
||||
separator.textContent = '›';
|
||||
children.push(separator);
|
||||
}
|
||||
if (index === chain.length - 1) {
|
||||
const current = document.createElement('span');
|
||||
current.className = 'wolai-breadcrumb-current';
|
||||
current.dataset.breadcrumbDocumentId = item.id;
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'material-symbols-outlined wolai-home-icon';
|
||||
icon.dataset.icon = 'home';
|
||||
icon.setAttribute('aria-hidden', 'true');
|
||||
const text = document.createElement('span');
|
||||
text.dataset.pageTitleCurrent = 'true';
|
||||
text.textContent = title || item.title;
|
||||
current.append(icon, text);
|
||||
children.push(current);
|
||||
} else {
|
||||
const link = document.createElement('a');
|
||||
link.className = 'wolai-breadcrumb-link';
|
||||
link.dataset.breadcrumbDocumentId = item.id;
|
||||
link.href = item.href || `/documents/${encodeURIComponent(item.id)}?${query.toString()}`;
|
||||
link.textContent = item.title;
|
||||
children.push(link);
|
||||
}
|
||||
});
|
||||
host.replaceChildren(...children);
|
||||
};
|
||||
|
||||
const parseJsonScript = (id) => {
|
||||
const node = document.getElementById(id);
|
||||
if (!node) return null;
|
||||
@@ -406,6 +475,33 @@ import {
|
||||
const ensureLocalFolderEventChannel = (...args) => documentSessions.ensureLocalFolderEventChannel(...args);
|
||||
const sessionMatchesDocumentWorkspace = (...args) => documentSessions.sessionMatchesDocumentWorkspace(...args);
|
||||
const getOrCreateDocumentSession = (...args) => documentSessions.getOrCreateDocumentSession(...args);
|
||||
const setDocumentSessionUserReadOnlyMode = (...args) => documentSessions.setDocumentSessionUserReadOnlyMode(...args);
|
||||
|
||||
const syncDocumentEditModeChrome = (session) => {
|
||||
if (!session) return;
|
||||
sessionViews(session).forEach((view) => {
|
||||
const pane = view.runtimeDescriptor.root.closest('[data-document-pane="true"]');
|
||||
const shell = view.runtimeDescriptor.root.closest('.document-shell');
|
||||
if (shell instanceof HTMLElement) {
|
||||
shell.setAttribute('data-document-user-readonly-mode', String(session.userReadOnlyMode !== false));
|
||||
shell.setAttribute('data-document-effective-readonly', String(session.readOnly === true));
|
||||
}
|
||||
const titleInput = pane?.querySelector('[data-page-title-input="true"]');
|
||||
if (titleInput instanceof HTMLTextAreaElement) {
|
||||
titleInput.readOnly = session.readOnly === true;
|
||||
titleInput.setAttribute('data-document-user-readonly-mode', String(session.userReadOnlyMode !== false));
|
||||
}
|
||||
const toggle = pane?.querySelector('[data-document-edit-mode-toggle]');
|
||||
if (toggle instanceof HTMLButtonElement) {
|
||||
const editing = session.readOnly !== true;
|
||||
toggle.textContent = editing ? '退出编辑' : '开始编辑';
|
||||
toggle.title = editing ? '切回只读浏览' : '进入编辑模式';
|
||||
toggle.setAttribute('aria-pressed', String(editing));
|
||||
toggle.setAttribute('data-document-editing', String(editing));
|
||||
toggle.disabled = session.permissionReadOnly === true && session.userReadOnlyMode === false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updatePaneChrome = (runtimeDescriptor) => {
|
||||
const pane = runtimeDescriptor.root.closest('[data-document-pane="true"]');
|
||||
@@ -467,8 +563,7 @@ import {
|
||||
document.body.dataset.mnoteShell = 'document';
|
||||
delete document.body.dataset.mindmapId;
|
||||
document.title = title;
|
||||
const topbarCurrent = document.querySelector('.wolai-breadcrumb-current [data-page-title-current]');
|
||||
if (topbarCurrent instanceof HTMLElement) topbarCurrent.textContent = title;
|
||||
refreshBreadcrumb(documentId, title, workspaceId);
|
||||
window.dispatchEvent(new CustomEvent('mnote:primary-document-activated', {
|
||||
detail: {
|
||||
documentId,
|
||||
@@ -566,13 +661,20 @@ import {
|
||||
titleInput.setAttribute('data-title-endpoint', '/api/documents/title');
|
||||
titleInput.rows = 1;
|
||||
titleHeading.appendChild(titleInput);
|
||||
const modeToggle = document.createElement('button');
|
||||
modeToggle.type = 'button';
|
||||
modeToggle.className = 'document-edit-mode-toggle';
|
||||
modeToggle.setAttribute('data-document-edit-mode-toggle', 'true');
|
||||
modeToggle.setAttribute('aria-pressed', 'false');
|
||||
modeToggle.setAttribute('data-document-editing', 'false');
|
||||
modeToggle.textContent = '开始编辑';
|
||||
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);
|
||||
header.append(titleHeading, modeToggle, meta);
|
||||
|
||||
const aggregateMarker = document.createElement('section');
|
||||
aggregateMarker.setAttribute('data-page-aggregate-snapshot', 'mnote.page_aggregate.v1');
|
||||
@@ -636,6 +738,7 @@ import {
|
||||
}
|
||||
const session = getOrCreateDocumentSession(runtimeDescriptor);
|
||||
const view = createEditorViewBinding(session, runtime, runtimeDescriptor);
|
||||
syncDocumentEditModeChrome(session);
|
||||
const mountOptions = {
|
||||
documentId: session.documentId,
|
||||
workspaceId: session.workspaceId,
|
||||
@@ -657,6 +760,7 @@ import {
|
||||
setStatus(runtimeDescriptor, 'mounting-editor');
|
||||
enhanceEditorAttachmentLinksSoon();
|
||||
paneViewRegistry.set(paneRole, view);
|
||||
syncDocumentEditModeChrome(session);
|
||||
return view;
|
||||
} catch (error) {
|
||||
unmountEditorViewBinding(view);
|
||||
@@ -739,6 +843,24 @@ import {
|
||||
anchor.removeAttribute('href');
|
||||
}, true);
|
||||
document.addEventListener('click', (event) => {
|
||||
const editToggle = event.target instanceof Element
|
||||
? event.target.closest('[data-document-edit-mode-toggle]')
|
||||
: null;
|
||||
if (editToggle instanceof HTMLButtonElement) {
|
||||
event.preventDefault();
|
||||
const pane = editToggle.closest('[data-document-pane="true"]');
|
||||
const documentId = pane?.getAttribute('data-pane-document-id') || document.body?.dataset.documentId || '';
|
||||
const session = Array.from(documentSessionRegistry.values())
|
||||
.find((item) => item && item.documentId === documentId);
|
||||
if (!session) return;
|
||||
setDocumentSessionUserReadOnlyMode(
|
||||
session,
|
||||
!(session.userReadOnlyMode !== false),
|
||||
'mnote-web-document-edit-mode-toggle',
|
||||
);
|
||||
syncDocumentEditModeChrome(session);
|
||||
return;
|
||||
}
|
||||
const target = event.target;
|
||||
const anchor = target instanceof Element ? target.closest('a.mnote-page-block-link') : null;
|
||||
if (!(anchor instanceof HTMLAnchorElement)) return;
|
||||
@@ -1022,6 +1144,7 @@ import {
|
||||
const runtime = await loadRuntime();
|
||||
const session = getOrCreateDocumentSession(runtimeDescriptor);
|
||||
const view = createEditorViewBinding(session, runtime, runtimeDescriptor);
|
||||
syncDocumentEditModeChrome(session);
|
||||
|
||||
const mountOptions = {
|
||||
documentId: session.documentId,
|
||||
@@ -1045,6 +1168,7 @@ import {
|
||||
setStatus(runtimeDescriptor, 'mounting-editor');
|
||||
enhanceEditorAttachmentLinksSoon();
|
||||
paneViewRegistry.set(runtimeDescriptor.paneRole, view);
|
||||
syncDocumentEditModeChrome(session);
|
||||
} catch (error) {
|
||||
unmountEditorViewBinding(view);
|
||||
throw error;
|
||||
|
||||
Reference in New Issue
Block a user