dsh-obsidian-memo 0.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +270 -0
  3. package/README.md +272 -0
  4. package/cordis.patch.yml +10 -0
  5. package/docs/ARCHITECTURE.md +88 -0
  6. package/docs/AUDIT-0.16.md +74 -0
  7. package/docs/CHANGELOG.md +120 -0
  8. package/docs/CONTRACT.md +544 -0
  9. package/docs/GUIDE.md +177 -0
  10. package/docs/KNOWN_ISSUES.md +113 -0
  11. package/icon.svg +7 -0
  12. package/lib/ai.js +161 -0
  13. package/lib/capture.js +175 -0
  14. package/lib/client.js +6994 -0
  15. package/lib/config-picker.js +29 -0
  16. package/lib/filename.js +33 -0
  17. package/lib/files.js +88 -0
  18. package/lib/host/ai-jobs.js +118 -0
  19. package/lib/host/configuration.js +151 -0
  20. package/lib/host/dispatch.js +32 -0
  21. package/lib/host/environment.js +24 -0
  22. package/lib/host/folders.js +77 -0
  23. package/lib/host/images.js +116 -0
  24. package/lib/host/mutations.js +123 -0
  25. package/lib/host/note-content.js +44 -0
  26. package/lib/host/notes.js +148 -0
  27. package/lib/host/rpc.js +82 -0
  28. package/lib/host/scanning.js +67 -0
  29. package/lib/host/session.js +67 -0
  30. package/lib/host/state.js +68 -0
  31. package/lib/host/vault.js +38 -0
  32. package/lib/host/writes.js +21 -0
  33. package/lib/index.js +88 -0
  34. package/lib/notes.js +454 -0
  35. package/lib/obsidian.js +93 -0
  36. package/lib/scan.js +269 -0
  37. package/lib/settings.js +228 -0
  38. package/lib/shared.js +309 -0
  39. package/lib/trash.js +205 -0
  40. package/locale/en.json +6 -0
  41. package/locale/zh.json +6 -0
  42. package/package.json +76 -0
  43. package/scripts/benchmark.mjs +58 -0
  44. package/scripts/build-client.mjs +39 -0
  45. package/src/client/ai/use-comments.js +75 -0
  46. package/src/client/ai/use-job-notifications.js +53 -0
  47. package/src/client/ai/use-project-dialog.js +58 -0
  48. package/src/client/capture/composer.js +266 -0
  49. package/src/client/capture/images.js +64 -0
  50. package/src/client/capture/keyboard.js +34 -0
  51. package/src/client/components/dialogs.js +67 -0
  52. package/src/client/components/feedback.js +50 -0
  53. package/src/client/components/icons.js +160 -0
  54. package/src/client/components/use-toasts.js +58 -0
  55. package/src/client/constants.js +127 -0
  56. package/src/client/data/state.js +116 -0
  57. package/src/client/data/use-notes-data.js +137 -0
  58. package/src/client/data/visibility.js +14 -0
  59. package/src/client/drag/drop-actions.js +47 -0
  60. package/src/client/drag/motion.js +84 -0
  61. package/src/client/drag/overlay.js +33 -0
  62. package/src/client/drag/use-card-drag.js +149 -0
  63. package/src/client/i18n/en.js +219 -0
  64. package/src/client/i18n/index.js +11 -0
  65. package/src/client/i18n/zh.js +219 -0
  66. package/src/client/images/cache.js +55 -0
  67. package/src/client/images/components.js +121 -0
  68. package/src/client/images/queue.js +106 -0
  69. package/src/client/index.js +40 -0
  70. package/src/client/panel/header.js +146 -0
  71. package/src/client/panel/use-shortcuts.js +79 -0
  72. package/src/client/panel.js +424 -0
  73. package/src/client/preferences.js +198 -0
  74. package/src/client/react.js +8 -0
  75. package/src/client/reader/blocks.js +51 -0
  76. package/src/client/reader/note-drawer.js +270 -0
  77. package/src/client/reader/use-dock-offset.js +23 -0
  78. package/src/client/reader/use-drawer-layout.js +96 -0
  79. package/src/client/reader/use-note-editor.js +230 -0
  80. package/src/client/reader/use-pinned-note.js +41 -0
  81. package/src/client/runtime.js +30 -0
  82. package/src/client/settings/ai-budget-row.js +65 -0
  83. package/src/client/settings/ai-budget.js +42 -0
  84. package/src/client/settings/callouts.js +21 -0
  85. package/src/client/settings/config-file.js +74 -0
  86. package/src/client/settings/image-processing.js +31 -0
  87. package/src/client/settings/official-page.js +32 -0
  88. package/src/client/settings/paths.js +131 -0
  89. package/src/client/settings/settings-drawer.js +244 -0
  90. package/src/client/settings/style-select.js +58 -0
  91. package/src/client/settings/trash.js +30 -0
  92. package/src/client/settings/use-settings-draft.js +207 -0
  93. package/src/client/styles/base.js +63 -0
  94. package/src/client/styles/capture.js +94 -0
  95. package/src/client/styles/index.js +26 -0
  96. package/src/client/styles/overrides.js +116 -0
  97. package/src/client/styles/reader.js +66 -0
  98. package/src/client/styles/settings.js +118 -0
  99. package/src/client/styles/tokens.js +29 -0
  100. package/src/client/styles/wall.js +83 -0
  101. package/src/client/testing.js +139 -0
  102. package/src/client/utils.js +66 -0
  103. package/src/client/wall/model.js +158 -0
  104. package/src/client/wall/note-card.js +201 -0
  105. package/src/client/wall/use-note-actions.js +107 -0
  106. package/src/client/wall/use-subfolders.js +42 -0
  107. package/src/client/wall/use-wall-content.js +135 -0
  108. package/src/client/wall/use-wall-filters.js +92 -0
  109. package/src/client/wall/use-wall-paging.js +55 -0
