diff --git a/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md b/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md
index d59caf80..a158b097 100644
--- a/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md
+++ b/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md
@@ -160,6 +160,7 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::web_shell
- 新建:`rust/crates/mnote-web/browser/sidebar-filetree-upload-runtime.js`
- 新建:`rust/crates/mnote-web/browser/sidebar-attachment-open-runtime.js`
- 新建:`rust/crates/mnote-web/browser/sidebar-tree-live-apply-runtime.js`
+- 新建:`rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js`
拆分项:
@@ -170,7 +171,7 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::web_shell
- [x] C5. `sidebar-filetree-upload-runtime.js`:local upload target plan、drop/paste preflight、readonly guard。
- [x] C6. `sidebar-attachment-open-runtime.js`:OnlyOffice/PDF/code/image open mode guard。
- [x] C7. `sidebar-tree-live-apply-runtime.js`:WS/SSE snapshot/delta/resync DOM apply。
-- [ ] C8. `sidebar-tree-runtime.js` 保留为 entrypoint,目标少于 3,000 行。
+- [x] C8. `sidebar-tree-runtime.js` 保留为 entrypoint,目标少于 3,000 行。
验收命令:
@@ -183,6 +184,7 @@ node --check rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-filetree-upload-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-attachment-open-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-tree-live-apply-runtime.js
+node --check rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js
cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib ssr::pages::layout -- --test-threads=1
```
@@ -247,7 +249,7 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::tree -- --
- [x] E2. `mount.rs`:迁出 mount context、mounted handles、`mount_app_into` 周边可独立部分;`#[wasm_bindgen]` wrapper 可暂留根文件。
- [x] E3. `runtime_bridge.rs`:迁出 `dispatch_runtime_event`、`dispatch_*_to_target`、host command listener install。
- [x] E4. `block_transform.rs`:迁出 `run_block_turn_into_action`、`top_level_block_matches_action`、page/block transform helpers。
-- [ ] E5. `slash_menu_view.rs`:迁出 slash menu Leptos view 和 click handling。
+- [x] E5. `slash_menu_view.rs`:迁出 slash menu Leptos view 和 click handling。
- [ ] E6. `block_handle_menu_view.rs`:迁出 block handle menu 和 submenu view,作为最后一刀。
验收标准:
diff --git a/rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js b/rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js
new file mode 100644
index 00000000..dc15b1de
--- /dev/null
+++ b/rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js
@@ -0,0 +1,700 @@
+export function createSidebarPageSettingsRuntime(context) {
+ const {
+ currentDocumentId,
+ currentPageAggregate,
+ currentRootUri,
+ currentSourceKind,
+ currentWorkspaceSourcePayload,
+ escapeHtml,
+ pageUiState,
+ resolveWorkspaceId,
+ searchText,
+ } = context;
+ const MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY = context.globalShowHeadingNumbersKey || 'mnote.global.showHeadingNumbers';
+
+ function defaultPageOptions() {
+ return {
+ wideLayout: false,
+ smallText: false,
+ layoutDensity: 'normal',
+ showHeadingNumbers: false,
+ showToc: false,
+ showStructure: false,
+ protectEditing: false,
+ showWordCount: true,
+ collapseBacklinks: false,
+ pageFont: 'default',
+ hideChildPages: false,
+ showBlockRefCount: false,
+ hideTitleHeader: false,
+ embedDefaultBlockId: null
+ };
+ }
+
+ function currentPageOptions() {
+ if (pageUiState.pageOptions) return pageUiState.pageOptions;
+ var aggregate = currentPageAggregate();
+ var current = aggregate && aggregate.layout && aggregate.layout.pageOptions && typeof aggregate.layout.pageOptions === 'object'
+ ? aggregate.layout.pageOptions
+ : null;
+ if (!current) {
+ return defaultPageOptions();
+ }
+ pageUiState.pageOptions = Object.assign(defaultPageOptions(), current);
+ return pageUiState.pageOptions;
+ }
+
+ function readGlobalShowHeadingNumbers() {
+ try {
+ var raw = window.localStorage ? window.localStorage.getItem(MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY) : '';
+ return raw === 'true' || raw === '1';
+ } catch (_) {
+ return false;
+ }
+ }
+
+ function writeGlobalShowHeadingNumbers(value) {
+ try {
+ if (window.localStorage) {
+ window.localStorage.setItem(MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY, value ? 'true' : 'false');
+ }
+ } catch (_) {}
+ document.documentElement.setAttribute('data-global-show-heading-numbers', String(Boolean(value)));
+ }
+
+ function effectiveShowHeadingNumbers(options) {
+ return readGlobalShowHeadingNumbers();
+ }
+
+ function pageOptionIsSupported(name) {
+ return name === 'wideLayout'
+ || name === 'smallText'
+ || name === 'layoutDensity'
+ || name === 'pageFont'
+ || name === 'showHeadingNumbers'
+ || (name === 'hideTitleHeader' && currentSourceKind() === 'local_folder');
+ }
+
+ function pageOptionDescription(name) {
+ if (name === 'wideLayout') return '自适应宽度';
+ if (name === 'smallText') return '小字体';
+ if (name === 'hideTitleHeader') return '隐藏本地 Markdown 文件标题';
+ if (name === 'showToc') return '标题目录';
+ if (name === 'showHeadingNumbers') return '标题自动编号';
+ if (name === 'protectEditing') return '编辑保护';
+ if (name === 'collapseBacklinks') return '折叠反向引用';
+ if (name === 'hideChildPages') return '隐藏子页面';
+ if (name === 'showBlockRefCount') return '显示块引用数字';
+ return name;
+ }
+
+ function pageOptionHint(name) {
+ if (name === 'wideLayout') return '已接通:主内容列宽度立即变化';
+ if (name === 'smallText') return '已接通:正文排版会更紧凑';
+ if (name === 'showHeadingNumbers') return '已接通:标题前显示顺序编号';
+ if (name === 'hideTitleHeader') return '已接通:本地 Markdown 页头标题立即隐藏或显示';
+ if (name === 'layoutDensity') return '已接通:段落与列表间距会变化';
+ if (name === 'pageFont') return '已接通:当前页面字体会切换';
+ if (name === 'showToc') return '待接线:当前 Rust 壳还没有正式目录面板';
+ if (name === 'protectEditing') return '待接线:当前主编辑器只显示降级说明';
+ if (name === 'collapseBacklinks') return '待接线:当前 Rust 壳未挂回链面板';
+ if (name === 'hideChildPages') return '待接线:当前页面壳还没有子页面块显隐';
+ if (name === 'showBlockRefCount') return '待接线:当前页面壳未显示块引用计数';
+ return '待接线';
+ }
+
+ function ensureHistorySnapshotsSeeded() {
+ if (pageUiState.historySnapshots.length > 0) return;
+ var aggregate = currentPageAggregate();
+ var stats = aggregate && aggregate.stats && typeof aggregate.stats === 'object' ? aggregate.stats : {};
+ pageUiState.historySnapshots = [{
+ id: 'snapshot-initial',
+ timestamp: Date.now(),
+ stats: {
+ wordCount: Number(stats.wordCount || 0),
+ characterCount: Number(stats.characterCount || 0),
+ blockCount: Number(stats.blockCount || 0),
+ todoTotal: Number(stats.todoTotal || 0),
+ todoDone: Number(stats.todoDone || 0)
+ }
+ }];
+ }
+
+ function computeLivePageStats() {
+ var aggregate = currentPageAggregate();
+ var fallbackStats = aggregate && aggregate.stats && typeof aggregate.stats === 'object' ? aggregate.stats : {};
+ var editor = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror');
+ if (!(editor instanceof HTMLElement)) {
+ return {
+ wordCount: Number(fallbackStats.wordCount || 0),
+ characterCount: Number(fallbackStats.characterCount || 0),
+ blockCount: Number(fallbackStats.blockCount || 0),
+ todoTotal: Number(fallbackStats.todoTotal || 0),
+ todoDone: Number(fallbackStats.todoDone || 0)
+ };
+ }
+ var text = (editor.textContent || '').trim();
+ var compact = text.replace(/\s+/g, ' ').trim();
+ var wordCount = compact ? compact.split(' ').filter(Boolean).length : 0;
+ var characterCount = text.replace(/\s/g, '').length;
+ var blockCount = editor.querySelectorAll(':scope > *').length;
+ var todos = Array.from(editor.querySelectorAll('input[type="checkbox"]'));
+ return {
+ wordCount: wordCount || Number(fallbackStats.wordCount || 0),
+ characterCount: characterCount || Number(fallbackStats.characterCount || 0),
+ blockCount: blockCount || Number(fallbackStats.blockCount || 0),
+ todoTotal: todos.length || Number(fallbackStats.todoTotal || 0),
+ todoDone: todos.filter(function(node){ return node.checked; }).length || Number(fallbackStats.todoDone || 0)
+ };
+ }
+
+ function recordPageHistorySnapshot(reason, stats) {
+ ensureHistorySnapshotsSeeded();
+ pageUiState.historySnapshots = [{
+ id: 'snapshot-' + Date.now(),
+ timestamp: Date.now(),
+ reason: reason || 'save',
+ stats: stats || computeLivePageStats()
+ }].concat(pageUiState.historySnapshots).slice(0, 15);
+ renderPageHistoryDrawer();
+ }
+
+ function applyPageOptionsToShell() {
+ var options = currentPageOptions();
+ var globalShowHeadingNumbers = readGlobalShowHeadingNumbers();
+ var showHeadingNumbers = effectiveShowHeadingNumbers(options);
+ var shell = document.querySelector('.document-shell');
+ var editorRoot = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
+ var editorSurface = editorRoot && editorRoot.querySelector('.editor-surface');
+ if (shell instanceof HTMLElement) {
+ shell.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
+ shell.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
+ shell.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
+ shell.setAttribute('data-page-font', String(options.pageFont || 'default'));
+ shell.setAttribute('data-page-show-heading-numbers', String(showHeadingNumbers));
+ shell.style.width = '100%';
+ shell.style.maxWidth = options.wideLayout ? '980px' : '760px';
+ }
+ if (editorRoot instanceof HTMLElement) {
+ editorRoot.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
+ editorRoot.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
+ editorRoot.setAttribute('data-page-show-heading-numbers', String(showHeadingNumbers));
+ editorRoot.setAttribute('data-page-embed-default-block-id', options.embedDefaultBlockId == null ? '' : String(options.embedDefaultBlockId));
+ }
+ if (editorSurface instanceof HTMLElement) {
+ editorSurface.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
+ editorSurface.setAttribute('data-page-font', String(options.pageFont || 'default'));
+ editorSurface.setAttribute('data-show-heading-numbers', String(showHeadingNumbers));
+ }
+ document.documentElement.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
+ document.documentElement.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
+ document.documentElement.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
+ document.documentElement.setAttribute('data-page-font', String(options.pageFont || 'default'));
+ document.documentElement.setAttribute('data-global-show-heading-numbers', String(globalShowHeadingNumbers));
+ document.documentElement.setAttribute('data-page-show-heading-numbers', String(showHeadingNumbers));
+ document.documentElement.setAttribute('data-page-hide-title-header', String(Boolean(options.hideTitleHeader)));
+ var titleHeader = document.querySelector('.document-pane[data-pane-role="primary"] .document-shell-header') || document.querySelector('.document-shell-header');
+ if (titleHeader instanceof HTMLElement) {
+ var hidden = Boolean(options.hideTitleHeader);
+ titleHeader.hidden = hidden;
+ titleHeader.setAttribute('data-page-title-hidden', String(hidden));
+ }
+ }
+
+ function updatePageSettingsTriggerState() {
+ var trigger = document.querySelector('[data-testid="wolai-page-settings-trigger"]');
+ if (!(trigger instanceof HTMLElement)) return;
+ trigger.setAttribute('data-state', pageUiState.pageSettingsOpen ? 'open' : 'closed');
+ trigger.setAttribute('aria-expanded', pageUiState.pageSettingsOpen ? 'true' : 'false');
+ }
+
+ function createPageOptionRow(key, type) {
+ var inputType = type || 'checkbox';
+ var supported = pageOptionIsSupported(key);
+ if (inputType === 'checkbox') {
+ return '' +
+ '';
+ }
+ return '' +
+ '';
+ }
+
+ function createGlobalHeadingNumbersRow() {
+ return '' +
+ '';
+ }
+
+ function renderGlobalOptions(popover) {
+ var globalHeadingNumbers = readGlobalShowHeadingNumbers();
+ popover.querySelectorAll('[data-global-option-checkbox="showHeadingNumbers"]').forEach(function(input) {
+ input.checked = globalHeadingNumbers;
+ });
+ }
+
+ function createPageFontRow() {
+ return '' +
+ '';
+ }
+
+ function ensurePageHistoryDrawer() {
+ var existing = document.querySelector('[data-testid="wolai-page-history-drawer"]');
+ if (existing instanceof HTMLElement) return existing;
+ var drawer = document.createElement('aside');
+ drawer.className = 'wolai-page-history-drawer';
+ drawer.setAttribute('data-testid', 'wolai-page-history-drawer');
+ drawer.setAttribute('data-mnote-surface', 'page-history');
+ drawer.hidden = true;
+ drawer.innerHTML = '' +
+ '
';
+ drawer.addEventListener('click', function(event) {
+ if (event.target === drawer) closePageHistoryDrawer();
+ });
+ document.body.appendChild(drawer);
+ return drawer;
+ }
+
+ function renderPageHistoryDrawer() {
+ var drawer = ensurePageHistoryDrawer();
+ var list = drawer.querySelector('[data-page-history-list]');
+ if (!(list instanceof HTMLElement)) return;
+ ensureHistorySnapshotsSeeded();
+ list.innerHTML = pageUiState.historySnapshots.length
+ ? pageUiState.historySnapshots.map(function(snapshot) {
+ var stats = snapshot.stats || {};
+ var label = new Date(snapshot.timestamp).toLocaleString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ return '' +
+ '' +
+ '
' +
+ '
' + escapeHtml(label) + '
' +
+ '
字数 ' + Number(stats.wordCount || 0) + ' · 字符 ' + Number(stats.characterCount || 0) + ' · 块数 ' + Number(stats.blockCount || 0) + '
' +
+ '
' +
+ '
' +
+ '
';
+ }).join('')
+ : '尚未产生历史快照,编辑后会自动生成。
';
+ }
+
+ function openPageHistoryDrawer() {
+ renderPageHistoryDrawer();
+ var drawer = ensurePageHistoryDrawer();
+ drawer.hidden = false;
+ document.documentElement.setAttribute('data-mnote-page-history-open', 'true');
+ }
+
+ function closePageHistoryDrawer() {
+ var drawer = document.querySelector('[data-testid="wolai-page-history-drawer"]');
+ if (drawer instanceof HTMLElement) drawer.hidden = true;
+ document.documentElement.removeAttribute('data-mnote-page-history-open');
+ }
+
+ function ensurePageShareDialog() {
+ var existing = document.querySelector('[data-testid="wolai-page-share-dialog"]');
+ if (existing instanceof HTMLElement) return existing;
+ var dialog = document.createElement('div');
+ dialog.className = 'wolai-page-share-dialog';
+ dialog.setAttribute('data-testid', 'wolai-page-share-dialog');
+ dialog.setAttribute('data-mnote-surface', 'page-share');
+ dialog.hidden = true;
+ dialog.innerHTML = '' +
+ '' +
+ '' +
+ '
' +
+ '全网公开' +
+ '任何拥有链接的人都可以访问当前页面。' +
+ '
' +
+ '
' +
+ '' +
+ '' +
+ '
' +
+ '' +
+ '
';
+ dialog.addEventListener('click', function(event) {
+ if (event.target === dialog) closePageShareDialog();
+ });
+ document.body.appendChild(dialog);
+ return dialog;
+ }
+
+ function openPageShareDialog() {
+ var dialog = ensurePageShareDialog();
+ var input = dialog.querySelector('[data-page-share-url]');
+ if (input instanceof HTMLInputElement) input.value = window.location.href;
+ dialog.hidden = false;
+ document.documentElement.setAttribute('data-mnote-page-share-open', 'true');
+ }
+
+ function closePageShareDialog() {
+ var dialog = document.querySelector('[data-testid="wolai-page-share-dialog"]');
+ if (dialog instanceof HTMLElement) dialog.hidden = true;
+ document.documentElement.removeAttribute('data-mnote-page-share-open');
+ }
+
+ function ensurePageSettingsPopover() {
+ var existing = document.querySelector('[data-testid="wolai-page-settings-popover"]');
+ if (existing instanceof HTMLElement) return existing;
+ var popover = document.createElement('div');
+ popover.className = 'wolai-page-settings-popover';
+ popover.setAttribute('data-testid', 'wolai-page-settings-popover');
+ popover.setAttribute('data-mnote-surface', 'page-settings');
+ popover.hidden = true;
+ popover.innerHTML = '' +
+ '' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ '
' +
+ createPageOptionRow('wideLayout', 'checkbox') +
+ createPageOptionRow('smallText', 'checkbox') +
+ createPageOptionRow('showToc', 'checkbox') +
+ createPageOptionRow('protectEditing', 'checkbox') +
+ createPageOptionRow('hideTitleHeader', 'checkbox') +
+ '
' +
+ '
' +
+ createPageFontRow() +
+ createPageOptionRow('layoutDensity', 'select') +
+ createPageOptionRow('collapseBacklinks', 'checkbox') +
+ createPageOptionRow('hideChildPages', 'checkbox') +
+ createPageOptionRow('showBlockRefCount', 'checkbox') +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ createGlobalHeadingNumbersRow() +
+ '
' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ '
' +
+ '
';
+ document.body.appendChild(popover);
+ return popover;
+ }
+
+ function pageSettingsLocalIndexScopeKey() {
+ return [
+ currentSourceKind(),
+ currentRootUri(),
+ resolveWorkspaceId(document.body),
+ currentDocumentId()
+ ].join('|');
+ }
+
+ function pageSettingsLocalIndexIsAvailable() {
+ return currentSourceKind() === 'local_folder' && Boolean(currentRootUri()) && Boolean(currentDocumentId());
+ }
+
+ function pageSettingsLocalIndexEmpty(message) {
+ return '' + escapeHtml(message) + '
';
+ }
+
+ function renderPageSettingsLocalIndexList(items, kind) {
+ var rows = Array.isArray(items) ? items : [];
+ if (!rows.length) {
+ return pageSettingsLocalIndexEmpty(kind === 'backlinks' ? '暂无反链' : '暂无标签');
+ }
+ if (kind === 'backlinks') {
+ return rows.slice(0, 12).map(function(item) {
+ var title = searchText(item && item.title) || searchText(item && item.path) || '未命名页面';
+ var path = searchText(item && item.path);
+ var snippet = searchText(item && item.snippet);
+ return '' +
+ '' +
+ '
' + escapeHtml(title) + '
' +
+ (path ? '
' + escapeHtml(path) + '
' : '') +
+ (snippet ? '
' + escapeHtml(snippet) + '
' : '') +
+ '
';
+ }).join('');
+ }
+ return rows.slice(0, 16).map(function(item) {
+ var tag = searchText(item && item.tag) || 'untagged';
+ var count = Number(item && item.count || 0);
+ return '' +
+ '' +
+ '
#' + escapeHtml(tag) + '
' +
+ '
' + count + ' 个页面
' +
+ '
';
+ }).join('');
+ }
+
+ function renderPageSettingsLocalIndex(popover) {
+ popover = popover || ensurePageSettingsPopover();
+ var statusNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-status"]');
+ var backlinksNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-backlinks"]');
+ var tagsNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-tags"]');
+ if (!(statusNode instanceof HTMLElement) || !(backlinksNode instanceof HTMLElement) || !(tagsNode instanceof HTMLElement)) return;
+
+ if (!pageSettingsLocalIndexIsAvailable()) {
+ statusNode.textContent = '本地索引仅在本地工作区页面可用';
+ backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('未连接本地 root');
+ tagsNode.innerHTML = pageSettingsLocalIndexEmpty('未连接本地 root');
+ return;
+ }
+
+ var summary = pageUiState.localIndexSummary || {};
+ if (summary.loading) {
+ statusNode.textContent = '正在读取本地索引...';
+ backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('加载中');
+ tagsNode.innerHTML = pageSettingsLocalIndexEmpty('加载中');
+ return;
+ }
+ if (summary.error) {
+ statusNode.textContent = '本地索引读取失败';
+ backlinksNode.innerHTML = pageSettingsLocalIndexEmpty(summary.error);
+ tagsNode.innerHTML = pageSettingsLocalIndexEmpty(summary.error);
+ return;
+ }
+ if (summary.scopeKey !== pageSettingsLocalIndexScopeKey()) {
+ statusNode.textContent = '切换到索引页签后读取本地索引';
+ backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('等待读取');
+ tagsNode.innerHTML = pageSettingsLocalIndexEmpty('等待读取');
+ return;
+ }
+
+ statusNode.textContent = '来自当前授权 root 的 .mnote/index/search-index.json';
+ backlinksNode.innerHTML = renderPageSettingsLocalIndexList(summary.backlinks, 'backlinks');
+ tagsNode.innerHTML = renderPageSettingsLocalIndexList(summary.tags, 'tags');
+ }
+
+ async function loadPageSettingsLocalIndex(force) {
+ if (!pageSettingsLocalIndexIsAvailable()) {
+ renderPageSettingsLocalIndex();
+ return;
+ }
+ var scopeKey = pageSettingsLocalIndexScopeKey();
+ var current = pageUiState.localIndexSummary || {};
+ if (!force && current.scopeKey === scopeKey && !current.error && !current.loading) {
+ renderPageSettingsLocalIndex();
+ return;
+ }
+ pageUiState.localIndexSummary = {
+ scopeKey: scopeKey,
+ loading: true,
+ error: '',
+ backlinks: null,
+ tags: null
+ };
+ renderPageSettingsLocalIndex();
+ try {
+ var baseParams = new URLSearchParams();
+ baseParams.set('workspaceId', resolveWorkspaceId(document.body));
+ baseParams.set('rootUri', currentRootUri());
+ var backlinksParams = new URLSearchParams(baseParams);
+ backlinksParams.set('documentId', currentDocumentId());
+ var backlinksUrl = '/api/search/local-index/backlinks?' + backlinksParams.toString();
+ var tagsUrl = '/api/search/local-index/tags?' + baseParams.toString();
+ var responses = await Promise.all([
+ fetch(backlinksUrl, { headers: { accept: 'application/json' } }),
+ fetch(tagsUrl, { headers: { accept: 'application/json' } })
+ ]);
+ var backlinksPayload = await responses[0].json().catch(function(){ return null; });
+ var tagsPayload = await responses[1].json().catch(function(){ return null; });
+ if (!responses[0].ok || !backlinksPayload || backlinksPayload.ok !== true) {
+ throw new Error('backlinks_' + responses[0].status);
+ }
+ if (!responses[1].ok || !tagsPayload || tagsPayload.ok !== true) {
+ throw new Error('tags_' + responses[1].status);
+ }
+ pageUiState.localIndexSummary = {
+ scopeKey: scopeKey,
+ loading: false,
+ error: '',
+ backlinks: backlinksPayload.result && Array.isArray(backlinksPayload.result.backlinks) ? backlinksPayload.result.backlinks : [],
+ tags: tagsPayload.result && Array.isArray(tagsPayload.result.tags) ? tagsPayload.result.tags : []
+ };
+ } catch (error) {
+ pageUiState.localIndexSummary = {
+ scopeKey: scopeKey,
+ loading: false,
+ error: error instanceof Error ? error.message : String(error),
+ backlinks: [],
+ tags: []
+ };
+ }
+ renderPageSettingsLocalIndex();
+ }
+
+ function renderPageSettingsPopover() {
+ var popover = ensurePageSettingsPopover();
+ var options = currentPageOptions();
+ popover.querySelectorAll('[data-page-option-checkbox]').forEach(function(input) {
+ var key = input.getAttribute('data-page-option-checkbox');
+ input.checked = Boolean(options[key]);
+ input.disabled = !pageOptionIsSupported(key);
+ });
+ popover.querySelectorAll('[data-page-option-select]').forEach(function(select) {
+ var key = select.getAttribute('data-page-option-select');
+ var value = key === 'layoutDensity' ? String(options.layoutDensity || 'normal') : String(options.pageFont || 'default');
+ select.value = value;
+ });
+ renderGlobalOptions(popover);
+ var statsNode = popover.querySelector('[data-testid="wolai-page-settings-stats"]');
+ if (statsNode instanceof HTMLElement) {
+ var stats = computeLivePageStats();
+ statsNode.innerHTML = '' +
+ '字数 ' + Number(stats.wordCount || 0) + '' +
+ '字符 ' + Number(stats.characterCount || 0) + '' +
+ '块数 ' + Number(stats.blockCount || 0) + '' +
+ '待办 ' + Number(stats.todoDone || 0) + '/' + Number(stats.todoTotal || 0) + '';
+ }
+ renderPageSettingsLocalIndex(popover);
+ }
+
+ function setActivePageSettingsTab(tabName) {
+ var popover = ensurePageSettingsPopover();
+ popover.querySelectorAll('[data-page-settings-tab]').forEach(function(tab) {
+ var active = tab.getAttribute('data-page-settings-tab') === tabName;
+ tab.classList.toggle('is-active', active);
+ tab.setAttribute('aria-selected', active ? 'true' : 'false');
+ });
+ popover.querySelectorAll('[data-page-settings-panel]').forEach(function(panel) {
+ panel.hidden = panel.getAttribute('data-page-settings-panel') !== tabName;
+ });
+ if (tabName === 'index') void loadPageSettingsLocalIndex(false);
+ }
+
+ async function persistPageOptionsPatch(patch) {
+ var previous = Object.assign({}, currentPageOptions());
+ pageUiState.pageOptions = Object.assign({}, previous, patch);
+ var nextOptions = Object.assign({}, pageUiState.pageOptions);
+ applyPageOptionsToShell();
+ renderPageSettingsPopover();
+ try {
+ var response = await fetch('/api/documents/options', {
+ method: 'POST',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({
+ documentId: currentDocumentId(),
+ workspaceId: resolveWorkspaceId(document.body),
+ ...currentWorkspaceSourcePayload(),
+ options: nextOptions,
+ commandName: 'page.layout.updateOptions'
+ })
+ });
+ var payload = await response.json().catch(function(){ return null; });
+ if (!response.ok || !payload || payload.ok !== true) {
+ throw new Error(payload && payload.error && payload.error.message ? payload.error.message : 'page_settings_save_failed_' + response.status);
+ }
+ document.documentElement.setAttribute('data-mnote-page-options-saved', 'true');
+ } catch (error) {
+ pageUiState.pageOptions = previous;
+ applyPageOptionsToShell();
+ renderPageSettingsPopover();
+ document.documentElement.setAttribute('data-mnote-page-options-error', error instanceof Error ? error.message : String(error));
+ }
+ }
+
+ function isPageSettingsOpen() {
+ var popover = document.querySelector('[data-testid="wolai-page-settings-popover"]');
+ return popover instanceof HTMLElement && !popover.hidden;
+ }
+
+ function openPageSettingsPopover() {
+ if (!currentDocumentId()) return;
+ var popover = ensurePageSettingsPopover();
+ renderPageSettingsPopover();
+ setActivePageSettingsTab('page');
+ popover.hidden = false;
+ pageUiState.pageSettingsOpen = true;
+ updatePageSettingsTriggerState();
+ }
+
+ function closePageSettingsPopover() {
+ var popover = document.querySelector('[data-testid="wolai-page-settings-popover"]');
+ if (popover instanceof HTMLElement) popover.hidden = true;
+ pageUiState.pageSettingsOpen = false;
+ updatePageSettingsTriggerState();
+ }
+
+ function togglePageSettingsPopover() {
+ if (isPageSettingsOpen()) closePageSettingsPopover();
+ else openPageSettingsPopover();
+ }
+
+ return {
+ applyPageOptionsToShell,
+ closePageHistoryDrawer,
+ closePageSettingsPopover,
+ closePageShareDialog,
+ currentPageOptions,
+ ensureHistorySnapshotsSeeded,
+ ensurePageHistoryDrawer,
+ ensurePageSettingsPopover,
+ ensurePageShareDialog,
+ isPageSettingsOpen,
+ openPageHistoryDrawer,
+ openPageSettingsPopover,
+ openPageShareDialog,
+ pageOptionIsSupported,
+ persistPageOptionsPatch,
+ recordPageHistorySnapshot,
+ renderPageSettingsPopover,
+ setActivePageSettingsTab,
+ togglePageSettingsPopover,
+ updatePageSettingsTriggerState,
+ writeGlobalShowHeadingNumbers,
+ };
+}
diff --git a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js
index 6d322941..e75b545d 100644
--- a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js
+++ b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js
@@ -6,6 +6,7 @@ import { createSidebarAttachmentOpenRuntime } from './sidebar-attachment-open-ru
import { createSidebarFileTreeCommandRuntime } from './sidebar-filetree-command-runtime.js';
import { createSidebarFileTreeUploadRuntime } from './sidebar-filetree-upload-runtime.js';
import { createSidebarPageAiRuntime } from './sidebar-page-ai-runtime.js';
+import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtime.js';
(function(){
if (window.__mnoteSidebarTreeRuntimeStarted) return;
@@ -191,195 +192,6 @@ import { createSidebarPageAiRuntime } from './sidebar-page-ai-runtime.js';
return parseJsonScript('__MNOTE_PAGE_AGGREGATE__') || {};
}
- function defaultPageOptions() {
- return {
- wideLayout: false,
- smallText: false,
- layoutDensity: 'normal',
- showHeadingNumbers: false,
- showToc: false,
- showStructure: false,
- protectEditing: false,
- showWordCount: true,
- collapseBacklinks: false,
- pageFont: 'default',
- hideChildPages: false,
- showBlockRefCount: false,
- hideTitleHeader: false,
- embedDefaultBlockId: null
- };
- }
-
- function currentPageOptions() {
- if (pageUiState.pageOptions) return pageUiState.pageOptions;
- var aggregate = currentPageAggregate();
- var current = aggregate && aggregate.layout && aggregate.layout.pageOptions && typeof aggregate.layout.pageOptions === 'object'
- ? aggregate.layout.pageOptions
- : null;
- if (!current) {
- return defaultPageOptions();
- }
- pageUiState.pageOptions = Object.assign(defaultPageOptions(), current);
- return pageUiState.pageOptions;
- }
-
- function readGlobalShowHeadingNumbers() {
- try {
- var raw = window.localStorage ? window.localStorage.getItem(MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY) : '';
- return raw === 'true' || raw === '1';
- } catch (_) {
- return false;
- }
- }
-
- function writeGlobalShowHeadingNumbers(value) {
- try {
- if (window.localStorage) {
- window.localStorage.setItem(MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY, value ? 'true' : 'false');
- }
- } catch (_) {}
- document.documentElement.setAttribute('data-global-show-heading-numbers', String(Boolean(value)));
- }
-
- function effectiveShowHeadingNumbers(options) {
- return readGlobalShowHeadingNumbers();
- }
-
- function pageOptionIsSupported(name) {
- return name === 'wideLayout'
- || name === 'smallText'
- || name === 'layoutDensity'
- || name === 'pageFont'
- || name === 'showHeadingNumbers'
- || (name === 'hideTitleHeader' && currentSourceKind() === 'local_folder');
- }
-
- function pageOptionDescription(name) {
- if (name === 'wideLayout') return '自适应宽度';
- if (name === 'smallText') return '小字体';
- if (name === 'hideTitleHeader') return '隐藏本地 Markdown 文件标题';
- if (name === 'showToc') return '标题目录';
- if (name === 'showHeadingNumbers') return '标题自动编号';
- if (name === 'protectEditing') return '编辑保护';
- if (name === 'collapseBacklinks') return '折叠反向引用';
- if (name === 'hideChildPages') return '隐藏子页面';
- if (name === 'showBlockRefCount') return '显示块引用数字';
- return name;
- }
-
- function pageOptionHint(name) {
- if (name === 'wideLayout') return '已接通:主内容列宽度立即变化';
- if (name === 'smallText') return '已接通:正文排版会更紧凑';
- if (name === 'showHeadingNumbers') return '已接通:标题前显示顺序编号';
- if (name === 'hideTitleHeader') return '已接通:本地 Markdown 页头标题立即隐藏或显示';
- if (name === 'layoutDensity') return '已接通:段落与列表间距会变化';
- if (name === 'pageFont') return '已接通:当前页面字体会切换';
- if (name === 'showToc') return '待接线:当前 Rust 壳还没有正式目录面板';
- if (name === 'protectEditing') return '待接线:当前主编辑器只显示降级说明';
- if (name === 'collapseBacklinks') return '待接线:当前 Rust 壳未挂回链面板';
- if (name === 'hideChildPages') return '待接线:当前页面壳还没有子页面块显隐';
- if (name === 'showBlockRefCount') return '待接线:当前页面壳未显示块引用计数';
- return '待接线';
- }
-
- function ensureHistorySnapshotsSeeded() {
- if (pageUiState.historySnapshots.length > 0) return;
- var aggregate = currentPageAggregate();
- var stats = aggregate && aggregate.stats && typeof aggregate.stats === 'object' ? aggregate.stats : {};
- pageUiState.historySnapshots = [{
- id: 'snapshot-initial',
- timestamp: Date.now(),
- stats: {
- wordCount: Number(stats.wordCount || 0),
- characterCount: Number(stats.characterCount || 0),
- blockCount: Number(stats.blockCount || 0),
- todoTotal: Number(stats.todoTotal || 0),
- todoDone: Number(stats.todoDone || 0)
- }
- }];
- }
-
- function computeLivePageStats() {
- var aggregate = currentPageAggregate();
- var fallbackStats = aggregate && aggregate.stats && typeof aggregate.stats === 'object' ? aggregate.stats : {};
- var editor = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror');
- if (!(editor instanceof HTMLElement)) {
- return {
- wordCount: Number(fallbackStats.wordCount || 0),
- characterCount: Number(fallbackStats.characterCount || 0),
- blockCount: Number(fallbackStats.blockCount || 0),
- todoTotal: Number(fallbackStats.todoTotal || 0),
- todoDone: Number(fallbackStats.todoDone || 0)
- };
- }
- var text = (editor.textContent || '').trim();
- var compact = text.replace(/\s+/g, ' ').trim();
- var wordCount = compact ? compact.split(' ').filter(Boolean).length : 0;
- var characterCount = text.replace(/\s/g, '').length;
- var blockCount = editor.querySelectorAll(':scope > *').length;
- var todos = Array.from(editor.querySelectorAll('input[type="checkbox"]'));
- return {
- wordCount: wordCount || Number(fallbackStats.wordCount || 0),
- characterCount: characterCount || Number(fallbackStats.characterCount || 0),
- blockCount: blockCount || Number(fallbackStats.blockCount || 0),
- todoTotal: todos.length || Number(fallbackStats.todoTotal || 0),
- todoDone: todos.filter(function(node){ return node.checked; }).length || Number(fallbackStats.todoDone || 0)
- };
- }
-
- function recordPageHistorySnapshot(reason, stats) {
- ensureHistorySnapshotsSeeded();
- pageUiState.historySnapshots = [{
- id: 'snapshot-' + Date.now(),
- timestamp: Date.now(),
- reason: reason || 'save',
- stats: stats || computeLivePageStats()
- }].concat(pageUiState.historySnapshots).slice(0, 15);
- renderPageHistoryDrawer();
- }
-
- function applyPageOptionsToShell() {
- var options = currentPageOptions();
- var globalShowHeadingNumbers = readGlobalShowHeadingNumbers();
- var showHeadingNumbers = effectiveShowHeadingNumbers(options);
- var shell = document.querySelector('.document-shell');
- var editorRoot = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
- var editorSurface = editorRoot && editorRoot.querySelector('.editor-surface');
- if (shell instanceof HTMLElement) {
- shell.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
- shell.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
- shell.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
- shell.setAttribute('data-page-font', String(options.pageFont || 'default'));
- shell.setAttribute('data-page-show-heading-numbers', String(showHeadingNumbers));
- shell.style.width = '100%';
- shell.style.maxWidth = options.wideLayout ? '980px' : '760px';
- }
- if (editorRoot instanceof HTMLElement) {
- editorRoot.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
- editorRoot.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
- editorRoot.setAttribute('data-page-show-heading-numbers', String(showHeadingNumbers));
- editorRoot.setAttribute('data-page-embed-default-block-id', options.embedDefaultBlockId == null ? '' : String(options.embedDefaultBlockId));
- }
- if (editorSurface instanceof HTMLElement) {
- editorSurface.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
- editorSurface.setAttribute('data-page-font', String(options.pageFont || 'default'));
- editorSurface.setAttribute('data-show-heading-numbers', String(showHeadingNumbers));
- }
- document.documentElement.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
- document.documentElement.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
- document.documentElement.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
- document.documentElement.setAttribute('data-page-font', String(options.pageFont || 'default'));
- document.documentElement.setAttribute('data-global-show-heading-numbers', String(globalShowHeadingNumbers));
- document.documentElement.setAttribute('data-page-show-heading-numbers', String(showHeadingNumbers));
- document.documentElement.setAttribute('data-page-hide-title-header', String(Boolean(options.hideTitleHeader)));
- var titleHeader = document.querySelector('.document-pane[data-pane-role="primary"] .document-shell-header') || document.querySelector('.document-shell-header');
- if (titleHeader instanceof HTMLElement) {
- var hidden = Boolean(options.hideTitleHeader);
- titleHeader.hidden = hidden;
- titleHeader.setAttribute('data-page-title-hidden', String(hidden));
- }
- }
-
const sidebarWorkspace = createSidebarWorkspaceRuntime({
cssEscape,
currentDocumentId,
@@ -409,6 +221,36 @@ import { createSidebarPageAiRuntime } from './sidebar-page-ai-runtime.js';
const openAccountMenu = (...args) => sidebarWorkspace.openAccountMenu(...args);
sidebarWorkspace.autoOpenRecentLocalRootOnHome();
+ const sidebarPageSettings = createSidebarPageSettingsRuntime({
+ currentDocumentId,
+ currentPageAggregate,
+ currentRootUri,
+ currentSourceKind,
+ currentWorkspaceSourcePayload,
+ escapeHtml,
+ globalShowHeadingNumbersKey: MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY,
+ pageUiState,
+ resolveWorkspaceId,
+ searchText,
+ });
+ const applyPageOptionsToShell = (...args) => sidebarPageSettings.applyPageOptionsToShell(...args);
+ const closePageHistoryDrawer = (...args) => sidebarPageSettings.closePageHistoryDrawer(...args);
+ const closePageSettingsPopover = (...args) => sidebarPageSettings.closePageSettingsPopover(...args);
+ const closePageShareDialog = (...args) => sidebarPageSettings.closePageShareDialog(...args);
+ const currentPageOptions = (...args) => sidebarPageSettings.currentPageOptions(...args);
+ const ensureHistorySnapshotsSeeded = (...args) => sidebarPageSettings.ensureHistorySnapshotsSeeded(...args);
+ const isPageSettingsOpen = (...args) => sidebarPageSettings.isPageSettingsOpen(...args);
+ const openPageHistoryDrawer = (...args) => sidebarPageSettings.openPageHistoryDrawer(...args);
+ const openPageShareDialog = (...args) => sidebarPageSettings.openPageShareDialog(...args);
+ const pageOptionIsSupported = (...args) => sidebarPageSettings.pageOptionIsSupported(...args);
+ const persistPageOptionsPatch = (...args) => sidebarPageSettings.persistPageOptionsPatch(...args);
+ const recordPageHistorySnapshot = (...args) => sidebarPageSettings.recordPageHistorySnapshot(...args);
+ const renderPageSettingsPopover = (...args) => sidebarPageSettings.renderPageSettingsPopover(...args);
+ const setActivePageSettingsTab = (...args) => sidebarPageSettings.setActivePageSettingsTab(...args);
+ const togglePageSettingsPopover = (...args) => sidebarPageSettings.togglePageSettingsPopover(...args);
+ const updatePageSettingsTriggerState = (...args) => sidebarPageSettings.updatePageSettingsTriggerState(...args);
+ const writeGlobalShowHeadingNumbers = (...args) => sidebarPageSettings.writeGlobalShowHeadingNumbers(...args);
+
function setCommandPending(trigger, pending) {
if (!(trigger instanceof HTMLElement)) return;
trigger.setAttribute('data-pending', pending ? 'true' : 'false');
@@ -1994,180 +1836,6 @@ import { createSidebarPageAiRuntime } from './sidebar-page-ai-runtime.js';
else openSearchModal();
}
- function updatePageSettingsTriggerState() {
- var trigger = document.querySelector('[data-testid="wolai-page-settings-trigger"]');
- if (!(trigger instanceof HTMLElement)) return;
- trigger.setAttribute('data-state', pageUiState.pageSettingsOpen ? 'open' : 'closed');
- trigger.setAttribute('aria-expanded', pageUiState.pageSettingsOpen ? 'true' : 'false');
- }
-
- function createPageOptionRow(key, type) {
- var inputType = type || 'checkbox';
- var supported = pageOptionIsSupported(key);
- if (inputType === 'checkbox') {
- return '' +
- '';
- }
- return '' +
- '';
- }
-
- function createGlobalHeadingNumbersRow() {
- return '' +
- '';
- }
-
- function renderGlobalOptions(popover) {
- var globalHeadingNumbers = readGlobalShowHeadingNumbers();
- popover.querySelectorAll('[data-global-option-checkbox="showHeadingNumbers"]').forEach(function(input) {
- input.checked = globalHeadingNumbers;
- });
- }
-
- function createPageFontRow() {
- return '' +
- '';
- }
-
- function ensurePageHistoryDrawer() {
- var existing = document.querySelector('[data-testid="wolai-page-history-drawer"]');
- if (existing instanceof HTMLElement) return existing;
- var drawer = document.createElement('aside');
- drawer.className = 'wolai-page-history-drawer';
- drawer.setAttribute('data-testid', 'wolai-page-history-drawer');
- drawer.setAttribute('data-mnote-surface', 'page-history');
- drawer.hidden = true;
- drawer.innerHTML = '' +
- '';
- drawer.addEventListener('click', function(event) {
- if (event.target === drawer) closePageHistoryDrawer();
- });
- document.body.appendChild(drawer);
- return drawer;
- }
-
- function renderPageHistoryDrawer() {
- var drawer = ensurePageHistoryDrawer();
- var list = drawer.querySelector('[data-page-history-list]');
- if (!(list instanceof HTMLElement)) return;
- ensureHistorySnapshotsSeeded();
- list.innerHTML = pageUiState.historySnapshots.length
- ? pageUiState.historySnapshots.map(function(snapshot) {
- var stats = snapshot.stats || {};
- var label = new Date(snapshot.timestamp).toLocaleString('zh-CN', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit'
- });
- return '' +
- '' +
- '
' +
- '
' + escapeHtml(label) + '
' +
- '
字数 ' + Number(stats.wordCount || 0) + ' · 字符 ' + Number(stats.characterCount || 0) + ' · 块数 ' + Number(stats.blockCount || 0) + '
' +
- '
' +
- '
' +
- '
';
- }).join('')
- : '尚未产生历史快照,编辑后会自动生成。
';
- }
-
- function openPageHistoryDrawer() {
- renderPageHistoryDrawer();
- var drawer = ensurePageHistoryDrawer();
- drawer.hidden = false;
- document.documentElement.setAttribute('data-mnote-page-history-open', 'true');
- }
-
- function closePageHistoryDrawer() {
- var drawer = document.querySelector('[data-testid="wolai-page-history-drawer"]');
- if (drawer instanceof HTMLElement) drawer.hidden = true;
- document.documentElement.removeAttribute('data-mnote-page-history-open');
- }
-
- function ensurePageShareDialog() {
- var existing = document.querySelector('[data-testid="wolai-page-share-dialog"]');
- if (existing instanceof HTMLElement) return existing;
- var dialog = document.createElement('div');
- dialog.className = 'wolai-page-share-dialog';
- dialog.setAttribute('data-testid', 'wolai-page-share-dialog');
- dialog.setAttribute('data-mnote-surface', 'page-share');
- dialog.hidden = true;
- dialog.innerHTML = '' +
- '' +
- '' +
- '
' +
- '全网公开' +
- '任何拥有链接的人都可以访问当前页面。' +
- '
' +
- '
' +
- '' +
- '' +
- '
' +
- '' +
- '
';
- dialog.addEventListener('click', function(event) {
- if (event.target === dialog) closePageShareDialog();
- });
- document.body.appendChild(dialog);
- return dialog;
- }
-
- function openPageShareDialog() {
- var dialog = ensurePageShareDialog();
- var input = dialog.querySelector('[data-page-share-url]');
- if (input instanceof HTMLInputElement) input.value = window.location.href;
- dialog.hidden = false;
- document.documentElement.setAttribute('data-mnote-page-share-open', 'true');
- }
-
- function closePageShareDialog() {
- var dialog = document.querySelector('[data-testid="wolai-page-share-dialog"]');
- if (dialog instanceof HTMLElement) dialog.hidden = true;
- document.documentElement.removeAttribute('data-mnote-page-share-open');
- }
-
const sidebarPageAi = createSidebarPageAiRuntime({
buildLocalFileOpenUrl,
currentDocumentId,
@@ -2216,305 +1884,6 @@ import { createSidebarPageAiRuntime } from './sidebar-page-ai-runtime.js';
const pageAiSetContextScope = (...args) => sidebarPageAi.pageAiSetContextScope(...args);
const updatePageAiTriggerState = (...args) => sidebarPageAi.updatePageAiTriggerState(...args);
- function ensurePageSettingsPopover() {
- var existing = document.querySelector('[data-testid="wolai-page-settings-popover"]');
- if (existing instanceof HTMLElement) return existing;
- var popover = document.createElement('div');
- popover.className = 'wolai-page-settings-popover';
- popover.setAttribute('data-testid', 'wolai-page-settings-popover');
- popover.setAttribute('data-mnote-surface', 'page-settings');
- popover.hidden = true;
- popover.innerHTML = '' +
- '' +
- '
' +
- '' +
- '' +
- '' +
- '' +
- '
' +
- '
' +
- createPageOptionRow('wideLayout', 'checkbox') +
- createPageOptionRow('smallText', 'checkbox') +
- createPageOptionRow('showToc', 'checkbox') +
- createPageOptionRow('protectEditing', 'checkbox') +
- createPageOptionRow('hideTitleHeader', 'checkbox') +
- '
' +
- '
' +
- createPageFontRow() +
- createPageOptionRow('layoutDensity', 'select') +
- createPageOptionRow('collapseBacklinks', 'checkbox') +
- createPageOptionRow('hideChildPages', 'checkbox') +
- createPageOptionRow('showBlockRefCount', 'checkbox') +
- '
' +
- '
' +
- '
' +
- '
' +
- '
' +
- '
' +
- '
' +
- '
' +
- '
' +
- createGlobalHeadingNumbersRow() +
- '
' +
- '
' +
- '' +
- '' +
- '' +
- '' +
- '' +
- '
' +
- '
' +
- '
';
- document.body.appendChild(popover);
- return popover;
- }
-
- function pageSettingsLocalIndexScopeKey() {
- return [
- currentSourceKind(),
- currentRootUri(),
- resolveWorkspaceId(document.body),
- currentDocumentId()
- ].join('|');
- }
-
- function pageSettingsLocalIndexIsAvailable() {
- return currentSourceKind() === 'local_folder' && Boolean(currentRootUri()) && Boolean(currentDocumentId());
- }
-
- function pageSettingsLocalIndexEmpty(message) {
- return '' + escapeHtml(message) + '
';
- }
-
- function renderPageSettingsLocalIndexList(items, kind) {
- var rows = Array.isArray(items) ? items : [];
- if (!rows.length) {
- return pageSettingsLocalIndexEmpty(kind === 'backlinks' ? '暂无反链' : '暂无标签');
- }
- if (kind === 'backlinks') {
- return rows.slice(0, 12).map(function(item) {
- var title = searchText(item && item.title) || searchText(item && item.path) || '未命名页面';
- var path = searchText(item && item.path);
- var snippet = searchText(item && item.snippet);
- return '' +
- '' +
- '
' + escapeHtml(title) + '
' +
- (path ? '
' + escapeHtml(path) + '
' : '') +
- (snippet ? '
' + escapeHtml(snippet) + '
' : '') +
- '
';
- }).join('');
- }
- return rows.slice(0, 16).map(function(item) {
- var tag = searchText(item && item.tag) || 'untagged';
- var count = Number(item && item.count || 0);
- return '' +
- '' +
- '
#' + escapeHtml(tag) + '
' +
- '
' + count + ' 个页面
' +
- '
';
- }).join('');
- }
-
- function renderPageSettingsLocalIndex(popover) {
- popover = popover || ensurePageSettingsPopover();
- var statusNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-status"]');
- var backlinksNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-backlinks"]');
- var tagsNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-tags"]');
- if (!(statusNode instanceof HTMLElement) || !(backlinksNode instanceof HTMLElement) || !(tagsNode instanceof HTMLElement)) return;
-
- if (!pageSettingsLocalIndexIsAvailable()) {
- statusNode.textContent = '本地索引仅在本地工作区页面可用';
- backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('未连接本地 root');
- tagsNode.innerHTML = pageSettingsLocalIndexEmpty('未连接本地 root');
- return;
- }
-
- var summary = pageUiState.localIndexSummary || {};
- if (summary.loading) {
- statusNode.textContent = '正在读取本地索引...';
- backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('加载中');
- tagsNode.innerHTML = pageSettingsLocalIndexEmpty('加载中');
- return;
- }
- if (summary.error) {
- statusNode.textContent = '本地索引读取失败';
- backlinksNode.innerHTML = pageSettingsLocalIndexEmpty(summary.error);
- tagsNode.innerHTML = pageSettingsLocalIndexEmpty(summary.error);
- return;
- }
- if (summary.scopeKey !== pageSettingsLocalIndexScopeKey()) {
- statusNode.textContent = '切换到索引页签后读取本地索引';
- backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('等待读取');
- tagsNode.innerHTML = pageSettingsLocalIndexEmpty('等待读取');
- return;
- }
-
- statusNode.textContent = '来自当前授权 root 的 .mnote/index/search-index.json';
- backlinksNode.innerHTML = renderPageSettingsLocalIndexList(summary.backlinks, 'backlinks');
- tagsNode.innerHTML = renderPageSettingsLocalIndexList(summary.tags, 'tags');
- }
-
- async function loadPageSettingsLocalIndex(force) {
- if (!pageSettingsLocalIndexIsAvailable()) {
- renderPageSettingsLocalIndex();
- return;
- }
- var scopeKey = pageSettingsLocalIndexScopeKey();
- var current = pageUiState.localIndexSummary || {};
- if (!force && current.scopeKey === scopeKey && !current.error && !current.loading) {
- renderPageSettingsLocalIndex();
- return;
- }
- pageUiState.localIndexSummary = {
- scopeKey: scopeKey,
- loading: true,
- error: '',
- backlinks: null,
- tags: null
- };
- renderPageSettingsLocalIndex();
- try {
- var baseParams = new URLSearchParams();
- baseParams.set('workspaceId', resolveWorkspaceId(document.body));
- baseParams.set('rootUri', currentRootUri());
- var backlinksParams = new URLSearchParams(baseParams);
- backlinksParams.set('documentId', currentDocumentId());
- var backlinksUrl = '/api/search/local-index/backlinks?' + backlinksParams.toString();
- var tagsUrl = '/api/search/local-index/tags?' + baseParams.toString();
- var responses = await Promise.all([
- fetch(backlinksUrl, { headers: { accept: 'application/json' } }),
- fetch(tagsUrl, { headers: { accept: 'application/json' } })
- ]);
- var backlinksPayload = await responses[0].json().catch(function(){ return null; });
- var tagsPayload = await responses[1].json().catch(function(){ return null; });
- if (!responses[0].ok || !backlinksPayload || backlinksPayload.ok !== true) {
- throw new Error('backlinks_' + responses[0].status);
- }
- if (!responses[1].ok || !tagsPayload || tagsPayload.ok !== true) {
- throw new Error('tags_' + responses[1].status);
- }
- pageUiState.localIndexSummary = {
- scopeKey: scopeKey,
- loading: false,
- error: '',
- backlinks: backlinksPayload.result && Array.isArray(backlinksPayload.result.backlinks) ? backlinksPayload.result.backlinks : [],
- tags: tagsPayload.result && Array.isArray(tagsPayload.result.tags) ? tagsPayload.result.tags : []
- };
- } catch (error) {
- pageUiState.localIndexSummary = {
- scopeKey: scopeKey,
- loading: false,
- error: error instanceof Error ? error.message : String(error),
- backlinks: [],
- tags: []
- };
- }
- renderPageSettingsLocalIndex();
- }
-
- function renderPageSettingsPopover() {
- var popover = ensurePageSettingsPopover();
- var options = currentPageOptions();
- popover.querySelectorAll('[data-page-option-checkbox]').forEach(function(input) {
- var key = input.getAttribute('data-page-option-checkbox');
- input.checked = Boolean(options[key]);
- input.disabled = !pageOptionIsSupported(key);
- });
- popover.querySelectorAll('[data-page-option-select]').forEach(function(select) {
- var key = select.getAttribute('data-page-option-select');
- var value = key === 'layoutDensity' ? String(options.layoutDensity || 'normal') : String(options.pageFont || 'default');
- select.value = value;
- });
- renderGlobalOptions(popover);
- var statsNode = popover.querySelector('[data-testid="wolai-page-settings-stats"]');
- if (statsNode instanceof HTMLElement) {
- var stats = computeLivePageStats();
- statsNode.innerHTML = '' +
- '字数 ' + Number(stats.wordCount || 0) + '' +
- '字符 ' + Number(stats.characterCount || 0) + '' +
- '块数 ' + Number(stats.blockCount || 0) + '' +
- '待办 ' + Number(stats.todoDone || 0) + '/' + Number(stats.todoTotal || 0) + '';
- }
- renderPageSettingsLocalIndex(popover);
- }
-
- function setActivePageSettingsTab(tabName) {
- var popover = ensurePageSettingsPopover();
- popover.querySelectorAll('[data-page-settings-tab]').forEach(function(tab) {
- var active = tab.getAttribute('data-page-settings-tab') === tabName;
- tab.classList.toggle('is-active', active);
- tab.setAttribute('aria-selected', active ? 'true' : 'false');
- });
- popover.querySelectorAll('[data-page-settings-panel]').forEach(function(panel) {
- panel.hidden = panel.getAttribute('data-page-settings-panel') !== tabName;
- });
- if (tabName === 'index') void loadPageSettingsLocalIndex(false);
- }
-
- async function persistPageOptionsPatch(patch) {
- var previous = Object.assign({}, currentPageOptions());
- pageUiState.pageOptions = Object.assign({}, previous, patch);
- var nextOptions = Object.assign({}, pageUiState.pageOptions);
- applyPageOptionsToShell();
- renderPageSettingsPopover();
- try {
- var response = await fetch('/api/documents/options', {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify({
- documentId: currentDocumentId(),
- workspaceId: resolveWorkspaceId(document.body),
- ...currentWorkspaceSourcePayload(),
- options: nextOptions,
- commandName: 'page.layout.updateOptions'
- })
- });
- var payload = await response.json().catch(function(){ return null; });
- if (!response.ok || !payload || payload.ok !== true) {
- throw new Error(payload && payload.error && payload.error.message ? payload.error.message : 'page_settings_save_failed_' + response.status);
- }
- document.documentElement.setAttribute('data-mnote-page-options-saved', 'true');
- } catch (error) {
- pageUiState.pageOptions = previous;
- applyPageOptionsToShell();
- renderPageSettingsPopover();
- document.documentElement.setAttribute('data-mnote-page-options-error', error instanceof Error ? error.message : String(error));
- }
- }
-
- function isPageSettingsOpen() {
- var popover = document.querySelector('[data-testid="wolai-page-settings-popover"]');
- return popover instanceof HTMLElement && !popover.hidden;
- }
-
- function openPageSettingsPopover() {
- if (!currentDocumentId()) return;
- var popover = ensurePageSettingsPopover();
- renderPageSettingsPopover();
- setActivePageSettingsTab('page');
- popover.hidden = false;
- pageUiState.pageSettingsOpen = true;
- updatePageSettingsTriggerState();
- }
-
- function closePageSettingsPopover() {
- var popover = document.querySelector('[data-testid="wolai-page-settings-popover"]');
- if (popover instanceof HTMLElement) popover.hidden = true;
- pageUiState.pageSettingsOpen = false;
- updatePageSettingsTriggerState();
- }
-
- function togglePageSettingsPopover() {
- if (isPageSettingsOpen()) closePageSettingsPopover();
- else openPageSettingsPopover();
- }
-
const sidebarAttachmentOpen = createSidebarAttachmentOpenRuntime({
attachmentClassForFileName: (...args) => attachmentClassForFileName(...args),
buildLocalFileOpenUrl,
diff --git a/rust/crates/mnote-web/src/routes/mod.rs b/rust/crates/mnote-web/src/routes/mod.rs
index 5c393d98..2b72371e 100644
--- a/rust/crates/mnote-web/src/routes/mod.rs
+++ b/rust/crates/mnote-web/src/routes/mod.rs
@@ -158,6 +158,10 @@ pub fn build_router(state: AppState) -> Router {
"/api/mnote-browser-runtime/sidebar-page-ai-runtime.js",
get(web_shell::sidebar_page_ai_runtime_asset),
)
+ .route(
+ "/api/mnote-browser-runtime/sidebar-page-settings-runtime.js",
+ get(web_shell::sidebar_page_settings_runtime_asset),
+ )
.route(
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
get(web_shell::sidebar_tree_runtime_asset),
@@ -655,6 +659,7 @@ mod tests {
"/api/mnote-browser-runtime/sidebar-filetree-upload-runtime.js",
"/api/mnote-browser-runtime/sidebar-attachment-open-runtime.js",
"/api/mnote-browser-runtime/sidebar-page-ai-runtime.js",
+ "/api/mnote-browser-runtime/sidebar-page-settings-runtime.js",
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
"/api/mnote-browser-runtime/tree-live-controller.js",
"/api/mnote-browser-runtime/tree-shell-runtime.js",
diff --git a/rust/crates/mnote-web/src/routes/web_shell.rs b/rust/crates/mnote-web/src/routes/web_shell.rs
index 71ec971d..016886ff 100644
--- a/rust/crates/mnote-web/src/routes/web_shell.rs
+++ b/rust/crates/mnote-web/src/routes/web_shell.rs
@@ -742,6 +742,20 @@ pub async fn sidebar_page_ai_runtime_asset() -> Response {
.unwrap_or_else(|_| Response::new(Body::empty()))
}
+pub async fn sidebar_page_settings_runtime_asset() -> Response {
+ const JS: &str = include_str!("../../browser/sidebar-page-settings-runtime.js");
+ Response::builder()
+ .status(StatusCode::OK)
+ .header(
+ header::CONTENT_TYPE,
+ "application/javascript; charset=utf-8",
+ )
+ .header(header::CACHE_CONTROL, "no-store")
+ .header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
+ .body(Body::from(JS))
+ .unwrap_or_else(|_| Response::new(Body::empty()))
+}
+
pub async fn sidebar_shell_runtime_asset() -> Response {
const JS: &str = include_str!("../../browser/sidebar-shell-runtime.js");
Response::builder()
diff --git a/rust/crates/mnote-web/src/ssr/pages/layout.rs b/rust/crates/mnote-web/src/ssr/pages/layout.rs
index 10524ae9..9f31d438 100644
--- a/rust/crates/mnote-web/src/ssr/pages/layout.rs
+++ b/rust/crates/mnote-web/src/ssr/pages/layout.rs
@@ -217,6 +217,8 @@ mod tests {
include_str!("../../../browser/sidebar-page-tree-runtime.js");
const SIDEBAR_PAGE_AI_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-page-ai-runtime.js");
+ const SIDEBAR_PAGE_SETTINGS_RUNTIME_JS: &str =
+ include_str!("../../../browser/sidebar-page-settings-runtime.js");
const FILETREE_CONTEXT_MENU_RUNTIME_JS: &str =
include_str!("../../../browser/filetree-context-menu-runtime.js");
const FILETREE_DND_RUNTIME_JS: &str = include_str!("../../../browser/filetree-dnd-runtime.js");
@@ -255,9 +257,8 @@ mod tests {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_SIDEBAR_TREE_MODE_KEY"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_LAST_CLOUD_WORKSPACE_KEY"));
- assert!(
- SIDEBAR_TREE_RUNTIME_JS.contains("data-global-option-checkbox=\"showHeadingNumbers\"")
- );
+ assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS
+ .contains("data-global-option-checkbox=\"showHeadingNumbers\""));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("restoreSidebarTreeTab"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("treeView"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree:local-command"));
diff --git a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs
index 5948425d..daefff81 100644
--- a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs
+++ b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs
@@ -19,6 +19,7 @@ pub(crate) mod overlays;
pub(crate) mod persistence;
pub(crate) mod runtime_bridge;
pub(crate) mod slash_actions;
+pub(crate) mod slash_menu_view;
pub(crate) mod style;
pub(crate) mod table_commands;
pub(crate) mod table_toolbar_view;
diff --git a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/slash_menu_view.rs b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/slash_menu_view.rs
new file mode 100644
index 00000000..f59ce24b
--- /dev/null
+++ b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/slash_menu_view.rs
@@ -0,0 +1,271 @@
+use crate::editor_runtime::{
+ block_hover_state::HoveredBlockState,
+ block_transform::run_slash_action,
+ bridge_events::{ChangeMetaPayload, ChangePayload},
+ command_sync::read_editor_snapshot,
+ overlays::toolbar_overlay_locked,
+ persistence::{persist_document_state, persisted_document_identity, PersistedDocumentIdentity},
+ runtime_bridge::dispatch_change_event_to_target,
+ slash_actions::{SlashActionKind, SLASH_ACTIONS},
+};
+use leptos::prelude::*;
+use leptos_tiptap::TiptapEditorHandle;
+use serde_json::Value;
+use std::sync::Arc;
+use web_sys::EventTarget;
+
+pub(crate) struct SlashMenuViewProps {
+ pub(crate) editor: TiptapEditorHandle,
+ pub(crate) signals: SlashMenuSignals,
+ pub(crate) runtime: SlashMenuRuntimeContext,
+ pub(crate) anchor_style: Arc String + Send + Sync>,
+}
+
+#[derive(Clone, Copy)]
+pub(crate) struct SlashMenuSignals {
+ pub(crate) slash_open: ReadSignal,
+ pub(crate) set_slash_open: WriteSignal,
+ pub(crate) slash_index: ReadSignal,
+ pub(crate) slash_query: ReadSignal,
+ pub(crate) dirty_count: ReadSignal,
+ pub(crate) set_dirty_count: WriteSignal,
+ pub(crate) document_json: WriteSignal,
+ pub(crate) html_output: WriteSignal,
+ pub(crate) json_output: WriteSignal,
+ pub(crate) document_id: ReadSignal