@@ -0,0 +1,146 @@
1
+ /** 顶栏的纯渲染函数:版本、计数、日期、子目录、排序和设置入口。 */
2
+ import { CalendarIcon, FolderTreeIcon, GearIcon, SortIcon } from '../components/icons.js';
3
+ import { DATE_FILTERS, SORT_OPTIONS } from '../constants.js';
4
+ import { h } from '../react.js';
5
+ import { formatVersionLabel } from '../utils.js';
6
+
7
+ function renderPanelHeader({
8
+ chooseDateFilter, chooseSort, dateEnabled, dateFilter, dateMenu, dateRangeLabel, dateRealtime,
9
+ dateWrapRef, dirLabel, filteredNotes, includeSubfolders, openDateMenu, openSettings, setDateMenu,
10
+ settingsOpen, sortId, sortMenu, sortWrapRef, subfoldersBusy, t, toggleDateEnabled, toggleDateRealtime,
11
+ toggleSortMenu, toggleSubfolders, view,
12
+ }) {
13
+ return h('header', { className: 'SN_head' },
14
+ h('div', { className: 'SN_headMain' },
15
+ h('div', { className: 'SN_titleRow' },
16
+ h('h1', { className: 'SN_title' }, t('panel.title')),
17
+ // 0.13:byline 只在顶栏大标题之后(h1 之后、计数之前);版本号自动取插件实际版本。
18
+ h('span', { className: 'SN_headByline' }, formatVersionLabel(view.version) ? 'v' + formatVersionLabel(view.version) : ''),
19
+ view.loaded ? h('span', { className: 'SN_count' }, t('panel.count', { count: filteredNotes.length })) : null,
20
+ ),
21
+ dirLabel ? h('div', { className: 'SN_headPath', title: dirLabel }, dirLabel) : null,
22
+ ),
23
+ h('div', { className: 'SN_headActions' },
24
+ // 0.14 日期筛选(顶栏第一个):点击切换,悬停或方向键打开档位菜单。
25
+ h('div', { className: 'SN_sortWrap SN_dateGroup', ref: dateWrapRef,
26
+ onMouseEnter: openDateMenu,
27
+ onMouseLeave: () => { if (!dateWrapRef.current?.querySelector('[role="menu"]')?.contains(document.activeElement)) setDateMenu(false); },
28
+ onBlur: (event) => { if (!event.currentTarget.contains(event.relatedTarget)) setDateMenu(false); },
29
+ },
30
+ h('button', {
31
+ type: 'button',
32
+ className: 'SN_iconBtn',
33
+ 'data-active': dateEnabled ? 'true' : 'false',
34
+ 'aria-pressed': dateEnabled ? 'true' : 'false',
35
+ 'aria-label': dateEnabled
36
+ ? t('date.toggleOn', { range: dateRangeLabel })
37
+ : t('date.toggleOff', { range: dateRangeLabel }),
38
+ title: dateEnabled
39
+ ? t('date.toggleOn', { range: dateRangeLabel })
40
+ : t('date.toggleOff', { range: dateRangeLabel }),
41
+ 'aria-haspopup': 'menu',
42
+ 'aria-expanded': dateMenu ? 'true' : 'false',
43
+ onKeyDown: (event) => {
44
+ if (event.key === 'ArrowDown') {
45
+ event.preventDefault(); openDateMenu();
46
+ requestAnimationFrame(() => dateWrapRef.current?.querySelector('[role="menuitemradio"]')?.focus());
47
+ }
48
+ },
49
+ onContextMenu: (event) => { event.preventDefault(); openDateMenu(); },
50
+ onClick: toggleDateEnabled,
51
+ }, h(CalendarIcon, null)),
52
+ dateMenu ? h('div', {
53
+ className: 'SN_sortMenu',
54
+ role: 'menu',
55
+ 'aria-label': t('date.label'),
56
+ },
57
+ DATE_FILTERS.map((option) => {
58
+ // 开关熄灭时菜单里不显示勾选(选任意档位会自动点亮)。
59
+ const current = dateEnabled && option.id === dateFilter;
60
+ return h('button', {
61
+ key: option.id,
62
+ type: 'button',
63
+ className: 'SN_sortItem',
64
+ role: 'menuitemradio',
65
+ 'aria-checked': current ? 'true' : 'false',
66
+ 'data-active': current ? 'true' : 'false',
67
+ onClick: () => chooseDateFilter(option.id),
68
+ },
69
+ h('span', { className: 'SN_sortCheck', 'aria-hidden': 'true' }, current ? '✓' : ''),
70
+ h('span', null, t(option.labelKey)),
71
+ );
72
+ }),
73
+ h('div', { className: 'SN_menuDivider', 'aria-hidden': 'true' }),
74
+ h('button', {
75
+ type: 'button',
76
+ className: 'SN_dateItem',
77
+ role: 'menuitemcheckbox',
78
+ 'aria-checked': dateRealtime ? 'true' : 'false',
79
+ onClick: toggleDateRealtime,
80
+ },
81
+ h('span', {
82
+ className: 'SN_checkBox',
83
+ 'data-checked': dateRealtime ? 'true' : 'false',
84
+ 'aria-hidden': 'true',
85
+ }, dateRealtime ? '✓' : ''),
86
+ h('span', { className: 'SN_dateItemMain' },
87
+ h('span', { className: 'SN_dateItemLabel' }, t('date.realtime')),
88
+ h('span', { className: 'SN_dateItemHint' }, t('date.realtimeHint')),
89
+ ),
90
+ ),
91
+ ) : null,
92
+ ),
93
+ // 0.13 子文件夹开关(§7.1):日期筛选右侧;aria-pressed 反映 settings.includeSubfolders。
94
+ h('button', {
95
+ type: 'button',
96
+ className: 'SN_iconBtn',
97
+ 'data-active': includeSubfolders ? 'true' : 'false',
98
+ 'aria-pressed': includeSubfolders ? 'true' : 'false',
99
+ 'aria-label': includeSubfolders ? t('panel.subfolders') : t('panel.topOnly'),
100
+ title: includeSubfolders ? t('panel.subfolders') : t('panel.topOnly'),
101
+ disabled: !view.loaded || subfoldersBusy,
102
+ onClick: toggleSubfolders,
103
+ }, h(FolderTreeIcon, null)),
104
+ h('div', { className: 'SN_sortWrap', ref: sortWrapRef },
105
+ h('button', {
106
+ type: 'button',
107
+ className: 'SN_iconBtn',
108
+ 'data-active': sortMenu ? 'true' : 'false',
109
+ 'aria-haspopup': 'menu',
110
+ 'aria-expanded': sortMenu ? 'true' : 'false',
111
+ 'aria-label': t('sort.label'),
112
+ title: t('sort.label'),
113
+ onClick: toggleSortMenu,
114
+ }, h(SortIcon, null)),
115
+ sortMenu ? h('div', {
116
+ className: 'SN_sortMenu',
117
+ role: 'menu',
118
+ 'aria-label': t('sort.label'),
119
+ },
120
+ SORT_OPTIONS.map((option) => h('button', {
121
+ key: option.id,
122
+ type: 'button',
123
+ className: 'SN_sortItem',
124
+ role: 'menuitemradio',
125
+ 'aria-checked': option.id === sortId ? 'true' : 'false',
126
+ 'data-active': option.id === sortId ? 'true' : 'false',
127
+ onClick: () => chooseSort(option.id),
128
+ },
129
+ h('span', { className: 'SN_sortCheck', 'aria-hidden': 'true' }, option.id === sortId ? '✓' : ''),
130
+ h('span', null, t(option.labelKey)),
131
+ )),
132
+ ) : null,
133
+ ),
134
+ h('button', {
135
+ type: 'button',
136
+ className: 'SN_iconBtn',
137
+ 'data-active': settingsOpen ? 'true' : 'false',
138
+ onClick: openSettings,
139
+ 'aria-label': t('panel.settings'),
140
+ title: t('panel.settings'),
141
+ }, h(GearIcon, null)),
142
+ ),
143
+ );
144
+ }
145
+
146
+ export { renderPanelHeader };
@@ -0,0 +1,79 @@
1
+ /** 管理浮层快捷键的监听生命周期与 Esc 优先级。 */
2
+ import { panelIsVisible } from '../data/visibility.js';
3
+ import { React } from '../react.js';
4
+
5
+ function usePanelShortcuts({
6
+ anyOverlay, cancelEditing, closeCommentConfirm, closeNoteDrawer, closeProjectDialog, closeSettings,
7
+ commentConfirm, currentDrawerNote, dateMenu, drawerArchive, editing, noteDrawer, panelRef, pinned,
8
+ pinnedUserClosedRef, projectBusy, projectTarget, runDelete, saveEditing, savingSettings, setDateMenu,
9
+ setDrawerArchive, setSortMenu, settingsOpen, sortMenu,
10
+ }) {
11
+ const keyHandlerRef = React.useRef(null);
12
+
13
+ React.useEffect(() => {
14
+ if (!anyOverlay) return undefined;
15
+ const listener = (event) => {
16
+ if (typeof keyHandlerRef.current === 'function') keyHandlerRef.current(event);
17
+ };
18
+ document.addEventListener('keydown', listener);
19
+ return () => document.removeEventListener('keydown', listener);
20
+ }, [anyOverlay]);
21
+
22
+ keyHandlerRef.current = function handleGlobalKeyDown(event) {
23
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Backspace' && !event.isComposing && event.keyCode !== 229 && !event.defaultPrevented) {
24
+ if (settingsOpen || projectTarget || commentConfirm || editing || event.target?.closest?.('input,textarea,[contenteditable]:not([contenteditable="false"])')) return;
25
+ const panel = panelRef.current;
26
+ if (!panel || !panelIsVisible(panel)) return;
27
+ const doc = panel.ownerDocument;
28
+ if (event.target !== doc?.body && !panel.contains(event.target)) return;
29
+ const note = currentDrawerNote();
30
+ if (note) { event.preventDefault(); event.stopPropagation(); runDelete(note); }
31
+ return;
32
+ }
33
+ if (event.key === 'Escape') {
34
+ event.stopPropagation();
35
+ // 0.13 同类评论确认框在最上层:Esc 只关它(§7.7)。
36
+ if (commentConfirm) {
37
+ closeCommentConfirm();
38
+ return;
39
+ }
40
+ if (drawerArchive) {
41
+ setDrawerArchive(false);
42
+ return;
43
+ }
44
+ if (dateMenu) {
45
+ setDateMenu(false);
46
+ return;
47
+ }
48
+ if (sortMenu) {
49
+ setSortMenu(false);
50
+ return;
51
+ }
52
+ if (projectTarget) {
53
+ if (!projectBusy) closeProjectDialog();
54
+ return;
55
+ }
56
+ if (settingsOpen) {
57
+ if (!savingSettings) closeSettings();
58
+ return;
59
+ }
60
+ if (noteDrawer) {
61
+ if (editing) cancelEditing();
62
+ else {
63
+ // 锁定态用 Esc 主动关闭:本次挂载内不要自动重开(重进面板仍会恢复/选第一篇)。
64
+ if (pinned) pinnedUserClosedRef.current = true;
65
+ closeNoteDrawer();
66
+ }
67
+ }
68
+ return;
69
+ }
70
+ if ((event.metaKey || event.ctrlKey) && (event.key === 's' || event.key === 'S') && editing) {
71
+ event.preventDefault();
72
+ event.stopPropagation();
73
+ saveEditing();
74
+ }
75
+ };
76
+
77
+ }
78
+
79
+ export { usePanelShortcuts };
@@ -0,0 +1,424 @@
1
+ /** 主面板组合入口:连接数据、编辑和交互 hooks,再组织视图与操作回调。 */
2
+ import { useComments } from './ai/use-comments.js';
3
+ import { useJobNotifications } from './ai/use-job-notifications.js';
4
+ import { useProjectDialog } from './ai/use-project-dialog.js';
5
+ import { CaptureComposer } from './capture/composer.js';
6
+ import { ConfirmDialog, ProjectDialog } from './components/dialogs.js';
7
+ import { ToastStack } from './components/feedback.js';
8
+ import { useToasts } from './components/use-toasts.js';
9
+ import { EMPTY_SETTINGS } from './constants.js';
10
+ import { useNotesData } from './data/use-notes-data.js';
11
+ import { CardDragOverlay } from './drag/overlay.js';
12
+ import { useCardDrag } from './drag/use-card-drag.js';
13
+ import { clearImageCache } from './images/cache.js';
14
+ import { clearImageQueue } from './images/queue.js';
15
+ import { renderPanelHeader } from './panel/header.js';
16
+ import { usePanelShortcuts } from './panel/use-shortcuts.js';
17
+ import { React, h } from './react.js';
18
+ import { NoteDrawer } from './reader/note-drawer.js';
19
+ import { useDockOffset } from './reader/use-dock-offset.js';
20
+ import { useDrawerLayout } from './reader/use-drawer-layout.js';
21
+ import { useNoteEditor } from './reader/use-note-editor.js';
22
+ import { usePinnedNote } from './reader/use-pinned-note.js';
23
+ import { runtime } from './runtime.js';
24
+ import { SettingsDrawer } from './settings/settings-drawer.js';
25
+ import { useSettingsDraft } from './settings/use-settings-draft.js';
26
+ import { isAbortError } from './utils.js';
27
+ import { dateFilterOption, filterNotesByDate, replaceNoteAtPath, sortNotes } from './wall/model.js';
28
+ import { useNoteActions } from './wall/use-note-actions.js';
29
+ import { useSubfolders } from './wall/use-subfolders.js';
30
+ import { useWallContent } from './wall/use-wall-content.js';
31
+ import { useWallFilters } from './wall/use-wall-filters.js';
32
+ import { useWallPaging } from './wall/use-wall-paging.js';
33
+
34
+ function StickyNotesPanel(props) {
35
+ const t = (props && props.t) || (runtime && runtime.t) || ((key) => key);
36
+ const tRef = React.useRef(t);
37
+
38
+ tRef.current = t;
39
+ const panelRef = React.useRef(null);
40
+ const {
41
+ resizingDrawer, setResizingDrawer, resizingRef, resizeCleanupRef, pinned, pinnedUserClosedRef,
42
+ effectiveDrawerWidth, startDrawerResize, resetDrawerWidth, togglePinned,
43
+ } = useDrawerLayout({ panelRef });
44
+ const { toasts, dismissToast, pushToast, pushErrorToast } = useToasts();
45
+ const {
46
+ sortId, sortMenu, setSortMenu, sortWrapRef, dateFilter, dateEnabled, dateRealtime, dateMenu,
47
+ setDateMenu, dateWrapRef, nowTick, setNowTick, toggleSortMenu, chooseSort, openDateMenu,
48
+ toggleDateEnabled, chooseDateFilter, toggleDateRealtime,
49
+ } = useWallFilters();
50
+ const refreshingRef = React.useRef(false);
51
+ const [drawerArchive, setDrawerArchive] = React.useState(false);
52
+ const drawerMenuRef = React.useRef(null);
53
+ const actionsRef = React.useRef({});
54
+ const [hoverPath, setHoverPath] = React.useState(null);
55
+ const [showBackTop, setShowBackTop] = React.useState(false);
56
+ const suppressCardOpenRef = React.useRef(false);
57
+ const { view, viewRef, loadError, aliveRef, controllersRef, updateView, request, refreshNow } = useNotesData({
58
+ panelRef,
59
+ onError: pushErrorToast,
60
+ onVisible() { setNowTick(Date.now()); },
61
+ onState(next, prev) {
62
+ jobNotifications.processJobs(next.jobs);
63
+ if (next.revision !== prev.revision && noteDrawerRef.current && !editingRef.current) {
64
+ loadNote(noteDrawerRef.current.path);
65
+ }
66
+ },
67
+ });
68
+ const {
69
+ noteDrawer, noteDrawerRef, editing, editingRef, bodyDraft, titleDraft, setTitleDraft, savingNote,
70
+ savingNoteRef, taskBusy, closeNoteDrawer, loadNote, openNoteDrawer, handleBodyChange, startEditing,
71
+ toggleTask, cancelEditing, saveEditing,
72
+ } = useNoteEditor({
73
+ tRef, aliveRef, controllersRef, request, refreshNow, replaceNote, replaceRenamedNote, pushToast, pushErrorToast,
74
+ onOpen() {
75
+ pinnedUserClosedRef.current = false;
76
+ setDrawerArchive(false);
77
+ setSortMenu(false);
78
+ setDateMenu(false);
79
+ },
80
+ onClose() {
81
+ if (resizeCleanupRef.current) resizeCleanupRef.current();
82
+ resizeCleanupRef.current = null;
83
+ resizingRef.current = false;
84
+ setResizingDrawer(false);
85
+ setDrawerArchive(false);
86
+ },
87
+ });
88
+ const {
89
+ settingsOpen, settingsDraft, picking, savingSettings, settingsTab, acceptConfigSettings, openSettings,
90
+ closeSettings, patchDraft, patchEmoji, removeEmoji, addEmoji, removeArchiveTarget, systemPick,
91
+ saveSettingsDraft,
92
+ } = useSettingsDraft({
93
+ t, tRef, viewRef, updateView, request, refreshNow, aliveRef, panelRef, pushToast, pushErrorToast,
94
+ onOpen() { setSortMenu(false); },
95
+ });
96
+ const jobNotifications = useJobNotifications({ tRef, pushToast, pushErrorToast, onOpenSettings: () => openSettings('ai') });
97
+ const { watchedJobsRef } = jobNotifications;
98
+ const { subfoldersBusy, toggleSubfolders } = useSubfolders({ aliveRef, pushErrorToast, pushToast, refreshNow, request, t, updateView, viewRef });
99
+ const currentSettings = view.settings && typeof view.settings === 'object' ? view.settings : EMPTY_SETTINGS;
100
+ const emojis = Array.isArray(currentSettings.emojis) ? currentSettings.emojis : [];
101
+ const archiveTargets = Array.isArray(currentSettings.archiveTargets) ? currentSettings.archiveTargets : [];
102
+ const { projectTarget, projectName, setProjectName, projectBusy, openProjectDialog, closeProjectDialog, confirmProject } = useProjectDialog({ aliveRef, pushErrorToast, pushToast, refreshNow, request, setDrawerArchive, setSortMenu, t, tRef, watchedJobsRef });
103
+ const { commentConfirm, localPending, requestComment, closeCommentConfirm, confirmComment } = useComments({ aliveRef, emojis, pushErrorToast, pushToast, refreshNow, request, setDrawerArchive, setHoverPath, tRef, viewRef, watchedJobsRef });
104
+ const { findTarget, runDelete, copyNote, runArchive, openInObsidian } = useNoteActions({
105
+ aliveRef, archiveTargets, closeNoteDrawer, localPending, noteDrawerRef, pushErrorToast, pushToast,
106
+ refreshNow, request, setDrawerArchive, setHoverPath, tRef, updateView, viewRef,
107
+ });
108
+ const {
109
+ dragSession, dragSessionRef, dragMotionRef, dragNote, dropAction, dropTargetId, endCardDrag,
110
+ startCardDrag, hoverDragAction, deleteDrop, archiveDrop, chooseArchiveTarget, projectDrop,
111
+ } = useCardDrag({
112
+ aliveRef, archiveTargets, findTarget, openProjectDialog, panelRef, pushToast, runArchive, runDelete,
113
+ setDateMenu, setDrawerArchive, setHoverPath, setSortMenu, tRef,
114
+ });
115
+ const filteredNotes = React.useMemo(
116
+ () => filterNotesByDate(view.notes, dateFilter, dateRealtime, nowTick, undefined, dateEnabled),
117
+ [view.notes, dateFilter, dateRealtime, nowTick, dateEnabled],
118
+ );
119
+ const sortedNotes = React.useMemo(() => sortNotes(filteredNotes, sortId), [filteredNotes, sortId]);
120
+
121
+ usePinnedNote({ closeNoteDrawer, editing, editingRef, noteDrawer, noteDrawerRef, openNoteDrawer, pinned, pinnedUserClosedRef, savingNoteRef, sortedNotes, view });
122
+ const { visibleLimit, loadingMore, loadMoreRef } = useWallPaging({ aliveRef, dateEnabled, dateFilter, dateRealtime, panelRef, pinned, sortId, sortedNotes, view });
123
+ const dateRangeLabel = t(dateFilterOption(dateFilter).labelKey);
124
+ const includeSubfolders = currentSettings.includeSubfolders !== false;
125
+ const { dockRight } = useDockOffset({ effectiveDrawerWidth, noteDrawer, panelRef, pinned });
126
+ const anyOverlay = Boolean(sortMenu || dateMenu || drawerArchive || commentConfirm || projectTarget || settingsOpen || noteDrawer);
127
+
128
+ function replaceNote(summary) {
129
+ if (!summary || typeof summary.path !== 'string') return;
130
+ const prev = viewRef.current;
131
+ let changed = false;
132
+ const notes = prev.notes.map((note) => {
133
+ if (note.path !== summary.path) return note;
134
+ changed = true;
135
+ return Object.assign({}, note, summary, { pending: note.pending || summary.pending || null });
136
+ });
137
+ if (changed) updateView(Object.assign({}, prev, { notes: notes }));
138
+ }
139
+
140
+ function replaceRenamedNote(oldPath, summary) {
141
+ const prev = viewRef.current;
142
+ const notes = replaceNoteAtPath(prev.notes, oldPath, summary);
143
+ if (notes !== null) updateView(Object.assign({}, prev, { notes: notes }));
144
+ }
145
+
146
+ function handleRefresh() {
147
+ if (refreshingRef.current) return;
148
+ refreshingRef.current = true;
149
+ Promise.resolve()
150
+ .then(() => request('rescan', {}))
151
+ .then(() => refreshNow(false))
152
+ .catch((error) => {
153
+ if (aliveRef.current && !isAbortError(error)) pushErrorToast(error);
154
+ })
155
+ .then(() => {
156
+ if (!aliveRef.current) return;
157
+ refreshingRef.current = false;
158
+ });
159
+ }
160
+
161
+ React.useEffect(() => {
162
+ return () => {
163
+ // 0.13:面板卸载时清空模块级缩略图缓存与图片队列(§7.2/§7.8)。
164
+ clearImageQueue();
165
+ clearImageCache();
166
+ };
167
+ }, []);
168
+
169
+ React.useEffect(() => {
170
+ if (!drawerArchive) return undefined;
171
+ const onPointerDown = (event) => {
172
+ const target = event.target;
173
+ if (drawerMenuRef.current && target && drawerMenuRef.current.contains(target)) return;
174
+ if (target && typeof target.closest === 'function' && target.closest('[data-sn-drawer-menu-trigger]')) return;
175
+ setDrawerArchive(false);
176
+ };
177
+ document.addEventListener('mousedown', onPointerDown, true);
178
+ return () => document.removeEventListener('mousedown', onPointerDown, true);
179
+ }, [drawerArchive]);
180
+
181
+ function handleWallMouseDown() {
182
+ // 归档菜单在墙内:不要在这里关闭(mousedown 关闭会让随后的菜单项 click 丢失),
183
+ // 外部点击已由 document capture 监听负责。
184
+ if (noteDrawer && editingRef.current) {
185
+ suppressCardOpenRef.current = true;
186
+ cancelEditing();
187
+ } else {
188
+ suppressCardOpenRef.current = false;
189
+ }
190
+ }
191
+
192
+ function handleWallClick(event) {
193
+ if (suppressCardOpenRef.current) {
194
+ suppressCardOpenRef.current = false;
195
+ return;
196
+ }
197
+ if (event.target !== event.currentTarget) return;
198
+ if (!noteDrawer || pinned) return;
199
+ closeNoteDrawer();
200
+ }
201
+
202
+ function handleCardOpen(note) {
203
+ if (!note) return;
204
+ if (suppressCardOpenRef.current) {
205
+ suppressCardOpenRef.current = false;
206
+ return;
207
+ }
208
+ const current = noteDrawerRef.current;
209
+ if (current && current.path === note.path) {
210
+ if (!pinned) closeNoteDrawer();
211
+ return;
212
+ }
213
+ openNoteDrawer(note);
214
+ }
215
+
216
+ function enterCard(note) {
217
+ // 抽屉打开时也记录 hover:menuVisible 已用 !drawerOpen 屏蔽菜单,
218
+ // 但关闭抽屉的那一刻 hoverPath 必须正好是鼠标/焦点所在的卡片(见 0.13.4 P1/P2 修复)。
219
+ if (!note || note.pending || dragSessionRef.current) return;
220
+ setHoverPath((current) => (current && current.path === note.path ? current : { path: note.path }));
221
+ }
222
+
223
+ function leaveCard(note, keepForFocus) {
224
+ // keepForFocus:鼠标移出但焦点仍在卡片内 → 不收起菜单(否则图片预览会闪一下)。
225
+ if (keepForFocus === true || dragSessionRef.current) return;
226
+ setHoverPath((current) => {
227
+ if (!current) return current;
228
+ if (note && current.path !== note.path) return current;
229
+ return null;
230
+ });
231
+ }
232
+
233
+ function currentDrawerNote() {
234
+ const current = noteDrawerRef.current;
235
+ if (!current) return null;
236
+ return viewRef.current.notes.find((item) => item.path === current.path)
237
+ || current.full
238
+ || current.summary
239
+ || null;
240
+ }
241
+
242
+ function toggleDrawerArchive() {
243
+ if (!noteDrawerRef.current) return;
244
+ if (archiveTargets.length === 0) {
245
+ pushToast({ text: t('archive.emptyHint'), kind: 'info', timeout: 6000 });
246
+ return;
247
+ }
248
+ setDrawerArchive((value) => !value);
249
+ setSortMenu(false);
250
+ }
251
+
252
+ usePanelShortcuts({
253
+ anyOverlay, cancelEditing, closeCommentConfirm, closeNoteDrawer, closeProjectDialog, closeSettings,
254
+ commentConfirm, currentDrawerNote, dateMenu, drawerArchive, editing, noteDrawer, panelRef, pinned,
255
+ pinnedUserClosedRef, projectBusy, projectTarget, runDelete, saveEditing, savingSettings, setDateMenu,
256
+ setDrawerArchive, setSortMenu, settingsOpen, sortMenu,
257
+ });
258
+
259
+ actionsRef.current = {
260
+ t: t,
261
+ onOpen: handleCardOpen,
262
+ onPreset: requestComment,
263
+ onCardEnter: enterCard,
264
+ onCardLeave: leaveCard,
265
+ onOpenObsidian: openInObsidian,
266
+ onCopy: copyNote,
267
+ onDelete: runDelete,
268
+ onDragStart: startCardDrag,
269
+ onDragEnd: () => dragSessionRef.current?.parked ? undefined : endCardDrag(),
270
+ };
271
+ const dirLabel = currentSettings.notesDir || currentSettings.vaultPath || '';
272
+ const errorBanner = view.loaded
273
+ ? (loadError || (view.notes.length > 0 ? view.error : null))
274
+ : null;
275
+ const { renderContent } = useWallContent({
276
+ actionsRef, dateRangeLabel, dragNote, emojis, filteredNotes, handleRefresh, handleWallClick,
277
+ handleWallMouseDown, hoverPath, loadError, loadMoreRef, loadingMore, localPending, noteDrawer,
278
+ openSettings, setDateMenu, setShowBackTop, sortedNotes, t, view, visibleLimit,
279
+ });
280
+ const drawerNote = noteDrawer
281
+ ? (view.notes.find((item) => item.path === noteDrawer.path) || noteDrawer.full || noteDrawer.summary || null)
282
+ : null;
283
+ const drawerPending = noteDrawer
284
+ ? (localPending[noteDrawer.path] || (drawerNote && drawerNote.pending) || null)
285
+ : null;
286
+ const noteDrawerProps = noteDrawer
287
+ ? {
288
+ t: t,
289
+ drawer: noteDrawer,
290
+ editing: editing,
291
+ bodyDraft: bodyDraft,
292
+ saving: savingNote,
293
+ drawerWidth: effectiveDrawerWidth,
294
+ resizing: resizingDrawer,
295
+ pinned: pinned,
296
+ drawerActions: {
297
+ emojis: emojis,
298
+ pending: drawerPending,
299
+ archiveOpen: drawerArchive,
300
+ targets: archiveTargets,
301
+ menuRef: drawerMenuRef,
302
+ onComment: (emoji) => requestComment(currentDrawerNote(), emoji),
303
+ onToggleArchive: toggleDrawerArchive,
304
+ onArchive: (targetId) => runArchive(currentDrawerNote(), targetId),
305
+ onProject: () => openProjectDialog(currentDrawerNote()),
306
+ onOpenObsidian: () => openInObsidian(currentDrawerNote()),
307
+ onCopy: () => copyNote(currentDrawerNote()),
308
+ onDelete: () => runDelete(currentDrawerNote()),
309
+ },
310
+ onTogglePinned: togglePinned,
311
+ onToggleTask: toggleTask,
312
+ taskBusy: taskBusy,
313
+ titleDraft: titleDraft,
314
+ onTitleChange: setTitleDraft,
315
+ onResizeStart: startDrawerResize,
316
+ onResizeReset: resetDrawerWidth,
317
+ onClose: closeNoteDrawer,
318
+ onRetry: () => loadNote(noteDrawer.path),
319
+ onStartEdit: startEditing,
320
+ onBodyChange: handleBodyChange,
321
+ onSave: saveEditing,
322
+ onCancelEdit: cancelEditing,
323
+ }
324
+ : null;
325
+ const confirmEmoji = commentConfirm
326
+ ? emojis.find((item) => item && item.id === commentConfirm.emojiId) || null
327
+ : null;
328
+ const confirmEmojiLabel = confirmEmoji ? (confirmEmoji.label || confirmEmoji.emoji || '') : '';
329
+
330
+ return h('div', {
331
+ className: 'SN_panel',
332
+ 'data-sticky-notes': '',
333
+ 'data-card-dragging': dragNote ? 'true' : 'false',
334
+ 'data-resizing': resizingDrawer ? 'true' : 'false',
335
+ ref: panelRef,
336
+ role: 'region',
337
+ 'aria-label': t('panel.title'),
338
+ },
339
+ renderPanelHeader({
340
+ chooseDateFilter, chooseSort, dateEnabled, dateFilter, dateMenu, dateRangeLabel, dateRealtime,
341
+ dateWrapRef, dirLabel, filteredNotes, includeSubfolders, openDateMenu, openSettings, setDateMenu,
342
+ settingsOpen, sortId, sortMenu, sortWrapRef, subfoldersBusy, t, toggleDateEnabled,
343
+ toggleDateRealtime, toggleSortMenu, toggleSubfolders, view,
344
+ }),
345
+ errorBanner ? h('div', { className: 'SN_banner' },
346
+ h('span', { className: 'SN_bannerText', title: errorBanner }, errorBanner),
347
+ h('button', { type: 'button', className: 'SN_textBtn', onClick: handleRefresh }, t('panel.retry')),
348
+ ) : null,
349
+ // 0.13 面板锁(§7.3):锁定 = SN_body flex row,抽屉作为普通 flex 子项与墙并排。
350
+ h('div', { className: 'SN_body', 'data-pinned': pinned ? 'true' : 'false' },
351
+ h('div', { className: 'SN_wallWrap', 'data-capture': view.configured ? 'true' : 'false', style: { '--sn-dock-right': dockRight + 'px' } },
352
+ renderContent(),
353
+ h(CaptureComposer, {
354
+ t, enabled: view.configured, imageCompression: currentSettings.imageCompression,
355
+ dragNote, archiveParked: Boolean(dragSession?.parked), dropAction, dropTargetId, archiveTargets,
356
+ onDragHover: hoverDragAction, onArchiveDrop: archiveDrop, onArchiveChoose: chooseArchiveTarget, onProjectDrop: projectDrop, onDeleteDrop: deleteDrop,
357
+ showBackTop,
358
+ onBackTop: () => panelRef.current?.querySelector('.SN_wall')?.scrollTo({ top: 0, behavior: 'smooth' }),
359
+ onCreate: (payload) => request('createNote', payload),
360
+ onCreated: (result) => {
361
+ if (result && result.note) {
362
+ const prev = viewRef.current;
363
+ if (prev.notes.some((note) => note.path === result.note.path)) replaceNote(result.note);
364
+ else updateView(Object.assign({}, prev, { notes: [result.note].concat(prev.notes) }));
365
+ }
366
+ pushToast({ text: t('capture.created'), kind: 'success' });
367
+ refreshNow(true);
368
+ },
369
+ onError: pushErrorToast,
370
+ }),
371
+ ),
372
+ pinned && noteDrawerProps
373
+ ? h(NoteDrawer, Object.assign({ key: 'inline', inline: true }, noteDrawerProps))
374
+ : null,
375
+ ),
376
+ // 未锁定 = 原有 overlay 态(绝对定位盖在便签墙之上)。
377
+ !pinned && noteDrawerProps
378
+ ? h(NoteDrawer, Object.assign({ key: 'overlay', inline: false }, noteDrawerProps))
379
+ : null,
380
+ h(ToastStack, { toasts: toasts, onDismiss: dismissToast }),
381
+ dragNote ? h(CardDragOverlay, { note: dragNote, point: dragSession.point, motionRef: dragMotionRef, action: dropAction, targetId: dropTargetId, targets: archiveTargets, t }) : null,
382
+ settingsOpen && settingsDraft
383
+ ? h(SettingsDrawer, {
384
+ t: t,
385
+ draft: settingsDraft, request, initialTab: settingsTab, onConfigSettings: acceptConfigSettings,
386
+ picking: picking,
387
+ saving: savingSettings,
388
+ onPatch: patchDraft,
389
+ onPick: systemPick,
390
+ onRemoveArchive: removeArchiveTarget,
391
+ onPatchEmoji: patchEmoji,
392
+ onRemoveEmoji: removeEmoji,
393
+ onAddEmoji: addEmoji,
394
+ onSave: saveSettingsDraft,
395
+ onCancel: closeSettings,
396
+ })
397
+ : null,
398
+ projectTarget
399
+ ? h(ProjectDialog, {
400
+ t: t,
401
+ note: projectTarget,
402
+ name: projectName,
403
+ busy: projectBusy,
404
+ onName: setProjectName,
405
+ onConfirm: confirmProject,
406
+ onCancel: closeProjectDialog,
407
+ })
408
+ : null,
409
+ // 0.13 同类评论确认(§7.7):确认后才发 comment。
410
+ commentConfirm
411
+ ? h(ConfirmDialog, {
412
+ t: t,
413
+ title: t('comment.confirmTitle'),
414
+ text: t('comment.confirmText', { label: confirmEmojiLabel }),
415
+ confirmLabel: t('comment.confirmAgain'),
416
+ cancelLabel: t('comment.cancel'),
417
+ onConfirm: confirmComment,
418
+ onCancel: closeCommentConfirm,
419
+ })
420
+ : null,
421
+ );
422
+ }
423
+
424
+ export { StickyNotesPanel };