From afb1ae6e17333079eedce89369a6795d7d6f69c9 Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Fri, 12 Jun 2026 09:48:10 -0500 Subject: [PATCH 01/66] docs: update in-app help for v2.4.0 Document CSV databases (how-to + core concept + database-grid shortcuts), the comments keyboard flow (Mod+Alt+M, Mod+Shift+C, panel j/k/e/r/d), the always-on Alt+H/J/K/L pane-focus motions, a new Palettes and pickers shortcut section, and the expanded theme family list in Appearance. --- packages/app-core/src/lib/help.ts | 45 +++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/packages/app-core/src/lib/help.ts b/packages/app-core/src/lib/help.ts index 0e7e38d7..9592ce1b 100644 --- a/packages/app-core/src/lib/help.ts +++ b/packages/app-core/src/lib/help.ts @@ -106,6 +106,11 @@ export const HELP_HOW_TO_GUIDES: HelpCard[] = [ body: 'Open Settings → Templates. Press “New template” to author one: a template is just markdown with optional YAML frontmatter (`name`, `description`, `category`, `titleTemplate`, `targetFolder`, `targetSubpath`) and a body. Use the variables `{{title}}`, `{{date}}`, `{{date:YYYY-MM-DD}}` (any moment-style format), `{{time}}`, `{{week}}`, and `{{cursor}}` (where the caret lands). Custom templates are saved as plain `.md` files under `.zennotes/templates/`. You can also fork a built-in by pressing Edit on it — that creates an editable copy that shadows the original, and Reset restores the built-in. From any note, the “Save Current Note as Template…” command captures it as a new template.' }, + { + title: 'Turn a CSV into a database', + body: + 'Run “New Database” from the command palette (or right-click a folder in the sidebar → New database) to create one, or just open an existing `.csv` file from the vault. ZenNotes stores the data as `.csv` plus a small `.csv.base.json` sidecar that holds field types, select options, and your saved views. Edit cells inline in the Table view, group records in a Board by any select field, switch the raw-CSV toggle to see the underlying file, and press `o` on a row to open it as a full Markdown page whose frontmatter mirrors the row’s properties. The whole grid is keyboard-driven — see the Database grid shortcuts.' + }, { title: 'Move a note without dragging', body: @@ -177,7 +182,7 @@ export const HELP_CORE_CONCEPTS: HelpCard[] = [ { title: 'Comments attach to selected text', body: - 'Select text in the editor and use the text menu to add a comment. ZenNotes stores note comments beside the note in vault metadata, then highlights the anchored text and line when the comment is active.' + 'Select text in the editor and press `Mod+Alt+M` — or open the text menu with `m` — to start a comment, and toggle the Comments panel itself with `Mod+Shift+C`. ZenNotes stores note comments beside the note in vault metadata, then highlights the anchored text and line when the comment is active. In the panel, move with `j` / `k` and use `e` to edit, `r` to resolve, and `d` to delete.' }, { title: 'Sessions restore on relaunch', @@ -239,6 +244,11 @@ export const HELP_CORE_CONCEPTS: HelpCard[] = [ body: 'Drop files into a note to insert local files. By default, ZenNotes keeps them as ordinary files in the vault root, can reveal them from the app, and opens images, SVGs, PDFs, audio, video, and generic files inside ZenNotes tabs or reference panes where possible.' }, + { + title: 'Any CSV is a database', + body: + 'A `.csv` file in your vault is a full Notion-style database, with zero new dependencies. The same data shows up as an editable Table (inline cell editing) and as a Board grouped by a select field; add and switch views freely. Fields are typed — text, number, checkbox, date, select, multi-select — and support sort, filter, and a raw-CSV toggle, while every row keeps a stable id so external edits round-trip cleanly. Open any row as a real Markdown note — a “record page” in a per-database folder — whose frontmatter mirrors the row’s properties and whose body is a freeform page. Create one with “New Database” in the command palette or by right-clicking a folder → New database.' + }, { title: 'The CLI is the bridge to launchers', body: @@ -280,6 +290,9 @@ export const HELP_SHORTCUT_SECTIONS: HelpShortcutSection[] = [ { keys: 'Mod+,', action: 'Open Settings', detail: 'Open settings for appearance, editor behavior, fonts, vault controls, and app details.' }, { keys: 'Mod+1', action: 'Toggle sidebar', detail: 'Hide or show the left sidebar.' }, { keys: 'Mod+2', action: 'Toggle connections', detail: 'Toggle the connections panel for the active editor pane.' }, + { keys: 'Mod+Shift+C', action: 'Toggle comments panel', detail: 'Show or hide the Comments panel for the active pane.' }, + { keys: 'Mod+Alt+M', action: 'Add comment', detail: 'Start a comment on the selected text (or the current line) without reaching for the mouse.' }, + { keys: 'Alt+H / Alt+J / Alt+K / Alt+L', action: 'Focus pane left / down / up / right', detail: 'Always-on pane-focus motions — they work even with Vim mode off and skip the Ctrl+W prefix some Linux setups intercept. (Ctrl+W h/j/k/l still works in Vim mode.)' }, { keys: 'Mod+.', action: 'Toggle Zen mode', detail: 'Hide or restore the app chrome so only the active editor, preview, or split view stays on screen.' }, { keys: 'Mod+W', action: 'Close active tab', detail: 'Close the current note or virtual tab.' }, { keys: 'Shift+Mod+E', action: 'Export note as PDF', detail: 'Export the active note as a PDF file.' }, @@ -326,6 +339,19 @@ export const HELP_SHORTCUT_SECTIONS: HelpShortcutSection[] = [ { keys: 'f', action: 'Hint mode', detail: 'Show jump labels for clickable targets when you are not in insert mode.' } ] }, + { + id: 'palettes-and-pickers', + title: 'Palettes and pickers', + description: + 'These apply once a palette, search overlay, or picker already has focus — the command palette, note search, vault text search, outline, buffer switcher, the [[ reference picker, the / slash menu, and the date and template pickers.', + items: [ + { keys: 'ArrowDown / Ctrl+N / Ctrl+J', action: 'Next result', detail: 'Move the selection down. Ctrl+J / Ctrl+K behave the same in every picker, so they no longer collide with the global Search-notes shortcut on Windows and Linux.' }, + { keys: 'ArrowUp / Ctrl+P / Ctrl+K', action: 'Previous result', detail: 'Move the selection up.' }, + { keys: 'Enter', action: 'Run or open', detail: 'Open the selected note, heading, buffer, command, or search hit.' }, + { keys: 'Type to filter', action: 'Narrow the list', detail: 'Each picker filters its own data live as you type.' }, + { keys: 'Esc', action: 'Close the picker', detail: 'Dismiss the overlay and return focus to the previous surface.' } + ] + }, { id: 'lists-and-sidebar', title: 'Sidebar and list navigation', @@ -409,6 +435,21 @@ export const HELP_SHORTCUT_SECTIONS: HelpShortcutSection[] = [ { keys: ':', action: 'Open local ex prompt', detail: 'Run the view-specific command line inside Tasks or Tags.' }, { keys: 'Esc', action: 'Close or clear', detail: 'Clear the filter first, then close the active virtual view on a second press.' } ] + }, + { + id: 'database-grid', + title: 'Database grid (Table view)', + description: 'Vim-style motions when a CSV database table has focus. The grid yields to these keys so they do not collide with global motions.', + items: [ + { keys: 'h / j / k / l', action: 'Move the cell cursor', detail: 'Arrow keys also work. 0 / ^ jump to the first column, $ to the last.' }, + { keys: 'g g / G', action: 'Jump to first / last row', detail: 'Fast travel within the current column.' }, + { keys: 'i / Enter', action: 'Edit the cell', detail: 'On a checkbox cell this toggles it instead of opening an editor.' }, + { keys: 'Space / x', action: 'Select the row', detail: 'Toggle the row’s selection for bulk actions.' }, + { keys: 'o', action: 'Open the record page', detail: 'Open the row as a Markdown note in the per-database folder.' }, + { keys: 'a', action: 'Add a row', detail: 'Append a new empty record and move the cursor to it.' }, + { keys: 'd d', action: 'Delete the row', detail: 'Remove the record at the cursor.' }, + { keys: 'Esc', action: 'Clear selection / leave the grid', detail: 'Clears a multi-row selection first, then blurs the grid.' } + ] } ] @@ -624,7 +665,7 @@ export const HELP_SETTINGS: HelpSettingsSection[] = [ { title: 'Appearance', items: [ - { label: 'Theme, mode, and variant', detail: 'Pick a theme family, light or dark mode, and the active flavor or contrast where the theme supports it.' }, + { label: 'Theme, mode, and variant', detail: 'Pick a theme family — Apple, Gruvbox, Catppuccin, GitHub, Solarized, One, Nord, Tokyo Night, or the monochrome, true-black (OLED-friendly) Black Metal — plus light or dark mode and the active flavor or contrast where the theme supports it.' }, { label: 'Dark sidebar', detail: 'Tint the sidebar slightly darker than the canvas so the chrome reads as a distinct surface.' }, { label: 'Sidebar arrows', detail: 'Show or hide disclosure arrows for collapsible sidebar folders and sections.' } ] From e4e1694ccc49c320ffb9a72b2d53d907d4e9b1ce Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Fri, 12 Jun 2026 10:01:53 -0500 Subject: [PATCH 02/66] Fix tab scroll position restore (#127) --- docs/releases/v2.4.0/RELEASE_NOTES.md | 19 ++++ .../app-core/src/components/EditorPane.tsx | 96 +++++++++++++------ .../src/lib/tab-scroll-memory.test.ts | 17 ++++ .../app-core/src/lib/tab-scroll-memory.ts | 18 +++- 4 files changed, 118 insertions(+), 32 deletions(-) create mode 100644 docs/releases/v2.4.0/RELEASE_NOTES.md diff --git a/docs/releases/v2.4.0/RELEASE_NOTES.md b/docs/releases/v2.4.0/RELEASE_NOTES.md new file mode 100644 index 00000000..cb2cdd0d --- /dev/null +++ b/docs/releases/v2.4.0/RELEASE_NOTES.md @@ -0,0 +1,19 @@ +ZenNotes 2.4.0 tightens the keyboard-first writing flow with refreshed in-app help, cleaner onboarding controls, and a fix for note tabs losing their place when you switch between them. + +> Installers for macOS, Windows, and Linux are attached below once the build finishes. On Arch, `yay -S zennotes-bin`. + +## ✨ Highlights + +### Refreshed in-app help +The built-in Help tab now reflects the current app more completely: CSV databases, database-grid shortcuts, comments from the keyboard, always-on pane focus with **Alt+H/J/K/L**, palette navigation, and the expanded theme family list are documented in one place. + +### Cleaner onboarding mode controls (#123) +The onboarding mode selector now uses a more consistent segmented-control layout, with clearer accessibility state for the active mode. + +## 🐛 Fixes + +- **Tabs keep their editor position (#127)** — switching between open note tabs now preserves each note's editor scroll position and cursor/selection. The editor no longer jumps back to line 1 when you scroll in one note, move somewhere else in another note, and return. + +--- + +Thanks to everyone filing issues and reporting bugs — closes #127. diff --git a/packages/app-core/src/components/EditorPane.tsx b/packages/app-core/src/components/EditorPane.tsx index a7f8315c..75a62e4b 100644 --- a/packages/app-core/src/components/EditorPane.tsx +++ b/packages/app-core/src/components/EditorPane.tsx @@ -110,7 +110,11 @@ import { type EditorHydrationState } from '../lib/editor-hydration' import { recordRendererPerf } from '../lib/perf' -import { rememberTabScroll, recallTabScroll } from '../lib/tab-scroll-memory' +import { + rememberTabScroll, + recallTabScroll, + type TabScrollPosition +} from '../lib/tab-scroll-memory' import { parseOutline } from '../lib/outline' import { findRenderedHeadingForOutlineLine, @@ -2789,11 +2793,12 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { ]) // Remember this tab's scroll position as the user scrolls, so switching - // away and back (e.g. opening a diagram in a tab) restores it instead of - // snapping to the top. `content` is only set for real notes — virtual - // tabs (tasks, diagrams, …) never reach here. Capture only; the restore - // happens in the activation layout effect below. - useEffect(() => { + // away and back (e.g. opening another note or a diagram in a tab) restores + // it instead of snapping to the top. This intentionally uses a layout + // effect: its cleanup runs before the next tab's doc-sync layout effect + // resets the shared CodeMirror scroller to 0, so the outgoing tab cannot be + // overwritten by that programmatic reset. + useLayoutEffect(() => { const path = content?.path if (!path) return const editorEl = editorReady ? viewRef.current?.scrollDOM ?? null : null @@ -2801,36 +2806,46 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { if (!editorEl && !previewEl) return let frame = 0 - const capture = (): void => { + const captureNow = (): void => { + frame = 0 + const prev = recallTabScroll(path) + const view = viewRef.current + const selection = + view && viewPathRef.current === path ? view.state.selection.main : null + const next: TabScrollPosition = { + // Keep the other surface's value when one isn't mounted (e.g. + // preview-only mode has no editor scroller), so we don't clobber it. + editor: editorEl?.scrollTop ?? prev?.editor ?? 0, + preview: previewEl?.scrollTop ?? prev?.preview ?? 0 + } + if (selection) { + next.editorSelectionAnchor = selection.anchor + next.editorSelectionHead = selection.head + } + rememberTabScroll(path, next) + } + const scheduleCapture = (): void => { if (frame) return - frame = requestAnimationFrame(() => { - frame = 0 - const prev = recallTabScroll(path) - rememberTabScroll(path, { - // Keep the other surface's value when one isn't mounted (e.g. - // preview-only mode has no editor scroller), so we don't clobber it. - editor: editorEl?.scrollTop ?? prev?.editor ?? 0, - preview: previewEl?.scrollTop ?? prev?.preview ?? 0 - }) - }) + frame = requestAnimationFrame(captureNow) } - editorEl?.addEventListener('scroll', capture, { passive: true }) - previewEl?.addEventListener('scroll', capture, { passive: true }) + editorEl?.addEventListener('scroll', scheduleCapture, { passive: true }) + previewEl?.addEventListener('scroll', scheduleCapture, { passive: true }) return () => { if (frame) cancelAnimationFrame(frame) - editorEl?.removeEventListener('scroll', capture) - previewEl?.removeEventListener('scroll', capture) + captureNow() + editorEl?.removeEventListener('scroll', scheduleCapture) + previewEl?.removeEventListener('scroll', scheduleCapture) } }, [content?.path, mode, editorReady]) - // Restore a note tab's remembered scroll on (re)activation. Keyed on the + // Restore a note tab's remembered editor state on (re)activation. Keyed on the // active path, which flips note → (diagram tab) → note even though the // editor view's own `pathChanged` does not — that's why opening a diagram - // in a tab and returning otherwise snapped the note to the top. Runs as a - // layout effect (after the doc-sync effect dispatches the body) so the - // editor is restored before paint; the preview is restored best-effort now - // and again from `onRendered` once async diagrams settle. Explicit jumps - // own the scroll, so defer to a matching `pendingJumpLocation`. + // in a tab and returning otherwise snapped the note to the top. Runs after + // the doc-sync effect dispatches the body so the editor selection and scroll + // are restored before paint; the preview is restored best-effort now and + // again from `onRendered` once async diagrams settle. Explicit jumps own the + // scroll, so defer to a matching `pendingJumpLocation`. useLayoutEffect(() => { const path = content?.path ?? null // Only act on a genuine activation (path change). The `pendingJumpLocation` @@ -2846,10 +2861,30 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { const applyEditor = (): void => { const view = viewRef.current - if (view) view.scrollDOM.scrollTop = remembered.editor + if (!view) return + if ( + remembered.editorSelectionAnchor != null && + remembered.editorSelectionHead != null + ) { + const docLength = view.state.doc.length + const anchor = Math.max( + 0, + Math.min(docLength, remembered.editorSelectionAnchor) + ) + const head = Math.max( + 0, + Math.min(docLength, remembered.editorSelectionHead) + ) + const current = view.state.selection.main + if (current.anchor !== anchor || current.head !== head) { + view.dispatch({ selection: { anchor, head } }) + } + } + view.scrollDOM.scrollTop = remembered.editor } applyEditor() const raf = requestAnimationFrame(applyEditor) + const postFocusTimeout = window.setTimeout(applyEditor, 0) if (remembered.preview > 0) { previewRestoreTargetRef.current = { path, top: remembered.preview } @@ -2859,7 +2894,10 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { lastProgrammaticPreviewTopRef.current = previewEl.scrollTop } } - return () => cancelAnimationFrame(raf) + return () => { + cancelAnimationFrame(raf) + window.clearTimeout(postFocusTimeout) + } }, [content?.path, pendingJumpLocation?.path]) const paneFrameClass = [ diff --git a/packages/app-core/src/lib/tab-scroll-memory.test.ts b/packages/app-core/src/lib/tab-scroll-memory.test.ts index b6fca566..4307d2e7 100644 --- a/packages/app-core/src/lib/tab-scroll-memory.test.ts +++ b/packages/app-core/src/lib/tab-scroll-memory.test.ts @@ -24,6 +24,23 @@ describe('tab scroll memory', () => { expect(recallTabScroll('a.md')).toEqual({ editor: 11, preview: 22 }) }) + it('keeps a remembered editor selection when a later capture only updates scroll', () => { + rememberTabScroll('a.md', { + editor: 10, + preview: 20, + editorSelectionAnchor: 100, + editorSelectionHead: 120 + }) + rememberTabScroll('a.md', { editor: 11, preview: 22 }) + + expect(recallTabScroll('a.md')).toEqual({ + editor: 11, + preview: 22, + editorSelectionAnchor: 100, + editorSelectionHead: 120 + }) + }) + it('forgets a single path', () => { rememberTabScroll('a.md', { editor: 1, preview: 2 }) forgetTabScroll('a.md') diff --git a/packages/app-core/src/lib/tab-scroll-memory.ts b/packages/app-core/src/lib/tab-scroll-memory.ts index 00932efc..d8350817 100644 --- a/packages/app-core/src/lib/tab-scroll-memory.ts +++ b/packages/app-core/src/lib/tab-scroll-memory.ts @@ -4,8 +4,9 @@ * The pane reuses a single editor + preview surface and renders only the * active tab's content, so switching tabs (or opening a diagram in a tab and * coming back) otherwise snaps a note to the top. This is a small imperative - * cache — keyed by note path — that remembers each tab's editor and preview - * scroll offsets so re-activating the tab can restore its position. + * cache — keyed by note path — that remembers each tab's editor selection + * and preview/editor scroll offsets so re-activating the tab can restore its + * position. * * It's intentionally a module-level cache, not store state: nothing renders * from it, it's read/written imperatively during tab switches and scrolls, @@ -15,6 +16,8 @@ export interface TabScrollPosition { editor: number preview: number + editorSelectionAnchor?: number + editorSelectionHead?: number } const TAB_SCROLL_MEMORY_LIMIT = 60 @@ -22,9 +25,18 @@ const memory = new Map() export function rememberTabScroll(path: string, position: TabScrollPosition): void { if (!path) return + const previous = memory.get(path) + const next: TabScrollPosition = { + editor: position.editor, + preview: position.preview + } + const anchor = position.editorSelectionAnchor ?? previous?.editorSelectionAnchor + const head = position.editorSelectionHead ?? previous?.editorSelectionHead + if (anchor != null) next.editorSelectionAnchor = anchor + if (head != null) next.editorSelectionHead = head // Re-insert so the most recently touched entry is last (LRU ordering). memory.delete(path) - memory.set(path, position) + memory.set(path, next) while (memory.size > TAB_SCROLL_MEMORY_LIMIT) { const oldest = memory.keys().next().value if (oldest === undefined) break From 33604d2dc5ec9fa6da9180be58ea0088b10b5b62 Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Fri, 12 Jun 2026 11:01:56 -0500 Subject: [PATCH 03/66] Fix calendar date note detection (#131) --- docs/releases/v2.4.0/RELEASE_NOTES.md | 3 +- .../app-core/src/components/CalendarPanel.tsx | 31 +-- .../src/components/SettingsModal.test.ts | 199 ++++++++++++++++++ .../app-core/src/components/SettingsModal.tsx | 40 +++- packages/app-core/src/components/Sidebar.tsx | 16 +- .../app-core/src/lib/vault-layout.test.ts | 50 +++++ packages/app-core/src/lib/vault-layout.ts | 71 ++++++- packages/app-core/src/store.ts | 21 +- 8 files changed, 369 insertions(+), 62 deletions(-) create mode 100644 packages/app-core/src/components/SettingsModal.test.ts create mode 100644 packages/app-core/src/lib/vault-layout.test.ts diff --git a/docs/releases/v2.4.0/RELEASE_NOTES.md b/docs/releases/v2.4.0/RELEASE_NOTES.md index cb2cdd0d..5bdc5bbb 100644 --- a/docs/releases/v2.4.0/RELEASE_NOTES.md +++ b/docs/releases/v2.4.0/RELEASE_NOTES.md @@ -13,7 +13,8 @@ The onboarding mode selector now uses a more consistent segmented-control layout ## 🐛 Fixes - **Tabs keep their editor position (#127)** — switching between open note tabs now preserves each note's editor scroll position and cursor/selection. The editor no longer jumps back to line 1 when you scroll in one note, move somewhere else in another note, and return. +- **Calendar recognizes existing daily notes (#131)** — daily and weekly note detection now uses the same vault-layout rules across the calendar, sidebar, and open/create commands, so synced or manually added date notes show their calendar dots and open without creating duplicates. Daily/weekly directory fields in Settings also wait until blur or Enter before saving, so they no longer snap back to the default while you are clearing or typing a path. --- -Thanks to everyone filing issues and reporting bugs — closes #127. +Thanks to everyone filing issues and reporting bugs — closes #127, #131. diff --git a/packages/app-core/src/components/CalendarPanel.tsx b/packages/app-core/src/components/CalendarPanel.tsx index 157940e0..038860c0 100644 --- a/packages/app-core/src/components/CalendarPanel.tsx +++ b/packages/app-core/src/components/CalendarPanel.tsx @@ -17,8 +17,8 @@ import type { NoteContent, NoteMeta } from '@shared/ipc' import { parseTasksFromBody } from '@shared/tasks' import { useStore } from '../store' import { + buildDateNoteIndexes, classifyDateNote, - noteFolderSubpath, normalizeVaultSettings, weeklyNoteTitle, } from '../lib/vault-layout' @@ -32,8 +32,6 @@ import { PanelResizeHandle } from './PanelResizeHandle' import { ContextMenu, type ContextMenuItem } from './ContextMenu' const FULL_DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] -const DAILY_RE = /^\d{4}-\d{2}-\d{2}$/ -const WEEKLY_RE = /^\d{4}-W\d{2}$/ const WORDS_PER_DOT = 80 const MAX_DOTS = 3 const HOVER_DELAY_MS = 280 @@ -111,8 +109,6 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { const settings = useMemo(() => normalizeVaultSettings(vaultSettings), [vaultSettings]) const dailyEnabled = settings.dailyNotes.enabled const weeklyEnabled = settings.weeklyNotes.enabled - const dailySubpath = settings.dailyNotes.directory - const weeklySubpath = settings.weeklyNotes.directory const firstDay = weekStart === 'sunday' ? 0 : weekStart === 'locale' ? localeFirstDay() : 1 const dayLabels = useMemo( @@ -143,27 +139,10 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { }, [note.path]) // title -> NoteMeta for the daily/weekly notes that exist on disk. - const dailyByTitle = useMemo(() => { - const m = new Map() - if (!dailyEnabled) return m - for (const n of notes) { - if (n.folder !== 'inbox') continue - if (noteFolderSubpath(n, settings) !== dailySubpath) continue - if (DAILY_RE.test(n.title)) m.set(n.title, n) - } - return m - }, [notes, settings, dailyEnabled, dailySubpath]) - - const weeklyByTitle = useMemo(() => { - const m = new Map() - if (!weeklyEnabled) return m - for (const n of notes) { - if (n.folder !== 'inbox') continue - if (noteFolderSubpath(n, settings) !== weeklySubpath) continue - if (WEEKLY_RE.test(n.title)) m.set(n.title, n) - } - return m - }, [notes, settings, weeklyEnabled, weeklySubpath]) + const { dailyByTitle, weeklyByTitle } = useMemo( + () => buildDateNoteIndexes(notes, settings), + [notes, settings] + ) const grid = useMemo(() => buildGrid(anchor, firstDay), [anchor, firstDay]) const rows = useMemo(() => { diff --git a/packages/app-core/src/components/SettingsModal.test.ts b/packages/app-core/src/components/SettingsModal.test.ts new file mode 100644 index 00000000..8b31fba6 --- /dev/null +++ b/packages/app-core/src/components/SettingsModal.test.ts @@ -0,0 +1,199 @@ +// @vitest-environment jsdom + +import { act, createElement } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { SettingsModal } from './SettingsModal' + +const mocks = vi.hoisted(() => { + const state = new Proxy( + { + autoCalendarPanel: true, + calendarShowWeekNumbers: true, + calendarWeekStart: 'monday', + customTemplates: [], + darkSidebar: false, + editorFontSize: 16, + editorLineHeight: 1.6, + fzfBinaryPath: null, + hideBuiltinTemplates: false, + interfaceFont: null, + keymapOverrides: {}, + lineNumberMode: 'off', + monoFont: null, + previewMaxWidth: 760, + quickNoteTitlePrefix: null, + remoteWorkspaceInfo: null, + remoteWorkspaceProfiles: [], + ripgrepBinaryPath: null, + setSettingsOpen: vi.fn(), + setVaultSettings: vi.fn(), + showSidebarChevrons: true, + systemFolderLabels: {}, + textFont: null, + themeFamily: 'apple', + themeId: 'apple-light', + themeMode: 'light', + vault: { root: '/tmp/zennotes-test-vault', name: 'Test Vault' }, + vaultSettings: { + primaryNotesLocation: 'inbox', + dailyNotes: { enabled: true, directory: 'Daily Not' }, + weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, + folderIcons: {} + }, + vaultTextSearchBackend: 'auto', + vimInsertEscape: '', + vimMode: false, + whichKeyHintMode: 'timed', + whichKeyHintTimeoutMs: 1200, + whichKeyHints: true, + workspaceMode: 'local' + }, + { + get(target, property: string) { + if (property in target) return target[property as keyof typeof target] + return vi.fn() + } + } + ) + + return { + state, + setSettingsOpen: state.setSettingsOpen, + setVaultSettings: state.setVaultSettings + } +}) + +vi.mock('../store', () => ({ + useStore: (selector: (state: typeof mocks.state) => unknown) => selector(mocks.state) +})) + +vi.mock('../lib/system-fonts', () => ({ + hasSystemFontAccess: () => false, + listSystemFonts: vi.fn().mockResolvedValue([]) +})) + +vi.mock('../lib/app-update-state', () => ({ + useAppUpdateState: () => ({ phase: 'idle', message: 'Manual check' }) +})) + +vi.mock('@zennotes/bridge-contract/bridge', () => ({ + getZenBridge: () => ({ + getAppInfo: () => ({ + runtime: 'desktop', + version: '2.4.0', + description: 'ZenNotes', + homepage: 'https://github.com/ZenNotes/zennotes/releases/latest' + }), + getCapabilities: () => ({ + supportsCustomTemplates: true, + supportsRemoteWorkspace: false + }) + }) +})) + +function changeInput(input: HTMLInputElement, value: string): void { + const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set + setter?.call(input, value) + input.dispatchEvent(new Event('input', { bubbles: true })) +} + +function blurInput(input: HTMLInputElement): void { + input.dispatchEvent(new FocusEvent('focusout', { bubbles: true })) +} + +describe('SettingsModal date note directories', () => { + let root: Root + let host: HTMLDivElement + + beforeEach(() => { + vi.clearAllMocks() + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + Object.defineProperty(window, 'zen', { + configurable: true, + value: { + getVaultTextSearchCapabilities: vi.fn().mockResolvedValue({ ripgrep: false, fzf: false }), + checkForAppUpdates: vi.fn().mockResolvedValue({ phase: 'idle', message: 'Manual check' }) + } + }) + Object.defineProperty(window, 'matchMedia', { + configurable: true, + value: vi.fn().mockReturnValue({ + matches: false, + addEventListener: vi.fn(), + removeEventListener: vi.fn() + }) + }) + host = document.createElement('div') + document.body.append(host) + root = createRoot(host) + }) + + afterEach(() => { + act(() => root.unmount()) + host.remove() + }) + + it('does not restore the default daily directory while the field is being cleared', async () => { + await act(async () => { + root.render(createElement(SettingsModal)) + }) + + const search = [...host.querySelectorAll('input')].find( + (input) => input.placeholder === 'Search settings…' + ) + expect(search).toBeTruthy() + + await act(async () => { + changeInput(search!, 'daily notes directory') + }) + + const dailyDirectory = [...host.querySelectorAll('input')].find( + (input) => input.value === 'Daily Not' + ) + expect(dailyDirectory).toBeTruthy() + + await act(async () => { + changeInput(dailyDirectory!, '') + }) + + expect(mocks.setVaultSettings).not.toHaveBeenCalled() + }) + + it('saves the daily directory when the edit is committed', async () => { + await act(async () => { + root.render(createElement(SettingsModal)) + }) + + const search = [...host.querySelectorAll('input')].find( + (input) => input.placeholder === 'Search settings…' + ) + expect(search).toBeTruthy() + + await act(async () => { + changeInput(search!, 'daily notes directory') + }) + + const dailyDirectory = [...host.querySelectorAll('input')].find( + (input) => input.value === 'Daily Not' + ) + expect(dailyDirectory).toBeTruthy() + + await act(async () => { + changeInput(dailyDirectory!, 'inbox/Journal') + }) + + expect(mocks.setVaultSettings).not.toHaveBeenCalled() + + await act(async () => { + blurInput(dailyDirectory!) + }) + + expect(mocks.setVaultSettings).toHaveBeenCalledWith({ + primaryNotesLocation: 'inbox', + dailyNotes: { enabled: true, directory: 'inbox/Journal' }, + weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, + folderIcons: {} + }) + }) +}) diff --git a/packages/app-core/src/components/SettingsModal.tsx b/packages/app-core/src/components/SettingsModal.tsx index 346064cc..acfa0345 100644 --- a/packages/app-core/src/components/SettingsModal.tsx +++ b/packages/app-core/src/components/SettingsModal.tsx @@ -1653,6 +1653,7 @@ export function SettingsModal(): JSX.Element { value={vaultSettings.dailyNotes.directory} placeholder={DEFAULT_DAILY_NOTES_DIRECTORY} settingId="daily-notes-directory" + commitOnBlur onChange={(next) => void persistVaultSettings({ ...vaultSettings, @@ -1727,6 +1728,7 @@ export function SettingsModal(): JSX.Element { value={vaultSettings.weeklyNotes.directory} placeholder={DEFAULT_WEEKLY_NOTES_DIRECTORY} settingId="weekly-notes-directory" + commitOnBlur onChange={(next) => void persistVaultSettings({ ...vaultSettings, @@ -2917,6 +2919,7 @@ function TextInputRow({ value, placeholder, settingId, + commitOnBlur = false, onChange }: { label: string @@ -2924,8 +2927,22 @@ function TextInputRow({ value: string placeholder?: string settingId?: string + commitOnBlur?: boolean onChange: (next: string | null) => void }): JSX.Element { + const [draft, setDraft] = useState(value) + const [focused, setFocused] = useState(false) + + useEffect(() => { + if (!commitOnBlur || !focused) setDraft(value) + }, [commitOnBlur, focused, value]) + + const commit = (raw: string): void => { + const next = raw.trim() + if (commitOnBlur && next === value) return + onChange(next ? next : null) + } + return (
{description}
} setFocused(true)} onChange={(e) => { - const next = e.target.value.trim() - onChange(next ? next : null) + if (commitOnBlur) { + setDraft(e.target.value) + return + } + commit(e.target.value) + }} + onBlur={(e) => { + if (!commitOnBlur) return + setFocused(false) + commit(e.target.value) + }} + onKeyDown={(e) => { + if (!commitOnBlur) return + if (e.key === 'Enter') e.currentTarget.blur() + if (e.key === 'Escape') { + setDraft(value) + e.currentTarget.blur() + } }} placeholder={placeholder} className="w-[23rem] max-w-[50vw] rounded-xl border border-paper-300/70 bg-paper-100/80 px-3 py-2 text-sm text-ink-900 outline-none placeholder:text-ink-400 focus:border-accent/45" diff --git a/packages/app-core/src/components/Sidebar.tsx b/packages/app-core/src/components/Sidebar.tsx index bada9929..2813bc4d 100644 --- a/packages/app-core/src/components/Sidebar.tsx +++ b/packages/app-core/src/components/Sidebar.tsx @@ -44,6 +44,7 @@ import { resolveQuickNoteTitle } from "../lib/quick-note-title"; import { recordRendererPerf } from "../lib/perf"; import { assetFolderSubpath, + classifyDateNote, folderIconKey, isPrimaryNotesAtRoot, folderForVaultRelativePath, @@ -1006,12 +1007,10 @@ export function Sidebar(): JSX.Element { if (s.dailyNotes.enabled) { const byYear = new Map>(); for (const n of notes) { - if (n.folder !== "inbox") continue; - if (noteFolderSubpath(n, vaultSettings) !== dailyDir) continue; - const m = /^(\d{4})-(\d{2})-\d{2}$/.exec(n.title); - if (!m) continue; - const year = Number(m[1]); - const month = Number(m[2]) - 1; + const info = classifyDateNote(n, s); + if (info?.kind !== "daily") continue; + const year = info.date.getFullYear(); + const month = info.date.getMonth(); let months = byYear.get(year); if (!months) byYear.set(year, (months = new Map())); (months.get(month) ?? months.set(month, []).get(month)!).push(n); @@ -1032,9 +1031,8 @@ export function Sidebar(): JSX.Element { if (s.weeklyNotes.enabled) { const byYear = new Map(); for (const n of notes) { - if (n.folder !== "inbox") continue; - if (noteFolderSubpath(n, vaultSettings) !== weeklyDir) continue; - if (!/^\d{4}-W\d{2}$/.test(n.title)) continue; + const info = classifyDateNote(n, s); + if (info?.kind !== "weekly") continue; const year = Number(n.title.slice(0, 4)); (byYear.get(year) ?? byYear.set(year, []).get(year)!).push(n); } diff --git a/packages/app-core/src/lib/vault-layout.test.ts b/packages/app-core/src/lib/vault-layout.test.ts new file mode 100644 index 00000000..b5ba4276 --- /dev/null +++ b/packages/app-core/src/lib/vault-layout.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest' +import type { NoteMeta, VaultSettings } from '@shared/ipc' +import { classifyDateNote } from './vault-layout' + +function note(path: string, title: string): NoteMeta { + return { + path, + title, + folder: 'inbox', + siblingOrder: 0, + createdAt: 0, + updatedAt: 0, + size: 0, + tags: [], + wikilinks: [], + hasAttachments: false, + excerpt: '' + } +} + +function settings(dailyDirectory: string, weeklyDirectory: string): VaultSettings { + return { + primaryNotesLocation: 'inbox', + dailyNotes: { enabled: true, directory: dailyDirectory }, + weeklyNotes: { enabled: true, directory: weeklyDirectory }, + folderIcons: {} + } +} + +describe('classifyDateNote', () => { + it('recognizes daily notes when the configured directory includes the primary inbox prefix', () => { + const info = classifyDateNote( + note('inbox/Journal/2026-06-12.md', '2026-06-12'), + settings('inbox/Journal', 'Weekly Notes') + ) + + expect(info).toMatchObject({ kind: 'daily' }) + expect(info?.date).toEqual(new Date(2026, 5, 12)) + }) + + it('recognizes weekly notes when the configured directory includes the primary inbox prefix', () => { + const info = classifyDateNote( + note('inbox/Weeks/2026-W24.md', '2026-W24'), + settings('Daily Notes', 'inbox/Weeks') + ) + + expect(info).toMatchObject({ kind: 'weekly' }) + expect(info?.date).toEqual(new Date(2026, 5, 8)) + }) +}) diff --git a/packages/app-core/src/lib/vault-layout.ts b/packages/app-core/src/lib/vault-layout.ts index 94499c31..b2fb3756 100644 --- a/packages/app-core/src/lib/vault-layout.ts +++ b/packages/app-core/src/lib/vault-layout.ts @@ -70,6 +70,15 @@ export function normalizeWeeklyNotesDirectory(directory: string | null | undefin return trimmed || DEFAULT_WEEKLY_NOTES_DIRECTORY } +function normalizePrimaryRelativeSubpath( + subpath: string, + settings: Pick +): string { + if (settings.primaryNotesLocation !== 'inbox') return subpath + if (subpath === 'inbox') return '' + return subpath.startsWith('inbox/') ? subpath.slice('inbox/'.length) : subpath +} + function normalizeTemplateId(value: string | null | undefined): string | undefined { const trimmed = (value ?? '').trim() return trimmed || undefined @@ -86,19 +95,29 @@ export function normalizeVaultSettings( normalizedFolderIcons[key] = value } } + const primaryNotesLocation = + settings?.primaryNotesLocation === 'root' + ? 'root' + : DEFAULT_VAULT_SETTINGS.primaryNotesLocation + const dailyDirectory = normalizePrimaryRelativeSubpath( + normalizeDailyNotesDirectory(settings?.dailyNotes?.directory), + { primaryNotesLocation } + ) + const weeklyDirectory = normalizePrimaryRelativeSubpath( + normalizeWeeklyNotesDirectory(settings?.weeklyNotes?.directory), + { primaryNotesLocation } + ) + return { - primaryNotesLocation: - settings?.primaryNotesLocation === 'root' - ? 'root' - : DEFAULT_VAULT_SETTINGS.primaryNotesLocation, + primaryNotesLocation, dailyNotes: { enabled: !!settings?.dailyNotes?.enabled, - directory: normalizeDailyNotesDirectory(settings?.dailyNotes?.directory), + directory: dailyDirectory, templateId: normalizeTemplateId(settings?.dailyNotes?.templateId) }, weeklyNotes: { enabled: !!settings?.weeklyNotes?.enabled, - directory: normalizeWeeklyNotesDirectory(settings?.weeklyNotes?.directory), + directory: weeklyDirectory, templateId: normalizeTemplateId(settings?.weeklyNotes?.templateId) }, folderIcons: normalizedFolderIcons @@ -233,7 +252,9 @@ export function classifyDateNote( settings: VaultSettings | null | undefined ): DateNoteInfo | null { const normalized = normalizeVaultSettings(settings) - const subpath = noteFolderSubpath(note, settings) + if (note.folder !== 'inbox') return null + + const subpath = noteFolderSubpath(note, normalized) if (normalized.dailyNotes.enabled && subpath === normalized.dailyNotes.directory) { const m = DAILY_TITLE_RE.exec(note.title) @@ -254,6 +275,42 @@ export function classifyDateNote( return null } +export interface DateNoteIndexes { + dailyByTitle: Map + weeklyByTitle: Map +} + +export function buildDateNoteIndexes( + notes: readonly NoteMeta[], + settings: VaultSettings | null | undefined +): DateNoteIndexes { + const dailyByTitle = new Map() + const weeklyByTitle = new Map() + + for (const note of notes) { + const info = classifyDateNote(note, settings) + if (!info) continue + if (info.kind === 'daily') dailyByTitle.set(note.title, note) + else weeklyByTitle.set(note.title, note) + } + + return { dailyByTitle, weeklyByTitle } +} + +export function findDateNoteByTitle( + notes: readonly NoteMeta[], + settings: VaultSettings | null | undefined, + kind: DateNoteInfo['kind'], + title: string +): NoteMeta | null { + for (const note of notes) { + if (note.title !== title) continue + const info = classifyDateNote(note, settings) + if (info?.kind === kind) return note + } + return null +} + export function folderForVaultRelativePath( relPath: string, settings: VaultSettings | null | undefined diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index 052fb93e..86b0299c 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -73,9 +73,8 @@ import { import { duplicateFolderIcons, folderForVaultRelativePath, + findDateNoteByTitle, isPrimaryNotesAtRoot, - normalizeDailyNotesDirectory, - normalizeWeeklyNotesDirectory, removeFolderIcons, normalizeVaultSettings, noteFolderSubpath, @@ -4348,13 +4347,8 @@ export const useStore = create((set, get) => { const settings = normalizeVaultSettings(state.vaultSettings) if (!settings.dailyNotes.enabled) return const title = noteTitleForDate(date) - const subpath = normalizeDailyNotesDirectory(settings.dailyNotes.directory) - const existing = state.notes.find( - (note) => - note.folder === 'inbox' && - note.title === title && - noteFolderSubpath(note, settings) === subpath - ) + const subpath = settings.dailyNotes.directory + const existing = findDateNoteByTitle(state.notes, settings, 'daily', title) if (existing) { set({ view: { kind: 'folder', folder: 'inbox', subpath } }) await get().selectNote(existing.path) @@ -4377,13 +4371,8 @@ export const useStore = create((set, get) => { const settings = normalizeVaultSettings(state.vaultSettings) if (!settings.weeklyNotes.enabled) return const title = weeklyNoteTitle(date) - const subpath = normalizeWeeklyNotesDirectory(settings.weeklyNotes.directory) - const existing = state.notes.find( - (note) => - note.folder === 'inbox' && - note.title === title && - noteFolderSubpath(note, settings) === subpath - ) + const subpath = settings.weeklyNotes.directory + const existing = findDateNoteByTitle(state.notes, settings, 'weekly', title) if (existing) { set({ view: { kind: 'folder', folder: 'inbox', subpath } }) await get().selectNote(existing.path) From 689431d8f069e8aeb65bfe4414b6a64550da0036 Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Fri, 12 Jun 2026 15:38:39 -0500 Subject: [PATCH 04/66] Add configurable daily and weekly note patterns (#77) Daily and weekly notes can use date-pattern tokens (yyyy, MM, MMM, dd, EEE, ww, ...) for both the directory and the title/filename, with a locale for localized month and weekday names. Literal text is single-quoted, e.g. 'Daily Notes'/yyyy/MM-MMM. Recognition re-formats each note's recovered date and accepts it only when it round-trips, so any token combination resolves -- including titles that encode the day via ISO week + weekday (yyyy-'W'ww-EEE) rather than day-of-month. Changing a pattern snapshots the previous one so existing notes stay recognized, and a Reset to defaults control restores the directory, naming, and locale. Closes #77 --- README.md | 27 +- apps/desktop/src/main/vault.ts | 114 ++- apps/server/internal/vault/types.go | 36 +- apps/server/internal/vault/vault.go | 110 ++- apps/server/internal/vault/vault_test.go | 28 +- docs/reference/settings-reference.md | 50 +- docs/reference/vault-and-folder-model.md | 15 +- .../app-core/src/components/CalendarPanel.tsx | 30 +- .../src/components/OnboardingWizard.tsx | 1 + .../app-core/src/components/SettingsModal.tsx | 309 +++++++- packages/app-core/src/components/Sidebar.tsx | 83 ++- packages/app-core/src/lib/help.ts | 6 +- .../app-core/src/lib/vault-layout.test.ts | 361 ++++++++- packages/app-core/src/lib/vault-layout.ts | 688 +++++++++++++++++- packages/app-core/src/store.test.ts | 206 ++++++ packages/app-core/src/store.ts | 102 ++- packages/bridge-contract/src/ipc.ts | 35 +- 17 files changed, 2081 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index 95ae20ce..4224ee80 100644 --- a/README.md +++ b/README.md @@ -140,14 +140,29 @@ System folders still exist, but the vault model is more flexible now: The built-in folder labels are also customizable in the UI without changing the underlying internal ids. -### Daily notes +### Daily and weekly notes -Daily notes are optional and can be enabled from Settings. +Daily and weekly notes are optional and can be enabled from Settings. -- when enabled, ZenNotes can open or create today's note automatically -- the title is a simple ISO date like `2026-04-21` -- daily notes live in a dedicated directory under your primary notes area -- the default directory is `Daily Notes` +- when enabled, ZenNotes can open or create today's daily note and this week's weekly note automatically +- the default daily title is a simple ISO date like `2026-04-21` +- the default weekly title is an ISO week title like `2026-W24` +- date notes live in dedicated directories under your primary notes area +- the default directories are `Daily Notes` and `Weekly Notes` +- the directory and title can use date patterns such as `yyyy/MM-MMM` and `yyyy-MM-dd-EEE` +- weekly title patterns can use ISO week tokens such as `yyyy-'W'ww` +- localized month and weekday names can follow `system`, `en-US`, `pt-BR`, or another BCP 47 locale + +Supported date note pattern tokens: + +- `yyyy` / `yy` for `2026` / `26` +- `M` / `MM` / `MMM` / `MMMM` for `6` / `06` / `Jun` / `June` +- `d` / `dd` for `9` / `09` +- `EEE` / `EEEE` for `Tue` / `Tuesday` +- `w` / `ww` for ISO week numbers like `24` +- single-quoted literals, such as `'Daily Notes'/yyyy/MM-MMM` + +Weekly notes render date tokens from the ISO week's Monday, and `yyyy` is the ISO week-year for weekly patterns. ### Editor and preview diff --git a/apps/desktop/src/main/vault.ts b/apps/desktop/src/main/vault.ts index 1fd2dc91..8640984b 100644 --- a/apps/desktop/src/main/vault.ts +++ b/apps/desktop/src/main/vault.ts @@ -12,7 +12,11 @@ import { type RenameNoteRef } from './wikilink-rename' import { + DEFAULT_DAILY_NOTE_LOCALE, + DEFAULT_DAILY_NOTE_TITLE_PATTERN, DEFAULT_DAILY_NOTES_DIRECTORY, + DEFAULT_WEEKLY_NOTE_LOCALE, + DEFAULT_WEEKLY_NOTE_TITLE_PATTERN, DEFAULT_WEEKLY_NOTES_DIRECTORY, AssetMeta, DeletedAsset, @@ -137,11 +141,15 @@ const DEFAULT_VAULT_SETTINGS: VaultSettings = { primaryNotesLocation: 'inbox', dailyNotes: { enabled: false, - directory: DEFAULT_DAILY_NOTES_DIRECTORY + directory: DEFAULT_DAILY_NOTES_DIRECTORY, + titlePattern: DEFAULT_DAILY_NOTE_TITLE_PATTERN, + locale: DEFAULT_DAILY_NOTE_LOCALE }, weeklyNotes: { enabled: false, - directory: DEFAULT_WEEKLY_NOTES_DIRECTORY + directory: DEFAULT_WEEKLY_NOTES_DIRECTORY, + titlePattern: DEFAULT_WEEKLY_NOTE_TITLE_PATTERN, + locale: DEFAULT_WEEKLY_NOTE_LOCALE }, folderIcons: {} } @@ -677,11 +685,17 @@ function cloneVaultSettings(settings: VaultSettings): VaultSettings { dailyNotes: { enabled: settings.dailyNotes.enabled, directory: settings.dailyNotes.directory, + titlePattern: settings.dailyNotes.titlePattern, + locale: settings.dailyNotes.locale, + legacyPatterns: settings.dailyNotes.legacyPatterns?.map((pattern) => ({ ...pattern })), templateId: settings.dailyNotes.templateId }, weeklyNotes: { enabled: settings.weeklyNotes.enabled, directory: settings.weeklyNotes.directory, + titlePattern: settings.weeklyNotes.titlePattern, + locale: settings.weeklyNotes.locale, + legacyPatterns: settings.weeklyNotes.legacyPatterns?.map((pattern) => ({ ...pattern })), templateId: settings.weeklyNotes.templateId }, folderIcons: { ...settings.folderIcons } @@ -694,6 +708,30 @@ function normalizeDailyNotesDirectory(value: unknown): string { return trimmed || DEFAULT_DAILY_NOTES_DIRECTORY } +function normalizeDailyNoteTitlePattern(value: unknown): string { + if (typeof value !== 'string') return DEFAULT_DAILY_NOTE_TITLE_PATTERN + const trimmed = value.trim().replace(/[\\/]+/g, '-') + return trimmed || DEFAULT_DAILY_NOTE_TITLE_PATTERN +} + +function normalizeDailyNoteLocale(value: unknown): string { + if (typeof value !== 'string') return DEFAULT_DAILY_NOTE_LOCALE + const trimmed = value.trim() + return trimmed || DEFAULT_DAILY_NOTE_LOCALE +} + +function normalizeWeeklyNoteTitlePattern(value: unknown): string { + if (typeof value !== 'string') return DEFAULT_WEEKLY_NOTE_TITLE_PATTERN + const trimmed = value.trim().replace(/[\\/]+/g, '-') + return trimmed || DEFAULT_WEEKLY_NOTE_TITLE_PATTERN +} + +function normalizeWeeklyNoteLocale(value: unknown): string { + if (typeof value !== 'string') return DEFAULT_WEEKLY_NOTE_LOCALE + const trimmed = value.trim() + return trimmed || DEFAULT_WEEKLY_NOTE_LOCALE +} + function normalizeWeeklyNotesDirectory(value: unknown): string { if (typeof value !== 'string') return DEFAULT_WEEKLY_NOTES_DIRECTORY const trimmed = value.trim().replace(/^\/+|\/+$/g, '') @@ -706,6 +744,46 @@ function normalizeTemplateId(value: unknown): string | undefined { return trimmed || undefined } +function normalizeDailyNoteLegacyPatterns(value: unknown): VaultSettings['dailyNotes']['legacyPatterns'] { + if (!Array.isArray(value)) return [] + const out: NonNullable = [] + const seen = new Set() + for (const item of value) { + if (!item || typeof item !== 'object') continue + const pattern = item as { directory?: unknown; titlePattern?: unknown; locale?: unknown } + const next = { + directory: normalizeDailyNotesDirectory(pattern.directory), + titlePattern: normalizeDailyNoteTitlePattern(pattern.titlePattern), + locale: normalizeDailyNoteLocale(pattern.locale) + } + const key = `${next.directory}\0${next.titlePattern}\0${next.locale}` + if (seen.has(key)) continue + seen.add(key) + out.push(next) + } + return out +} + +function normalizeWeeklyNoteLegacyPatterns(value: unknown): VaultSettings['weeklyNotes']['legacyPatterns'] { + if (!Array.isArray(value)) return [] + const out: NonNullable = [] + const seen = new Set() + for (const item of value) { + if (!item || typeof item !== 'object') continue + const pattern = item as { directory?: unknown; titlePattern?: unknown; locale?: unknown } + const next = { + directory: normalizeWeeklyNotesDirectory(pattern.directory), + titlePattern: normalizeWeeklyNoteTitlePattern(pattern.titlePattern), + locale: normalizeWeeklyNoteLocale(pattern.locale) + } + const key = `${next.directory}\0${next.titlePattern}\0${next.locale}` + if (seen.has(key)) continue + seen.add(key) + out.push(next) + } + return out +} + function normalizePrimaryNotesLocation(value: unknown): PrimaryNotesLocation { return value === 'root' ? 'root' : 'inbox' } @@ -719,19 +797,37 @@ function normalizeVaultSettings( primaryNotesLocation: fallbackPrimary, dailyNotes: { enabled: DEFAULT_VAULT_SETTINGS.dailyNotes.enabled, - directory: DEFAULT_DAILY_NOTES_DIRECTORY + directory: DEFAULT_DAILY_NOTES_DIRECTORY, + titlePattern: DEFAULT_DAILY_NOTE_TITLE_PATTERN, + locale: DEFAULT_DAILY_NOTE_LOCALE }, weeklyNotes: { enabled: DEFAULT_VAULT_SETTINGS.weeklyNotes.enabled, - directory: DEFAULT_WEEKLY_NOTES_DIRECTORY + directory: DEFAULT_WEEKLY_NOTES_DIRECTORY, + titlePattern: DEFAULT_WEEKLY_NOTE_TITLE_PATTERN, + locale: DEFAULT_WEEKLY_NOTE_LOCALE }, folderIcons: {} } } const candidate = value as { primaryNotesLocation?: unknown - dailyNotes?: { enabled?: unknown; directory?: unknown; templateId?: unknown } | null - weeklyNotes?: { enabled?: unknown; directory?: unknown; templateId?: unknown } | null + dailyNotes?: { + enabled?: unknown + directory?: unknown + titlePattern?: unknown + locale?: unknown + legacyPatterns?: unknown + templateId?: unknown + } | null + weeklyNotes?: { + enabled?: unknown + directory?: unknown + titlePattern?: unknown + locale?: unknown + legacyPatterns?: unknown + templateId?: unknown + } | null folderIcons?: Record | null } const folderIcons: Record = {} @@ -751,6 +847,9 @@ function normalizeVaultSettings( ? candidate.dailyNotes.enabled : DEFAULT_VAULT_SETTINGS.dailyNotes.enabled, directory: normalizeDailyNotesDirectory(candidate.dailyNotes?.directory), + titlePattern: normalizeDailyNoteTitlePattern(candidate.dailyNotes?.titlePattern), + locale: normalizeDailyNoteLocale(candidate.dailyNotes?.locale), + legacyPatterns: normalizeDailyNoteLegacyPatterns(candidate.dailyNotes?.legacyPatterns), templateId: normalizeTemplateId(candidate.dailyNotes?.templateId) }, weeklyNotes: { @@ -759,6 +858,9 @@ function normalizeVaultSettings( ? candidate.weeklyNotes.enabled : DEFAULT_VAULT_SETTINGS.weeklyNotes.enabled, directory: normalizeWeeklyNotesDirectory(candidate.weeklyNotes?.directory), + titlePattern: normalizeWeeklyNoteTitlePattern(candidate.weeklyNotes?.titlePattern), + locale: normalizeWeeklyNoteLocale(candidate.weeklyNotes?.locale), + legacyPatterns: normalizeWeeklyNoteLegacyPatterns(candidate.weeklyNotes?.legacyPatterns), templateId: normalizeTemplateId(candidate.weeklyNotes?.templateId) }, folderIcons diff --git a/apps/server/internal/vault/types.go b/apps/server/internal/vault/types.go index 2034b552..9b1d7e63 100644 --- a/apps/server/internal/vault/types.go +++ b/apps/server/internal/vault/types.go @@ -20,10 +20,14 @@ const ( FolderArchive NoteFolder = "archive" FolderTrash NoteFolder = "trash" - PrimaryNotesInbox PrimaryNotesLocation = "inbox" - PrimaryNotesRoot PrimaryNotesLocation = "root" - DefaultDailyNotesDirectory = "Daily Notes" - DefaultWeeklyNotesDirectory = "Weekly Notes" + PrimaryNotesInbox PrimaryNotesLocation = "inbox" + PrimaryNotesRoot PrimaryNotesLocation = "root" + DefaultDailyNotesDirectory = "Daily Notes" + DefaultDailyNoteTitlePattern = "yyyy-MM-dd" + DefaultDailyNoteLocale = "system" + DefaultWeeklyNotesDirectory = "Weekly Notes" + DefaultWeeklyNoteTitlePattern = "yyyy-'W'ww" + DefaultWeeklyNoteLocale = "system" ) func IsValidFolder(f NoteFolder) bool { @@ -51,16 +55,28 @@ func FolderForRelativePath(rel string) (NoteFolder, bool) { return FolderInbox, true } +type DateNotePatternSettings struct { + Directory string `json:"directory"` + TitlePattern string `json:"titlePattern,omitempty"` + Locale string `json:"locale,omitempty"` +} + type DailyNotesSettings struct { - Enabled bool `json:"enabled"` - Directory string `json:"directory"` - TemplateID string `json:"templateId,omitempty"` + Enabled bool `json:"enabled"` + Directory string `json:"directory"` + TitlePattern string `json:"titlePattern,omitempty"` + Locale string `json:"locale,omitempty"` + LegacyPatterns []DateNotePatternSettings `json:"legacyPatterns,omitempty"` + TemplateID string `json:"templateId,omitempty"` } type WeeklyNotesSettings struct { - Enabled bool `json:"enabled"` - Directory string `json:"directory"` - TemplateID string `json:"templateId,omitempty"` + Enabled bool `json:"enabled"` + Directory string `json:"directory"` + TitlePattern string `json:"titlePattern,omitempty"` + Locale string `json:"locale,omitempty"` + LegacyPatterns []DateNotePatternSettings `json:"legacyPatterns,omitempty"` + TemplateID string `json:"templateId,omitempty"` } type VaultSettings struct { diff --git a/apps/server/internal/vault/vault.go b/apps/server/internal/vault/vault.go index f115dd46..e43dc08e 100644 --- a/apps/server/internal/vault/vault.go +++ b/apps/server/internal/vault/vault.go @@ -224,17 +224,27 @@ func cloneSettings(settings VaultSettings) VaultSettings { for key, value := range settings.FolderIcons { folderIcons[key] = value } + dailyLegacyPatterns := make([]DateNotePatternSettings, len(settings.DailyNotes.LegacyPatterns)) + copy(dailyLegacyPatterns, settings.DailyNotes.LegacyPatterns) + weeklyLegacyPatterns := make([]DateNotePatternSettings, len(settings.WeeklyNotes.LegacyPatterns)) + copy(weeklyLegacyPatterns, settings.WeeklyNotes.LegacyPatterns) return VaultSettings{ PrimaryNotesLocation: settings.PrimaryNotesLocation, DailyNotes: DailyNotesSettings{ - Enabled: settings.DailyNotes.Enabled, - Directory: settings.DailyNotes.Directory, - TemplateID: settings.DailyNotes.TemplateID, + Enabled: settings.DailyNotes.Enabled, + Directory: settings.DailyNotes.Directory, + TitlePattern: settings.DailyNotes.TitlePattern, + Locale: settings.DailyNotes.Locale, + LegacyPatterns: dailyLegacyPatterns, + TemplateID: settings.DailyNotes.TemplateID, }, WeeklyNotes: WeeklyNotesSettings{ - Enabled: settings.WeeklyNotes.Enabled, - Directory: settings.WeeklyNotes.Directory, - TemplateID: settings.WeeklyNotes.TemplateID, + Enabled: settings.WeeklyNotes.Enabled, + Directory: settings.WeeklyNotes.Directory, + TitlePattern: settings.WeeklyNotes.TitlePattern, + Locale: settings.WeeklyNotes.Locale, + LegacyPatterns: weeklyLegacyPatterns, + TemplateID: settings.WeeklyNotes.TemplateID, }, FolderIcons: folderIcons, } @@ -248,6 +258,22 @@ func normalizeDailyNotesDirectory(value string) string { return trimmed } +func normalizeDailyNoteTitlePattern(value string) string { + trimmed := strings.TrimSpace(strings.NewReplacer("/", "-", "\\", "-").Replace(value)) + if trimmed == "" { + return DefaultDailyNoteTitlePattern + } + return trimmed +} + +func normalizeDailyNoteLocale(value string) string { + trimmed := strings.TrimSpace(value) + if trimmed == "" { + return DefaultDailyNoteLocale + } + return trimmed +} + func normalizeWeeklyNotesDirectory(value string) string { trimmed := strings.Trim(value, "/") if trimmed == "" { @@ -256,6 +282,60 @@ func normalizeWeeklyNotesDirectory(value string) string { return trimmed } +func normalizeWeeklyNoteTitlePattern(value string) string { + trimmed := strings.TrimSpace(strings.NewReplacer("/", "-", "\\", "-").Replace(value)) + if trimmed == "" { + return DefaultWeeklyNoteTitlePattern + } + return trimmed +} + +func normalizeWeeklyNoteLocale(value string) string { + trimmed := strings.TrimSpace(value) + if trimmed == "" { + return DefaultWeeklyNoteLocale + } + return trimmed +} + +func normalizeDailyNoteLegacyPatterns(value []DateNotePatternSettings) []DateNotePatternSettings { + out := []DateNotePatternSettings{} + seen := map[string]bool{} + for _, pattern := range value { + next := DateNotePatternSettings{ + Directory: normalizeDailyNotesDirectory(pattern.Directory), + TitlePattern: normalizeDailyNoteTitlePattern(pattern.TitlePattern), + Locale: normalizeDailyNoteLocale(pattern.Locale), + } + key := next.Directory + "\x00" + next.TitlePattern + "\x00" + next.Locale + if seen[key] { + continue + } + seen[key] = true + out = append(out, next) + } + return out +} + +func normalizeWeeklyNoteLegacyPatterns(value []DateNotePatternSettings) []DateNotePatternSettings { + out := []DateNotePatternSettings{} + seen := map[string]bool{} + for _, pattern := range value { + next := DateNotePatternSettings{ + Directory: normalizeWeeklyNotesDirectory(pattern.Directory), + TitlePattern: normalizeWeeklyNoteTitlePattern(pattern.TitlePattern), + Locale: normalizeWeeklyNoteLocale(pattern.Locale), + } + key := next.Directory + "\x00" + next.TitlePattern + "\x00" + next.Locale + if seen[key] { + continue + } + seen[key] = true + out = append(out, next) + } + return out +} + func normalizePrimaryNotesLocation(value PrimaryNotesLocation) PrimaryNotesLocation { if value == PrimaryNotesRoot { return PrimaryNotesRoot @@ -282,14 +362,20 @@ func normalizeVaultSettings(value VaultSettings, fallbackPrimary PrimaryNotesLoc return value.PrimaryNotesLocation }()), DailyNotes: DailyNotesSettings{ - Enabled: value.DailyNotes.Enabled, - Directory: normalizeDailyNotesDirectory(value.DailyNotes.Directory), - TemplateID: value.DailyNotes.TemplateID, + Enabled: value.DailyNotes.Enabled, + Directory: normalizeDailyNotesDirectory(value.DailyNotes.Directory), + TitlePattern: normalizeDailyNoteTitlePattern(value.DailyNotes.TitlePattern), + Locale: normalizeDailyNoteLocale(value.DailyNotes.Locale), + LegacyPatterns: normalizeDailyNoteLegacyPatterns(value.DailyNotes.LegacyPatterns), + TemplateID: value.DailyNotes.TemplateID, }, WeeklyNotes: WeeklyNotesSettings{ - Enabled: value.WeeklyNotes.Enabled, - Directory: normalizeWeeklyNotesDirectory(value.WeeklyNotes.Directory), - TemplateID: value.WeeklyNotes.TemplateID, + Enabled: value.WeeklyNotes.Enabled, + Directory: normalizeWeeklyNotesDirectory(value.WeeklyNotes.Directory), + TitlePattern: normalizeWeeklyNoteTitlePattern(value.WeeklyNotes.TitlePattern), + Locale: normalizeWeeklyNoteLocale(value.WeeklyNotes.Locale), + LegacyPatterns: normalizeWeeklyNoteLegacyPatterns(value.WeeklyNotes.LegacyPatterns), + TemplateID: value.WeeklyNotes.TemplateID, }, FolderIcons: folderIcons, } diff --git a/apps/server/internal/vault/vault_test.go b/apps/server/internal/vault/vault_test.go index b884e751..5ef18368 100644 --- a/apps/server/internal/vault/vault_test.go +++ b/apps/server/internal/vault/vault_test.go @@ -549,8 +549,20 @@ func TestVaultSettingsWeeklyNotesRoundTrip(t *testing.T) { // the toggle always reverted after a reload. (#117) if _, err := v.SetSettings(VaultSettings{ PrimaryNotesLocation: PrimaryNotesInbox, - DailyNotes: DailyNotesSettings{Enabled: true, Directory: "Daily", TemplateID: "daily-tmpl"}, - WeeklyNotes: WeeklyNotesSettings{Enabled: true, Directory: "My Weeks", TemplateID: "weekly-tmpl"}, + DailyNotes: DailyNotesSettings{ + Enabled: true, + Directory: "Daily", + TitlePattern: "yyyy-MM-dd-EEE", + Locale: "pt-BR", + TemplateID: "daily-tmpl", + }, + WeeklyNotes: WeeklyNotesSettings{ + Enabled: true, + Directory: "My Weeks", + TitlePattern: "yyyy-'W'ww-EEE", + Locale: "en-US", + TemplateID: "weekly-tmpl", + }, }); err != nil { t.Fatal(err) } @@ -568,9 +580,21 @@ func TestVaultSettingsWeeklyNotesRoundTrip(t *testing.T) { if got.WeeklyNotes.TemplateID != "weekly-tmpl" { t.Errorf("weekly templateId = %q, want %q", got.WeeklyNotes.TemplateID, "weekly-tmpl") } + if got.WeeklyNotes.TitlePattern != "yyyy-'W'ww-EEE" { + t.Errorf("weekly titlePattern = %q, want %q", got.WeeklyNotes.TitlePattern, "yyyy-'W'ww-EEE") + } + if got.WeeklyNotes.Locale != "en-US" { + t.Errorf("weekly locale = %q, want %q", got.WeeklyNotes.Locale, "en-US") + } if got.DailyNotes.TemplateID != "daily-tmpl" { t.Errorf("daily templateId = %q, want %q", got.DailyNotes.TemplateID, "daily-tmpl") } + if got.DailyNotes.TitlePattern != "yyyy-MM-dd-EEE" { + t.Errorf("daily titlePattern = %q, want %q", got.DailyNotes.TitlePattern, "yyyy-MM-dd-EEE") + } + if got.DailyNotes.Locale != "pt-BR" { + t.Errorf("daily locale = %q, want %q", got.DailyNotes.Locale, "pt-BR") + } // The key must actually reach vault.json — the original bug was that it // never hit disk. diff --git a/docs/reference/settings-reference.md b/docs/reference/settings-reference.md index febda672..4dc411ac 100644 --- a/docs/reference/settings-reference.md +++ b/docs/reference/settings-reference.md @@ -175,9 +175,55 @@ Daily notes are optional. Related settings include: - enable/disable daily notes -- daily notes directory +- daily notes directory pattern +- daily note naming pattern +- daily note locale -When enabled, ZenNotes can open or create today's note using an ISO-style date title. +The directory pattern is stored inside the primary notes area. The default is `Daily Notes`. + +The naming pattern is used as the daily note title and filename. The default is `yyyy-MM-dd`. + +Supported pattern tokens: + +| Token | Example | Meaning | +| --- | --- | --- | +| `yyyy` | `2026` | 4-digit year; ISO week-year for weekly notes | +| `yy` | `26` | 2-digit year | +| `M` | `6` | month | +| `MM` | `06` | padded month | +| `MMM` | `Jun` | short month name | +| `MMMM` | `June` | full month name | +| `d` | `9` | day of month | +| `dd` | `09` | padded day of month | +| `EEE` | `Tue` | short weekday name | +| `EEEE` | `Tuesday` | full weekday name | +| `w` | `24` | ISO week number | +| `ww` | `24` | padded ISO week number | + +Wrap literal text in single quotes, for example `'Daily Notes'/yyyy/MM-MMM`. + +The locale controls localized month and weekday names. Use `system`, `en-US`, `pt-BR`, or another BCP 47 locale. + +For example, directory `yyyy/MM-MMM`, naming `yyyy-MM-dd-EEE`, and locale `en-US` creates a note like `2026/06-Jun/2026-06-09-Tue.md`. + +### Weekly notes + +Weekly notes are optional. + +Related settings include: + +- enable/disable weekly notes +- weekly notes directory pattern +- weekly note naming pattern +- weekly note locale + +The directory pattern is stored inside the primary notes area. The default is `Weekly Notes`. + +The naming pattern is used as the weekly note title and filename. The default is `yyyy-'W'ww`. + +Weekly notes use the same pattern tokens as daily notes, plus `w` and `ww` for ISO week numbers. Weekly date tokens render from the ISO week's Monday, and `yyyy` is the ISO week-year. + +For example, directory `yyyy/MM-MMM`, naming `yyyy-'W'ww-EEE`, and locale `en-US` creates a note like `2026/06-Jun/2026-W24-Mon.md`. ### Quick Notes label diff --git a/docs/reference/vault-and-folder-model.md b/docs/reference/vault-and-folder-model.md index 7a7d776a..013451f6 100644 --- a/docs/reference/vault-and-folder-model.md +++ b/docs/reference/vault-and-folder-model.md @@ -81,15 +81,22 @@ Trash: - allows restore - can be emptied -## Daily notes +## Daily and weekly notes -Daily notes are optional. +Daily and weekly notes are optional. When enabled: - ZenNotes creates one note per day -- title format is an ISO date -- notes live in a dedicated daily notes directory under the primary notes area +- ZenNotes creates one note per ISO week +- the default daily title format is an ISO date +- the default weekly title format is an ISO week title +- notes live in dedicated date-note directories under the primary notes area +- the directory and title can use date patterns such as `yyyy/MM-MMM` and `yyyy-MM-dd-EEE` +- weekly title patterns can use ISO week tokens such as `yyyy-'W'ww` +- localized month and weekday names can use `system`, `en-US`, `pt-BR`, or another BCP 47 locale + +Supported pattern tokens are `yyyy`, `yy`, `M`, `MM`, `MMM`, `MMMM`, `d`, `dd`, `EEE`, `EEEE`, `w`, and `ww`. Weekly notes render date tokens from the ISO week's Monday, and `yyyy` is the ISO week-year for weekly patterns. Literal words can be wrapped in single quotes, for example `'Daily Notes'/yyyy/MM-MMM`. ## Assets and local files diff --git a/packages/app-core/src/components/CalendarPanel.tsx b/packages/app-core/src/components/CalendarPanel.tsx index 038860c0..217cd081 100644 --- a/packages/app-core/src/components/CalendarPanel.tsx +++ b/packages/app-core/src/components/CalendarPanel.tsx @@ -138,8 +138,8 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { // eslint-disable-next-line react-hooks/exhaustive-deps }, [note.path]) - // title -> NoteMeta for the daily/weekly notes that exist on disk. - const { dailyByTitle, weeklyByTitle } = useMemo( + // Calendar key -> NoteMeta for the daily/weekly notes that exist on disk. + const { dailyByDate, weeklyByWeek } = useMemo( () => buildDateNoteIndexes(notes, settings), [notes, settings] ) @@ -165,10 +165,10 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { list.push(n) } } - for (const d of grid) push(dailyByTitle.get(isoDateStr(d))) - for (const { monday } of rows) push(weeklyByTitle.get(isoWeekStr(monday))) + for (const d of grid) push(dailyByDate.get(isoDateStr(d))) + for (const { monday } of rows) push(weeklyByWeek.get(isoWeekStr(monday))) return list - }, [grid, rows, dailyByTitle, weeklyByTitle]) + }, [grid, rows, dailyByDate, weeklyByWeek]) const [stats, setStats] = useState>(new Map()) const statsRef = useRef(stats) @@ -213,7 +213,7 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { const handleDayClick = useCallback( async (day: Date, iso: string) => { if (!dailyEnabled) return - if (dailyByTitle.has(iso)) { + if (dailyByDate.has(iso)) { await openDailyNoteForDate(day) return } @@ -225,13 +225,13 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { }) if (ok) await openDailyNoteForDate(day) }, - [dailyEnabled, dailyByTitle, openDailyNoteForDate] + [dailyEnabled, dailyByDate, openDailyNoteForDate] ) const handleWeekClick = useCallback( async (monday: Date, weekIso: string) => { if (!weeklyEnabled) return - if (weeklyByTitle.has(weekIso)) { + if (weeklyByWeek.has(weekIso)) { await openWeeklyNoteForDate(monday) return } @@ -243,7 +243,7 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { }) if (ok) await openWeeklyNoteForDate(monday) }, - [weeklyEnabled, weeklyByTitle, openWeeklyNoteForDate] + [weeklyEnabled, weeklyByWeek, openWeeklyNoteForDate] ) // --- Hover preview ------------------------------------------------------- @@ -274,7 +274,7 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { if (!dailyEnabled) return e.preventDefault() clearHover() - const meta = dailyByTitle.get(iso) + const meta = dailyByDate.get(iso) const items: ContextMenuItem[] = meta ? [ { label: 'Open note', onSelect: () => void openDailyNoteForDate(day) }, @@ -284,14 +284,14 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { : [{ label: `Create ${iso}`, onSelect: () => void handleDayClick(day, iso) }] setMenu({ x: e.clientX, y: e.clientY, items }) }, - [dailyEnabled, dailyByTitle, openDailyNoteForDate, handleDayClick, trashNote, clearHover] + [dailyEnabled, dailyByDate, openDailyNoteForDate, handleDayClick, trashNote, clearHover] ) const openWeekMenu = useCallback( (e: React.MouseEvent, monday: Date, weekIso: string) => { if (!weeklyEnabled) return e.preventDefault() clearHover() - const meta = weeklyByTitle.get(weekIso) + const meta = weeklyByWeek.get(weekIso) const items: ContextMenuItem[] = meta ? [ { label: 'Open note', onSelect: () => void openWeeklyNoteForDate(monday) }, @@ -306,7 +306,7 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { ] setMenu({ x: e.clientX, y: e.clientY, items }) }, - [weeklyEnabled, weeklyByTitle, openWeeklyNoteForDate, handleWeekClick, trashNote, clearHover] + [weeklyEnabled, weeklyByWeek, openWeeklyNoteForDate, handleWeekClick, trashNote, clearHover] ) const atRefMonth = @@ -408,7 +408,7 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { const weekIso = isoWeekStr(monday) const weekNum = getISOWeek(monday) const isActiveWeek = weekIso === activeWeekIso - const weekMeta = weeklyByTitle.get(weekIso) + const weekMeta = weeklyByWeek.get(weekIso) const weekDots = dotsFor(weekMeta ? stats.get(weekMeta.path) : undefined) const weekTasks = weekMeta ? stats.get(weekMeta.path)?.openTasks ?? 0 : 0 @@ -457,7 +457,7 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { const inMonth = day.getMonth() === anchorMonth const isActiveDay = iso === activeDayIso const isToday = iso === todayIso - const dayMeta = dailyByTitle.get(iso) + const dayMeta = dailyByDate.get(iso) const dayStats = dayMeta ? stats.get(dayMeta.path) : undefined const dots = dotsFor(dayStats) const openTasks = dayStats?.openTasks ?? 0 diff --git a/packages/app-core/src/components/OnboardingWizard.tsx b/packages/app-core/src/components/OnboardingWizard.tsx index 6ba7eb5e..5e622f93 100644 --- a/packages/app-core/src/components/OnboardingWizard.tsx +++ b/packages/app-core/src/components/OnboardingWizard.tsx @@ -798,6 +798,7 @@ function LayoutStep({ ...current, primaryNotesLocation: patch.primaryNotesLocation ?? current.primaryNotesLocation, dailyNotes: { + ...current.dailyNotes, enabled: patch.dailyEnabled ?? current.dailyNotes.enabled, directory: patch.dailyDirectory !== undefined diff --git a/packages/app-core/src/components/SettingsModal.tsx b/packages/app-core/src/components/SettingsModal.tsx index acfa0345..27b1f63d 100644 --- a/packages/app-core/src/components/SettingsModal.tsx +++ b/packages/app-core/src/components/SettingsModal.tsx @@ -1,6 +1,13 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { createPortal } from 'react-dom' -import { DEFAULT_DAILY_NOTES_DIRECTORY, DEFAULT_WEEKLY_NOTES_DIRECTORY } from '@shared/ipc' +import { + DEFAULT_DAILY_NOTE_LOCALE, + DEFAULT_DAILY_NOTE_TITLE_PATTERN, + DEFAULT_DAILY_NOTES_DIRECTORY, + DEFAULT_WEEKLY_NOTE_LOCALE, + DEFAULT_WEEKLY_NOTE_TITLE_PATTERN, + DEFAULT_WEEKLY_NOTES_DIRECTORY +} from '@shared/ipc' import type { AppUpdateState, CliInstallStatus, @@ -36,7 +43,14 @@ import { DEFAULT_SYSTEM_FOLDER_LABELS, getSystemFolderLabel } from '../lib/system-folder-labels' -import { normalizeDailyNotesDirectory, normalizeWeeklyNotesDirectory } from '../lib/vault-layout' +import { + normalizeDailyNoteLocale, + normalizeDailyNotesDirectory, + normalizeDailyNoteTitlePattern, + normalizeWeeklyNoteLocale, + normalizeWeeklyNoteTitlePattern, + normalizeWeeklyNotesDirectory +} from '../lib/vault-layout' import { BUILTIN_TEMPLATES } from '@shared/builtin-templates' import { composeTemplateFile, mergeTemplates } from '@shared/template-files' import { TemplateEditorModal } from './TemplateEditorModal' @@ -1381,9 +1395,33 @@ export function SettingsModal(): JSX.Element { }, { id: 'daily-notes-directory', - title: 'Daily notes directory', + title: 'Daily notes directory pattern', description: 'Stored inside your primary notes area.', - keywords: ['daily notes', 'directory', 'folder'] + keywords: ['daily notes', 'directory', 'folder', 'pattern', 'date'] + }, + { + id: 'daily-note-title-pattern', + title: 'Daily note naming pattern', + description: 'Used as the daily note title and filename.', + keywords: ['daily notes', 'title', 'filename', 'pattern', 'date'] + }, + { + id: 'daily-note-locale', + title: 'Daily note locale', + description: 'Used for localized month and weekday names in daily note patterns.', + keywords: ['daily notes', 'locale', 'month', 'weekday', 'pattern'] + }, + { + id: 'daily-note-pattern-support', + title: 'Supported date note pattern tokens', + description: 'Reference for supported date tokens, quoted literals, and example outputs.', + keywords: ['daily notes', 'weekly notes', 'pattern', 'tokens', 'format', 'yyyy', 'mmm', 'weekday', 'week'] + }, + { + id: 'daily-note-pattern-reset', + title: 'Reset daily note patterns to defaults', + description: 'Restore the daily directory, naming, and locale patterns to their defaults.', + keywords: ['daily notes', 'reset', 'default', 'defaults', 'restore', 'pattern'] }, { id: 'open-todays-daily-note', @@ -1405,9 +1443,33 @@ export function SettingsModal(): JSX.Element { }, { id: 'weekly-notes-directory', - title: 'Weekly notes directory', + title: 'Weekly notes directory pattern', description: 'Stored inside your primary notes area.', - keywords: ['weekly notes', 'directory', 'folder'] + keywords: ['weekly notes', 'directory', 'folder', 'pattern', 'date', 'week'] + }, + { + id: 'weekly-note-title-pattern', + title: 'Weekly note naming pattern', + description: 'Used as the weekly note title and filename.', + keywords: ['weekly notes', 'title', 'filename', 'pattern', 'date', 'week'] + }, + { + id: 'weekly-note-locale', + title: 'Weekly note locale', + description: 'Used for localized month and weekday names in weekly note patterns.', + keywords: ['weekly notes', 'locale', 'month', 'weekday', 'pattern'] + }, + { + id: 'weekly-note-pattern-support', + title: 'Supported date note pattern tokens', + description: 'Reference for supported date tokens, ISO week tokens, quoted literals, and example outputs.', + keywords: ['weekly notes', 'pattern', 'tokens', 'format', 'yyyy', 'ww', 'iso week'] + }, + { + id: 'weekly-note-pattern-reset', + title: 'Reset weekly note patterns to defaults', + description: 'Restore the weekly directory, naming, and locale patterns to their defaults.', + keywords: ['weekly notes', 'reset', 'default', 'defaults', 'restore', 'pattern'] }, { id: 'weekly-notes-template', @@ -1648,7 +1710,7 @@ export function SettingsModal(): JSX.Element { } /> + + void persistVaultSettings({ + ...vaultSettings, + dailyNotes: { + ...vaultSettings.dailyNotes, + titlePattern: normalizeDailyNoteTitlePattern(next) + } + }) + } + /> + + void persistVaultSettings({ + ...vaultSettings, + dailyNotes: { + ...vaultSettings.dailyNotes, + locale: normalizeDailyNoteLocale(next) + } + }) + } + /> + + void persistVaultSettings({ + ...vaultSettings, + dailyNotes: { + ...vaultSettings.dailyNotes, + directory: DEFAULT_DAILY_NOTES_DIRECTORY, + titlePattern: DEFAULT_DAILY_NOTE_TITLE_PATTERN, + locale: DEFAULT_DAILY_NOTE_LOCALE + } + }) + } + /> + + + void persistVaultSettings({ + ...vaultSettings, + weeklyNotes: { + ...vaultSettings.weeklyNotes, + titlePattern: normalizeWeeklyNoteTitlePattern(next) + } + }) + } + /> + + void persistVaultSettings({ + ...vaultSettings, + weeklyNotes: { + ...vaultSettings.weeklyNotes, + locale: normalizeWeeklyNoteLocale(next) + } + }) + } + /> + + void persistVaultSettings({ + ...vaultSettings, + weeklyNotes: { + ...vaultSettings.weeklyNotes, + directory: DEFAULT_WEEKLY_NOTES_DIRECTORY, + titlePattern: DEFAULT_WEEKLY_NOTE_TITLE_PATTERN, + locale: DEFAULT_WEEKLY_NOTE_LOCALE + } + }) + } + /> +
Open this week's note
- Opens this week's note if it exists, otherwise creates it with a YYYY-Www title. + Opens this week's note if it exists, otherwise creates it with the configured weekly title pattern.
+ + ) +} + const DEFAULT_QUICK_CAPTURE_HOTKEY = 'CommandOrControl+Shift+Space' function toElectronAccelerator(binding: string): string { diff --git a/packages/app-core/src/components/Sidebar.tsx b/packages/app-core/src/components/Sidebar.tsx index 2813bc4d..ef5d9e3b 100644 --- a/packages/app-core/src/components/Sidebar.tsx +++ b/packages/app-core/src/components/Sidebar.tsx @@ -42,9 +42,12 @@ import { confirmApp } from '../lib/confirm-requests' import { promptApp } from '../lib/prompt-requests' import { resolveQuickNoteTitle } from "../lib/quick-note-title"; import { recordRendererPerf } from "../lib/perf"; +import { DEFAULT_DAILY_NOTES_DIRECTORY, DEFAULT_WEEKLY_NOTES_DIRECTORY } from "@shared/ipc"; import { assetFolderSubpath, classifyDateNote, + dateNoteFolderMayBelongToDatePattern, + dateNoteDirectoryDisplayLabel, folderIconKey, isPrimaryNotesAtRoot, folderForVaultRelativePath, @@ -79,6 +82,7 @@ import { } from "../lib/sidebar-scroll"; import { buildVaultSwitcherEntries } from "../lib/vault-switcher"; import { appUpdateBadgeLabel, useAppUpdateState } from "../lib/app-update-state"; +import { getISOWeekYear } from "../lib/template-render"; const ACTIVE_TAG_PARSE_DELAY_MS = 220; const ACTIVE_TAG_PARSE_LARGE_BODY_CHARS = 120_000; @@ -935,6 +939,23 @@ export function Sidebar(): JSX.Element { // separately. const trees = useMemo(() => { const startedAt = performance.now(); + const ds = normalizeVaultSettings(vaultSettings); + const dateNotePaths = new Set(); + const dateFolderSubpaths = new Set(); + if (ds.dailyNotes.enabled || ds.weeklyNotes.enabled) { + for (const note of notes) { + if (note.folder !== "inbox") continue; + const info = classifyDateNote(note, ds); + if (!info) continue; + dateNotePaths.add(note.path); + addSubpathAndAncestors(dateFolderSubpaths, noteFolderSubpath(note, ds)); + } + for (const folder of allFolders) { + if (folder.folder !== "inbox") continue; + if (!dateNoteFolderMayBelongToDatePattern(folder.subpath, ds)) continue; + addSubpathAndAncestors(dateFolderSubpaths, folder.subpath); + } + } const next = { quick: buildTree( notes.filter((n) => n.folder === "quick"), @@ -946,7 +967,7 @@ export function Sidebar(): JSX.Element { vaultSettings, ), inbox: buildTree( - notes.filter((n) => n.folder === "inbox"), + notes.filter((n) => n.folder === "inbox" && !dateNotePaths.has(n.path)), assetFiles.filter( (asset) => folderForVaultRelativePath(asset.path, vaultSettings) === "inbox", ), @@ -973,17 +994,13 @@ export function Sidebar(): JSX.Element { vaultSettings, ), }; - // Daily/Weekly directories are surfaced in their own pinned, date-grouped - // section above NOTES (see DateNotesNav), so drop them from the inbox tree - // to avoid showing them twice. - const ds = normalizeVaultSettings(vaultSettings); - const hideSubpaths = new Set(); - if (ds.dailyNotes.enabled) hideSubpaths.add(ds.dailyNotes.directory); - if (ds.weeklyNotes.enabled) hideSubpaths.add(ds.weeklyNotes.directory); - if (hideSubpaths.size) { + // Daily/weekly notes are surfaced in their own pinned, date-grouped section + // above NOTES. Remove only the empty folder spine left behind by those date + // notes so unrelated files under the same year/month folders remain visible. + if (dateFolderSubpaths.size) { next.inbox = { ...next.inbox, - children: next.inbox.children.filter((c) => !hideSubpaths.has(c.subpath)), + children: pruneEmptyDateNoteFolders(next.inbox.children, dateFolderSubpaths), }; } recordRendererPerf("sidebar.tree-build", performance.now() - startedAt, { @@ -1003,12 +1020,15 @@ export function Sidebar(): JSX.Element { const daily: { year: number; total: number; months: { month: number; notes: NoteMeta[] }[] }[] = []; const weekly: { year: number; notes: NoteMeta[] }[] = []; + const dailyTimes = new Map(); + const weeklyTimes = new Map(); if (s.dailyNotes.enabled) { const byYear = new Map>(); for (const n of notes) { const info = classifyDateNote(n, s); if (info?.kind !== "daily") continue; + dailyTimes.set(n.path, info.date.getTime()); const year = info.date.getFullYear(); const month = info.date.getMonth(); let months = byYear.get(year); @@ -1020,7 +1040,11 @@ export function Sidebar(): JSX.Element { const mlist = [...months.entries()] .sort((a, b) => b[0] - a[0]) .map(([month, ns]) => { - ns.sort((a, b) => b.title.localeCompare(a.title)); + ns.sort( + (a, b) => + (dailyTimes.get(b.path) ?? 0) - (dailyTimes.get(a.path) ?? 0) || + b.title.localeCompare(a.title), + ); total += ns.length; return { month, notes: ns }; }); @@ -1033,11 +1057,16 @@ export function Sidebar(): JSX.Element { for (const n of notes) { const info = classifyDateNote(n, s); if (info?.kind !== "weekly") continue; - const year = Number(n.title.slice(0, 4)); + weeklyTimes.set(n.path, info.date.getTime()); + const year = getISOWeekYear(info.date); (byYear.get(year) ?? byYear.set(year, []).get(year)!).push(n); } for (const [year, ns] of [...byYear.entries()].sort((a, b) => b[0] - a[0])) { - ns.sort((a, b) => b.title.localeCompare(a.title)); + ns.sort( + (a, b) => + (weeklyTimes.get(b.path) ?? 0) - (weeklyTimes.get(a.path) ?? 0) || + b.title.localeCompare(a.title), + ); weekly.push({ year, notes: ns }); } } @@ -1047,8 +1076,8 @@ export function Sidebar(): JSX.Element { weeklyEnabled: s.weeklyNotes.enabled, dailyDir, weeklyDir, - dailyLabel: dailyDir.split("/").pop() || dailyDir, - weeklyLabel: weeklyDir.split("/").pop() || weeklyDir, + dailyLabel: dateNoteDirectoryDisplayLabel(dailyDir, DEFAULT_DAILY_NOTES_DIRECTORY), + weeklyLabel: dateNoteDirectoryDisplayLabel(weeklyDir, DEFAULT_WEEKLY_NOTES_DIRECTORY), daily, weekly, dailyTotal: daily.reduce((sum, y) => sum + y.total, 0), @@ -3285,6 +3314,30 @@ function buildTree( return root; } +function addSubpathAndAncestors(target: Set, subpath: string): void { + const parts = subpath.split("/").filter(Boolean); + let acc = ""; + for (const part of parts) { + acc = acc ? `${acc}/${part}` : part; + target.add(acc); + } +} + +function pruneEmptyDateNoteFolders( + children: TreeNode[], + dateFolderSubpaths: Set, +): TreeNode[] { + return children + .map((child) => ({ + ...child, + children: pruneEmptyDateNoteFolders(child.children, dateFolderSubpaths), + })) + .filter((child) => { + if (!dateFolderSubpaths.has(child.subpath)) return true; + return child.notes.length > 0 || child.assets.length > 0 || child.children.length > 0; + }); +} + function getTreeRenderEntries( node: TreeNode, showNotes: boolean, diff --git a/packages/app-core/src/lib/help.ts b/packages/app-core/src/lib/help.ts index 9592ce1b..6f0e5347 100644 --- a/packages/app-core/src/lib/help.ts +++ b/packages/app-core/src/lib/help.ts @@ -492,7 +492,7 @@ export const HELP_VIM_COMMANDS: HelpExCommand[] = [ { command: ':weekly', summary: "Open this week's note", - detail: 'Open or create this week’s note with a YYYY-Www title (requires weekly notes enabled in Settings → Vault). Uses the assigned weekly template if one is set.' + detail: 'Open or create this week’s note with the configured weekly note pattern (requires weekly notes enabled in Settings → Vault). Uses the assigned weekly template if one is set.' }, { command: ':tag foo bar', @@ -710,8 +710,8 @@ export const HELP_SETTINGS: HelpSettingsSection[] = [ items: [ { label: 'Vault location', detail: 'Reveal or change the root folder ZenNotes treats as the active vault.' }, { label: 'Primary notes location', detail: 'Treat `inbox/` as the main notes area, or use the vault root directly for an Obsidian-style flat vault.' }, - { label: 'Daily notes', detail: 'Enable a daily-notes workflow, choose its directory, and assign a template so each day’s note starts pre-filled. Open today’s note with `Space d`, `:daily`, or the command palette.' }, - { label: 'Weekly notes', detail: 'Enable weekly notes with a YYYY-Www title, choose a directory, and assign a template. Open this week’s note with `Space w`, `:weekly`, or the command palette.' }, + { label: 'Daily notes', detail: "Enable a daily-notes workflow, choose a directory pattern, naming pattern, locale, and template so each day’s note starts in the right place. Supported tokens are `yyyy`, `yy`, `M`, `MM`, `MMM`, `MMMM`, `d`, `dd`, `EEE`, `EEEE`, `w`, and `ww`; quote literal words like `'Daily Notes'/yyyy/MM-MMM`. Open today’s note with `Space d`, `:daily`, or the command palette." }, + { label: 'Weekly notes', detail: "Enable weekly notes with a directory pattern, naming pattern, locale, and template. Weekly patterns support the same tokens as daily notes plus ISO week `w` and `ww`; the default title pattern is `yyyy-'W'ww`. Open this week’s note with `Space w`, `:weekly`, or the command palette." }, { label: 'System folder labels', detail: 'Rename how Inbox, Quick Notes, Archive, and Trash appear in the UI without renaming the real folders on disk.' } ] }, diff --git a/packages/app-core/src/lib/vault-layout.test.ts b/packages/app-core/src/lib/vault-layout.test.ts index b5ba4276..c35227cd 100644 --- a/packages/app-core/src/lib/vault-layout.test.ts +++ b/packages/app-core/src/lib/vault-layout.test.ts @@ -1,6 +1,12 @@ import { describe, expect, it } from 'vitest' import type { NoteMeta, VaultSettings } from '@shared/ipc' -import { classifyDateNote } from './vault-layout' +import { + classifyDateNote, + dailyNoteLocationForDate, + dateNoteFolderMayBelongToDatePattern, + dateNoteDirectoryDisplayLabel, + weeklyNoteLocationForDate +} from './vault-layout' function note(path: string, title: string): NoteMeta { return { @@ -47,4 +53,357 @@ describe('classifyDateNote', () => { expect(info).toMatchObject({ kind: 'weekly' }) expect(info?.date).toEqual(new Date(2026, 5, 8)) }) + + it('recognizes weekly notes created from date-based directory and title patterns', () => { + const info = classifyDateNote( + note('inbox/2026/06-Jun/2026-W24-Mon.md', '2026-W24-Mon'), + { + primaryNotesLocation: 'inbox', + dailyNotes: { enabled: false, directory: 'Daily Notes' }, + weeklyNotes: { + enabled: true, + directory: 'yyyy/MM-MMM', + titlePattern: "yyyy-'W'ww-EEE", + locale: 'en-US' + }, + folderIcons: {} + } as VaultSettings + ) + + expect(info).toMatchObject({ kind: 'weekly' }) + expect(info?.date).toEqual(new Date(2026, 5, 8)) + }) + + it('recognizes daily notes created from date-based directory and title patterns', () => { + const info = classifyDateNote( + note('inbox/2026/06-Jun/2026-06-09-Tue.md', '2026-06-09-Tue'), + { + primaryNotesLocation: 'inbox', + dailyNotes: { + enabled: true, + directory: 'yyyy/MM-MMM', + titlePattern: 'yyyy-MM-dd-EEE', + locale: 'en-US' + }, + weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, + folderIcons: {} + } as VaultSettings + ) + + expect(info).toMatchObject({ kind: 'daily' }) + expect(info?.date).toEqual(new Date(2026, 5, 9)) + }) + + it('keeps existing lowercase daily directories literal', () => { + const date = new Date(2026, 5, 9) + const vaultSettings = settings('daily', 'Weekly Notes') + + expect(dailyNoteLocationForDate(date, vaultSettings)).toEqual({ + subpath: 'daily', + title: '2026-06-09' + }) + + const info = classifyDateNote(note('inbox/daily/2026-06-09.md', '2026-06-09'), vaultSettings) + + expect(info).toMatchObject({ kind: 'daily' }) + expect(info?.date).toEqual(date) + }) + + it('renders daily note locations from date-based directory and title patterns', () => { + const location = dailyNoteLocationForDate(new Date(2026, 5, 9), { + primaryNotesLocation: 'inbox', + dailyNotes: { + enabled: true, + directory: 'yyyy/MM-MMM', + titlePattern: 'yyyy-MM-dd-EEE', + locale: 'en-US' + }, + weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, + folderIcons: {} + } as VaultSettings) + + expect(location).toEqual({ + subpath: '2026/06-Jun', + title: '2026-06-09-Tue' + }) + }) + + it('supports month-only daily directory patterns when the title supplies the date', () => { + const vaultSettings = { + primaryNotesLocation: 'inbox', + dailyNotes: { + enabled: true, + directory: 'MM-MMM', + titlePattern: 'yyyy-MM-dd', + locale: 'en-US' + }, + weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, + folderIcons: {} + } as VaultSettings + + expect(dailyNoteLocationForDate(new Date(2026, 5, 9), vaultSettings)).toEqual({ + subpath: '06-Jun', + title: '2026-06-09' + }) + + const info = classifyDateNote( + note('inbox/06-Jun/2026-06-09.md', '2026-06-09'), + vaultSettings + ) + + expect(info).toMatchObject({ kind: 'daily' }) + expect(info?.date).toEqual(new Date(2026, 5, 9)) + }) + + it('renders quoted literals inside daily directory patterns', () => { + const location = dailyNoteLocationForDate(new Date(2026, 5, 9), { + primaryNotesLocation: 'inbox', + dailyNotes: { + enabled: true, + directory: "'Daily Notes'/yyyy/MM-MMM", + titlePattern: 'yyyy-MM-dd-EEE', + locale: 'en-US' + }, + weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, + folderIcons: {} + } as VaultSettings) + + expect(location).toEqual({ + subpath: 'Daily Notes/2026/06-Jun', + title: '2026-06-09-Tue' + }) + }) + + it('renders weekly note locations from date-based directory and title patterns', () => { + const location = weeklyNoteLocationForDate(new Date(2026, 5, 9), { + primaryNotesLocation: 'inbox', + dailyNotes: { enabled: false, directory: 'Daily Notes' }, + weeklyNotes: { + enabled: true, + directory: "'Weekly Notes'/yyyy/MM-MMM", + titlePattern: "yyyy-'W'ww-EEE", + locale: 'en-US' + }, + folderIcons: {} + } as VaultSettings) + + expect(location).toEqual({ + subpath: 'Weekly Notes/2026/06-Jun', + title: '2026-W24-Mon' + }) + }) + + it('uses the ISO week-year for weekly pattern years', () => { + const location = weeklyNoteLocationForDate(new Date(2021, 0, 1), { + primaryNotesLocation: 'inbox', + dailyNotes: { enabled: false, directory: 'Daily Notes' }, + weeklyNotes: { + enabled: true, + directory: 'yyyy', + titlePattern: "yyyy-'W'ww", + locale: 'en-US' + }, + folderIcons: {} + } as VaultSettings) + + expect(location).toEqual({ + subpath: '2020', + title: '2020-W53' + }) + }) + + it('supports ISO week-only weekly directory patterns', () => { + const location = weeklyNoteLocationForDate(new Date(2026, 5, 9), { + primaryNotesLocation: 'inbox', + dailyNotes: { enabled: false, directory: 'Daily Notes' }, + weeklyNotes: { + enabled: true, + directory: 'ww', + titlePattern: "yyyy-'W'ww", + locale: 'en-US' + }, + folderIcons: {} + } as VaultSettings) + + expect(location).toEqual({ + subpath: '24', + title: '2026-W24' + }) + }) + + it('keeps existing lowercase weekly directories literal', () => { + const vaultSettings = { + primaryNotesLocation: 'inbox', + dailyNotes: { enabled: false, directory: 'Daily Notes' }, + weeklyNotes: { + enabled: true, + directory: 'week', + titlePattern: "yyyy-'W'ww", + locale: 'en-US' + }, + folderIcons: {} + } as VaultSettings + + expect(weeklyNoteLocationForDate(new Date(2026, 5, 9), vaultSettings)).toEqual({ + subpath: 'week', + title: '2026-W24' + }) + + const info = classifyDateNote(note('inbox/week/2026-W24.md', '2026-W24'), vaultSettings) + + expect(info).toMatchObject({ kind: 'weekly' }) + expect(info?.date).toEqual(new Date(2026, 5, 8)) + }) + + it('recognizes a daily note whose title encodes the day via ISO week and weekday', () => { + // The title carries no day-of-month token; the day is implied by the ISO + // week (`ww`) plus the weekday name (`EEE`). 2026-W24-Fri is Fri Jun 12. + const vaultSettings = { + primaryNotesLocation: 'inbox', + dailyNotes: { + enabled: true, + directory: 'yyyy/MM-MMM', + titlePattern: "yyyy-'W'ww-EEE", + locale: 'en-US' + }, + weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, + folderIcons: {} + } as VaultSettings + + const info = classifyDateNote( + note('inbox/2026/06-Jun/2026-W24-Fri.md', '2026-W24-Fri'), + vaultSettings + ) + + expect(info).toMatchObject({ kind: 'daily' }) + expect(info?.date).toEqual(new Date(2026, 5, 12)) + }) + + it('recognizes a daily note when the year comes only from the directory', () => { + const vaultSettings = { + primaryNotesLocation: 'inbox', + dailyNotes: { + enabled: true, + directory: 'yyyy', + titlePattern: 'MM-dd', + locale: 'en-US' + }, + weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, + folderIcons: {} + } as VaultSettings + + const info = classifyDateNote(note('inbox/2026/06-09.md', '06-09'), vaultSettings) + + expect(info).toMatchObject({ kind: 'daily' }) + expect(info?.date).toEqual(new Date(2026, 5, 9)) + }) + + it('does not classify a note whose directory and title disagree on the date', () => { + const vaultSettings = { + primaryNotesLocation: 'inbox', + dailyNotes: { + enabled: true, + directory: 'yyyy/MM-MMM', + titlePattern: "yyyy-'W'ww-EEE", + locale: 'en-US' + }, + weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, + folderIcons: {} + } as VaultSettings + + // ISO week 24 falls entirely in June, so a July folder cannot round-trip. + const info = classifyDateNote( + note('inbox/2026/07-Jul/2026-W24-Fri.md', '2026-W24-Fri'), + vaultSettings + ) + + expect(info).toBeNull() + }) + + it('recognizes daily notes from legacy patterns after the active pattern changes', () => { + const vaultSettings = { + primaryNotesLocation: 'inbox', + dailyNotes: { + enabled: true, + directory: 'yyyy/MM-MMM', + titlePattern: 'yyyy-MM-dd-EEE', + locale: 'en-US', + legacyPatterns: [ + { directory: 'Daily Notes', titlePattern: 'yyyy-MM-dd', locale: 'en-US' } + ] + }, + weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, + folderIcons: {} + } as VaultSettings + + const info = classifyDateNote( + note('inbox/Daily Notes/2026-06-12.md', '2026-06-12'), + vaultSettings + ) + + expect(info).toMatchObject({ kind: 'daily' }) + expect(info?.date).toEqual(new Date(2026, 5, 12)) + }) + + it('recognizes weekly notes from legacy patterns after the active pattern changes', () => { + const vaultSettings = { + primaryNotesLocation: 'inbox', + dailyNotes: { enabled: false, directory: 'Daily Notes' }, + weeklyNotes: { + enabled: true, + directory: 'yyyy/MM-MMM', + titlePattern: "yyyy-'W'ww-EEE", + locale: 'en-US', + legacyPatterns: [ + { directory: 'Weekly Notes', titlePattern: "yyyy-'W'ww", locale: 'en-US' } + ] + }, + folderIcons: {} + } as VaultSettings + + const info = classifyDateNote( + note('inbox/Weekly Notes/2026-W24.md', '2026-W24'), + vaultSettings + ) + + expect(info).toMatchObject({ kind: 'weekly' }) + expect(info?.date).toEqual(new Date(2026, 5, 8)) + }) +}) + +describe('dateNoteDirectoryDisplayLabel', () => { + it('uses the fallback label for fully date-based directory patterns', () => { + expect(dateNoteDirectoryDisplayLabel('yyyy/MM-MMM', 'Daily Notes')).toBe('Daily Notes') + }) + + it('uses quoted literal directory segments as the label', () => { + expect(dateNoteDirectoryDisplayLabel("'Journal'/yyyy/MM-MMM", 'Daily Notes')).toBe('Journal') + }) + + it('keeps literal legacy directories unchanged', () => { + expect(dateNoteDirectoryDisplayLabel('week', 'Weekly Notes')).toBe('week') + }) +}) + +describe('dateNoteFolderMayBelongToDatePattern', () => { + it('matches active and legacy date-pattern folders for sidebar pruning', () => { + const vaultSettings = { + primaryNotesLocation: 'inbox', + dailyNotes: { + enabled: true, + directory: 'yyyy/MMM-MMM', + titlePattern: 'yyyy-MM-dd-EEE', + locale: 'en-US', + legacyPatterns: [ + { directory: 'Daily Notes', titlePattern: 'yyyy-MM-dd', locale: 'en-US' } + ] + }, + weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, + folderIcons: {} + } as VaultSettings + + expect(dateNoteFolderMayBelongToDatePattern('2026/Jun-Jun', vaultSettings)).toBe(true) + expect(dateNoteFolderMayBelongToDatePattern('Daily Notes', vaultSettings)).toBe(true) + expect(dateNoteFolderMayBelongToDatePattern('Projects', vaultSettings)).toBe(false) + }) }) diff --git a/packages/app-core/src/lib/vault-layout.ts b/packages/app-core/src/lib/vault-layout.ts index b2fb3756..65d377b2 100644 --- a/packages/app-core/src/lib/vault-layout.ts +++ b/packages/app-core/src/lib/vault-layout.ts @@ -1,8 +1,13 @@ import { + DEFAULT_DAILY_NOTE_LOCALE, + DEFAULT_DAILY_NOTE_TITLE_PATTERN, DEFAULT_DAILY_NOTES_DIRECTORY, + DEFAULT_WEEKLY_NOTE_LOCALE, + DEFAULT_WEEKLY_NOTE_TITLE_PATTERN, DEFAULT_WEEKLY_NOTES_DIRECTORY, DEFAULT_VAULT_SETTINGS, type AssetMeta, + type DateNotePatternSettings, type FolderIconId, type NoteFolder, type NoteMeta, @@ -70,6 +75,78 @@ export function normalizeWeeklyNotesDirectory(directory: string | null | undefin return trimmed || DEFAULT_WEEKLY_NOTES_DIRECTORY } +export function normalizeDailyNoteTitlePattern(value: string | null | undefined): string { + const trimmed = (value ?? '').trim().replace(/[\\/]+/g, '-') + return trimmed || DEFAULT_DAILY_NOTE_TITLE_PATTERN +} + +export function normalizeDailyNoteLocale(value: string | null | undefined): string { + const trimmed = (value ?? '').trim() + return trimmed || DEFAULT_DAILY_NOTE_LOCALE +} + +export function normalizeWeeklyNoteTitlePattern(value: string | null | undefined): string { + const trimmed = (value ?? '').trim().replace(/[\\/]+/g, '-') + return trimmed || DEFAULT_WEEKLY_NOTE_TITLE_PATTERN +} + +export function normalizeWeeklyNoteLocale(value: string | null | undefined): string { + const trimmed = (value ?? '').trim() + return trimmed || DEFAULT_WEEKLY_NOTE_LOCALE +} + +function dateNotePatternKey(pattern: DateNotePatternSettings): string { + return `${pattern.directory}\0${pattern.titlePattern ?? ''}\0${pattern.locale ?? ''}` +} + +function normalizeDailyNotePatternHistory( + value: readonly DateNotePatternSettings[] | null | undefined, + primaryNotesLocation: VaultSettings['primaryNotesLocation'] +): DateNotePatternSettings[] { + if (!Array.isArray(value)) return [] + const out: DateNotePatternSettings[] = [] + const seen = new Set() + for (const pattern of value) { + const next = { + directory: normalizePrimaryRelativeSubpath( + normalizeDailyNotesDirectory(pattern?.directory), + { primaryNotesLocation } + ), + titlePattern: normalizeDailyNoteTitlePattern(pattern?.titlePattern), + locale: normalizeDailyNoteLocale(pattern?.locale) + } + const key = dateNotePatternKey(next) + if (seen.has(key)) continue + seen.add(key) + out.push(next) + } + return out +} + +function normalizeWeeklyNotePatternHistory( + value: readonly DateNotePatternSettings[] | null | undefined, + primaryNotesLocation: VaultSettings['primaryNotesLocation'] +): DateNotePatternSettings[] { + if (!Array.isArray(value)) return [] + const out: DateNotePatternSettings[] = [] + const seen = new Set() + for (const pattern of value) { + const next = { + directory: normalizePrimaryRelativeSubpath( + normalizeWeeklyNotesDirectory(pattern?.directory), + { primaryNotesLocation } + ), + titlePattern: normalizeWeeklyNoteTitlePattern(pattern?.titlePattern), + locale: normalizeWeeklyNoteLocale(pattern?.locale) + } + const key = dateNotePatternKey(next) + if (seen.has(key)) continue + seen.add(key) + out.push(next) + } + return out +} + function normalizePrimaryRelativeSubpath( subpath: string, settings: Pick @@ -84,6 +161,284 @@ function normalizeTemplateId(value: string | null | undefined): string | undefin return trimmed || undefined } +function localeArg(locale: string): string | undefined { + return locale === DEFAULT_DAILY_NOTE_LOCALE ? undefined : locale +} + +function localeDatePart( + date: Date, + locale: string, + options: Intl.DateTimeFormatOptions +): string { + try { + return date.toLocaleDateString(localeArg(locale), options) + } catch { + return date.toLocaleDateString(undefined, options) + } +} + +function escapeRegex(value: string): string { + return value.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&') +} + +const DATE_NOTE_PATTERN_TOKENS = [ + 'yyyy', + 'yy', + 'MMMM', + 'MMM', + 'MM', + 'M', + 'dd', + 'd', + 'EEEE', + 'EEE', + 'ww', + 'w' +] as const + +type DateNotePatternToken = (typeof DATE_NOTE_PATTERN_TOKENS)[number] + +type DateNotePatternPart = + | { kind: 'literal'; value: string } + | { kind: 'token'; value: DateNotePatternToken } + +function parseDateNotePattern(pattern: string): DateNotePatternPart[] { + const parts: DateNotePatternPart[] = [] + let literal = '' + let quoted = false + + const flushLiteral = (): void => { + if (!literal) return + parts.push({ kind: 'literal', value: literal }) + literal = '' + } + + for (let i = 0; i < pattern.length; ) { + const ch = pattern[i] + if (ch === "'") { + if (pattern[i + 1] === "'") { + literal += "'" + i += 2 + continue + } + quoted = !quoted + i += 1 + continue + } + + if (!quoted) { + const token = DATE_NOTE_PATTERN_TOKENS.find((candidate) => + pattern.startsWith(candidate, i) + ) + if (token) { + flushLiteral() + parts.push({ kind: 'token', value: token }) + i += token.length + continue + } + } + + literal += ch + i += 1 + } + flushLiteral() + return parts +} + +function pad2(value: number): string { + return String(value).padStart(2, '0') +} + +function formatDateNotePattern( + date: Date, + pattern: string, + locale: string, + parts: { year?: number; week?: number } = {} +): string { + const year = parts.year ?? date.getFullYear() + const week = parts.week ?? getISOWeek(date) + const month = date.getMonth() + 1 + const day = date.getDate() + return parseDateNotePattern(pattern) + .map((part) => { + if (part.kind === 'literal') return part.value + switch (part.value) { + case 'yyyy': + return String(year) + case 'yy': + return pad2(year % 100) + case 'MMMM': + return localeDatePart(date, locale, { month: 'long' }) + case 'MMM': + return localeDatePart(date, locale, { month: 'short' }) + case 'MM': + return pad2(month) + case 'M': + return String(month) + case 'dd': + return pad2(day) + case 'd': + return String(day) + case 'EEEE': + return localeDatePart(date, locale, { weekday: 'long' }) + case 'EEE': + return localeDatePart(date, locale, { weekday: 'short' }) + case 'ww': + return pad2(week) + case 'w': + return String(week) + } + }) + .join('') +} + +interface DateNotePatternMatch { + year?: number + month?: number + day?: number + week?: number +} + +function mergeCapture( + match: DateNotePatternMatch, + key: keyof DateNotePatternMatch, + value: number +): boolean { + if (match[key] !== undefined && match[key] !== value) return false + match[key] = value + return true +} + +function readTwoDigitYear(value: string): number { + const n = Number(value) + return n >= 70 ? 1900 + n : 2000 + n +} + +function matchDateNotePattern(pattern: string, text: string): DateNotePatternMatch | null { + const captures: Array<{ token: DateNotePatternToken; index: number }> = [] + let captureIndex = 1 + const source = parseDateNotePattern(pattern) + .map((part) => { + if (part.kind === 'literal') return escapeRegex(part.value) + switch (part.value) { + case 'yyyy': + captures.push({ token: part.value, index: captureIndex++ }) + return '(\\d{4})' + case 'yy': + captures.push({ token: part.value, index: captureIndex++ }) + return '(\\d{2})' + case 'MM': + case 'M': + case 'dd': + case 'd': + case 'ww': + case 'w': + captures.push({ token: part.value, index: captureIndex++ }) + return '(\\d{1,2})' + case 'MMMM': + case 'MMM': + case 'EEEE': + case 'EEE': + return '[^/]+' + } + }) + .join('') + const re = new RegExp(`^${source}$`) + const m = re.exec(text) + if (!m) return null + + const out: DateNotePatternMatch = {} + for (const capture of captures) { + const raw = m[capture.index] + if (!raw) continue + const value = Number(raw) + switch (capture.token) { + case 'yyyy': + if (!mergeCapture(out, 'year', value)) return null + break + case 'yy': + if (!mergeCapture(out, 'year', readTwoDigitYear(raw))) return null + break + case 'MM': + case 'M': + if (!mergeCapture(out, 'month', value)) return null + break + case 'dd': + case 'd': + if (!mergeCapture(out, 'day', value)) return null + break + case 'ww': + case 'w': + if (!mergeCapture(out, 'week', value)) return null + break + } + } + return out +} + +function shouldFormatDirectoryPattern(pattern: string): boolean { + const tokens = parseDateNotePattern(pattern).filter((part) => part.kind === 'token') + return ( + pattern.includes("'") || + tokens.length >= 2 || + tokens.some((part) => part.value === 'yyyy' || part.value === 'yy' || part.value === 'ww') + ) +} + +function formatDirectoryPattern( + date: Date, + pattern: string, + locale: string, + parts: { year?: number; week?: number } = {} +): string { + if (!shouldFormatDirectoryPattern(pattern)) return pattern + return formatDateNotePattern(date, pattern, locale, parts) +} + +function matchDirectoryPattern(pattern: string, text: string): DateNotePatternMatch | null { + if (!shouldFormatDirectoryPattern(pattern)) return pattern === text ? {} : null + return matchDateNotePattern(pattern, text) +} + +export function dateNoteDirectoryDisplayLabel( + pattern: string, + fallbackLabel: string +): string { + if (!shouldFormatDirectoryPattern(pattern)) { + return pattern.split('/').filter(Boolean).pop() || fallbackLabel + } + + for (const segment of pattern.split('/')) { + const parts = parseDateNotePattern(segment) + if (parts.some((part) => part.kind === 'token')) continue + const literal = parts + .map((part) => (part.kind === 'literal' ? part.value : '')) + .join('') + .trim() + if (literal) return literal + } + + return fallbackLabel +} + +const DATE_PATTERN_MATCH_KEYS: Array = [ + 'year', + 'month', + 'day', + 'week' +] + +function mergePatternMatch( + target: DateNotePatternMatch, + source: DateNotePatternMatch +): boolean { + for (const key of DATE_PATTERN_MATCH_KEYS) { + const value = source[key] + if (value !== undefined && !mergeCapture(target, key, value)) return false + } + return true +} + export function normalizeVaultSettings( settings: VaultSettings | null | undefined ): VaultSettings { @@ -113,11 +468,23 @@ export function normalizeVaultSettings( dailyNotes: { enabled: !!settings?.dailyNotes?.enabled, directory: dailyDirectory, + titlePattern: normalizeDailyNoteTitlePattern(settings?.dailyNotes?.titlePattern), + locale: normalizeDailyNoteLocale(settings?.dailyNotes?.locale), + legacyPatterns: normalizeDailyNotePatternHistory( + settings?.dailyNotes?.legacyPatterns, + primaryNotesLocation + ), templateId: normalizeTemplateId(settings?.dailyNotes?.templateId) }, weeklyNotes: { enabled: !!settings?.weeklyNotes?.enabled, directory: weeklyDirectory, + titlePattern: normalizeWeeklyNoteTitlePattern(settings?.weeklyNotes?.titlePattern), + locale: normalizeWeeklyNoteLocale(settings?.weeklyNotes?.locale), + legacyPatterns: normalizeWeeklyNotePatternHistory( + settings?.weeklyNotes?.legacyPatterns, + primaryNotesLocation + ), templateId: normalizeTemplateId(settings?.weeklyNotes?.templateId) }, folderIcons: normalizedFolderIcons @@ -228,12 +595,35 @@ export function noteTitleForDate(date = new Date()): string { return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` } +export interface DailyNoteLocation { + title: string + subpath: string +} + +export function dailyNoteLocationForDate( + date = new Date(), + settings: VaultSettings | null | undefined +): DailyNoteLocation { + const normalized = normalizeVaultSettings(settings) + return dailyNoteLocationForPattern(date, normalized, currentDailyNotePattern(normalized)) +} + export function weeklyNoteTitle(date = new Date()): string { return `${getISOWeekYear(date)}-W${pad(getISOWeek(date))}` } -const DAILY_TITLE_RE = /^(\d{4})-(\d{2})-(\d{2})$/ -const WEEKLY_TITLE_RE = /^(\d{4})-W(\d{2})$/ +export interface WeeklyNoteLocation { + title: string + subpath: string +} + +export function weeklyNoteLocationForDate( + date = new Date(), + settings: VaultSettings | null | undefined +): WeeklyNoteLocation { + const normalized = normalizeVaultSettings(settings) + return weeklyNoteLocationForPattern(date, normalized, currentWeeklyNotePattern(normalized)) +} export interface DateNoteInfo { kind: 'daily' | 'weekly' @@ -241,11 +631,201 @@ export interface DateNoteInfo { date: Date } +function currentDailyNotePattern(settings: VaultSettings): DateNotePatternSettings { + return { + directory: settings.dailyNotes.directory, + titlePattern: settings.dailyNotes.titlePattern ?? DEFAULT_DAILY_NOTE_TITLE_PATTERN, + locale: settings.dailyNotes.locale ?? DEFAULT_DAILY_NOTE_LOCALE + } +} + +function currentWeeklyNotePattern(settings: VaultSettings): DateNotePatternSettings { + return { + directory: settings.weeklyNotes.directory, + titlePattern: settings.weeklyNotes.titlePattern ?? DEFAULT_WEEKLY_NOTE_TITLE_PATTERN, + locale: settings.weeklyNotes.locale ?? DEFAULT_WEEKLY_NOTE_LOCALE + } +} + +function dailyNotePatterns(settings: VaultSettings): DateNotePatternSettings[] { + const current = currentDailyNotePattern(settings) + const seen = new Set([dateNotePatternKey(current)]) + const out = [current] + for (const pattern of settings.dailyNotes.legacyPatterns ?? []) { + const key = dateNotePatternKey(pattern) + if (seen.has(key)) continue + seen.add(key) + out.push(pattern) + } + return out +} + +function weeklyNotePatterns(settings: VaultSettings): DateNotePatternSettings[] { + const current = currentWeeklyNotePattern(settings) + const seen = new Set([dateNotePatternKey(current)]) + const out = [current] + for (const pattern of settings.weeklyNotes.legacyPatterns ?? []) { + const key = dateNotePatternKey(pattern) + if (seen.has(key)) continue + seen.add(key) + out.push(pattern) + } + return out +} + +function dailyNoteLocationForPattern( + date: Date, + settings: VaultSettings, + pattern: DateNotePatternSettings +): DailyNoteLocation { + const locale = pattern.locale ?? DEFAULT_DAILY_NOTE_LOCALE + const subpath = normalizePrimaryRelativeSubpath( + normalizeDailyNotesDirectory(formatDirectoryPattern(date, pattern.directory, locale)), + settings + ) + const title = normalizeDailyNoteTitlePattern( + formatDateNotePattern( + date, + pattern.titlePattern ?? DEFAULT_DAILY_NOTE_TITLE_PATTERN, + locale + ) + ) + return { title, subpath } +} + +function weeklyNoteLocationForPattern( + date: Date, + settings: VaultSettings, + pattern: DateNotePatternSettings +): WeeklyNoteLocation { + const weekYear = getISOWeekYear(date) + const week = getISOWeek(date) + const monday = mondayOfISOWeek(weekYear, week) + const locale = pattern.locale ?? DEFAULT_WEEKLY_NOTE_LOCALE + const patternParts = { year: weekYear, week } + const subpath = normalizePrimaryRelativeSubpath( + normalizeWeeklyNotesDirectory( + formatDirectoryPattern(monday, pattern.directory, locale, patternParts) + ), + settings + ) + const title = normalizeWeeklyNoteTitlePattern( + formatDateNotePattern( + monday, + pattern.titlePattern ?? DEFAULT_WEEKLY_NOTE_TITLE_PATTERN, + locale, + patternParts + ) + ) + return { title, subpath } +} + +/** + * Numerics a note's directory + title expose, used only to bound the set of + * candidate dates we round-trip. Tokens we cannot reverse cheaply (localized + * month/weekday names) contribute nothing here — the round-trip below verifies + * them instead, so the locale never has to be parsed backwards. + */ +function patternNumerics( + pattern: DateNotePatternSettings, + subpath: string, + title: string, + defaultTitlePattern: string +): DateNotePatternMatch | null { + const directoryMatch = matchDirectoryPattern(pattern.directory, subpath) + const titleMatch = matchDateNotePattern(pattern.titlePattern ?? defaultTitlePattern, title) + if (!directoryMatch || !titleMatch) return null + const merged: DateNotePatternMatch = {} + if (!mergePatternMatch(merged, directoryMatch) || !mergePatternMatch(merged, titleMatch)) { + return null + } + return merged +} + +/** + * Every calendar day consistent with the extracted numerics. A day-of-month + * pins it to one date; otherwise we enumerate the relevant month(s)/week and + * let the round-trip pick the match — so a title built from ISO week + weekday + * (no day-of-month) still resolves to its day. + */ +function dailyCandidateDates(n: DateNotePatternMatch): Date[] { + if (n.year === undefined) return [] + if (n.month !== undefined && n.day !== undefined) { + const date = new Date(n.year, n.month - 1, n.day) + const valid = + date.getFullYear() === n.year && + date.getMonth() === n.month - 1 && + date.getDate() === n.day + return valid ? [date] : [] + } + const firstMonth = n.month !== undefined ? n.month - 1 : 0 + const lastMonth = n.month !== undefined ? n.month - 1 : 11 + const out: Date[] = [] + for (let month = firstMonth; month <= lastMonth; month++) { + const daysInMonth = new Date(n.year, month + 1, 0).getDate() + for (let day = 1; day <= daysInMonth; day++) { + if (n.day !== undefined && day !== n.day) continue + const date = new Date(n.year, month, day) + if (n.week !== undefined && getISOWeek(date) !== n.week) continue + out.push(date) + } + } + return out +} + +/** Mondays of every ISO week consistent with the extracted numerics. */ +function weeklyCandidateMondays(n: DateNotePatternMatch): Date[] { + if (n.year === undefined) return [] + if (n.week !== undefined) { + const monday = mondayOfISOWeek(n.year, n.week) + return getISOWeekYear(monday) === n.year ? [monday] : [] + } + const out: Date[] = [] + for (let week = 1; week <= 53; week++) { + const monday = mondayOfISOWeek(n.year, week) + if (getISOWeekYear(monday) !== n.year) continue + out.push(monday) + } + return out +} + +function matchDailyPattern( + subpath: string, + title: string, + settings: VaultSettings, + pattern: DateNotePatternSettings +): Date | null { + const numerics = patternNumerics(pattern, subpath, title, DEFAULT_DAILY_NOTE_TITLE_PATTERN) + if (!numerics) return null + for (const candidate of dailyCandidateDates(numerics)) { + const expected = dailyNoteLocationForPattern(candidate, settings, pattern) + if (expected.subpath === subpath && expected.title === title) return candidate + } + return null +} + +function matchWeeklyPattern( + subpath: string, + title: string, + settings: VaultSettings, + pattern: DateNotePatternSettings +): Date | null { + const numerics = patternNumerics(pattern, subpath, title, DEFAULT_WEEKLY_NOTE_TITLE_PATTERN) + if (!numerics) return null + for (const candidate of weeklyCandidateMondays(numerics)) { + const expected = weeklyNoteLocationForPattern(candidate, settings, pattern) + if (expected.subpath === subpath && expected.title === title) return candidate + } + return null +} + /** * Classify a note as a daily or weekly note, or `null` if it is neither. - * A note qualifies only when its title matches the date/week format, it lives - * in the configured daily/weekly directory, and that feature is enabled — so a - * stray note titled `2026-06-08` outside the daily folder is not treated as one. + * A note qualifies only when re-formatting its recovered date with the + * configured directory/title pattern reproduces this exact path — so a stray + * note titled `2026-06-08` outside the daily folder is not treated as one, and + * a note is recognized regardless of which tokens its title uses to encode the + * day (day-of-month, or ISO week + weekday). */ export function classifyDateNote( note: Pick, @@ -256,19 +836,17 @@ export function classifyDateNote( const subpath = noteFolderSubpath(note, normalized) - if (normalized.dailyNotes.enabled && subpath === normalized.dailyNotes.directory) { - const m = DAILY_TITLE_RE.exec(note.title) - if (m) { - const [, y, mo, d] = m - return { kind: 'daily', date: new Date(Number(y), Number(mo) - 1, Number(d)) } + if (normalized.dailyNotes.enabled) { + for (const pattern of dailyNotePatterns(normalized)) { + const date = matchDailyPattern(subpath, note.title, normalized, pattern) + if (date) return { kind: 'daily', date } } } - if (normalized.weeklyNotes.enabled && subpath === normalized.weeklyNotes.directory) { - const m = WEEKLY_TITLE_RE.exec(note.title) - if (m) { - const [, y, w] = m - return { kind: 'weekly', date: mondayOfISOWeek(Number(y), Number(w)) } + if (normalized.weeklyNotes.enabled) { + for (const pattern of weeklyNotePatterns(normalized)) { + const date = matchWeeklyPattern(subpath, note.title, normalized, pattern) + if (date) return { kind: 'weekly', date } } } @@ -276,25 +854,91 @@ export function classifyDateNote( } export interface DateNoteIndexes { - dailyByTitle: Map - weeklyByTitle: Map + dailyByDate: Map + weeklyByWeek: Map } export function buildDateNoteIndexes( notes: readonly NoteMeta[], settings: VaultSettings | null | undefined ): DateNoteIndexes { - const dailyByTitle = new Map() - const weeklyByTitle = new Map() + const dailyByDate = new Map() + const weeklyByWeek = new Map() for (const note of notes) { const info = classifyDateNote(note, settings) if (!info) continue - if (info.kind === 'daily') dailyByTitle.set(note.title, note) - else weeklyByTitle.set(note.title, note) + if (info.kind === 'daily') dailyByDate.set(noteTitleForDate(info.date), note) + else weeklyByWeek.set(weeklyNoteTitle(info.date), note) } - return { dailyByTitle, weeklyByTitle } + return { dailyByDate, weeklyByWeek } +} + +export function dateNoteFolderMayBelongToDatePattern( + subpath: string, + settings: VaultSettings | null | undefined +): boolean { + const normalized = normalizeVaultSettings(settings) + if (normalized.dailyNotes.enabled) { + for (const pattern of dailyNotePatterns(normalized)) { + if (matchDirectoryPattern(pattern.directory, subpath)) return true + } + } + if (normalized.weeklyNotes.enabled) { + for (const pattern of weeklyNotePatterns(normalized)) { + if (matchDirectoryPattern(pattern.directory, subpath)) return true + } + } + return false +} + +export function findDailyNoteForDate( + notes: readonly NoteMeta[], + settings: VaultSettings | null | undefined, + date: Date +): NoteMeta | null { + const expected = dailyNoteLocationForDate(date, settings) + const normalized = normalizeVaultSettings(settings) + const dateKey = noteTitleForDate(date) + return ( + notes.find( + (note) => + note.folder === 'inbox' && + note.title === expected.title && + noteFolderSubpath(note, normalized) === expected.subpath && + classifyDateNote(note, normalized)?.kind === 'daily' + ) ?? + notes.find((note) => { + const info = classifyDateNote(note, normalized) + return info?.kind === 'daily' && noteTitleForDate(info.date) === dateKey + }) ?? + null + ) +} + +export function findWeeklyNoteForDate( + notes: readonly NoteMeta[], + settings: VaultSettings | null | undefined, + date: Date +): NoteMeta | null { + const expected = weeklyNoteLocationForDate(date, settings) + const normalized = normalizeVaultSettings(settings) + const weekKey = weeklyNoteTitle(date) + return ( + notes.find( + (note) => + note.folder === 'inbox' && + note.title === expected.title && + noteFolderSubpath(note, normalized) === expected.subpath && + classifyDateNote(note, normalized)?.kind === 'weekly' + ) ?? + notes.find((note) => { + const info = classifyDateNote(note, normalized) + return info?.kind === 'weekly' && weeklyNoteTitle(info.date) === weekKey + }) ?? + null + ) } export function findDateNoteByTitle( diff --git a/packages/app-core/src/store.test.ts b/packages/app-core/src/store.test.ts index 3605a1ac..32a960a4 100644 --- a/packages/app-core/src/store.test.ts +++ b/packages/app-core/src/store.test.ts @@ -138,6 +138,212 @@ describe('tasks cache freshness', () => { }) }) +describe('daily note patterns', () => { + it('creates daily notes using the configured directory and title patterns', async () => { + const created = { + ...makeNote(''), + path: 'inbox/2026/06-Jun/2026-06-09-Tue.md', + title: '2026-06-09-Tue' + } + const createNote = vi.fn().mockResolvedValue(created) + installZen({ + createNote, + listNotes: vi.fn().mockResolvedValue([created]), + readNote: vi.fn().mockResolvedValue({ ...created, body: '' }) + }) + + const { useStore } = await loadStore() + useStore.setState({ + notes: [], + customTemplates: [], + vaultSettings: { + primaryNotesLocation: 'inbox', + dailyNotes: { + enabled: true, + directory: 'yyyy/MM-MMM', + titlePattern: 'yyyy-MM-dd-EEE', + locale: 'en-US' + }, + weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, + folderIcons: {} + } + }) + + await useStore.getState().openDailyNoteForDate(new Date(2026, 5, 9)) + + expect(createNote).toHaveBeenCalledWith('inbox', '2026-06-09-Tue', '2026/06-Jun') + }) +}) + +describe('weekly note patterns', () => { + it('creates weekly notes using the configured directory and title patterns', async () => { + const created = { + ...makeNote(''), + path: 'inbox/2026/06-Jun/2026-W24-Mon.md', + title: '2026-W24-Mon' + } + const createNote = vi.fn().mockResolvedValue(created) + installZen({ + createNote, + listNotes: vi.fn().mockResolvedValue([created]), + readNote: vi.fn().mockResolvedValue({ ...created, body: '' }) + }) + + const { useStore } = await loadStore() + useStore.setState({ + notes: [], + customTemplates: [], + vaultSettings: { + primaryNotesLocation: 'inbox', + dailyNotes: { enabled: false, directory: 'Daily Notes' }, + weeklyNotes: { + enabled: true, + directory: 'yyyy/MM-MMM', + titlePattern: "yyyy-'W'ww-EEE", + locale: 'en-US' + }, + folderIcons: {} + } + }) + + await useStore.getState().openWeeklyNoteForDate(new Date(2026, 5, 9)) + + expect(createNote).toHaveBeenCalledWith('inbox', '2026-W24-Mon', '2026/06-Jun') + }) +}) + +describe('date note pattern history', () => { + it('keeps existing daily notes dynamic when the daily pattern changes', async () => { + const oldSettings = { + primaryNotesLocation: 'inbox' as const, + dailyNotes: { + enabled: true, + directory: 'Daily Notes', + titlePattern: 'yyyy-MM-dd', + locale: 'en-US' + }, + weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, + folderIcons: {} + } + const nextSettings = { + ...oldSettings, + dailyNotes: { + ...oldSettings.dailyNotes, + directory: 'yyyy/MM-MMM', + titlePattern: 'yyyy-MM-dd-EEE' + } + } + const existing = { + ...makeNote('daily'), + path: 'inbox/Daily Notes/2026-06-12.md', + title: '2026-06-12', + body: '# 2026-06-12\n' + } + const setVaultSettings = vi.fn().mockImplementation(async (settings) => settings) + const moveNote = vi.fn() + const renameNote = vi.fn() + const createNote = vi.fn() + installZen({ + setVaultSettings, + moveNote, + renameNote, + createNote, + listNotes: vi.fn().mockResolvedValue([existing]), + readNote: vi.fn().mockResolvedValue(existing) + }) + + const { useStore } = await loadStore() + useStore.setState({ + notes: [existing], + vaultSettings: oldSettings + }) + + await useStore.getState().setVaultSettings(nextSettings) + await useStore.getState().openDailyNoteForDate(new Date(2026, 5, 12)) + + expect(setVaultSettings).toHaveBeenCalledWith( + expect.objectContaining({ + dailyNotes: expect.objectContaining({ + directory: 'yyyy/MM-MMM', + titlePattern: 'yyyy-MM-dd-EEE', + legacyPatterns: [ + { directory: 'Daily Notes', titlePattern: 'yyyy-MM-dd', locale: 'en-US' } + ] + }) + }) + ) + expect(moveNote).not.toHaveBeenCalled() + expect(renameNote).not.toHaveBeenCalled() + expect(createNote).not.toHaveBeenCalled() + expect(useStore.getState().selectedPath).toBe(existing.path) + }) + + it('keeps existing weekly notes dynamic when the weekly pattern changes', async () => { + const oldSettings = { + primaryNotesLocation: 'inbox' as const, + dailyNotes: { enabled: false, directory: 'Daily Notes' }, + weeklyNotes: { + enabled: true, + directory: 'Weekly Notes', + titlePattern: "yyyy-'W'ww", + locale: 'en-US' + }, + folderIcons: {} + } + const nextSettings = { + ...oldSettings, + weeklyNotes: { + ...oldSettings.weeklyNotes, + directory: 'yyyy/MM-MMM', + titlePattern: "yyyy-'W'ww-EEE" + } + } + const existing = { + ...makeNote('weekly'), + path: 'inbox/Weekly Notes/2026-W24.md', + title: '2026-W24', + body: '# 2026-W24\n' + } + const setVaultSettings = vi.fn().mockImplementation(async (settings) => settings) + const moveNote = vi.fn() + const renameNote = vi.fn() + const createNote = vi.fn() + installZen({ + setVaultSettings, + moveNote, + renameNote, + createNote, + listNotes: vi.fn().mockResolvedValue([existing]), + readNote: vi.fn().mockResolvedValue(existing) + }) + + const { useStore } = await loadStore() + useStore.setState({ + notes: [existing], + vaultSettings: oldSettings + }) + + await useStore.getState().setVaultSettings(nextSettings) + await useStore.getState().openWeeklyNoteForDate(new Date(2026, 5, 12)) + + expect(setVaultSettings).toHaveBeenCalledWith( + expect.objectContaining({ + weeklyNotes: expect.objectContaining({ + directory: 'yyyy/MM-MMM', + titlePattern: "yyyy-'W'ww-EEE", + legacyPatterns: [ + { directory: 'Weekly Notes', titlePattern: "yyyy-'W'ww", locale: 'en-US' } + ] + }) + }) + ) + expect(moveNote).not.toHaveBeenCalled() + expect(renameNote).not.toHaveBeenCalled() + expect(createNote).not.toHaveBeenCalled() + expect(useStore.getState().selectedPath).toBe(existing.path) + }) +}) + describe('local vault shortcuts', () => { it('stores known local vaults for the sidebar switcher', async () => { const localVaults = [ diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index 86b0299c..764beffc 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -3,6 +3,7 @@ import type { EditorView } from '@codemirror/view' import { DEFAULT_VAULT_SETTINGS } from '@shared/ipc' import type { AssetMeta, + DateNotePatternSettings, DeletedAsset, FolderEntry, LocalVaultEntry, @@ -72,14 +73,15 @@ import { } from './lib/workspace-tabs' import { duplicateFolderIcons, + dailyNoteLocationForDate, folderForVaultRelativePath, - findDateNoteByTitle, + findDailyNoteForDate, + findWeeklyNoteForDate, isPrimaryNotesAtRoot, removeFolderIcons, normalizeVaultSettings, noteFolderSubpath, - noteTitleForDate, - weeklyNoteTitle, + weeklyNoteLocationForDate, rewriteFolderIconsForRename } from './lib/vault-layout' import { renderTemplate, renderTitle } from './lib/template-render' @@ -2145,6 +2147,87 @@ function renameNoteState( } } +const MAX_DATE_NOTE_PATTERN_HISTORY = 20 + +function dateNotePatternKey(pattern: DateNotePatternSettings): string { + return `${pattern.directory}\0${pattern.titlePattern ?? ''}\0${pattern.locale ?? ''}` +} + +function currentDailyPatternFromSettings(settings: VaultSettings): DateNotePatternSettings { + return { + directory: settings.dailyNotes.directory, + titlePattern: settings.dailyNotes.titlePattern, + locale: settings.dailyNotes.locale + } +} + +function currentWeeklyPatternFromSettings(settings: VaultSettings): DateNotePatternSettings { + return { + directory: settings.weeklyNotes.directory, + titlePattern: settings.weeklyNotes.titlePattern, + locale: settings.weeklyNotes.locale + } +} + +function appendDateNotePatternHistory( + history: readonly DateNotePatternSettings[] | undefined, + previous: DateNotePatternSettings, + current: DateNotePatternSettings +): DateNotePatternSettings[] { + const out: DateNotePatternSettings[] = [] + const seen = new Set([dateNotePatternKey(current)]) + for (const pattern of [previous, ...(history ?? [])]) { + const key = dateNotePatternKey(pattern) + if (seen.has(key)) continue + seen.add(key) + out.push(pattern) + if (out.length >= MAX_DATE_NOTE_PATTERN_HISTORY) break + } + return out +} + +function withDateNotePatternHistory( + previousSettings: VaultSettings, + requestedSettings: VaultSettings +): VaultSettings { + const previous = normalizeVaultSettings(previousSettings) + const next = normalizeVaultSettings(requestedSettings) + const previousDaily = currentDailyPatternFromSettings(previous) + const nextDaily = currentDailyPatternFromSettings(next) + const previousWeekly = currentWeeklyPatternFromSettings(previous) + const nextWeekly = currentWeeklyPatternFromSettings(next) + + return { + ...next, + dailyNotes: { + ...next.dailyNotes, + legacyPatterns: + previous.dailyNotes.enabled && + next.dailyNotes.enabled && + dateNotePatternKey(previousDaily) !== dateNotePatternKey(nextDaily) + ? appendDateNotePatternHistory( + next.dailyNotes.legacyPatterns, + previousDaily, + nextDaily + ) + : next.dailyNotes.legacyPatterns + }, + weeklyNotes: { + ...next.weeklyNotes, + legacyPatterns: + previous.weeklyNotes.enabled && + next.weeklyNotes.enabled && + dateNotePatternKey(previousWeekly) !== dateNotePatternKey(nextWeekly) + ? appendDateNotePatternHistory( + next.weeklyNotes.legacyPatterns, + previousWeekly, + nextWeekly + ) + : next.weeklyNotes.legacyPatterns + } + } +} + function rewriteNoteCommentsPath( comments: Record, oldPath: string, @@ -2772,7 +2855,8 @@ export const useStore = create((set, get) => { }), setVaultSettings: async (next) => { try { - const settings = normalizeVaultSettings(await window.zen.setVaultSettings(next)) + const settingsToSave = withDateNotePatternHistory(get().vaultSettings, next) + const settings = normalizeVaultSettings(await window.zen.setVaultSettings(settingsToSave)) set({ vaultSettings: settings }) @@ -4346,9 +4430,8 @@ export const useStore = create((set, get) => { const state = get() const settings = normalizeVaultSettings(state.vaultSettings) if (!settings.dailyNotes.enabled) return - const title = noteTitleForDate(date) - const subpath = settings.dailyNotes.directory - const existing = findDateNoteByTitle(state.notes, settings, 'daily', title) + const { title, subpath } = dailyNoteLocationForDate(date, settings) + const existing = findDailyNoteForDate(state.notes, settings, date) if (existing) { set({ view: { kind: 'folder', folder: 'inbox', subpath } }) await get().selectNote(existing.path) @@ -4370,9 +4453,8 @@ export const useStore = create((set, get) => { const state = get() const settings = normalizeVaultSettings(state.vaultSettings) if (!settings.weeklyNotes.enabled) return - const title = weeklyNoteTitle(date) - const subpath = settings.weeklyNotes.directory - const existing = findDateNoteByTitle(state.notes, settings, 'weekly', title) + const { title, subpath } = weeklyNoteLocationForDate(date, settings) + const existing = findWeeklyNoteForDate(state.notes, settings, date) if (existing) { set({ view: { kind: 'folder', folder: 'inbox', subpath } }) await get().selectNote(existing.path) diff --git a/packages/bridge-contract/src/ipc.ts b/packages/bridge-contract/src/ipc.ts index 6899638f..d1a53b97 100644 --- a/packages/bridge-contract/src/ipc.ts +++ b/packages/bridge-contract/src/ipc.ts @@ -245,16 +245,39 @@ export type FolderIconId = | 'chart' | 'home' +export interface DateNotePatternSettings { + /** Directory or date-based directory pattern inside the primary notes area. */ + directory: string + /** Date-based title/filename pattern. */ + titlePattern?: string + /** BCP 47 locale used for localized pattern tokens. `system` = OS/browser locale. */ + locale?: string +} + export interface DailyNotesSettings { enabled: boolean + /** Directory or date-based directory pattern inside the primary notes area. */ directory: string + /** Date-based title/filename pattern for new daily notes. */ + titlePattern?: string + /** BCP 47 locale used for localized pattern tokens. `system` = OS/browser locale. */ + locale?: string + /** Prior patterns used only to recognize existing daily notes after settings changes. */ + legacyPatterns?: DateNotePatternSettings[] /** Template applied to new daily notes. Empty/undefined = blank note. */ templateId?: string } export interface WeeklyNotesSettings { enabled: boolean + /** Directory or date-based directory pattern inside the primary notes area. */ directory: string + /** Date-based title/filename pattern for new weekly notes. */ + titlePattern?: string + /** BCP 47 locale used for localized pattern tokens. `system` = OS/browser locale. */ + locale?: string + /** Prior patterns used only to recognize existing weekly notes after settings changes. */ + legacyPatterns?: DateNotePatternSettings[] /** Template applied to new weekly notes. Empty/undefined = blank note. */ templateId?: string } @@ -267,17 +290,25 @@ export interface VaultSettings { } export const DEFAULT_DAILY_NOTES_DIRECTORY = 'Daily Notes' +export const DEFAULT_DAILY_NOTE_TITLE_PATTERN = 'yyyy-MM-dd' +export const DEFAULT_DAILY_NOTE_LOCALE = 'system' export const DEFAULT_WEEKLY_NOTES_DIRECTORY = 'Weekly Notes' +export const DEFAULT_WEEKLY_NOTE_TITLE_PATTERN = "yyyy-'W'ww" +export const DEFAULT_WEEKLY_NOTE_LOCALE = 'system' export const DEFAULT_VAULT_SETTINGS: VaultSettings = { primaryNotesLocation: 'inbox', dailyNotes: { enabled: false, - directory: DEFAULT_DAILY_NOTES_DIRECTORY + directory: DEFAULT_DAILY_NOTES_DIRECTORY, + titlePattern: DEFAULT_DAILY_NOTE_TITLE_PATTERN, + locale: DEFAULT_DAILY_NOTE_LOCALE }, weeklyNotes: { enabled: false, - directory: DEFAULT_WEEKLY_NOTES_DIRECTORY + directory: DEFAULT_WEEKLY_NOTES_DIRECTORY, + titlePattern: DEFAULT_WEEKLY_NOTE_TITLE_PATTERN, + locale: DEFAULT_WEEKLY_NOTE_LOCALE }, folderIcons: {} } From 64318cce4128ee44222faf1c9c376e0a568fb90d Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Fri, 12 Jun 2026 15:49:47 -0500 Subject: [PATCH 05/66] Fix duplicated hover text on sidebar icons (#141) IconBtn rendered its label through both the native title attribute and a custom hover bubble, so macOS showed the tooltip text twice. Drop the redundant native title; the styled bubble and aria-label remain, matching the editor toolbar's tooltip pattern. --- packages/app-core/src/components/Sidebar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/app-core/src/components/Sidebar.tsx b/packages/app-core/src/components/Sidebar.tsx index ef5d9e3b..e04c0f2c 100644 --- a/packages/app-core/src/components/Sidebar.tsx +++ b/packages/app-core/src/components/Sidebar.tsx @@ -4805,7 +4805,6 @@ function IconBtn({ ) : (
- +
- {vault?.name ?? "ZenNotes"} + {headerVaultName}
{workspaceMode === "remote" && (
diff --git a/packages/app-core/src/lib/vault-switcher.test.ts b/packages/app-core/src/lib/vault-switcher.test.ts index 45e90d07..d0c0b37b 100644 --- a/packages/app-core/src/lib/vault-switcher.test.ts +++ b/packages/app-core/src/lib/vault-switcher.test.ts @@ -111,4 +111,35 @@ describe('buildVaultSwitcherEntries', () => { ['local', 'Work', false] ]) }) + + it('names the current remote vault after the profile, not the server vault folder (#153)', () => { + // The sidebar header derives its label and badge from the current entry's + // name. In remote mode the server reports a vault folder name ("workspace"), + // but the header should show the connection the user named ("Home"). + const entries = buildVaultSwitcherEntries({ + localVaults: [], + remoteProfiles: [ + { + id: 'home', + name: 'Home', + baseUrl: 'https://home.example.com', + hasCredential: true, + vaultPath: null, + lastConnectedAt: 1 + } + ], + currentVault: { root: '/srv/workspace', name: 'workspace' }, + workspaceMode: 'remote', + remoteWorkspaceInfo: { + mode: 'remote', + baseUrl: 'https://home.example.com', + authConfigured: true, + capabilities: null, + profileId: 'home' + } + }) + + const current = entries.find((entry) => entry.current) + expect(current?.name).toBe('Home') + }) }) From 61bc6f419d3ea564d7f9052132938f145d456da2 Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Sat, 13 Jun 2026 12:26:02 -0500 Subject: [PATCH 10/66] Fix switching from a remote workspace back to a local vault openLocalVault bailed out when the target path equaled the current vault.root, without checking workspaceMode. In remote mode vault.root is the server-reported path, and a localhost server reports the served folder's real on-disk path -- which equals the local vault's own path. Switching back to that local vault was therefore mistaken for reopening the current vault and silently did nothing. Only short-circuit when already in local mode on the same root. --- packages/app-core/src/store.test.ts | 47 +++++++++++++++++++++++++++++ packages/app-core/src/store.ts | 7 ++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/packages/app-core/src/store.test.ts b/packages/app-core/src/store.test.ts index 32a960a4..fc975fea 100644 --- a/packages/app-core/src/store.test.ts +++ b/packages/app-core/src/store.test.ts @@ -391,6 +391,53 @@ describe('local vault shortcuts', () => { expect(useStore.getState().hasAssetsDir).toBe(true) }) + it('switches from a remote workspace to a local vault that shares the server path', async () => { + // A localhost server reports the served folder's real on-disk path as the + // vault root, so the current remote vault.root can equal a local vault's + // path. Switching back to local must not be blocked by that coincidence. + const sharedRoot = '/Users/test/Shared' + const openLocalVault = vi + .fn() + .mockResolvedValue({ root: sharedRoot, name: 'Shared' }) + installZen({ + openLocalVault, + getRemoteWorkspaceInfo: vi.fn().mockResolvedValue(null), + getVaultSettings: vi.fn().mockResolvedValue({}), + listLocalVaults: vi.fn().mockResolvedValue([]), + listAssets: vi.fn().mockResolvedValue([]), + hasAssetsDir: vi.fn().mockResolvedValue(false) + }) + + const { useStore } = await loadStore() + useStore.setState({ + vault: { root: sharedRoot, name: 'Shared' }, + workspaceMode: 'remote' + }) + + await useStore.getState().openLocalVault(sharedRoot) + + expect(openLocalVault).toHaveBeenCalledWith(sharedRoot) + expect(useStore.getState().workspaceMode).toBe('local') + expect(useStore.getState().vault).toEqual({ root: sharedRoot, name: 'Shared' }) + }) + + it('ignores reopening the current local vault', async () => { + const openLocalVault = vi + .fn() + .mockResolvedValue({ root: '/Users/test/Notes', name: 'Notes' }) + installZen({ openLocalVault }) + + const { useStore } = await loadStore() + useStore.setState({ + vault: { root: '/Users/test/Notes', name: 'Notes' }, + workspaceMode: 'local' + }) + + await useStore.getState().openLocalVault('/Users/test/Notes') + + expect(openLocalVault).not.toHaveBeenCalled() + }) + it('closes the current local vault and clears workspace state', async () => { const closeVault = vi.fn().mockResolvedValue(null) const listLocalVaults = vi.fn().mockResolvedValue([]) diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index 764beffc..a13056e5 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -5517,7 +5517,12 @@ export const useStore = create((set, get) => { openLocalVault: async (root: string) => { const trimmed = root.trim() - if (!trimmed || trimmed === get().vault?.root) return + if (!trimmed) return + // Only a no-op when we are already in this exact local vault. In remote + // mode vault.root holds the server-reported path, which for a localhost + // server equals the local vault's own path -- comparing against it here + // would wrongly block switching back from remote to local. + if (get().workspaceMode === 'local' && trimmed === get().vault?.root) return try { await get().flushDirtyNotes() set({ workspaceSetupError: null }) From c30eaf8a42585ece0ce9bd28a5c125add3b14463 Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Sat, 13 Jun 2026 12:53:37 -0500 Subject: [PATCH 11/66] Fix hint mode in the tasks and tags views (#151) 2.3.0 moved hint mode to h, but VimNav bailed out of the tasks/tags tabs for every key except the old standalone `f`, so the leader -- and therefore hint mode and every other leader command -- was dead there. Let leader input through in those panels; VimNav now arms and consumes the leader before TasksView can see it, so h works and Space-to-toggle falls back to x (Space still toggles for anyone who remapped their leader off Space). While the hint overlay is open, the tasks/tags handlers (list, calendar, kanban) now yield every key to it, so hint labels that collide with panel keys (j/k/x/o, Esc) are no longer stolen. Also stop Esc from closing the tasks and tags tabs. They are tabs like any note tab, so Esc now only clears an active filter and leaves the tab open; close with :q, the header button, or the OS shortcut. --- .../app-core/src/components/HintOverlay.tsx | 2 +- packages/app-core/src/components/TagView.tsx | 15 +++++----- .../app-core/src/components/TasksCalendar.tsx | 2 ++ .../app-core/src/components/TasksKanban.tsx | 2 ++ .../app-core/src/components/TasksView.tsx | 19 ++++++------ packages/app-core/src/components/VimNav.tsx | 30 ++++++++++++++----- 6 files changed, 45 insertions(+), 25 deletions(-) diff --git a/packages/app-core/src/components/HintOverlay.tsx b/packages/app-core/src/components/HintOverlay.tsx index 99df3c18..6db7769b 100644 --- a/packages/app-core/src/components/HintOverlay.tsx +++ b/packages/app-core/src/components/HintOverlay.tsx @@ -200,7 +200,7 @@ export function HintOverlay({ if (targets.length === 0) return null return createPortal( -
+
{targets.map((t) => { const isMatch = t.label.startsWith(buffer) const matchedPart = buffer diff --git a/packages/app-core/src/components/TagView.tsx b/packages/app-core/src/components/TagView.tsx index e908e8d8..4f9e610c 100644 --- a/packages/app-core/src/components/TagView.tsx +++ b/packages/app-core/src/components/TagView.tsx @@ -190,6 +190,9 @@ export function TagView(): JSX.Element { useEffect(() => { if (!amActive) return const handler = (e: KeyboardEvent): void => { + // While the Vim hint overlay is open it owns the keyboard; don't let + // tag navigation (or Esc closing the view) steal its keys. (#151) + if (document.querySelector('[data-vim-hint-overlay]')) return const focused = document.activeElement as HTMLElement | null if (focused) { const t = focused.tagName @@ -205,13 +208,11 @@ export function TagView(): JSX.Element { } if (k === 'Escape') { - if (filter) { - consume() - setFilter('') - return - } + // Tags is a tab like a note tab — Esc clears an active filter but must + // never close the tab (other tabs don't close on Esc). Close with :q, + // the header ✕, or ⌘W. (#151) consume() - closeTagView() + if (filter) setFilter('') return } if (matchesSequenceToken(e, overrides, 'nav.filter')) { @@ -453,7 +454,7 @@ export function TagView(): JSX.Element { ) : (
- j/k move · Enter/o open · click chips to toggle · / filter · : command · Esc close + j/k move · Enter/o open · click chips to toggle · / filter · : command · :q close
)}
diff --git a/packages/app-core/src/components/TasksCalendar.tsx b/packages/app-core/src/components/TasksCalendar.tsx index 2fc06fc5..b379f423 100644 --- a/packages/app-core/src/components/TasksCalendar.tsx +++ b/packages/app-core/src/components/TasksCalendar.tsx @@ -132,6 +132,8 @@ export function TasksCalendar({ tasks, today, onOpenTask, onToggleTask }: Props) // sidebar bindings (same trick TasksView's list mode uses). useEffect(() => { const handler = (e: KeyboardEvent): void => { + // While the Vim hint overlay is open it owns the keyboard; yield to it. (#151) + if (document.querySelector('[data-vim-hint-overlay]')) return const active = document.activeElement as HTMLElement | null if (active) { const tag = active.tagName diff --git a/packages/app-core/src/components/TasksKanban.tsx b/packages/app-core/src/components/TasksKanban.tsx index 5cdc1750..243e0509 100644 --- a/packages/app-core/src/components/TasksKanban.tsx +++ b/packages/app-core/src/components/TasksKanban.tsx @@ -781,6 +781,8 @@ export function TasksKanban({ tasks, today, onOpenTask, onToggleTask }: Props): // beat VimNav's global handler (which otherwise hijacks h/j/k/l). useEffect(() => { const handler = (e: KeyboardEvent): void => { + // While the Vim hint overlay is open it owns the keyboard; yield to it. (#151) + if (document.querySelector('[data-vim-hint-overlay]')) return const active = document.activeElement as HTMLElement | null if (active) { const tag = active.tagName diff --git a/packages/app-core/src/components/TasksView.tsx b/packages/app-core/src/components/TasksView.tsx index bb607e08..1b223f5d 100644 --- a/packages/app-core/src/components/TasksView.tsx +++ b/packages/app-core/src/components/TasksView.tsx @@ -202,6 +202,9 @@ export function TasksView(): JSX.Element { useEffect(() => { if (!isActivePanel) return const handler = (e: KeyboardEvent): void => { + // While the Vim hint overlay is open it owns the keyboard; don't let + // task navigation (or Esc closing the view) steal its keys. (#151) + if (document.querySelector('[data-vim-hint-overlay]')) return const active = document.activeElement as HTMLElement | null if (active) { const tag = active.tagName @@ -217,13 +220,11 @@ export function TasksView(): JSX.Element { } if (key === 'Escape') { - if (filter) { - consume() - setFilter('') - return - } + // Tasks is a tab like a note tab — Esc clears an active filter but must + // never close the tab (other tabs don't close on Esc). Close with :q, + // the header ✕, or ⌘W. (#151) consume() - closeTasksView() + if (filter) setFilter('') return } @@ -501,10 +502,10 @@ export function TasksView(): JSX.Element { ) : (
{viewMode === 'list' - ? 'j/k move · Enter/o open · Space/x toggle · / filter · 1/2/3 view · : command · Esc close' + ? 'j/k move · Enter/o open · Space/x toggle · / filter · 1/2/3 view · : command · :q close' : viewMode === 'calendar' - ? 'h/j/k/l day · [ ] month · gt today · Enter open · 1/2/3 view · : command · Esc close' - : 'h/l column · j/k card · Space toggle · Enter open · 1/2/3 view · : command · Esc close'} + ? 'h/j/k/l day · [ ] month · gt today · Enter open · 1/2/3 view · : command · :q close' + : 'h/l column · j/k card · Space toggle · Enter open · 1/2/3 view · : command · :q close'}
)}
diff --git a/packages/app-core/src/components/VimNav.tsx b/packages/app-core/src/components/VimNav.tsx index 156cb7fa..f8a618f3 100644 --- a/packages/app-core/src/components/VimNav.tsx +++ b/packages/app-core/src/components/VimNav.tsx @@ -536,12 +536,7 @@ export function VimNav(): JSX.Element | null { return } - // ------- Tasks / Tag view active → defer to its own window handler - // Both panels install capture-phase window keydowns that handle - // j/k/gg/G/Enter/o/Esc/etc. themselves. We bail here so VimNav - // doesn't swallow those keys with stale sidebar routing. Exception: - // let `f` (hint mode) fall through — a global affordance that - // should still work anywhere, and its handler sits further down. + // Cancel a pending leader sequence on Escape or a second leader press. if (leaderPending.current && e.key === 'Escape') { e.preventDefault() e.stopImmediatePropagation() @@ -557,8 +552,20 @@ export function VimNav(): JSX.Element | null { resetLeader() return } + // ------- Tasks / Tag view active → defer to its own window handler + // Both panels install capture-phase window keydowns that handle + // j/k/gg/G/Enter/x/Esc/etc. themselves, so we bail and let them — with + // one exception: leader input. The leader (Space) and any in-progress + // leader sequence fall through to the leader logic below so h + // (hint mode) and every other leader command work in these panels too. + // VimNav consumes the leader keypress before TasksView sees it, so the + // leader no longer collides with Space-to-toggle. (#151) const panelViewActive = isTasksViewActive(state) || isTagsViewActive(state) - if (panelViewActive && e.key !== 'f') { + if ( + panelViewActive && + !leaderPending.current && + sequenceTokenFromEvent(e) !== leaderToken + ) { return } @@ -697,6 +704,13 @@ export function VimNav(): JSX.Element | null { resetLeader() } + // In the tasks/tags panels, only leader input is handled above; hand + // every other key (including a just-reset leader sequence) back to the + // panel's own capture handler. (#151) + if (panelViewActive && sequenceTokenFromEvent(e) !== leaderToken) { + return + } + if ( sequenceTokenFromEvent(e) === leaderToken && !editorInsertMode @@ -800,7 +814,7 @@ export function VimNav(): JSX.Element | null { } // `f` (and operator+motion sequences like df/cf/yf) are Vim find-char - // motions here — hint mode lives on the leader (f) so it never + // motions here — hint mode lives on the leader (h) so it never // hijacks them. (#107) return } From e43d9c291eb34dc1aa239c89c37ff3aaf62447ca Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Mon, 15 Jun 2026 08:43:44 -0500 Subject: [PATCH 12/66] Fix remapped pane-focus shortcuts colliding with the editor (#124) Remapping the pane-focus shortcuts (global.focusPane*) onto a key CodeMirror also binds -- e.g. Ctrl-h (delete char) or Ctrl-k (kill line) -- ran both: the bubble-phase window handler fired *after* CodeMirror had already executed its command, so the key deleted text and moved focus. Handle these shortcuts in the capture phase and consume the event so the editor never sees a key bound to focus. The handler bails while Settings, a palette, or a modal is open so it can't steal keys from the keybinding recorder. Also fix sidebar->pane navigation: focusPaneOrEdgePanel always moved geometrically from activePaneId, so pressing focus-right from the sidebar jumped from the last active pane and skipped the pane next to the sidebar. When focus is on a horizontal edge panel, move relative to that panel and land on the edge-most pane when crossing into the editor. --- packages/app-core/src/App.tsx | 68 +++++++++++++++++++-------- packages/app-core/src/lib/pane-nav.ts | 41 +++++++++++++++- 2 files changed, 89 insertions(+), 20 deletions(-) diff --git a/packages/app-core/src/App.tsx b/packages/app-core/src/App.tsx index 8a916bb4..65d2f424 100644 --- a/packages/app-core/src/App.tsx +++ b/packages/app-core/src/App.tsx @@ -610,24 +610,9 @@ function App(): JSX.Element { window.dispatchEvent(new Event('zen:add-comment')) return } - // ⌥h/j/k/l — focus the neighbouring pane/panel. Works regardless of vim - // mode (unlike hjkl) and never needs the command palette. - { - const paneDir = matchesShortcut(e, overrides, 'global.focusPaneLeft') - ? 'h' - : matchesShortcut(e, overrides, 'global.focusPaneDown') - ? 'j' - : matchesShortcut(e, overrides, 'global.focusPaneUp') - ? 'k' - : matchesShortcut(e, overrides, 'global.focusPaneRight') - ? 'l' - : null - if (paneDir) { - e.preventDefault() - focusPaneOrEdgePanel(paneDir) - return - } - } + // Pane-focus shortcuts (⌥h/j/k/l by default) are handled by a separate + // capture-phase listener so a remap onto an editor key still wins over + // CodeMirror — see focusPaneHandler below. (#124) if (matchesShortcut(e, overrides, 'global.modeEdit')) { e.preventDefault() requestPaneMode('edit') @@ -656,8 +641,53 @@ function App(): JSX.Element { return } } + // Pane-focus shortcuts must win over the editor. CodeMirror binds keys such + // as Ctrl-h (delete character) and Ctrl-k (delete to line end), so when a + // user remaps a focusPane shortcut onto one of them the bubble-phase handler + // above would run *after* CodeMirror already executed its command — the key + // would both delete text and move focus (#124). Handle these in the capture + // phase and consume the event so the editor never sees the key. + const focusPaneHandler = (e: KeyboardEvent): void => { + const state = useStore.getState() + // Don't move focus (or swallow the key) while a modal, palette, or the + // Settings keybinding recorder is open — the recorder also captures keys + // in this phase, so a focusPane shortcut bound to e.g. Ctrl+H must not + // intercept it. (#124) + if ( + state.settingsOpen || + state.searchOpen || + state.vaultTextSearchOpen || + state.commandPaletteOpen || + state.bufferPaletteOpen || + state.templatePaletteOpen || + state.outlinePaletteOpen || + document.querySelector('[data-ctx-menu]') || + document.querySelector('[data-prompt-modal]') || + document.querySelector('[data-confirm-modal]') + ) { + return + } + const overrides = state.keymapOverrides + const paneDir = matchesShortcut(e, overrides, 'global.focusPaneLeft') + ? 'h' + : matchesShortcut(e, overrides, 'global.focusPaneDown') + ? 'j' + : matchesShortcut(e, overrides, 'global.focusPaneUp') + ? 'k' + : matchesShortcut(e, overrides, 'global.focusPaneRight') + ? 'l' + : null + if (!paneDir) return + e.preventDefault() + e.stopImmediatePropagation() + focusPaneOrEdgePanel(paneDir) + } window.addEventListener('keydown', handler) - return () => window.removeEventListener('keydown', handler) + window.addEventListener('keydown', focusPaneHandler, true) + return () => { + window.removeEventListener('keydown', handler) + window.removeEventListener('keydown', focusPaneHandler, true) + } }, [ setBufferPaletteOpen, setCommandPaletteOpen, diff --git a/packages/app-core/src/lib/pane-nav.ts b/packages/app-core/src/lib/pane-nav.ts index c834e5e8..26b1f458 100644 --- a/packages/app-core/src/lib/pane-nav.ts +++ b/packages/app-core/src/lib/pane-nav.ts @@ -176,10 +176,49 @@ export function focusPaneInDirection(direction: PaneDirection): boolean { return true } +/** Focus the editor pane at the left or right edge of the editor area. Used + * when crossing in from an edge panel (sidebar / note list / connections) so + * we land on the pane that actually sits next to it, not wherever + * `activePaneId` happened to be. */ +function focusEditorEdgePane(direction: PaneDirection): boolean { + const rects = getPaneRects().filter((p) => p.id !== PINNED_REF_PANE_ID) + if (rects.length === 0) return false + const sorted = rects.slice().sort((a, b) => a.rect.left - b.rect.left) + const target = direction === 'l' ? sorted[0] : sorted[sorted.length - 1] + const state = useStore.getState() + state.setActivePane(target.id) + state.setFocusedPanel('editor') + requestAnimationFrame(() => { + const cm = document.querySelector( + `[data-pane-id="${target.id}"] .cm-content` + ) + if (cm) cm.focus() + else useStore.getState().editorViewRef?.focus() + }) + return true +} + export function focusPaneOrEdgePanel(direction: PaneDirection): boolean { + const state = useStore.getState() + const focused = state.focusedPanel + + // When focus is on a horizontal edge panel, move relative to THAT panel + // instead of geometrically from activePaneId — otherwise `l` from the sidebar + // navigates from the last active pane and skips the pane that sits right next + // to the sidebar. (#124) + if ( + (direction === 'h' || direction === 'l') && + (focused === 'sidebar' || focused === 'notelist' || focused === 'connections') + ) { + const next = resolveNeighborEdgePanel(focused, direction, getVisibleEdgePanels(state)) + if (!next || next === focused) return false + if (next === 'editor') return focusEditorEdgePane(direction) + focusEdgePanel(next) + return true + } + if (focusPaneInDirection(direction)) return true - const state = useStore.getState() const next = resolveNeighborEdgePanel('editor', direction, getVisibleEdgePanels(state)) if (!next || next === 'editor') return false focusEdgePanel(next) From 3c63a6b9ac427a2d5445c0e3f7bf409dcd3a6b4b Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Mon, 15 Jun 2026 09:24:37 -0500 Subject: [PATCH 13/66] Add Vim yank-to-clipboard and yank highlight; fix Linux clipboard hints (#144) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Linux the editor's right-click menu hardcoded Mac hints ("⌘X" etc.), and the Ctrl clipboard keys collided with codemirror-vim's own Ctrl bindings (Ctrl+A increment, Ctrl+V visual block, Ctrl+X decrement), so they appeared broken. - Menu hints now derive from formatKeyToken('Mod+...'), showing Ctrl on Linux/Windows and ⌘ on macOS. - New 'Yank to system clipboard' Vim setting (clipboard=unnamed): yank/delete/change to the unnamed register also copy to the OS clipboard, while vim's Ctrl bindings keep working. Implemented by wrapping codemirror-vim's register controller (created once, so the patch is stable). Default off. - Highlight on yank (like nvim's vim.highlight.on_yank): the yanked range briefly flashes. The range is read from the editor selection, which codemirror-vim sets to the operator range while the yank runs. --- .../app-core/src/components/EditorPane.tsx | 26 +++-- .../app-core/src/components/SettingsModal.tsx | 9 ++ .../app-core/src/lib/cm-vim-clipboard.test.ts | 56 ++++++++++ packages/app-core/src/lib/cm-vim-clipboard.ts | 74 +++++++++++++ .../app-core/src/lib/cm-yank-highlight.ts | 101 ++++++++++++++++++ packages/app-core/src/store.ts | 18 ++++ 6 files changed, 277 insertions(+), 7 deletions(-) create mode 100644 packages/app-core/src/lib/cm-vim-clipboard.test.ts create mode 100644 packages/app-core/src/lib/cm-vim-clipboard.ts create mode 100644 packages/app-core/src/lib/cm-yank-highlight.ts diff --git a/packages/app-core/src/components/EditorPane.tsx b/packages/app-core/src/components/EditorPane.tsx index 75a62e4b..e802b6e3 100644 --- a/packages/app-core/src/components/EditorPane.tsx +++ b/packages/app-core/src/components/EditorPane.tsx @@ -51,6 +51,8 @@ import { markdown, markdownLanguage } from '@codemirror/lang-markdown' import { resolveCodeLanguage } from '../lib/cm-code-languages' import { markdownListIndentPlugin } from '../lib/cm-markdown-list-indent' import { completionNavKeymap } from '../lib/cm-completion-nav' +import { setYankToClipboardEnabled } from '../lib/cm-vim-clipboard' +import { wireYankHighlight, yankHighlightExtension } from '../lib/cm-yank-highlight' import { frontmatterStyle } from '../lib/cm-frontmatter' import { codeBlockFontPlugin } from '../lib/cm-code-block-font' import { @@ -183,7 +185,7 @@ import { isDiagramTabPath } from '../lib/diagram-tabs' import { classifyLocalAssetHref } from '../lib/local-assets' -import { getKeymapDisplay, type KeymapId } from '../lib/keymaps' +import { formatKeyToken, getKeymapDisplay, type KeymapId } from '../lib/keymaps' import { isTabStripOverflowing } from '../lib/tab-strip-overflow' const MODE_OPTIONS: Array<{ @@ -588,6 +590,7 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { const pendingJumpLocation = useStore((s) => s.pendingJumpLocation) const clearPendingJumpLocation = useStore((s) => s.clearPendingJumpLocation) const vimMode = useStore((s) => s.vimMode) + const vimYankToClipboard = useStore((s) => s.vimYankToClipboard) const livePreview = useStore((s) => s.livePreview) const editorFontSize = useStore((s) => s.editorFontSize) const editorLineHeight = useStore((s) => s.editorLineHeight) @@ -741,6 +744,14 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { return () => window.removeEventListener('zen:toggle-connections', handler) }, [isActive, toggleConnectionsPanel]) + // Mirror `set clipboard=unnamed`: when enabled, Vim yank/delete/change also + // copy to the system clipboard. The patch is global, so any pane can drive it. + // Also install the highlight-on-yank handler (idempotent). (#144) + useEffect(() => { + setYankToClipboardEnabled(vimYankToClipboard) + wireYankHighlight() + }, [vimYankToClipboard]) + const toggleOutlinePanel = useCallback(() => { setOutlineOpen((open) => !open) }, []) @@ -1308,6 +1319,7 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { drawSelection(), highlightActiveLine(), taskJumpHighlightField, + yankHighlightExtension, commentDecorationField, wordWrapCompartment.of(s0.wordWrap ? EditorView.lineWrapping : []), markdownCompartment.of(deferInitialRichMarkdown ? [] : markdownEditingExtensions()), @@ -3300,13 +3312,13 @@ function buildEditorContextItems( } }, { kind: 'separator' }, - { label: 'Cut', hint: '⌘X', disabled: !hasSelection, onSelect: cut }, - { label: 'Copy', hint: '⌘C', disabled: !hasSelection, onSelect: copy }, - { label: 'Paste', hint: '⌘V', onSelect: paste }, + { label: 'Cut', hint: formatKeyToken('Mod+X'), disabled: !hasSelection, onSelect: cut }, + { label: 'Copy', hint: formatKeyToken('Mod+C'), disabled: !hasSelection, onSelect: copy }, + { label: 'Paste', hint: formatKeyToken('Mod+V'), onSelect: paste }, { kind: 'separator' }, { label: 'Select All', - hint: '⌘A', + hint: formatKeyToken('Mod+A'), onSelect: async () => { selectAll(view) view.focus() @@ -3315,7 +3327,7 @@ function buildEditorContextItems( { kind: 'separator' }, { label: 'Undo', - hint: '⌘Z', + hint: formatKeyToken('Mod+Z'), onSelect: async () => { undo(view) view.focus() @@ -3323,7 +3335,7 @@ function buildEditorContextItems( }, { label: 'Redo', - hint: '⇧⌘Z', + hint: formatKeyToken('Mod+Shift+Z'), onSelect: async () => { redo(view) view.focus() diff --git a/packages/app-core/src/components/SettingsModal.tsx b/packages/app-core/src/components/SettingsModal.tsx index 27b1f63d..c4d86601 100644 --- a/packages/app-core/src/components/SettingsModal.tsx +++ b/packages/app-core/src/components/SettingsModal.tsx @@ -215,6 +215,8 @@ export function SettingsModal(): JSX.Element { const setVimMode = useStore((s) => s.setVimMode) const vimInsertEscape = useStore((s) => s.vimInsertEscape) const setVimInsertEscape = useStore((s) => s.setVimInsertEscape) + const vimYankToClipboard = useStore((s) => s.vimYankToClipboard) + const setVimYankToClipboard = useStore((s) => s.setVimYankToClipboard) const keymapOverrides = useStore((s) => s.keymapOverrides) const setKeymapBinding = useStore((s) => s.setKeymapBinding) const resetAllKeymaps = useStore((s) => s.resetAllKeymaps) @@ -1003,6 +1005,13 @@ export function SettingsModal(): JSX.Element { settingId="vim-insert-escape" onChange={(next) => setVimInsertEscape(next ?? '')} /> + unnamed.value } +const original = vi.fn((registerName: string, _operator: string, text: string) => { + if (!registerName || registerName === '"') unnamed.value = text +}) +const controller = { pushText: original, unnamedRegister: unnamed } + +vi.mock('@replit/codemirror-vim', () => ({ + Vim: { getRegisterController: () => controller } +})) + +const writeText = vi.fn().mockResolvedValue(undefined) +Object.defineProperty(globalThis.navigator, 'clipboard', { + value: { writeText }, + configurable: true +}) + +// Imported after the mocks so the lib captures the fake controller. +const { setYankToClipboardEnabled } = await import('./cm-vim-clipboard') + +afterEach(() => { + writeText.mockClear() + original.mockClear() + unnamed.value = '' +}) + +describe('vim yank to clipboard', () => { + it('writes a default-register yank to the system clipboard when enabled', () => { + setYankToClipboardEnabled(true) + controller.pushText('', 'yank', 'hello') + expect(original).toHaveBeenCalled() + expect(writeText).toHaveBeenCalledWith('hello') + }) + + it('does not write when disabled', () => { + setYankToClipboardEnabled(false) + controller.pushText('', 'yank', 'nope') + expect(original).toHaveBeenCalled() + expect(writeText).not.toHaveBeenCalled() + }) + + it('only syncs the unnamed register, not explicit named ones', () => { + setYankToClipboardEnabled(true) + controller.pushText('a', 'yank', 'named') + expect(writeText).not.toHaveBeenCalled() + }) + + it('ignores the black hole register', () => { + setYankToClipboardEnabled(true) + controller.pushText('_', 'delete', 'gone') + expect(writeText).not.toHaveBeenCalled() + }) +}) diff --git a/packages/app-core/src/lib/cm-vim-clipboard.ts b/packages/app-core/src/lib/cm-vim-clipboard.ts new file mode 100644 index 00000000..030c06fe --- /dev/null +++ b/packages/app-core/src/lib/cm-vim-clipboard.ts @@ -0,0 +1,74 @@ +/** + * Central hook for Vim yank side effects. codemirror-vim funnels every + * yank/delete/change through the register controller's `pushText`, so we wrap it + * once (the controller is created a single time at module load, via + * `resetVimGlobalState`) and from there drive two features: + * + * - `clipboard=unnamed` emulation: copy the unnamed register to the system + * clipboard. codemirror-vim only syncs the explicit `"+` register natively. + * - a yank handler other modules register (used for highlight-on-yank), invoked + * on yank so the active view can flash the yanked range. + */ +import { Vim } from '@replit/codemirror-vim' + +interface PatchableRegisterController { + pushText: ( + registerName: string, + operator: string, + text: string, + linewise?: boolean, + blockwise?: boolean + ) => void + unnamedRegister?: { toString(): string } +} + +let clipboardEnabled = false +let yankHandler: (() => void) | null = null +let patched = false + +function ensurePatched(): void { + if (patched) return + const controller = Vim.getRegisterController() as unknown as PatchableRegisterController | null + if (!controller || typeof controller.pushText !== 'function') return + const original = controller.pushText.bind(controller) + controller.pushText = (registerName, operator, text, linewise, blockwise): void => { + original(registerName, operator, text, linewise, blockwise) + + // Fire the yank handler (highlight-on-yank) regardless of the clipboard + // setting. The handler reads the live editor selection, which is still the + // yank range at this point. + if (operator === 'yank' && yankHandler) { + try { + yankHandler() + } catch { + /* never let a handler break the yank */ + } + } + + if (!clipboardEnabled) return + // Only the unnamed/default register mirrors the clipboard. Explicit named + // registers (e.g. `"ay`) and the black hole register (`"_`) are left alone; + // the `"+` register already writes to the clipboard natively. + if (registerName && registerName !== '"') return + // Read back the unnamed register so linewise yanks keep their trailing + // newline; fall back to the raw text if it is unavailable. + const out = controller.unnamedRegister?.toString() ?? text + if (out) void navigator.clipboard?.writeText(out).catch(() => {}) + } + patched = true +} + +/** + * Toggle whether Vim yank/delete/change also copy to the system clipboard. + * Safe to call repeatedly; the underlying patch is installed at most once. + */ +export function setYankToClipboardEnabled(on: boolean): void { + clipboardEnabled = on + if (on) ensurePatched() +} + +/** Register a handler invoked on every Vim yank (used for highlight-on-yank). */ +export function setVimYankHandler(handler: (() => void) | null): void { + yankHandler = handler + ensurePatched() +} diff --git a/packages/app-core/src/lib/cm-yank-highlight.ts b/packages/app-core/src/lib/cm-yank-highlight.ts new file mode 100644 index 00000000..5a6ff42b --- /dev/null +++ b/packages/app-core/src/lib/cm-yank-highlight.ts @@ -0,0 +1,101 @@ +/** + * Brief highlight over the just-yanked text, like Neovim's + * `vim.highlight.on_yank()`. codemirror-vim sets the editor selection to the + * operator range while a yank runs, so the range is read from the active view's + * selection at yank time (see cm-vim-clipboard's yank hook) and flashed here. + */ +import { StateEffect, StateField, type Extension } from '@codemirror/state' +import { Decoration, type DecorationSet, EditorView } from '@codemirror/view' +import { useStore } from '../store' +import { setVimYankHandler } from './cm-vim-clipboard' + +const FLASH_MS = 160 + +const setYankHighlight = StateEffect.define() + +const yankMark = Decoration.mark({ class: 'cm-yank-highlight' }) + +const yankHighlightField = StateField.define({ + create: () => Decoration.none, + update(deco, tr) { + deco = deco.map(tr.changes) + for (const effect of tr.effects) { + if (!effect.is(setYankHighlight)) continue + deco = effect.value + ? Decoration.set(effect.value.map((r) => yankMark.range(r.from, r.to))) + : Decoration.none + } + return deco + }, + provide: (f) => EditorView.decorations.from(f) +}) + +const yankHighlightTheme = EditorView.baseTheme({ + '.cm-yank-highlight': { + backgroundColor: 'rgb(var(--z-accent) / 0.3)', + borderRadius: '2px' + } +}) + +export const yankHighlightExtension: Extension = [yankHighlightField, yankHighlightTheme] + +const clearTimers = new WeakMap>() + +/** + * Flash a highlight over `ranges`, then clear it after a short delay. The + * dispatch is deferred because the yank runs inside codemirror-vim's own update, + * where a synchronous re-dispatch would throw. + */ +function flashYankHighlight( + view: EditorView, + ranges: readonly { from: number; to: number }[] +): void { + const doc = view.state.doc + const trimmed = ranges + .map((r) => { + let to = Math.min(r.to, doc.length) + const from = Math.min(r.from, to) + // Drop trailing newlines so a linewise yank doesn't bleed into the next line. + while (to > from && doc.sliceString(to - 1, to) === '\n') to-- + return { from, to } + }) + .filter((r) => r.to > r.from) + if (trimmed.length === 0) return + setTimeout(() => { + try { + view.dispatch({ effects: setYankHighlight.of(trimmed) }) + } catch { + return + } + const prev = clearTimers.get(view) + if (prev) clearTimeout(prev) + clearTimers.set( + view, + setTimeout(() => { + clearTimers.delete(view) + try { + view.dispatch({ effects: setYankHighlight.of(null) }) + } catch { + /* view already destroyed */ + } + }, FLASH_MS) + ) + }, 0) +} + +let wired = false + +/** + * Install the yank handler that flashes the active editor on every Vim yank. + * Idempotent — safe to call from each editor pane's mount. + */ +export function wireYankHighlight(): void { + if (wired) return + wired = true + setVimYankHandler(() => { + const view = useStore.getState().editorViewRef + if (!view) return + const ranges = view.state.selection.ranges.map((r) => ({ from: r.from, to: r.to })) + flashYankHighlight(view, ranges) + }) +} diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index a13056e5..90e2c126 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -264,6 +264,9 @@ interface Prefs { /** Key sequence that exits insert mode (maps to ), e.g. "jk". * Empty disables it. */ vimInsertEscape: string + /** When true, Vim yank/delete/change also copy to the system clipboard + * (like `set clipboard=unnamed`). */ + vimYankToClipboard: boolean keymapOverrides: KeymapOverrides /** When true, pressing the leader key shows the next available Vim-style actions. */ whichKeyHints: boolean @@ -410,6 +413,7 @@ function normalizeKanbanColumnTitles(raw: unknown): Record { const DEFAULT_PREFS: Prefs = { vimMode: true, vimInsertEscape: '', + vimYankToClipboard: false, keymapOverrides: {}, whichKeyHints: true, whichKeyHintMode: 'timed', @@ -488,6 +492,10 @@ function normalizePrefs(p: Partial): Prefs { typeof p.vimInsertEscape === 'string' ? p.vimInsertEscape.trim().slice(0, 5) : DEFAULT_PREFS.vimInsertEscape, + vimYankToClipboard: + typeof p.vimYankToClipboard === 'boolean' + ? p.vimYankToClipboard + : DEFAULT_PREFS.vimYankToClipboard, keymapOverrides: normalizeKeymapOverrides(p.keymapOverrides), whichKeyHints: typeof p.whichKeyHints === 'boolean' @@ -1058,6 +1066,7 @@ async function rewriteTagAcrossVault( function collectPrefs(s: { vimMode: boolean vimInsertEscape: string + vimYankToClipboard: boolean keymapOverrides: KeymapOverrides whichKeyHints: boolean whichKeyHintMode: WhichKeyHintMode @@ -1115,6 +1124,7 @@ function collectPrefs(s: { return { vimMode: s.vimMode, vimInsertEscape: s.vimInsertEscape, + vimYankToClipboard: s.vimYankToClipboard, keymapOverrides: s.keymapOverrides, whichKeyHints: s.whichKeyHints, whichKeyHintMode: s.whichKeyHintMode, @@ -1465,6 +1475,8 @@ interface Store { vimMode: boolean /** Key sequence that exits insert mode (maps to ), e.g. "jk". Persisted. */ vimInsertEscape: string + /** When true, Vim yank/delete/change also copy to the system clipboard. Persisted. */ + vimYankToClipboard: boolean keymapOverrides: KeymapOverrides whichKeyHints: boolean whichKeyHintMode: WhichKeyHintMode @@ -1743,6 +1755,7 @@ interface Store { setFocusMode: (focus: boolean) => void setVimMode: (on: boolean) => void setVimInsertEscape: (sequence: string) => void + setVimYankToClipboard: (on: boolean) => void setKeymapBinding: (id: KeymapId, binding: string | null) => void resetAllKeymaps: () => void setWhichKeyHints: (on: boolean) => void @@ -2768,6 +2781,7 @@ export const useStore = create((set, get) => { zenRestoreState: null, vimMode: loadPrefs().vimMode, vimInsertEscape: loadPrefs().vimInsertEscape, + vimYankToClipboard: loadPrefs().vimYankToClipboard, keymapOverrides: loadPrefs().keymapOverrides, whichKeyHints: loadPrefs().whichKeyHints, whichKeyHintMode: loadPrefs().whichKeyHintMode, @@ -4105,6 +4119,10 @@ export const useStore = create((set, get) => { set({ vimInsertEscape: sequence.trim().slice(0, 5) }) savePrefs(collectPrefs(get())) }, + setVimYankToClipboard: (on) => { + set({ vimYankToClipboard: on }) + savePrefs(collectPrefs(get())) + }, setKeymapBinding: (id, binding) => { set((s) => { const nextOverrides = { ...s.keymapOverrides } From 527d5fd0694b2678654ca62d0b7e4a2fd92ad8ee Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Mon, 15 Jun 2026 09:39:56 -0500 Subject: [PATCH 14/66] Let Space reach Vim while a command awaits an argument (#147) VimNav's capture-phase leader handling armed on Space even while codemirror-vim was mid-command in the focused editor. After f/t/F/T/r (or an operator/count) the next key is the command's argument, so r (replace with space) and f (find next space) instead opened the which-key leader UI. Add isVimAwaitingArgument() -- true when codemirror-vim has a partial command buffered (inputState.keyBuffer) or expects a literal next char (expectLiteralNext) -- and skip arming the leader when the editor is focused and awaiting an argument, so Space falls through to codemirror-vim. The leader still works at rest. --- packages/app-core/src/components/VimNav.tsx | 7 +++- packages/app-core/src/lib/vim-nav.test.ts | 36 +++++++++++++++++++-- packages/app-core/src/lib/vim-nav.ts | 16 +++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/app-core/src/components/VimNav.tsx b/packages/app-core/src/components/VimNav.tsx index f8a618f3..faf9f836 100644 --- a/packages/app-core/src/components/VimNav.tsx +++ b/packages/app-core/src/components/VimNav.tsx @@ -8,6 +8,7 @@ import { hintTargetOpensNote, isEditorInsertMode, isEditorFocused, + isVimAwaitingArgument, resolveNextPanel } from '../lib/vim-nav' import { focusPaneInDirection } from '../lib/pane-nav' @@ -713,7 +714,11 @@ export function VimNav(): JSX.Element | null { if ( sequenceTokenFromEvent(e) === leaderToken && - !editorInsertMode + !editorInsertMode && + // While Vim is mid-command in the focused editor (e.g. after f/t/r or an + // operator), Space is the command's argument (r, f), not + // the leader — let it fall through to codemirror-vim. (#147) + !(isEditorFocused(state.editorViewRef) && isVimAwaitingArgument(state.editorViewRef)) ) { const tag = (e.target as HTMLElement | null)?.tagName if (tag !== 'INPUT' && tag !== 'TEXTAREA') { diff --git a/packages/app-core/src/lib/vim-nav.test.ts b/packages/app-core/src/lib/vim-nav.test.ts index 0b6a3d88..e2eed61c 100644 --- a/packages/app-core/src/lib/vim-nav.test.ts +++ b/packages/app-core/src/lib/vim-nav.test.ts @@ -1,9 +1,16 @@ // @vitest-environment jsdom -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' +import type { EditorView } from '@codemirror/view' import { TASKS_TAB_PATH } from '@shared/tasks' import { databaseTabPath } from '@shared/databases' -import { hintTargetOpensNote } from './vim-nav' + +const cmMock = vi.hoisted(() => ({ vim: undefined as unknown })) +vi.mock('@replit/codemirror-vim', () => ({ + getCM: () => ({ state: { vim: cmMock.vim } }) +})) + +import { hintTargetOpensNote, isVimAwaitingArgument } from './vim-nav' function el(html: string): HTMLElement { const container = document.createElement('div') @@ -46,3 +53,28 @@ describe('hintTargetOpensNote (#100 — hint into a note lands in the editor)', expect(hintTargetOpensNote(null)).toBe(false) }) }) + +describe('isVimAwaitingArgument (#147 — Space is the Vim arg, not the leader)', () => { + const view = {} as unknown as EditorView // getCM is mocked, so the view is unused + + it('is true while a partial command is buffered (f/t/r, operators, counts)', () => { + cmMock.vim = { inputState: { keyBuffer: ['f'] } } + expect(isVimAwaitingArgument(view)).toBe(true) + }) + + it('is true when a literal next character is expected (e.g. r)', () => { + cmMock.vim = { expectLiteralNext: true, inputState: { keyBuffer: [] } } + expect(isVimAwaitingArgument(view)).toBe(true) + }) + + it('is false when Vim is at rest', () => { + cmMock.vim = { expectLiteralNext: false, inputState: { keyBuffer: [] } } + expect(isVimAwaitingArgument(view)).toBe(false) + }) + + it('is false with no vim state, and for a null view', () => { + cmMock.vim = null + expect(isVimAwaitingArgument(view)).toBe(false) + expect(isVimAwaitingArgument(null)).toBe(false) + }) +}) diff --git a/packages/app-core/src/lib/vim-nav.ts b/packages/app-core/src/lib/vim-nav.ts index 85c1dc4d..3d3f90af 100644 --- a/packages/app-core/src/lib/vim-nav.ts +++ b/packages/app-core/src/lib/vim-nav.ts @@ -74,6 +74,22 @@ export function isEditorInsertMode(view: EditorView | null, vimMode: boolean): b return cm?.state.vim?.insertMode === true } +/** + * True when codemirror-vim is mid-command, waiting for a character or motion + * argument — e.g. after `f`/`t`/`F`/`T`/`r`, an operator like `d`/`c`, or a + * pending count. In that state the next key (including Space) belongs to the Vim + * sequence, not the global leader, so the leader must stand down. (#147) + */ +export function isVimAwaitingArgument(view: EditorView | null): boolean { + if (!view) return false + const vim = getCM(view)?.state?.vim as + | { expectLiteralNext?: boolean; inputState?: { keyBuffer?: unknown[] } } + | undefined + if (!vim) return false + if (vim.expectLiteralNext) return true + return (vim.inputState?.keyBuffer?.length ?? 0) > 0 +} + export function clearEditorPendingVimStatus(view: EditorView | null): void { if (!view) return const cm = getCM(view) From c7b41b7a2c01f3f819e35838140ee520f7c92cfe Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Mon, 15 Jun 2026 09:59:44 -0500 Subject: [PATCH 15/66] Ship a complete macOS app icon (.icns) (#165) electron-builder auto-generated the macOS icon from build/icon.png, producing an .icns that omitted the standard 32x32 (@1x) and 1024x1024 representations. Add a canonical build/icon.icns (all 10 Apple iconset sizes, built from the 1024 source) and point mac.icon at it, so future builds carry a complete icon for every consumer. The reported Raycast case is most likely a stale Raycast icon cache -- the bundle icon is correct and renders in Spotlight/Finder -- but a complete .icns rules out a strict icon reader. --- apps/desktop/build/icon.icns | Bin 0 -> 2329324 bytes apps/desktop/package.json | 1 + 2 files changed, 1 insertion(+) create mode 100644 apps/desktop/build/icon.icns diff --git a/apps/desktop/build/icon.icns b/apps/desktop/build/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..f99da4127e3513f542a3de14f758bc208d3b0209 GIT binary patch literal 2329324 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%0DS+q_z0T+9j$P#H;DCTXax7G1(-rEkmPjA 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%0AwBCQ1Kt>3A_-HVFE@Yot>N?7OQy35g?3HW$ZM; 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�(yn_h+Z83JIW`?tCOG2P(w_D}y-vojg@ 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%0AwBCQ1Kt>3A_-HVFE@Yot>N?7OQy3 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

MvCT{e~Vi<50Q_3T@6K60{xxECq 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

41aspKDPW!Z8SIQp-?>IBSp>&ZDQjucjj5z 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

@-5ejQ#l`GQ10oeXD6kB zOGf=PYkoVvt63PNfe&g8e`(7>0wfk3002M$Nkl!c5!92Dbt|tn z?R@D^^+7){W*TN1GNmXtMKbf#oe<%_l79?(Mmq@PqfCS7{k+M{xY+YwNQDJn!K+P$ zcV+18%k=*tlvB=sCsEnV?+Bl6LAo@n_!#&;w}dR0XPWi$bLupu)+8m z*a8pl>A>Eo?ZJDfaShWWB$k^9`x|OInmh%Z%He-#+Msgm1H4SynF@r57!3SLx*8_j z&!6(Psj5`*jFzyRV9G-n)T#3zD&8i(0wtH^(D>`)Gq z{}FisK7+?e51cOY$pew9 zOq2M4@r2*J*IJ<_vwG4Ng#hX zYN90!E{I43hv8ex9EKgb9qb4_8wAX3^Snw1`|Dw^yXBtnz5SLw#`XsEcZ}m@JJ43k zalp>{1s@Hzx{5(Yeh;4z44;eKA9*K?eN}Z>rLW6erJd7=5W&OrA?d5L_Fx}zAwB%X zMHHFuX=i*d@%tgfj_iANkQyqV{)xy94+(-F*hkpg+j~;a?#e%u;U4S0?b)Dx486$n zP4M?c5uNw2+B5d|)e%>B+zw;^`(nc)Mxl`ZvHZnStWR)e=r59=Sbwv|SXh^*WuF~Q zVcyajXN=6Q1KMu!C&@^Je&L_0&OrhGRjWlEZZf|=*wc7~LdnR;c{Sq$=B-+Rc`hio ztYrD}P2p2tIR^#YLymFVMDy<*Ba(5dv+@!`sFzFZ;##MfCE5m(FL-9nK6z>3`srZUbLHCi->wHh&6R z{K3UKN5d6mQ^Ky^+vh9F(z#!cxXSvsc&EwaW?ixCJXm*ERq8E#tbOm{`|!T59bs7< z^Incy8V7&$4)aX*+~r&_HfUuW_~t#R_3*e@SGWIp{y6CRx|lCxm>2c1SYgZnAB(w; zNIEF`t1ETdk3JTg9L{|CBD7q80(zT1arcTXN1r?H(b(tfp~L>X-|~K-ekFE!)Cdeh zR+qXELw{3dB!8FwI!_tgxbx@ZLEvG_h^yIN+Bq-IsEjXcVd&uy@Udd|ifc=6XF5CN zk0`WY5b1sR*%S6#e1+0ir2*N+XE%D@8V`Tz8f*=M?v@QLZ7cCMWVTF6BlZdg3{CFW zd-^HokK#ACK7b#4BE4^V7c;My#A7Mv32qfKEnK7h*wmRX=!d>U&-TIJgWm`#85~Rh zow@Z3pryIdd2{pcfCpP`+pH&;r`7I1wf*$7Vd(mhK=R9IXD!s{o>%av=O@-VP2cFF zf68L-AHFrzyTFLin^RAC=7XF>$dNVJ8OVMKObhunkabp5)gdbNW`+M9lcvU zQPkVha@x9-`r?dAzPBLP(*u5Yo+tlhwU32;p*7UL^u<%YTE45$&AK#J`>Uj_t*j$0 z-#`x@I=0yF@%;=8R-t!VbMM!hO22Mms8x=6Xr|vbT9aOf3Sl^~i!w|+1>RFONe34A zOl3*WjRXt=hmlU=DBgs2>hj2tEY|}wt1o~a)RrxQxk>_e3K{8QpJ^aA7&=OS;WoES zo)k-@%gKD-K}nJk6ELhfXsjSzkEb@lH=JeWn2`w{Dy>bA`&{H?>@)uVk;z{czy+U^ zE{~}W^F*opM44fMp{V3IAAX$;l=q`#pc6iS z3y6FW7a>{S_i!ZmC#S|yz{YS0=mcYFVe)Gy^3ZmqBjDVb^m{(F$?;i>Pw+tS zf9PS6{F)ueJQT_2i}VlwMf58>jl*ou3d##JvfH1?8-8J*hQl{vz|-r-tMVR(|9* z2mZ@B1P9v(7-=gafnCA5hvxx4F!}d95o!_edP;oR{#M%M==)l0 zNm>CHh-lwOJ`S+jOIjT*ADE(D&`JR_wMSYiFiTT44(3`5t*^#GT+7pb)?nOPBIUWD zc#mrb8JKtayEY;Xy z41fS`$KNi3^exo)wIOit^6zx=e?i1130)G`ZQ8@LahFLT!wGuH#TTQ7Gp2=c2d)lW zWH@YRoB5?E$3>IxMiZWe1G#8saDh^E0#sH4K7;Vy2WD{0uMF%48!8RV!35wc@Buz8 z<$(8jBtY)20In>Q#}6P=D*BBy?6ze+*HH5(0ddBV0s%Z^I+z9#^SN$tp%Wj_E9TqS zir6ZgQbpq{T0e=mMfh2?|BC2K5&isM@rONdrH9aq<8VXx3Cfferl+Lje^OBO|APNv zlqUsJ&E#8^23j#C8X8~s%k11CkMgDU5`a*YmFW;+vYe#~*BVe~Kvv3jet=BsH z#`AkC&q&{T>VKO^KC-?3yyA=?&-#EM$3K%Fr4E*gUU5X!kr96^mn=?o6YackmU8gB zCEsbYb52kT9doP|5rmJ-c`YA$itzNP;M}3UqMouWA?>zZu{K6f`l;n>DHyixvd*>! z(B27OYd-=^%Ol4r>lo6fh?GP*u}_Q@Ly-6F4ajvMU^GgoF;bD{%g5w#C4%733s!h? z5VS8X^IOaUuECS~fKsLwbXM%Ui_(rpCI{Q{FB{=o6c`FPmP$ryD1e7EDR zFADy&97%Nrh@v~7*Oxzd_;+xUlR2(pzd z-KJ&Wxb<~~)0K;%XkS%nRh9KDXRs?aW&`K1p?Y(#j!fxfZnH->k%P|hk9Ds@vE#Hh z4f>Vz!@>eL0%wn1+v~lIfclc-ub_*JbF3xXv5a~6O>JxC;;C*`KeW(qYj?{FOGoB) zw3eI4`aDT|G4n3{cs4vW^uaTJ4^&-TU6oiz*q0T)18o|XIMFbOdbNO$!wVS4YsNTl zF7-_#s{BCuKUz5{*H>8abd%?+RmKUuEO5M7Bu<-;0*XsA4~pOqnq@678Ziw zbJ0bqdB8jut~NRtElD5H8|foJisuKXq1gEtd=L%gh0LaUTVkJ+cUpFT*qe+>NVlWIv~ zA3XLyKYb58(#HFi_`n~_OU7jDOpYV(7keDf{Fir@eqQkv6g=l6c1MFp!5;+djsTC; zbn%6aa{iJ~g6Yglgl(t9OaQSe+YyqtFdILs53`5Jis zIQMqm`zQ#vJRbYJ1BNZ_5;I=(qyNo}CzeCh8(tV{FpliJ^cJ~Gz`vIZ9v8At{O{|N z{XbLgmPitEow}lyV865WqopSwZ`JQKJrQ1d{$9R#mKu z;<~DMi@A`IFYd{9d84?uDJuH+_)1}#y^Z#h2w4d|;Qx{T{m;hN9eiTmsq-gpax#>?e6?Ec%MGSyP@t$U`LO8Dh<*Hint8YJ`%~k* zq&JpsmJ$~5-k5H@WpJM~@}O5sDL+9>F$H`o=4Io_|I`?08q~YM1UcVF5&J=Bg%wY8 z*3&l9R@2s*eb+YZMUb3_5L==JJoPQnH~5=C*Q+?{+ui{k--@qNJOOyL?As*{gI+;V zYQ!x1F->GikL0jYA$cKwzVE@Ub0pX7N*Qq(u_)5#rQgVTk7^4-T&^deYxlxW3fPyn zBi1@9_-ky{AIrD7?Gx?2ZC|nv_qjXxz|-ezQ&y%!SMu^vOtOB~P1e4ab8L^j>QOZj zz1!2U33r-~cl%kyfisyCGJ)(L77!nwjH{4IPTj5AwV;bHyQ zm;2?uxBMhNv$7rp@K07*N4%cDRA;a*4;al^(zTTzRPY0-KnrCA-7GQ_9hX?Vd`P+V2oV%vR{`1KTGw9I#Y56-#xQ#(;0W& zKWju(+lpRkY+O!U9OS7*%7hfwrFP#c&+6Z1+EPPH2d6+^pST}ojAZ@}Wj9WxKRL|{ zrWAr#o{5D&dZ0gvaY<93Rp9%qq)BNt;s5uZypT`4B=oo+oeZ9a)Csu~mJ0kj#2T2% zy5?&$HP*vMgar%pS3U;}p?vnAf8y`NSt{TJHt`OC?tWQa7h$X^{i z9+k5?e+ToiC+mxxcC6nswUY(>R5yt|#;4%_1luet^QuSqztq8Fe>wj^J%M^=X+QfM zK&yM1w-x1<+E+Px^1W_bu1==iErD}f(d3tNw6mqtPPhDseEO4b&nYONAELFdwNxYr zdV*TX{fB}$*7n1hFaoW<4XPAY#$gjnDNRU5~FXDj)DwC!_FBBsfXy+yZe#0L+NxF*c?kP1Z z)Y==K#M7P_Y{_zI#wp3qiDpn4)x0#=swg88@bFHEG{_SzjNj$&JdtGbKg<^Hnug7< zaU$zXeln*A+3z^ygWG^Fh;y(Q*ci@r1Q?_IpZWWL?SE%*npUVOS{*J9QCe9OKGB3}2rxqHqTL~1#V~-2hjyEM53rr4 zX&k_{yILjf8t{^KLj&8i#+0Z5jMrLfLx7vLtJ+}TDeW(9HRt;I-mjzp+c!wdmKNIH z{+JH<`TaL-owgJBfp59SMTbO%1e?U zV`Zz!3xU!MUz4%Gc=J9!EA5VS9b%9yx5^xU$SbldM??JdpKU)>03Iz%?l^^gl zCNjwoW};Hf;QksEJ^B5-{^gBd-u#s7IGELtxBl|zOCJ6FpYaD@TtO}th#&iWF(hKP zi!vM^|K@*i5`+H{5C9*-xKsldi+rx2;pq9V_+JsY(hQ#a4F^XIuqx)Mn?T7WUtq}n z)N$A*zqeMu%I<7MtFA8BRsp}z67*=+YeV*q9>4(E%=*+S`CKF+aKpL25JR|^>$+7n zPlMk*!t{2J_quzma%Z;phDY<7dHx{kOjVvAD3V1d6e6doxT!6M;xT>~e6I6esJ@e3 z=ZZ32?e1C*DSe_-Wf9IX7Hs!lF&CSgxxIWi3LMnQePpyZ{FR8z*v!??E zPcKYT-#HjyU%GbXQm^`l>h?}04q|>q-*V0HBNwWFv1VN|g;p%xqT-*ld!SF8bsWR= z*6&L2eZ-DS5C3=w-mYG_v*QkUa`n#H=h=q6Lo3=ZKltUqf*71gO4j(S?XJVvS8Jv8 zNR6lcEo|9l+MTabzHYuKkdXtz4+YO46J6iPz~Vn+*uNB>Sgag);b(t`V`jFxD=8a$ z=7*UpVt7wItxl2#-+YIy6N9;*Iwx~^@&xcZN4&AnUZ4H8MKYs+bGj|scR&4X!H7zO z^fSGx9fPtMX1*uow|A9O4##|n!N1D8g%5zP{GJ8bJRf%7?@yUZxI|23=?Y6RnNb47l+Qw#^3amKe;AhziPX=EkkxQ0QP9q{jQ)hQwz2d=!nR4=Qm5{fP|MZVZ3 z{yrOhKLL{%Q1yZO?=s**S*7=tslK%mT4|YbS@$UI^~yY-LYoF<;d(mh~ttTl>#xGsex7VS(!L-bVe9W%jN_r3A8e9vE zJ4X<#(vTOGr{WUHgYf`h7)A~zjO0@ph(HDcMFY>eqA$}JKfR*fSjvxDJ@)SLoT#f* zR+llJGc+f=jTP;r-dKD@J*7mJ1;c^JB48Hz9mB&J890#CB<+3-2JSAYX$*5uFF+k-1IJ92D5u|di8xkM%F%A`9zntLdh^@Ds zU+3dGnY3OypbH+S_g6K5_eUw2n#9+3Wvtv8n;M&$KY@AsE+M&nG~@HWMQTRM0Om

X{&@wmI?^{(U=y9KPKL>noL5AcRg`Agg-VZfKT5uX9CLQm;G&W!_dXQ60>yM_6x zAID_#1J}#oh>`ucq}&m$x&Nb5!8k^384msrx$di^3LoHULRGGE|2w6CSjhX^aH~9~ z+)&j1$APgxI`~;+87A02`cXZ0KzF9xnEuK6zL!}}JNYFV z`q5t8MRFW}q6H^_OK_pq0Q!R;U*H+^BU#@JuE_7L0c*@3f+-3cfjGW|;;_DWYcQDp z@BKSwxZkCI_Ba1423FxGxPSiUe>eZl|Nib@0$~Pe+;0ngx(qD*Z~ZUCa7g{5|KSb_ zFapNU(2kcpSCbyK16EX8>z#mm6}KD*-gz(UAw1-mq5rKHA1J4|zK?nI39z*?0o==e zPcrHofX3O2@(B*tde$X3JLDL~K z^P}M2@F8KW=itsE>wTe5;g`btg~NxiKlJdzmtQV@Gwn6~DcT{tZxQCF?@i}?AMknM z&CNwunP=(F%n50!w5z1D$2*K+yj;3vCGb~ba^%Y5W$B-f-`X(NqkXIOq0b-S`$zGo z3cV6mWuFWbv-#K+Sy9kXXEzG zG51P5IH)M6JSQ7pIY9lDeDnK&r|_HtKB46xb6M$Y(;1(HfeEJ*Ycg&Y^9#Xz$@nZS zAtMAlza;>oH|?`%169UZ`B7`5eZqKrk9^Je*hl-sIplgApyy~Cd2d+SrmQbnhY7Z; zHr9()#`Bj7oio|Z7tG9a=Qn->eZaR^WjSYJJ!Ut|x}Eca@AS+(kpsVW_OchW@8|vi zQ%zBj;rUj*qXzSSYmTakrR?t=AmvV1FZTgAbjY+)&x9r^o>nf8oA#JiSVoyy$08q@ zv+A?%N4g(*U-O>Jv(tNua#?kU%KB1X5&#BXv1v7savCyz5krso1a}7Oj#+xx2#Bl6 z(Mt09RNVO^fe(e<*w~T}$tyqK8G&1!`2m+O=oMp8+XzS-3H|oxdNj4uKb-J|4bA=| z_}g&aYbS;vLol%Hd1kzzf58U-I}bNSKBR38;W!G%)zp&#d1S{4f#xdE*jPg16!zO! zl}8(4Z!_WKmw|r~#t}G0#bWtg(NkY6>To@ca0BqccAQ+v|3oeL-%zeM(+?W*Q#H}k z;FH(r4Se_C_Q3=+ALsE8X08j6GDpo#I}cx;xNyRLfe=~xw>-7gd9%B zo36@Nn?gkroF;ipUG(u|iX~<5rAe2jT$;@Nyx)H`1}y*4z0d3Xf5*Ucb|Af@I8BSu zoB1UWVT=HuZw}{$tc7$)!&#;k_ZN7p!^Q)19^)&*0 zE6P7&m~GGf3!G?roAMc490r5W+bO!MzaI*JYp|D1|`&zwZOaV+o1m# z*^U|b+JQSnaJ?42E(YA7BuPR~ctTNj*(l0!5{h01MiGN0pNqhJlg|Ayn5*9Kz4mM_ zXhiuGrwLBsekcYx@T8x{!1pobK*AP&G2YMXABvyI8^65ynYaG(=t~~`{F^@n{lg!M z;DC_)?}R>`SYgRWR~Y;_8`skR!2fV}G$j^Tf#=pU_*Fx=&0ES2%w{pbVQ@_NA3Xh% z@(=$rQkE5go6h6Z*)j9$L>m}zz`{&%> zn_I<=BILP~QzqBOd+D}dYd`84k*|#JuNXu#Or6Yg`JV7@RKMf-=dNe&!PL{=@|zia zmv!TutJu)4#di*sO~*W3i~HAytHuA=?tvNID>*noB>Y5rsGzbW)S z=UIO?LS+U1S*Sc22i_FTtn(Kenuhv!i_8z+i&_?2QHa@be?a|>?{?SAd!@W}(B!hL z1MXmt*Gc+YM!9F24xFNt(0C4M=tn6JDl<{7!3U|py(aL79MwWSPyc4=b7Trvy@&qN zFqo!k6|~vhe}Efh5!e0nIx=;+Zr4q62b=e>dS3IRd_W(f&EUD>B)Lwfo}OBgmwAI6 zdQ3=pYo&5a8px6AI-1)9*DDW96M%O`s&tHd+o+2&n0M}Zo>OVym2c0+_^X&-nf~xY zkvF$Bt&pj=Os4FT-) zZ|g0{bBl1Lf^X$ZB;Y5M>+5w@+0G+cR}b@rz=d0egE2#dJDP!?Q003Q^>ZHKJ514v zt5|NnROZ?1vhTF#kFiI$eQ@Ls?Y&yz+Jmde$(%_2(9QfQv#|KElFXl$Ikj&#se=P; zWWINWMzYKw25l}tKO9RP$kRujiR>BZ>2IRl-i7MY>`2V>!|_z^;5BV;Cl)W#W4Q&?x7uCDd&Ed&BAjTxW9`_E=P-T8V6^J9d#U|~JF zc4syAIDoCQcjhvmJl}hVO9ut+=A%;H7uUsJ6ZjX^P3276x$eshDMPt4?teM%EtTD3 zHT{Ysf%;;~BcU6ofsggYI&!sWOzsn&(=$YSBjxa1rL&mI{a(oBJ%AI?8)%=Nte2MK zLcrl>y(Z8>F0ekpF1Q=!0edNv4F^Vj90TAR`eexS+{-q0==&>nF^20~Ia-YMhAuQm z*#R`82h;^7Ltozj&*64p-6@}7sA&tl#)u362BL4Ut~eohpYAKm(D@KxHQW`@8+0+3 zq~BsPdj{uo{S-C2fe$emwFFi|e|W|Hp~^MEe5Y?t+AbYBM-4|q67CYlmtTgFYPk-s zWBJ~6&Z^%FoQ(Fxc%aRN7<{3?-}(_W$7MKL-Vxr=j{CcDaohpg;3#rD%(zRs1H-_F z9hi^yZozdwTCoi{7yXZUt3<(wh2oqyNpOe>}%3d?8wM|5sv#c;$0gRLXW#>l@z8hXJEG#4n{y)>**c-!+@G9|#6KJ>d zXYT>qDbU~zt`Sg+_FdAwGLS2|R_fvm#yfLu_S4+=ptWDZ+*J4p``!!69l(K&0|)yb zhOXrI#z==`ZqwXK?gpUA&vwSaI3$nuL}WluQ?4c5%_zZZ_3c~D9Vi!1yO3I$?+$Uk zu#aQ;4E3MzhH)vLQsi|}=96EVt*dhr?Hl(t_4P*DRW)RD{^gh>x>c`Khw*v;^u0Ba z^>lOcqbs+Lz<1jw?uvu{<}CBn&i(*0GWKcY%lZTav@Eoucrepyf92El`|_QxzMK8e zgU|WOX*cuQG|$vr9}B+!P!`IgBiQNHpS}JZ?V#H==9720*4e{zIQ-|EnXg)=9&Tz# z|L-Uh2|iFvADQf817))|f!)m<-2w`~pq_7WIF8`?mAao1uya>gDVP`LKTJ7VEO?rp z`-l5?=;>nd-0=kVS+UZGO@?5;C|9CJm1n?>1X58f4F881E1 z>nQU&d})DqLBF!!z`x+E!ZrB*57r!+0V&}{a#AArI1!lXIzu}uhZS2;p*jHhjNijz zjf#QqMi=TQSEv5s9SiLUXMA>c+r8=7D4sv@;OnP%k&_aG&IUwK&sN7w=TGn>xB5(G zuyOA5JL9*T_I{&nktyYK)@5zW;eE5O>9HBQNLa^e;mh-nyn9qD>m96S*%hx96t?u%E>up6q#kZLsWb?lB4}Z50^V(nd`8dI|wq1+Y;UDe- zxgUC_FfK8k2i{}Ux6Sd`mO(qZC_A(|ls~q0vr=Z;$T}G?A4aZoi?T-05B)lsSCY8iMJ$$qY4DO@gP!@o3DPOw zfG!sY_N2CIz}Z;${CRH+|L@J;XM{{acW`(_8i4~2QBg=f`H6DMaP*I2*3l2Ba+;B` z%%2kDioAy82doKy`xaP>0IW}dFFDc#yy>%$3Nk4d*SF|hfT?h>Hoy->6G8j+32=vw ze0LyJ9X!I-9vK^a{;eMoF8UeQH;McNrAdB12H8PTqLECQw{W}rU`7Ti#PIL`!#*ej zxWG?QHfm1I6;)Z=r0vzD!c#|GqYehP*5;}R@akCgs@fGrp}!iat)_fk zZJ?qEP%fdi1d`lN?FYQ39#N^Dc;#>vg-`ud?IU*vX=BxNbtLeJc15E;&7(ck<^p$X zsnQ^?ey1gCvi?(Qrdk-n*IgfK?DQLfA?-&1b3ly0Dm(^BL%~3e&A)Sfg{BzLLV^ji0c?-(8N$| z5`iT#EmZ?16QlbaSROJQO}*Vn8738k1(<-&0`p^fssXg|JvZpp1b9;F<^WGPjQ;`Ss zK3W$eFd+mLN`5cOccH@zo4oFSQl@If9g~EqP4PBz_wWQ^*!6c-bX0)tcJWK{0Z-ad z?=p1}?HZc#Y0f#mx7E_jTm%DWuJeE`TDIRl$X=57n|bzn7W3SMtTfjo`ZLD1(|Q$H z%y~#U8nWVC9=X#|3^50pD)QbW=OF6<9GI^XLXt2CN<0ugjl(mQDnY}CZ{i=CA;MeumJzk!TeBsN-JCDJap7)Bm z{{;M3TU{n!opn+yDpJ3`z)@7dw|OXdFds4BFtw*+BlRj?+9w_;TeQ}cS1PXs{Syfy z6WIqvn0;5wBatC0sIoj%X)NcdxTFM`G|IZ(M(6vYDe@WfQhTcTYA3k7Qg5!G;=YSn zgUqBK)oSR>E4>AV-YYyeK-;4tcj;5~esYJU-d+1trVQ40Xcn2_0W<4-DX-}&E@2<) zpP;m`j^cW4az!!U^>6h)o?hT*gSI8(7@tYb^(OJ0c2@Iq?~(X2S^dGI?SZ*pF!C-( zXtnSy$os|gMW%~Cu4+lT8jHLL9x+Vy8{FLz@cH3w7Ay-K9o zX~%xp;zvf@e#avZ(2rY>9<;hcy>DB_4UEHJU+8xJ5|OO?W|6frn(oA*)3EfSI$emX z_#td_G1|>p+9Gf^-#e>45Ug|WWbZLG2{_t4%{znd^>KeIkDy46%M6L9zQ=`Q3*Xv~ z|N7heewSH)k9}W6JPyR6cJ*EDWZD(~=0!--XUvyD4?cLq_{VH~QT1&ZywBgIl+GT8 zgR)@es$9m$6XpKN^Euy5QD!2mD`FmQvkkx?4t;0w z-W@m#gT2&amMCY8oqTV|0kDtff8lIj);H#mIO$M>dSr7Rubelv42S#`s3ra5x6g3M zu7>g*rF5K30gN9G|6s##GZrlYdZk1mnKO@&=Y5R8l}EUF<@-Kjf!z7XN#M^6M|Lqx zGlK8;h2GLnv<@@Ie+4Io3FSy3pUHHox3HuCrF3YJ_Hm#Q2+}|tGIl=p=dhEHRQi#P;qgrftzCi@UKUx=VI7~vs3hIIoiZsIkUm8=B#7|?w z2s#2Sa39t+SWT6DN%UjA762Y)gtCCg2{7vm+(p<%6wiG^$O3dqDI#hbJ6ZaOzl`!m z@mlU=Q8e*Z&>m5Rd1cX3o4(GwGP5ehL3wd zFYcoDz!6}dbwO)F4gEDxSqHA<&eoCiQ3lu-H0nLi&ib+R;{YL3a$YRPkoc5x7u}|t zfjjjS>1fkyKye!LUb0>U-Y)rhNm;M>Qdq@Y(7H&E)8hDCCr=6QLZ0919jv>#e%#&A zI}(0aJFAw9d7k-Y@~U*!X==WN-_n}HUpAzc$?gX~E1hya{RiIbP9lHsn4RG|pBv4x zYLk2@1v+J4W@~4u1^MP1Op7Rj_1y|FLq*Q`TOm~g&oS>dJX&W;bulF0%{NLRNKe5?U z(z;ULl^Bms7LbLTk#WfLeblET75F*FO?`lHbAHtz{(?YgBIdueBdWPaeh1e zkZ_yZXQAsIwOcv!NgU-{<#I*FM|-O^^(>_uwuJL( zRhSQs?14EOQS%Cy+*kM-c=t^ym`Q&#re~bZna#Ky3o04#G4tuPrLLt9^Di^4XC~`8 z=b~$aoAsD^Hv4%t^CxG1_CXow;_2pDrrH^&i|^ejdwAZF?vwQ#JPdTC`Ghi#tJ4pp zjRDP{yQbva059Vr90d+yU*GR{%|4v_U!;GT%KOnPUq5|QgZ8%fex{9}U#AFD4Wqt2 zX%jLSf14jiTBU=SiDZCq(2oJN9%R5NJDaLYRLK&vuM8}bQuaXf!_VyqCXT^f|g6q|wqRoH~@)Xnr z?!tP9yhYh210wnUW~nzIQ-sO@2KeY4Be3*|_<`UnMW0L#GWlG#5(K9MuSzSKSw%lD zzL5cD9LzFN%;EkUaYL8Tse}>|A?3Y(M!R3o%4?LXL6_r!{ootnKp(iyS)jBJ?gL8P zxcU1p`=CGCRVT^~Xx9;-AEg*zf6`Z|zTNL@-ysA}|2OTrJ7jX$a!lD0Q8RqNUwdx2v;(6SWSSR52Wwnr;K* z5UF#3u_zp?fgfWoKoP^(!D+0(!Wh1!BG4UmgZ86LXGp?H#Bpb7&?41XPa~CCC}O%K zz%7)<#{EArXEmanhTtw0o8p+p{DG$+sr`T&bk` zo1b~>FOR@-Ai!1>S{=N3D?*xxAmUmLiSG{7Cfa5fNlo5Yq|fZq~f44+oUndXV=u+hPsEeap^ z%+KBAUW|dP4W{R!z_D7K245Dz;#*Sy2HS`BiWcaX^%uumTW1W5y)3)rbuH7|RmITh zIh$L{gW_r$X#T=H5BRhBC({YupJ@77gkvz>?u>LSL7#2wJKm1r%yTW*?LmQ3&#Mt~ zC${MghXK%E`%>RFKFAy9HRcMYmb5ck-K6oJwX3tQ9l6AE*ICaVjUDcuBSV(mPuvqd zTX|o%95e;-yuhI-Z`)7!D7JX&S)f55IReTl;5z~B34xt$XFAQn80>G^ClF%(^%heRjf`20F1 zB>pmDOGsS)cQ%Hpfqy$kEgT>5`Qq)L(yz5zTXh!C4~SmzivDHP&K#aX|2~}bLG21< zu$Pvp`A6NhGSzWtLcu`nk9+K69VHMiR(TxB)bBZ|%#taV-_Oe$nqZ+{UG$}LN2)zm z40hC|{>!d@8t-L&=jxty2Rsb%M(0$Ze=ki1B;NEI9M!NEyuGjl*~54~NPQ%CmPLQ|wkDLO3ju7if%Lc@yQPsZgcL^~oAZ&9vlM4)%V} zbCs1)Q)S>KeS|m#tfbF1F+ZuNmJATkvXo;k`swbgPDll>>OSw}%!ynNRd;yyFpm;U zA-?&+&vxr?!K@2&hUtk<8s8~{q7A;Z`Py;sO6nV-x0LN8ZU0jvg8^sPvg=HKr^)w>)Y4)Fo^HsF7L;&1d8{7x01#O4)mTF%caA212aq>6){2{@qpRnn2`x!uinSlxqXVg66st&fLoy& zun2B|x4@e?a$f+Gpo6~vhqA9e1Xzf$5EJmWa>oduazRxYcl3aQhM|50`z*k-=q?5* z1GwH8&4KR=-0#K?_s%3^jOMx%H^5M?pHOOx@{}it2V`lWjO$@IZJzXM#t&X?I-f3BaE>zM$L`^I?p)o04}FHX|3vE08W zc8J@+TA0}|sA2XxDh*zonVCs)eL0_w$oIr@r4Xv0k@ru_FpAz;vVHdr9B}DQg8gm*ufLb_@1YHmR2#3 z2%vz=^1F(A>^bl@Pt5^NC7gwI#Lk{(2ER^4lbpx(SF8;1p+r+-C14P)x0}F<)YF1? ze!>AO)B{#K8hYwqgi|%;e!Oy6I@XXQ1~C&j;s!1czV+JLcN*7qFZ;?rq4_nvR?PxE zXqVk8XB>R#LE7h;(67YhZ^Zje(1ngM5sA#F%;)KAvZ0sB4U#LTf~U-L85?qbWf^?v z`@7F_=*w#FdG8^1-WLju3wDEsn*mk*nGdP;)Anao<9;jehcfelsjg|iIhbi1z3REn>IGfW=Q8E)srk*+7 z^@QjB-8;RPa5#UIn3H0KZYDlj`@FjhOiJwX4*HuNpCdA2n9jF?vgNV}x)~TAa2K^} zb-o`0Sy!GE^%L)F+Ld9>BIrW#PWR@Q8y<2}T%&TJe`aC%LKc=-jHTRQ_XHRvtm)!ZUF48J^S>Mq6LXt0t=THIKq5J4=FJ--iazJDL=(F@O z@@N`Sn@~B{nbJv55R9+>Lf@r@(Vms2)uyI=pMVk5X3DR`Iumq)Ad&tu1EKH{30$LG?od^LQx1bznJt`izl2>J(8``%{{e%H-K@8v6mf3-;zCAeyF zI~(3^7Qb4#Bz&fu-%LN&;q%__+B|qbW`a`H#?@N^+1yuaOkJLPaQ;R5!vQ${Bh&V# zUVvHlO8;cKtv*5(~F25Cxv^sB698@0$1Otp}{Hx6R)jd+#A9`~pe7~j5YOhay?Q*y0?&tdj-PPS(*QSs! zCYtfdfV0$~+>Aqrk{I{Nwr@=IJK&K2RUha@PQk3vF4l#+p0}esVnUs!9hLSn=L%;Z zpYgnBSA%3G2X9YLfA={0y*c+&xZ{8wI%*VB%1A>|N?r85jwHf_vFyF*)1in3FyvC4^K*}F2#bAT34TbyMqr3|j zKkJ{2GqWXK!p?Wd;2RBjswL$~1jw-tl~VH1ChGeg>d1W6cXB%NHp<_@KV?1HgxA@C z_msvmfB~*u#NeahgjPW}(3ZmZmZG%63K$2>$J6zAzpvW}L9z)IRVT<~rbmdR>!wHFG)0&)XPp?pTW zDh;`$3VW)duxaajE97oZ(;M;YKT93CiC zC@Gf>!&VXm7)5~lH!-a!nDt(>D^jNP)Qz1Qafs`Z@b<(!F6((sY!(7LX0lM8l{<-A z8WFlS6cPDz5>Oz3$(Ylm;vjKVfxm|^^PuoxZrqEpQX$^C5~LlAC<=gNL~%UX5tzkW z5Oi8o24{`jL2^(k8oXV~i13ZWK!oA|8%Z`IqnqP)!H|{Lzr69wo1d(My!DqyU;dNn zXFLIGl0Otc+=tJ?jph(shD94bCi%`W_{DJG|1SS~h&k(T{&&JK?2X0nRGNDFi+qNI z;u8EQjB-~T9`IptRa8eM;mmvej7-g`@6o<7QZw(@Z)p9wzfgQ3;KRO)0vkxfzSHg> z<3zFX9?0$Ff$_WZ60uD2B?5 zwhyh>d2TTdv=bP*?^w=T-oh4_Ij1-hxc|Lvll5obi{S8|kvz9stESt46D^xe@O^7V z+c+!y*s|1;Vrjzno!Q%6801W|#6eM<_b{zW1EjOEbGi@mh}F;D%ZhyCt*Z6Wz^9fV zYDq;+3*m@@AkbSgB+Y-aqO{9wn$;8oamx}7yEV_oU$~)Yy~qf@^PAsAJqknb1~nol z1;5PquTR9$zSj?9`o{0T9{C9w4;El(?_J<(P%4Juz<~13&KPb-xH8?eH}-aX>x5Sr z+V|Kd%T%ddziBbA)h!&kKb3r1W)ki0XWDDd;CUY{!G$3*v7aj@5xK@2&JJ?QR*Sf5 zE=0X;EMb8&sK?)QMy9UCx?LfCD(~%cr)F29*9*l~DUbNrZ9n+M06(;D4V(u$dMS@I z#x?%W8vQ|rgOJj%Ixz42rZ{x}wzTV#Xd&e} zeTmqmEu{WaN*#3<@VGusET>+x@>uYks3e}s9r|LE@`3mqxLGtZc2u<`FyJ)Lt<_hE zKH$5iRtA+;(W>aX!&ycSJCF_O*te$rret)?(uvHWxm#%_q%DqIcvb*DRoKOAz&wOgY0zL z6{F0tG9Ko5Q@k(Nt;v>se#{5)ovFW1IQ2M5iOYRGTkq+e0j!{`*H&^pM5(3A>s6IF zO)B86wXfv3aD>(f(&OV;(--N@fVFg8Ld~NGYKMW7v@*I*{iQ^V<1zP_>xrg81a^$i zY?sdbj5jCde=`X`SjOW$XTi&X1!q_8=+BPL8V`@&jmC~WKJe#&rUZ&`{z4rS=$cZt zdV^%_#Gb&mCBToPoJ}!~bnr9P!Wh9$Rb{sMGrse)`ZIgf74?R9m6v&*_AqmG>TAAt zE9GT0hQ$}p;zr%4-#2RA?|u>a{Z`nW>i1cv$@aI^<=M|qf4cSj4f5~HN>8@EVVu$@ zrKV(jgQKgf$D&F+7lKag1}^1@uSMXap#B#Ux`I)=y*$$pL$(W}yL4bTF&x!S#gAcNHnZ;Y-UX~lnDH@S z3Jwt2zI@Py#=uE94$cBIq0c~3oD{-0fV)MG)DLi1FBZRYzc_o~p%2ip6YN5l->D&( zkL-}YNiunbvmGBAj@zoZ3;I!>fyMwG;8zTntfEALA;yQR9SnVDG+_(O53tyl5Cs-6 z?&aygn#@c&k3OMR<_TXNU^q?&D;)&=!?}|r9r?;KT-uB$Zk_RlW2ZU$PGo<(G9q5! zVs@xB;`znO2>l`DxniL55#@q7Ud{u56Nz%XjQ(xmer0hHilXq1v&Ih5ib^TbiR+(; zYx;4nCviIZ5#T=M0c%Skydc-m!y?(!XyLg?`soYI;LOYsz~O|q_2;>3n6701oa6=p zVE76)bgwUHtsK{i;EVAFSC>RSU`?=73mAmE<(hFnQ}z!hxZNnfgmXf-m@n0h9rdpn z#ozhPbkRp1IYSu93Bh}Wp}dj$A^lrqjNkvaet|KP1w4`<0>*eGzX>=UXloO zMR@LK<(53^55wgdBXFaka?Eh_-69F;&pa1_L$3wvEJ@!a9n)%p_ma%m zl3T!)YeQG7|!Pqu&-+ft~eKnK6L85t~H~pa-f}ochznI04?2b7F)HM054_Hcn4r+$QP! zTnni0fT&7PkK*6kUNdMV#3nnuV+rP0REBC0vc)Y3>9T*3mp7nUSFq-wK9`Z(z%|OX_ zJ>Ru})3mpea#aS1Xy0?VPg}-epf$h(fAQwHr@L9Nj=DZw9gXPsXfe>lI(mOK;b1c3 z?f2Y&s`CbDIqkjXxdIu_Nm2Z1#&yq4yKMPw-K}=!^;7?mezYTZU#`QQNdL>K<8x<(PhI-CQXQ@{ zPrS+6VU2ZS>1^q13FLb=ZL9tUJo@YNWri+s+;mMkxJ^?{2TUz_ZjH60^)dMU$lJk? z->$)*TGOxl=F=wDJK;su<-F2fa!BZT%Dt7rl0IMui<9~RTFD~A{*?vwWBmEW7u%v2F~4%vK5MroQ(rK!k~+7 z$iw*CP!T+;p&tH^7;4>~@ z0mA1beftYe{scIYQdwYMxK=6P3pjQaUVp{^mjD+N81OCs&qs)Z1L#DOFmZjfvO#7H zpdY%}0iJ&oEA_ct??$GEja&~T1CLzK(mp8h_g~ouTW~Z$9qyNg`m+uR0&Fe$FW-}I z%0Ta9;6i>U&;BXrS)P42&wdS`NAAh9&r7tBgMPu&BZFQGJrW0LxxFmL!K zTw=dKjuB7ETn2xJD~>{AQwa0EC8S{BBfggUooPN&p``D96;&W75Tw4v$ZS?y=_R6g z{ydg9t-a7HYo}0nGE_gcB;|7I=PHVZ8me}aM*+xxwS2%eYGV~aSKFx; z(onS2j|m)r(5YFfTP?!%$6A_p3%G+n7l2pQi5i4cU8N0|DN@za>LPU`a1v*_qDW~2 z)g)~f0(CGZ)F!m^I|So+U=<8=g@G>284H2g81DRmr?_4lSPnJ94YaV|eiE=gss#iR z<>a{XEAR<~bR@7e=8xXML6nvP$MV06z@exk<$ya7^iiOxE5|ULeMF3|tT!3eKpL24 z6I)ysxRsdYYry=Nm!v@QhE|3GBQeMo1U{z4;Eh&1;$0MX_LDe(i%@J{0HsN?692Bi ze3$~v>tEjZ<;~B$^_NFq{xj)kmw))f0=S1Xd`II-mHh2X{)K;H#&=1>{r@)qOa6!d zJt74<{2N1<)7V+_m=v@aurP3q6k%Lnj_Iu*FpZs-1G#>UJ-2&-{r3(gLo5c~t>{J3E; z7$)>gte*0m00H0lws8rFU!6l6`u#|zb=BE)^o-(cV#Z3*y2u5olwR{JgI`CU0(b3eraamW2@+D!0sL$!IS$M&tI zuj2%G3bqap0NR4gkDT9Qmu-=D|LuK_CaCnL>!l9#qfctT7j>ByCj$ZsZ^A%dzGS{u zQ)z$2por2_z-N7>uXi2&m@1Zfwgan*+Zu8iJG$yhIgQlzgJl4uslPa2y3O^f$`PG1 zJFObX9Z{l~^1|cvIOWCM=}1|gY|pr3Aoml@TWHNY!*R77NB=&`n{i=YY02f*!0t{4EE#-77K;1wMIvMrg)H_`#FuUD23 zOiao~>SO)mtPV3Ac0o#jbbzy~!;8evKyp=%O9eC<<`r)36Na8qkA38&Da)2s;onk( zO_Tuo;%4~>SO}Lm^NE!1zZnjK55Ry)J}27XJZ6W#I45^pk{WUYNrV;gr`T+yOfDc| zz5^0nt(;DzgVr7fW@WAMoFfyP$!WAR&L@IcAm)yCQ-*8Ab!cf(K zvPSP(1M`8O5Z?a^vz8p+fAy~0Slvr{Q-=iXL9`BaJ?U}11@km z&gF3BuLlmqee(zyLhmjEz06YPCAGB{79cyRjX;lixPVwk%-vqbeEF~Xw}Sb(pXbVg zDLJpMvVT66cK-$DC4Kw`JkWleRfA*z8yNvUGIowG6;qA*@RcYd0|#jHDMnfmMKB;T zfA#fX4LZcwEhW?h!yo;V^B`RO(?9W(k%_J?ob)HI`w-Ig4X_#f`!F!X$Q40FVC8$t zZK-!UFpXLZ0Y`(0^T6*2U}+DO{R;&~aTo)1fnfS{aG{9FVwi!ql4#I!TwZ}aJ8+sf zi^hv1O1xkVE96CZCt3hi(<2Ezo!mdkGwX8-Wp$HBg=EZ5?wm>gGw-C{&8*G!&}>(3 z6#Q@0TU&BZV7vFj-*1EO_$DPLHD*{Yd$K&xk(A4+Lo&c84zQ2)c1*wgwuv_8P2g4k zx&gBp_Rk}?hc^N(l?%)aX8uG>jW`?nE9B+eo6LkZ)YCy->7^b`XkTi~TeXtfPg_Mh zELo$npCb>v_K9*p7i<-*k8Qo6bD8h9B)5g`#m3lQ4Fp{gnW?GNpEe-tQEDQ5;UiOX z6Z6XSMm(|2W%|}5Byc;+(&9*RFz-_arp!%w0sY+gAoy+oaSFX|&AI`e?tS;<(+6v5 z|9hWNPUua6v%%F1GoAs%oXvvsA*b|Im*teDeRs7}Ug)v+jyl2pDfnDMW{USfKTNzE z!NUoCstn9D-xbZw%nS0J)E4KteorZA%AxFL{Ny}*qYp6F!FF;5gme8%rJt1NtSc;+ z?enOw4FNmQRb`AmN@sl&a4b$<(tF_l?<;>>r3v*k zvn*8^uX9r_zrHqt%m9JcSKVP9Wm_}Da$G#Wsl?>s%!lxw^H+!*guK!`Yjz5BDfb(1 z6W1^F`&7W6Mf@OBB?~Pn>4P}?CSmlmW5`|o-8DQvKo*wPf7$3E@{~Dfo!@KtQj@sY zxRP{7#E(csUQp)ioy9oX-&Azs5EvzQzsoQ82KeY~nQLA{KxBu61v0~fqp{C*M@#y< z*Vf0%IA*)D^SLInObR>SI?55(H7X=9kom|eSSjr`&-1-({N`KauN|SEhty|&{TVPX zkncI#`fRZL$-GL*Zw5_D*v*g%!Hi?PE#chTznIrwWfgUS2b{q^rX}F#mUpbgqv)hB zmvr5k&YF5oWi1ux22 zQ!h!k%?nMNOw3cOYVBe_rnTmAru@v`3+6^<#!21dDd4?HJ5^I#6ObbT>KhJ+n_4j`Pbq~> zyQExZQcaW0rzy99kMMovYvi;#z*6L63_SA;c_WN4gdtoXBYrUGRdF2m)40DwNf*ous9Btp$Dm@JBwfbQ zA8*+8KH%UTK{<%tvo2UOFAO_gEE0o!ErI!4;xa61005Qz?UJT*Sb*bMXZo_F6 z{a^OM$x4n9Kz5(pPHygxV%q}T$72tj_!8p;U|3&*CmPPObonCA3BOk1TwJ7gm zcFB42Fa0m)f3SbDzLZmW_Rl=~F7qJIew}BZmvUX6|Ka~%|4JFGaDAZnOUQo%Um)oA zcS-lr>+Oc4e=-#LB;`xE@*{wy+3SCm=juVp`vJQX5Ds7F2*RNTe;O?O2qBh^{lg*- zn26pP&vO;XaWK`Jew*Z=a9z>0jAXgXcRzNx5AikSy z5@1;Iu2V;Qmta@y^*$nbGmeD`nF~@X<42xaszi}}_-QWGuiCsSJ044ht|&@Bvp>^E ztQ*DacNbX#`dh+RxQ;YB)RE&u@(+oT1eEULw4|c_U#RfHL zOuH&ZUTLrjacZn>qzo|Noca`(Gys32ob+A=k4Aa!uhHEa^^&mqSMM6&1+AafitGORBsuTJY`v*D zhUW(*%z-am>Ser=Lwe7mVFD`O5HJw^@#^7nkodUIFbKeZn3iCcY}Nvm)24 zE-n*Pw+H^;RiPJ3ccPw#im!Pc{d~*L+9J%aGG3e9$*-K&w+Sb(t=>!hgLxK|aVCl9 zqBp!XJswH@wPS|gvEfg@`ryR1;q36Qczwu&+HBXG_I&z#NKB0mHuhel!&2Ja8}$aK>FJfr+1W zLt=Oj|C6cezaaMpflOi|Mi43I)-!e@M~KSO!A1QKjUBAha7DYo<2I7(GL8b1zkZVE z({OeF4y=peTguIik=kXs9Fn`#V>GB(abPFp>`>r4!ZPXrhw~-$5uPc68;6{DUYcM^(oxcz(b{jJIkLa`Ka9j!ptR}6Oq_~S(2zXOma^Pfyn#cScH_?IEqCsd;i7`! z!21vvNJ*~uBy zOXaPY0j$P%`*Z&<@FwYTb==+;fxoe{+XVawvr{2pdA=&!H-S)v!oX4lhRgAN12(z? z`+{NSjp8qs8L5^d4Q&a+#TQryN))bn<|AvQ2LCG1?lT5l1fH+)KM(z`32X|Mp?9JO z^M)tLoY2r1&yi?t%p;sBla2W_U3nz?gEOWmPb$h9MxhDi8n^dPNn>*GQe=K{3OO2{O}dw1&lxQIMp!B`HH26 z9$5tUx5Q5r{iy&f3T7o&hAM9HH{zly`#F1mGI9ZsxTZOaO_T-_67WnK?l3sO{(Y3Q3o`prNz#6$5Ah97ps=I!06@c!>rZ!F+$o0=XbyYvwxmNg?Uh#apj1}2V_-3%lZnoop zeEPaqOPSs5#Av#_(Q=z!oMhRlDL~M%WkRggyoXI z=#`RZSsthTy32sRN8h~Y`21%A0xn!1d(|Jd;`glHQ|O3$Nv@U~1Nz5CTrUnk^WM^H zs_W_BG|!LOjE6o=pRG-Xtxfe_%w(OYXZ2y;(>%99@1lLi`yuK|nNnV#B78KyqxaRL zWS|_@ivPymd&XH&tc|{1y~5}1eBmilq^UN z5+vs|Fyu7A3{1{p&Fat3Dr=DC-{VoOopCNV7 zPi2}CqO#uR2c%3NMY)c$(_WWxM=Mipthd@hG*VeN$81}oV>ac##4X1@z87ac&?5&qgv9YYvROe-%IL=)>53f;$eV8yV^XAj>#FzY)a_6o~#$gq1 zRSf#md10H-*TA3b99-?J>t5d7kuq z_AcM~`Hu58{k3H%yJAIwGjBlc{*v9;kccdvCKv8drN9tL=H%Q-R7<^}kx>d^8hRwKc zg8xb_eFQ;^ib%Cx6BT){t$9WAU*&spelZV5OMQyW0ID1$uMc!l99KWHPUKfXPVYqh zEoGjhtLA(qUCM9J?G4Hh>TOC7T;XX*B(=T09R-J#87e&RHZx|dg(`b1%Y3yDTkT+ExPVS zfZHC*Md@J-5DrO?+;?)NgZb01iku)#J!FSyM$V1fO!z`o1AF0fx0 z*yjuU5A1{gTmBUl>H84vkAOn=g3hZ*+{g{&OKT>+qP`;`Xp%o&h65lsk(o|IBan1| zu+%5SXfe%l*jMN6anzSbV|YaU6S2tB>)_~w`YHsVrC4(Q5~V94Zi>hw1KttptD}ib zpQ&nyAb$C>m7{Sz)a(XO!2RGGmh&G zB-B6ACjw_tgP@_;)8Eh$sP(~m106wLA4F<@6d%2^aa!Na?-%L&WJ)*vzTU)`Nx3Um z1YmH|dl)WbE9I@mC)Sk!QwcRd0nr;8EA&dhK6+REBl>yFbXy8aHsbj)R_+m^ZZhyZ z3IzmK*+~+_8^GqcNcavyu?rI<4dXyzoETGTS&sa97;Gj{-iL~O5g5ver9!}}n5^Cc zW@0jA+$b51P!^;ja8uqf#_5V|nerd8JKH+pBvXXWpqzvOI1$(lRYWQVr4ie^0;giw zyaWuuFc}Zb#>~|Lcm;LmG|+=tI1?x^xkLgVAl5Tpz8fV^&=riYVEqd4lYLOI{|e~K z--~`uv-rauxW!fQKNdbCr8UVPDgq^ckP5`V@juK^7XK56G26f7f0`9R{H6JWbg+_2 zeW!FFn3Hi?45SS`FQno@_i*xYb37{GJZ8p zqXqW5_P!}zT_}G;(t75>T%z_8)oFfiAy+_a{9^NK*01D+U2<^MA1lqm{)=EvBh z_m?SEliy6KoAN9DJ!iitQ~%ZvuDQPQB~HF=Xz*JL=BLm%zMG5ZGmp2;;kjXaZ+Tv& zBn;v9BWhba@5bUPZpEM}2ATq(}ZTw{vD5 zImlRHUNzpNe9?H%WS%jSFSSwuw={1k^C?$0J{RkF?||`f{s!hH-ZLS6Kl8ChKb%>T zek?#bYz@9d*;)invm6~A0%+GZRh+Z0=edW_1@PMZ#Z1&u*fH9!GmFyR4)eVEInVu+ zFU<=;hq6mLUX@v9X?eohykP`#>Qd3y8^3EMfJgK+PYvpeDG~PVythT!YkN+4m9f%( z0~9l&a0uOeeDlm@94GB>oISw4PO6eRIkr5GfI~rdDlt!07DwK$zexK3%3i3y zn04Rp-=Ih$-R!OxGnqe6nkP;=_KnI$VPggGxp7ZNo-`)w!P;2-k8w#0@9rkS)y3Fp zQ3;TZn$IRhv%XQEMjeUnOrXk{^HH}>N_=?qdCCCrxA>El86h~v-cA~lMSt^-#OmlJyCki;e--VYR9Nt8!A2anqNEmf?aWq4nshtL7BhlJ<1DfbYU!4>IXOSUqO$^2d#<<|VF zm3+=w^C`f>{GR#dJbZt7Vh^*nh>$(+T60AW500;Xc{y3TNJ zbJ0g0&l#V9?pklk@vOyV(}B`0aT5(s28<6EGk_h?zQBLcKpD?}3W+u=ihSzlq1AN( zmIf=DQ9nZYTn2!ETQAqIHa)#cxgn1Fjlf`W8X6;0kAEe`08c3kMH=;$z|W6?738^B z{)Dq5WWTd;h$R6d*(F7R6-6A)p7Xwi56WBaq*H9qm}I_g-@6`LGTr$Oyvk#Q^q|oTraz z{Y%OX2-qme^BQI|LSqz<-BU_BaPyz$t&kalJjY!7J#Yv-F0Z9Hy&X`d1knkVC@qHoKlO zoL`Er4V{eN@8crBhg^a$p0RJWUxOd7P3@EM7(VzstzE{qO!ovWP@4ofE9G6?o3fQC zJqpKl+xSmXt3mFz#?4Mz0^i_7wXGbE*CYO0e7b_(6@t=&`-6_z<^kzQbrsS^<$lhx z?<(7^{A`xvtIC^e-bcO&2rN|y{JVGL@z%Sm32>;IJ2$Bv8D5&+TM!MsR^}No(s5h* zRI!*+SNP8BwvWBnG5#@T2q6`Us3j7)ZC-I5*K6m2Kb{`SbayE5urWld2t1`{YlE1l zc8Z@KM|%yG7AEt>zX@g*<2ik>d-{d6lYqOe%ubA_ zy?Vngzw?JaMmmIIPWaas{-5~^%0n|JWCk<-#jy$Zs?m>4wmf|`?VK&Ut;(;=V@JQSp$kE) z1y3tIXJ1AYyICo!7jbM2+YM;LxGGmJyQ?hobid7`-VbO$F+252`YHO?LHj@hUwroX z9QR@#yyiO}I#+=oi!#D;_+4Ia*KRlCchqzQIap6^j#k05MdBIMqz&U8tA^Gead(08KVvQ z9k|eMt#5Jq`+-^0jAvXUpkxPm?r(IJ>q)+(mGX)wwYTFFyMynIwpF(8U>=&oc}MVk znHnMYuX;>0l!0YNqW+#<8u&4na~iZ0VP?pywv@l%7f&b`HtSn@Lp1``kXOhERZRuo z#TVpGGI?*4nL~~-X?Ht8P#jkdDR)fx2mCPJ;!8H=vdHFn4qO6=Zt}k6;8>_UCe%$a zKNn4;+(pnpBY3*BpPdwEfS1X;#JY2`KHs8uIauF9DZc|Xm2}C0zQq29AAf4`y|A36u04 z`kf%i-uVg!y;O6&$N8#FKfX9%kSQl{GZ3Xa2 z`{zi?1@_MZ`|dm7L4p0cz&;P3D)2x2U+}L;{?iFQgWMqgf{*CHv%+CrMfU?5+BTm1 zq9;J-xf0+cq0LgCt**k4xsdWbOOJ{mW6qD1$8$B>GM--+K1lWoTA;Ueu(cg}MG?v? zm7bD66G-qUp>t%CCPz;y<=;h|lphJ4UPF*B1|78AHumv6-6XE>w4;YSRg4?EOAae> zxCHz&Zn6B!dj)^~mNQ`cTaLP5(Epr)4#hfj?$8B6w4wQt*~cOvbH!(M+L)b<>&BNo zn;5tB*R7NPWAyg=DCEfA#%#SV@T?J~Bj6gT#vaaMDaLN&PrVm#n-ORTpogTWFMv02 z`AY>NT5oDRL_vSRZKqPPxMjrXC82v( zqNvgIllNRQ-~lDXWf{aCWB!uwKjYWm0&{V`1 z!tf#$qU#t;p8*TTSFnCkK`p?~g8f%OU;g{k&nO&bl0O6y;N}b5fIC1MXeEEh080LV zfd8NPg9-n`5in1lK!N`~ll;v543k?$>Yt0jB#VZTVn}O3If@pa16Se#fzRUD_q759 zX5!ARLj5z`3d?wZm`D`mdHyCDcHmQ@i?T*48op-0kMg9x_rQR6e9vIutdi}Qi(xc> zUT#y@V}4hSH{p+lzrM%ph(R$`ZKu9X{X})V45V{hw!h|}U%q35X8N_mpjpW4Yo{~} zwy{sOArEk3{!8&Y-~CYAWqeBc2YZZd4&}Le6(gDVgB^G57?RbVwrVyEuqa*iq$9_E z&Ux1EqP~*(zH~gp%b&?RbahoNk4nTzT1RcQ6XpKM5&dazq_RPOg2Q!H#q7xKu;os* z?pM8vU#L^dmK`HF(f4bmZ|llzhDzq6CHvB`4YFkd=0VG#BxKl${a{|7w%Y~VrOyY4BOduhMEspNl8 z{ZHa2+gbWk-S)PxKd`aAf8fP}R-AJae5Y@p!ZOp$rVr4;Z&)N@C@#In+QdGPBLyMj+p7sh2m} zQcf@*$UACfVt*6)(Clk8HOBM)5)7_KD8Hes#O0_Mh0PdsH*kkx^PFSeujnN+T7!QZ z^g$_nA9t^hbZj|>Gq0C`8*QtDm_OSZvDa$>?Tk138{oH5-zcTaet%$w$qW?A3R3j# z1l~Zpq`zji(ZE>6d+8*0Z$|l^(ad;g=Do_CkC-rcXSe3C?c&&wY^2G8TXA5KlZfny{tIW)H_zn_t_&b^*>TBDrf zasp|P9xmyQ2v?7bGQe}n4iUzDTvRHWvv~diR3nA@4V;@RN_{3PQwsPAj=x6KH^2q@ z0nijFmLcP;SOblbC(#>&NeW@c5%T0T35Ba$a(hwph&V)j6fRBvkWs`V1D23}D_M?1 ze@xtolnZkryCm=sE>I0v1u2|;ilh0STxZ(OBJ>0TKq~hpz#ce0&jarZw{&=6j_)TO z(VX7(P0Dlvuq=STv2cKGo zn@YGZ4^ys>BV6K52$DYYMyW1;Sx8*<(ZC2?s^B$2$k}o}lu$wrpx7XE^!!+4{ z^RyDf8ZpnT&;#HmM%$O)cOei$?z`NV%5$=DKZeiTnCkU%6s$JFVH94 zHIGDVU@IJ}>@)Ns>Mj5PKmbWZK~yGyTqMuO+D;%!Wn^hd0g0fyH2mEMGlF@K`3I40MPgyZL=UacM_#rZ;> zUjmC|`-hYbGH}D=tqt@r51uU^FFg}FSlm@d-tq2<%irqh1sd*FcS^pPb~fuuF6}*U zoA7-q-wpgS;G7@7x1F_pDcFb3d5%m6bPa37By%6_8yG~F!|%LH-4E}HBQ`Z?ncr&2 z#!0(P{T%)@&T#78@aru~I+!N=@xJGj7SHptw)%F~9ookw8_?17acz?q6UcS`zWiSEiEc32(-Twx~A|!$V{JrKZ`a-9;j#E z>PJ7k#@J%~*@xy2$`X09I`1v*$J|p4^Cx4P@h#uIXM4>l>FWzHtp)Qxjk945fwRp1 z(tymhIlsv}-1XOu2=@)%+obG~S8%OjIjHegv%%a^lZ1=rC zf!sU{zFUg+D=mq9RN-~}r%|=)RL|l4USPrkrm=oEl8eVPuA(6!t;7C=ybp}%S92VRUJm4DrnAmhKX|nF%39EF zWW_R7-evyE)*4n7ys{thiC0^LW?#8H`8x=7YkK>1ObGmbkJmjJ09h`yd{RjpXkRgV zWiI<5^LEC^Ia!e5si9|sx`Q9no!-tB$PL-I5ykAV;r-ziF!M> z#f;dT1ok?-dq(BTI^ae);r^;>~2}io9B@E zTn6ab7TCVFPvScha@XX43Vi08E;Bc%uM=X)KEPQ&Uplgl$A-(KzkvyXv3}r{?YTN$ z2FPY@$f}=vf$wkAL*)S(WgVdl=Xrir{ej>eGAoJwgl;K>X*ASRX!kj*`iymMrB3F| zzqC`WlMV%Qr>BI>uxWk;ew3s>*?v=n9(kQ}6m@{-$U#349IV(rcK+sIoOZjq)OL<` z_M6M(z9IN9+GO135u=PzpZ8D0*K5-6HhN`!Fy&9o3i4o$ZHi-w?QQCd=!u3G^#{yD zvOmzj30C0UDFXf^9W=@8^AMP*oU!y9r?Q$hrJN9A$rT!BCz%K6cqeRX^2*E~WuOwH z{m|mGe^Q73J|UpJvlmnq+ZTg&-n=dKwKoaVm3qoKu1L%UrecjlzEH|?h1)I4g3JOM%gaX?b zzai=?dQCmbK+wj~zd=XvBvtD*E9L110s|iNe2acg-$MOQoZv4{`3jDG6b$_h0tT*H z^}F?cl$Z0n>6A|x`>hmX*9ky4Y_+$Mvj7AX=m!lHGS1=bH@)d+GsvR{SeJA9IlvRd z#rFkzFsMj{LMBEG0A5EuSq!|%e-Km@+>d1HLX3iWmcu;>_q?65iQq2<=_bTyK{yeV zKDHbne_-&Ggnc&@ zlbgY)A~KR#LrIY*EQ(s+wi0YD)l!A|7XfuDrAO$buoaHP2z&R9)|8W)u#ef9s zU&BcxKWigT>WNRq6L|uNb0MbWGlcooH&;`Ch2+2^sBeV>VKn7@^-a+WgI}tBfgS$H z&9N&i1KU9yPxMQ?hTEhD>}#oSzV{Oh+}sWC$M;<4azBzOa9#7VJ-H(>Jl?a7Rwe)1 z%Q+w3BhlGX*^XI?v$Y#}URw=RQ9uJuhkh0GGX~fmf%SY3QUA<#O`VD1@GcIL+Q33DH&JGOfVyNkq4n5BT zeV?Z+{rX5R&Z&6CcTT`Hzbf>*gRkj541H{WaLWLJ0lrMxxusFa8f~fFr&cJ2_F6?; zh0D;c>b=L|&jHvEQ@s*tFRppw=ocGU7Mrn5AP@L=_4e`l9{b%?ccz=)I}B$9+fmy6 zG^nvZaztWoLS~8|ezAIwk3Rd86M$uFMwWjAKi9SvYg^4ikDDA;t~>@%=XP6PZ@!n_ z*Y($nFY)_q&-Q;|(%;#RGeNT59%h7hB;%Rm?rkuh{9T?tS!)<)ThY(vVji~H)BTaZ zyawAV2h3;wUWkLvQy8W%q8#P&{{$@vQ0OS8v8E-l81Js*l5uUGCH(3s?ok%cKd0rWO`URLIhR6U5 zb*pfxtd}^*NhsRIuzAts`(`_{FU}?INabYYN?@?!XEX9{cZ=o9rJJpT!43Ii}%0lu&???$+>P&(4Ie7$Qb&t1*5B1yNwmlEb?*8=9@ zTYF?sZ(y$dbm$TKwNWYN{fKr_l_wHUOmB077EC*hNYa0Wa+o#UgJW+PJrRyI@R6;^t8<6X}U)}fx>%+-@}Idi%WUZ|VIcqek8G1zGCIEy`fb6VO1 z=I2VgyW(I|90x<=n?=)3%*MEV_rr1c_YLwc^Bw-O@RGfnITR)Av!I@perKHr*gE?1 zJ3Y`C>_Wcu)Y8{@>eKEFeXYz`;aTeWO?wT;!jtS7@yy$uIx$DDFu&Jk+_PW(3jDnE ze0uy*#LQhc%fwGcES(fRIf3i}G_$&{Z$LDm87qNWw-+Rj{n zgsq4N80eWt#l;Exg_PHecwqor6By8h`b(U^X8fcmTw;CqfGA51%pF?m2n`*er9pDc{eZ};2~?2hIKd(; zgZ&)L{pF|+6Zfo>)*leskSL#v2+@lAL4@XfNBN1S2%D3_IO%Z8WC6ThP}Mz*EXJ(TC@a2#xp* zIE2sw_Br+KrNa*Q1|c~>&gqMJJTFahN$?1F_ziX@fzmj_R{%Shi)DXAI4<(dl&cVC z&=y!;ypZdMlPN&Re%O^?1pAl^59TrwMcFNiS`Ohb^cjE3mz4zR&?1Av8cWX_hG8t8 zcB9e2-le`BqpJs;O-nt2CMHStHx8FbY0yG@Yi6xaFJ`?MWpLlR%3vS0F8+T6?^OWy zWme?#=Y(s24lF~D<#!{|@ah0_NhW;&=*GQV5_l0!PWE>mZ!&K<$NO0QsDT?ExtkEcqbLEW=tqLSd}_3?h^~j^}%YpIk4oi*;WMOh6x` zA7Z*_DD_KD=&U9*LlH;8gv!8ZrHCwp)1Pu`N8-CYO)nkgKgxN)DA?1&yHNzK8Q6b*>g3BfrQFf!9ZwESfn|*0cHd~p$Qcfd)fceHD*U^;y{6Iun0R<~r zWyZ_N{|&}xu$A1rc^d1TS1+fg%rIoEbO(9%@czdHmeq&OIdj_Q^W1gaeaX%DiarWE z64HfsGq|yw?<6E8I?|XgcNcB7=M?liOz84E;fKQdhb$v*aYy9_WzW#PFPsdI8Wnak8a|+=?n+}l zLtYd*8vuQNel+Q$7diB=OU~L%+7GH6xWbq9FT6f@X3@9#u2#;gCjG7uQ1)s?-Zxdp zSCUVi8+l>z#Yy;2KR-I{7<4)|JYi(aK-#?(@x5Nzb1ytD;~vUaznsR>~n;V-&6Qo%b?hB=tuce zkv&V*MLaNbK1vLPU)8;}@?l@(oJtuZvw?+96z(5*nA@GOeM<{+gh{)&u^FCR~Gzeu(-y$8q;aN zPEPx*cW7tO&GQddf-hC`YUD>EK7SH9r9?98_g&tpyq>f(UaMye0)NiJx!bbslKgM^ zf(KkEbY2G1=HJObm5;p0447t3{@rYJml=YR`X-%DPbCgyX=+H?74R@M|7p&fyx%}y zr9GoRY301jeGEUY`n=(D@cwDa^QkZ1W4#K8lnVY0Hu7%v{A}=8@1spL<}sh|8t+QE zi*Tq0Ci9|n!%=Rhy&dj$+FtM^Ie4=__-#MpSYczH4Z{q8vMM>{bEfBwp`C%`9D2yQ z)^_x9jOV?c&UFs(*tN?gT+f-`ampz|n-rVdaoEm&FdW7fQ?A=Y_HS#-Z3u;|$9F5K zm*K#Q@tuB5XI{*2jU;mv^63vg?|LzB_VV`jcJKwhu}#Y7#!Y>Yv6gn$L2uc|#yn%7 zfgE6Zadl`Wcy-TqLv6|PwW6`4gB&ngV0^&u7w~1qr_{!h$G+k?jn3E!RKl&_nX;k0 zq%UDekn(#2ak)$W`VcC@d_Z5mElA2wl{2H!yFJOIs z1wK;uljWdKMmo97@8{y6iJ-m*bS)6rjkhKO8<{f+l~TkFF~$m5$b`!JQ|}Mul>My( zm8=T9&h6@Rfv2Eo%n#uhFYSXfpfmIjK4M)dgiH}&(N(Fxx2Ar97;f?PKpaCRzk5Tu zV4d`g5kHdCM+Uh0SyvnsgUZ?{|C@b~uuIE6_#gB?e@p)hA;1AT!CM`%Fv@<`DzJYt z4+Zw!0{itb@SwmxU*La0K12G{`1k*$f2Ez}Kk7!i9iZ!*L@N zmVz-F_24hcr!nzG0Y_oB8VsC++kXbI0irmH8?9czNX*ed6p6>cs{A_!_=5Q1XTZA3 z25Ddc4^=9xs1>oEFpM2w_; zZGi>jD_FmB;6VX?7VN(Q`XcXs{O>_O*S+EoCE++a`}+lM2>2gHpm@tLCHccG>fK5Z zCMAV)sLy0zn;2mpvK%!D;@|nheaZhgLA`}DHHv7#SxpR9;%i$oi$8WHVLE(=)UQjV zV^e&p)|ZNi{h9N;V>@t-{VN;&AoY1M1w&)L$X)Cj#(T{<@ir9uS59zXKxOPoTYTy| zt7BlCp1nI4{^lByGs{&CyWJw=J!2yV!(@`LN<&(Jh?9;F&VBcmCv$D@scqFRR6hz` z6GYh&=hx3Gf%?hnaQhPs?X@!|W@4B$9_CKhD$t*c&KmZo7!sE`OW7LHkJh3k=`%U` z=l$3V19s^HH3fLw(+J!e`xKxr;RTr)`vXAx9~GA7ua4wu^S{W@%$H z>~^$%+Z{pt;KGK_>z&2kmr?sl)9-n{(HN$|_mlt1`6(T~pZ-g}D}M_1#yH;z{>Tx9 zyAuGiIPG&z^^pqYAyLP2`1et+TMqvgoNLFx3eLpZYx2B2D(V5s z$JCjTC{IxmaguR@ z(|qRfu)b@xLumt;?@#idrhW{b?6=qRVccFf9e0Z$$Ey3pG3Olm(M?n*^hGhNnMJLD z)X~N;ncAOh8mBpRtl*sfksYWQGtIN|gt0lmjFW*0WHji{*heuUF z?)4qyeY?ma)-BS}DsU9-Ue|ZZ%pPVVW1Cd?)qFKU1s}{aW*6fk2LAK817vFI%+|T1 z6It&^!(v13fxi!q#B6*R3Le(UDK?De=E^I9S=onO$jUA8eg?4?}*QPT32& zSOr$%ebzyVL~`y-yVaHcqAcaw%pB`v{`JZ&TAbSmyHmb|p&$Cdjl{F$KEX+9v%dQ_ zF3aAOf99Mq^Mbo}fE7q^5H~XO$!|X9i_$^hlqXvWY?8O7V-PVnNQa%dTMR*RRzxRO z54wb5Z-D%rc^64r;%ggv>!jWvM{f!+3Rf`ej9LC5-^5{$ga18X5Tp1UXui@PDieC3 zQ?9IB6g_FD0?8GNQ?B)LeAJe;=CKH8F*@?IlcuFVh;nRfR!07^FcjG!SB17>nsPt3(?GMMO`grEG_|=;lQv0Dg<{0-=Z9hCes91 zf%Po`EX8~s2d*GY>=Cdj4utW*7_=qX&PWVUM}ena=|`i{qXtv{$M_n)(vNnb9m#&y z;NpPCz*?{Lqi3)5qZR~g7p49RC+TYdE1Op>$F&#ZJ4YG*E}8-1f2QoGN1mYv{!>3P zf3wDyfPU15`d~86d;v6Z&r7@%oUa5A!Qb72c@$FrljAF`*kp=toO>BoAW2cB9JnL( zz2landXRX_IwAfem*}jeeh_Z9zQBLzfB!YU3uu7;!jDCSFr-mV zVK-d?`p}yE-M`8&|2n=X98eiN@5>k~eIJN(yBJLuz0$QJ$5VwAP=fP?oc zzoRl`fAtnsAD6ELT2zi$RqQ$r>L2fwiUA)|Zln%Kb)eSQsa~rBcomi#IzQw)9KU*s zk-QH+T`r}5#&F2k?H7%b-)G#n%#$`h_>oVZ-Tf_ecOp429zf?;dcCk8Ovh?c^W8IsjQ2wx^0!*Z}z96hu0S7Jzt$|mZH5QrmqZ`P%Db64)~oqMr1jl z8!`}120oY{a0~nxS#Mh*PmHTe#3aEwnd8J(>#CoT?z%zJk7)MUPIWST##(7o~vDwd6a z{-mx?4oT+sJqCC9svYZF-BGS`t*?m}3CN60htH-=&AyVzIu@@O`b{aG=N`@Nljl^F z-$rje`FmMK*`K%n@~NHR!@gom!oZVaJ&ONcgz@G#%59Nf962vAb!hfw(C%w4ap=!_ zE%o}t`91iOMu-4-rp37vwK~XIbDhVWt(m`5w$JVC3y-el=hNTkD_?}f*Tu|Kq0-&5 zmFZ{s_U${czQzUpkehzrTK-_`9mex0`pJN24cOMvQJe3;zeS9GL2b)CJv40UAHb2e zZZ_86Lk1b?katY>`q^Q^)-D9>@P$sAbwv}wI@l`NUf4YJ>vwIwXD#z{Bl%p$T>AaC zkB_qh@O7V-&P>|5rWMu?@w;vMSz`|P+0!*oWBme81is@>yI#RwX-@Disb-Ryn!tYQ z?Q!Ts_--b-PjdKuq#mGmrTl~MUhiYEq&9woZ)%2&P zd!rkErw{gI>#V!cm#aUU(C!=pvm{<*34g)(kv`tF=+02(9WGH)%qQk@8E8Vd$!pLA z92n*uD|5(SV!Xw#{=wg?;Z{%PJvSV05U`m=?OAwH+p_yfYCt<;jCqu^mMpj!XTAp{ z79 z;sb%4#sLKbjZuO2?-udELtM5qjrxt^2wAygrnle776+eIx>$PE62j2o?_>lpMNQtD zNht54v1^qrXjS!=4HgsC1ame4rt#-SoiEDsfrrbDBY5p3D3pcYlH3*Z|LQB`&zzEH*MFLN1nubEH-E*(kbf8;1 z;(mgB|4@6VZKJ$h`_cN&W%o`E0a)wr*{EHV0=gM$4xszRFh^YlK81ln=rHJ2L`df7 z6C!>~pf8HXN#H3=Dqg^Dn7jG|U5M8EEqK{dD5_$vqrVtIPvN?khRq0h$1K4BNkS?R zBS_XLcZnAU2fmLn=cy%755Y{)o$>(8D-dp?uu`nZ+ggb5ttcNx-C!L2>X4;)l!n0+ zqa21x!aOO-m`mk%eNiMLfbBRRu>-i590RhSB`{3M@f3`&VEqd4vta)f(3k%n^i!Iv zB!4&vpV|t1gbB$2D&}G7z>)j`(~Wc-L}L>BSN^b2It<|th2c5>kw3_j))=27h2(#w zloyu6D4M%2F|?X_xF5VRH11LZ#Ag^1Z`yybT>x&k?Y3c!FTboLD7dr`_^d6&ERL_H-9 z@#+$FhMJ85alcnHXD9q{&XBzU@HIzO?_*vVwADy;i;6;%KRYWnuL^A0ofDngmESvT z4Qv>My}kV}`}CpRdFFEC6Z*H!zDCxoxz2AL^xHUZyk=l9*&U?7Vx3}W$z&THX|z4soH@|$t~*s*qrwg>t>Ws!eMkR_(AS< z`ez^D{kwm0{Ez*sMpqk6JDr_Rqs8Bf#r{vq+iTPvsehs6B&OBwtWrb&tAb_E7 z_Q>q*__L;#UlifspipA9=nAx3$mw@Fv>(g(v$~_`hx~qm@jQol%UG#3jr*Q?%+CMN zb&T&UG7C$`gt|cQZXd>Y97cqmL;0CFqOxwb%j(beGL)0mQMM(F`z^hLo&G;+oe*~A zEC;I3-&=pM2lJa{|K0aK2f6RnzSQSaPB!)l=}{R4mpj%sYm7&k#kI5;EX61hqn4zc zq_ifK5lgrdZKuC#XB648lt(CMA=^Ab9Iy5dN$4|53ELr0e?7of0dSSSm`GX=G{odeRYrr z?YJ#GiQg?V4tSn{heeGMxfNN*OZxrvZoHRo{9&^0UYG523h_JJSlfbt$^0%?xozFC z*U%`Y^E>4eQQkn#BGG>flkb}4&1jQ(HToGR83Pkfit`$j&zXazV~GHkg&y$3yk~x{ z%>{-MY9aS&Z)K(p{4=|m9x3N#w#^!rUX$&anDJ>Gj+of)2@`If!QsCzCo+$DicAZC zuU<3oZJ1B>qO7}KRIjP6r=5;wKVv@M>27TC45fahUe*BOyew`5pYm^M7xE^87v*z5 zN@QHOm){SJs*m`$Ja)+o<}Y(^a?L#GL|lo-o0I1uKE9hYApI)+l4tbUUrIkS(Q<@m zAvda&ui=n84lK;=&h>!JkbD~eBXNi)0JD*lnSTdmR{!PW#DF%6!UU zxcaYCPExAN6V0L?>z_>dg6Js%DEH&bqiClb)H?=f!#(N(R-v7Cz{|Lu<+wZ36Mx_t z!W`tjlH-$q!kLGMz#Jt0UbHus%ms3M{gvX-FxkIU%fR0O{HI=dCOVimfu?yFQ;~FF z%g`+8SPe75YgWldP6G*58DOQf#fAC@@E`g^aWt4H$~IO#7}ywhVK(p{J!uZ~`8WMY z<`V#4NnBjfa^OdT7lr9(9Ztk|qZ{Z)@@`{rCBh2KKtHlhfR7+Agv2+gAAt|%74wBR*?NMcjB~{;|sC^Bn}hE^De&|31-Xn6U@kI-jm^ja=tpz%LbICe#`!& zk7pT+?8h@1Pz247u=HCO0YgP8y9qCl_+@~xXMw5YhG-9TvhH%+MR4uS0A7Lvd;m;0 zf06+M<`yy@fd^_awY@qSc+XZ{ZO!)smEWZSR7=&0Xg~A*9o|X@{)4_;AXCOs-U}e* zDg80ub;u9z`kwV323q#?YUmgu`GwagXF2HZNkZAaL*B^MFM6OSA=Qdp z2oAw9?^k5qdjE9O5wC2>#cdkl92A&@T^eu?zySr4dhoA7?hU-aiYuZCW~F%5gdVnUw42EVEF zR;gIF=QDc)+cE6KH|zGTxtHmasax5v*Oc2oMwf+rw+Of#?u(=U?Xt~k^k$qZ%r36F zBs`pSe)n0%cP8q>%JV(p4{Kc$GMJ|_{$-m-OjVTDYSX37!uZaM@||l`fgBF?YZE+* z`3&~7%z}=1nwX1ZK#XsgY+V`W+2Ft;UlRwF7<(&;bHz|zFN3#(OJDY6lKsOyf?`JMTl-bFyE)x+?SIrO`;V}O(WtNvu0gzZSN zb+z|)%w-&h#5DB|^~uUzZl71Qz4~z-x{;KUa5M@0inNugQjBqSdEV}7tL?;>z3=~F z5c829lW9+VlXdNWV^=ime(b>MDVLJKrwth+bH70D9H+#qDd0(v&tC80v|GeDA~O@% zYd9X;YBE2?3O6Zqkp50myQ}Az-zcq}#&`2)=Z(!DC)rBw99I@{hauRi0MZeBH@ z54kO~!}C6%ywDsjf`CJilRqLpYP;VapMA{#CEWesUHX*tix~$PXBDG5p<9aCN~@{! zJ@q#0OuP0A-s5Cogy(f#c>2?iZJu}ZpD1U#=DU%P;C~5He)Al0pOx~HmXrU3JRro$ z{+|SRr#vTAj`=X-;j{y2_dU@@@^LfSTp$BQ&6#GN$$E;m@ataIdtId?8{YQ-$wTse zVbQ(j1iU#Y2S9B`0P91aI`ZByvBRR1kCf(?!@mWVGw=Xeqlx5WP^+4&$T!*aIBKln?xBT$|97>k0iiP<*0eul5smhseLF%<52k;IO3K3 zQWojtIpthhU|a+Q;EDkb^y_AKi_5k z7-tr||0RFl0sIGl{}1-TSNyM}#s7i`*#S=ooSY_F0DCg-LBIn0XMugU8h9`SOzjRV zuwNJ0=Oz6q@IUI;LM4JShf~#NhHD6_w z44@?3`Lqo97D;4$Sw;CS$@}9dj}aG;%oXy}jFfV@a$bBZ&QdWdhz{b6T4X_0tRW+fisqGbx6fTa zFZJ(&KYz;^C|LTx<+A_RpgI5l#@tU4{0G`E+V?rfXiM|2xpO`lqdzn%=|yJL(vr2F zS~voIORbRB1~^F@BTt}dcR8~k0Bo#z$&*sr4Np@ygbhc3B`pv|ps4#74}{2l*VDy~ zpsi`{KQ)v3GMef^V0T~gv~Z))X%X&wS{U%Sc2s)|yr{joefQ4|V_yRrV4$HunOB_0c7O~%91puUBHn>bV8H1Q)bfqy# z=)gyuDUbqtjPkbi`z#DW5Kx>l{j5mYw#sUZ4bpI$h>=Ya)CN|4J4AK)J(&m>g8)X; z4wk`D#^~Om+z4IlV_+0!&pp7dl$!u$B4$w!Xfh`xl&5QR19_ZuEoxc83MbFt6i!#@=I`B;4g(PVaIEfcQbD- z2G$z+dtGnfK)C1>cKCMs_4L}Aj4!=%)*G4aaUfg_zUbGIWjs=GNZC;sfGdQj74Ofu z!@VAc@q1EQ-?#IA-Y)&ObQgZtjh>}xi<$2QdMgvT!m-Opkd6t@@A;t`_#plifoelw zYuh$!UM4EPIT(*uy6sLN>z^?(ds`|}`<3hWvJcn9PZ*nY`9AVhq$516GICSXU#oxk zMiuOb0D(Y$zgxZg1sCQ!B@ET|4)YOaX6p1)2&JMS@u8%UE;&U#D|)N*DSs?XbsNt+ znt~ zHDxvK{;NE1hFfku-x+3B_aw@)p(Bf&c3!1u#yRj)W45bFaSkvJ*}&j^}QdLa#I$}?L!{r<%$>Si3C4el=a z%p)n)eQkV~RO_F~$ot$1`KdW)a1{NR<^7EP5Z^sFvfl(S^Wy=JcX1ei$z?`U8b*0Cv& zyTZY-A4Wy>I+$9UwQ$IKM%<3teRW7<2_05z7YyNJC zmL}syXK81h$S6rd>DoM(VP1EE4lXl!2z-wK1ok0ZVZ6-1!TRM`>zDpYe~>43&QdQh z=Y#+3(<|1=^BK5YM^Ju`@>F0=dOHSKpWZ?znA0d3Eka`V>2e-KGu(V9>1UdfAx}Ok zDdcWo-eq6`E_&I1fbapTVk$ID&ckEr*x)ouKkIj|^dr$6{RsTPY0^N}Gn!JCgTxyx z2Kr{Xf$sqo^M-XI9Q}xp5%eG2Lc4$?!4}%3jZ(NiB%T(s#&-qIDDgBJSMzn?*R1A9 z;6KKPegpm{Q)ijSORx#^j?&=DRKVmw7S&AkbzL?-%F0F+gAQD{H=PD($U# zN?;wJW6bweD1+2rU#?$szEX|yIb8d(z)Yl$!`Abc`t2d*H+rTJ4D;9uP}oLb1kc$E z$LXnb3|^Ms6Jx~)zC^xKN21-A=W|S(S&Ly;@WgoD=Dkhk$CkrR1{BEe?BVxEfO7wN z0V@%PFb^`Z#kNE}M0>4NfAu-&yWMtOeTQ~`u#dJ^1}?A_v8C|d0k66a-cwI`ZMGkU zOib}F?#+CZ7NzbNgML<+Ti%RhU+7DX!Sc@Z{CKUq7Qyd)Y=MIJ9BrMI9N?cT%yrKl z%`{(la`VLxw13(DrJZ^5PVznDHHqg#^R{c?wRYGOCa;W12v4}4ibK|Z&Dq~xl<)t} zZLv!+XH=_dFTamCrk?Hmn}1FFcb)sZ;yF2g{-)~`ITXPZp9A0c= zapuof$C2;2PlofbDBHCwoLdMwXPku2WX#V`%-Bj|hu$TRmW7`;46C)Ffj`^3NU?WI z1+d-j6+KX-KjRwhnUoU<|J!Fq=JOrr7~68+MzrfzUA7*qLsR7qJ&5vJrMpwok*(r= zQ{veMEX>vn{t50^U}mRrVorYLscvuFbR?#~9~H^{PYVd3W0c#}N2l zy{u}fz=KmRG(PBu@#Og3@SCh_R^v3oO*vy_YP9Pra!pdio|4}%Khf0>RPBY_6L9DL z73iLGfUTkJSNx*yG%nJrDSpzX!F_!{gWT-6A8<1a^!>(buXiWLJ+1212Jqzvw#Rp$ zuupdH{d3w8;FEWrE>CdNpZ>x}2C8~}>UG<14&zBts+c2z<3($gc@TS)ih}+-S~%Zy zMDpHwuj5|e69E(R*(pkJnV`PGf&hT$3AI2AB`?|aRo-`GFi0x%af$aKyO&CIEXsRo zs%RwxlKfBmCiypGUB}#LcpLn@vF&D=d!=FDqvGFB@$lYa{XDv^;+XDS=BP-6J?!}^ zc;_|G=NGRjjLYWV+@F5glkBx@Et%&wrWN3r(N3CO)vmT`tY2?W zTjLk{5o*jg;Rp6MURg5WL;H>#p`q-L9r|B-AHMUZdxQtP)JwZNdVT}%_vE$6Uq`!L zpkF1Z-)DSF=%vEB_lA<*s}X8(HH3C2h!~kc${cN6vaVWKZ5IUwX0FBGu@4IHp09>jj{9^% z)?`02kxDZ8?kps_F?9dm^}kp4PxIgGpU`z*%g$I4dBs6H=}_-TV1fO!z`pAX9u(NG z3+(go1?W?O|3U7Df1{uOpZ!P2&`|Es-ZOGmE`x5zD@&z5!#Vzk;$2`CDf|!cd?)

V9@`Zfex43pY0Gsu|EQKoKH>jS96pe-|?Vv$T($GZ~B%#l1u;y zp2la~%8DRtBpcTZ6cgf;6Acs-qqdQ#qc9jY{WaqWaHn3yc!(VPnh~PEO*vAZAPxS; z2_wKrr+x%UVSl82K%ZlL2|Q@bH`W8cGd4*@L!YYmmWqNtU4KWO@H4I(NA(tz-_yU- zf2N=Nl^)sfElF7`~kR6IVn(75%e2SCr|d@ zkO~$?jFOh2B$QtSQg&hh+6i=!dU84NvZ9H{z|Rn*w*yP?97P6m=~+x<7%mXoC1IXG zJ5s>U;rtPVn_1j#8R8xw_{(+@=y5dgAU&l$^B^bbObGKi94(EhFUgnf)_W?ym4=>Z zX_zb+U%~nn;3xZ_VE+}+m;WC0vyJ5t$;R>14nA;1Y%sq7zsGR{GX^mfj&ah!1oTzD zfup+OzP#0%91}!;f_rB1M0;vP<*5|g$?)hPIkiI z)#mncwsO25VQVA5=NRq$-GO4|@N%qoOoLq>(Cc}M(#}uzhU#c3FNh@`7YBa2mM9(` zgIF-A)!;Ui!}E$xEkQq?-%r1qyb!<8^*mE%9H{H64%Ww>Y&%$Ce;_Knp zEbE$<%X$Qi^Z(fg1t9vH}V?B&9escBUF;Yy$?L5v{T0dJ)Fn-jORNDRU8id zQh7^AgK9sejCjEFAH^@~TFR42(L9XydgJmxP5V*CIK3C;Dx59-5SV7XF#Ul;&6`F~ z;C5U%`M^X|l_vsuK2kdT3I7NewFz+2vI@sd*U6``a!FRGc=X6VD{+=F*kGpmV ze0iAs=S}b;ZEo7F%rp%C>-@4x(tepv#mCm)$9mWEo*qiQUd(7FuTtlJ}f7DDYx@!-h44PVoDJCD~)ucoe#YRE!?=Y~wcE^&BLvU4)=bem<`C^RuEW`6F z41?8qe;5>oeyTe}K!C5@7`%Z#aAUigq*ynlqbOlNU^Gp&94Lk=Llo`w_LAk$5RJ5d zj&ckmVqKW6_bmC}O-!DH_a`#mDBx~X>Q(uDB{R{A@?jD=cA-3)FMUb*J4^=Pr8kJ_L3&qWp;Y zDhmwY^yC9v#M^wARLxJ+5nx^7T4n(SlLdcee9#FT9(=8NI7^FpD1WAPvJCz$Fv(Q1 zo-1QzE+`2W%X|-D^*RG{gH`8&L*bgKz;IS99$1j^?F8P1b1nc@A_g)DC<&5T51hRt ztaDd6=Cu_4-$XBNP#?u?@BjZXzCy%KALjjns)jgTKr7z_Gpz9m9l*Kc_4@VVa)+Pz zeJJ?Jd=k&IfhZq5#?*RnOs=RKE@f^EealkZEdUQ>{f4SZ7JjbM`dH`7yga*&e-B|s z`V?60uSnK7`VT$A;BKuSXUy05S)TQ|PJIZgxSIApLQ0rK`3Kxku2J63RV&*mwJAM##PB8xlmp1=0Mx(_)XOL*gvb$&UY^>B|!)CcgsH`?-bz7T;77d^hcSE9q?(b zhWb$qk?NM#*C@hxbD3?uw96jd@0d${wpwUgO8Jhq)O-YF%~g3|FD(EK2MTN=u~`?- zd&WEZQQi+QR|uYXYZ_j;QXnAQ)hv*ABAtbsN3uWjyp#84@H*!I*C&~=ZP>Qc?hT1$ zJ+rQ4Cug%BFC9tKQ;Xr(G_TCna_rydU0>Wwgl-&OalFpS+Mr>w??_i12KSE&JuJd} zBs5Lx{-PP|E+lSWY$#;0ZFoX4*1b~C3L7F>r+`wv?R|g8f9i|JBNI&2H%5AF7UK_` z9=tG!aXI$e*V*AqZr$Bjhi`boJk`Cu8CNY7_}{P|WgVeT${Eelnr3D4d?YytmQdfz ze#-?vN~n?8KdBGvbpCPJ<2RV!vH9!g1Ff4GP6`>(c^LRSpsooS>XZ_oC9 z=GaXAdD}|KY^49{uHpF}c6KOt;b8DQ@YBF`e$4yJq@)umGhi=qjvoZ?JgYqIJVlYS znzp!yg$qH(8IW#rcbob1Mqpd?Sb%7jpjusB#8x_<<6-@u< ztLEwh+WFSG)yY1x&9F7KG5^Nz-lCrRbo^5@-8%0$$m9WkGS=ak`8D-VshvYRt5i0U ziSR3b^$u_kxhD8Nw6umr=WD^Wm~ZG=fXH|1B%G`!<)UyIC-5o%=Hp*46etMTNNZ#{ z(hbpC2wg$n_70K+8(2G|1jcI@El0qe+F^_|!uP9TB$rG@cP)KgJ2315^Kr--CTozbA=wCyNF8XWht?^~4 z7>oZtV-#2UUG#(RS!8J*t@B@FxDD&9J?GM>|89J0 z9GT=AQlAX<%|$==YAunZH1(q;U5Nhja*qC!`N`4m=IF1Ng9kbK`5gNL^z(n4{U}ho zW&S{W`N*7vJcFD0O?Z-~UZ|gh4#VG;D(Jo1rTpa_qZ$&szk&KDoWcfEeylmI0FX^8 z6xA3YM-h|B;8yt+xMe*{)804S_Op!7BRbA2YTkRGg81J=_oxt9U2xzBp)0PmAQ z;1p1EPtf;p@S00|5KeaocN2FvzH{4i%Cn7?`bwK&b_8DFE|nU<2;w}N0OK*Vry=m3 zLu5xl$4Q`KMA``Xb5KCeAlGM#I&hy%*syZ-dd^zivgP%G3FNePT zd(zJo#QagvdF~81YeUaNNXpm}df(LiPy9jsH9jIoE&iaIsQ)Q{zyuF}FfYPYg#U$T zTU0dgfVp1`bttf2TFTfYTL(1;_EX$e1k-`09;nG;Aaw5YDd{r^_=az~GnD5?s-3C; z3ccfwW{!*S-*H(>vR9y}T#$YtlX|b)WApal{dpuQy9b*!nM!^ zUsECWYZ!$@AvjGGzxE5RT}~8GjuC$GKJZ1`db6nTO+&c{>d-U0*XNuQMY5+`c4@sT z{V=p}iHvbh@f+cyY(GZK_Y>N)+t#Y_)bH|n;X1^)ixNlLntra=>v%WuUJu&`o|3?e zYNEjC&Sl4zTY%!cS+xzdC(!Ks+yzQM?+uDS8~Y3WspwzWe*ku@17*fVcA>1j^Q7BDd-L?B<{;*! zAuINfaYm{W_9=|JoXz3OxL+;L==g$z-k%Mw#q24JK5tgW@C4Q~a$%7tH9C`6pmX?* zde<-%xC2Mz`h)iO8C%o8U>+u-POJtzqB;`$teLUIg&y_rTUQOsiuYApskBc9Q0 z1_l~W^rp1y_O#0E$9u76S#cCl&s8x8{Uy?TK!6;Gea-r|KImy(8GR(9!lUeG^R9-W zPhFcBd+&D?*>5;%2X#WsYZr1QqB(lw&92k=uF{^#)lDTCUt4dZ%vpTrl;>T8dd9cX z-kSOY`UWp}^JLA-J@=;2eqzSHr18MO%-IR7=e_>7pFEjL<_UGvzo{aBE?Pz7Dti5_>|nip2%~TQ58UkFxM>;#XC_5pK9+=4o1;@8JI|_ zLhuZy)`OOzFE^&$aLUgxdoiD=6M9>6>>%xsL_DDAI3)%#bCfw4)2^)3+g3{Y+p2`+ zz*GgrwBTEF%+zIg?i|u{Fy%Lpq|fqvZr=V7*boK&IbcbiqCFJ+JuwAHKOa~IVaX5h z5$}zK^6jKvp>F>aSQffe3fK`7FY8HkMI(u+^%w8~U2Ydv9~mfY8e9W{G{DB`%wQr>#~COT4IQp zMp@h`WPHxQ_(Ns$_4(n3>Z&^M{a?*ha<@BFcQ~eXxjN=CY2@X6^!oe|jzdHv%DJF2 z#eq+mRpA@Wnb$kO%HYK+U^8Z{1n^^e&=(kidEraoHC6!r!Fpe{3>#$`mp|o`te7uQ z#wYt!&PUaOji40gfpR_qZ<*6KEJua!wQF4GAQ@5eTLx^-M;3j-re%%K!K#}4Uiq){ z(G6T77#|9=DOQC0MATz)?&Ex9t=|O_YRdOYu>wtia{hx($oi=wz?Q0)l~JKG7sRY$ zej2C)g8ya@v%R(MrEx5P-Y||}jMcI~Hd_5I#*UGG{Kxt4e?7irt*VvLK=?a!j(N^z zuKwfv$NEX?&wri&{vMz7x<2(^;;=CvI0o#NeS1PXC{STkt&4GGMg;Uq9#fCs3*$z&l7Z@HuWU|JiE) zlxn6L^4@+h^f>h)s6pd_aX28X1D;TWt;`Yzr*ry^+s#kbdw<}l0R6`up(;)`T!COF znX=B0!OA1F*OC3zg0ffhvG&VmGA_JLIn(^YO3@v~$=(lsx7|M8CgYU+G)sYB;NHN# zvdzRrK*p7acq}LFwc*t6!?+f*BjKaAy7tTVz3`zwohf$k+h=A#cfWBg>wtV?Lb`(1 z)eC%=dkB2JPjIGx9pr(Deu2ITl#2!T4CFh>{>huurZUVz@t-F|SoFVe7V?EN-Fd`e z(B9j=@A;nOy(j+DUEqZ++TP2)5qV)&#=5LuK<}7@<*Dl#US;oC_fyE=^WrOteurWI zOf8=(^fy|S|Dq)8nt3OCqye7BKTkLk*B-~ju}{XufCsJtwpc&te$=Cqhs*zlU(vDx zPYToim5Dd9ZoCWmZvDLWlR-@D!gCk?gdY0Nv(Iw0!LRB??SZu?qn@Z0y)2r2RNPyr zY$5s)(8y<;e<$QE<@qCv@1pC-qcx)64Q#`lrzjsWKR3z&7jg+rCFb$0Y9sqem9+&3 zU(y!Cl`c}Au3feZpDJTJ?|}Ze3wf7j(!N@$o$wB4K5o12<)`0CqtlkWc!Nym?mx=! znnT2X&4cxK)4za-pgy_o!tOrseJA)b?4nVwJ%K>yI7bbi=Qs?!y5HoUolHz6BfVuF zXj$PwB;Z=5(NmU+MEqlj)~PRMn&(v|)iF&@7;_vd0> z2WD3GzKKJ{dj%Y!e9!)=`ohNi>K}VfdKjP2Vpq7UJmp3D8BYN7aKqNcww7^(YD+f6mjcNXG{}*Fi7i^`kt%FUkk}aXoQOa5iCl zDO`QvW`6nwrw4$)-ix>%9t9?Odx)GCb|7ph7d{ z^ST)W?bTfG*uS*1AG}fCO`b0p-)FX+HXHp|>nI@^A@so+{oKuvdoFo<$Q4bt;*QSt zuX(EMwO<~?Ifp<*p`@+r_4%FFyC z)xQ!40eImDmZ2jGxnlsoN5Pf81=dzS3SU)S$Tj1j{8+uOdh^~YE;m?5{bscS7ighd zxh%ex%-6e9Zf!oZ<69u9$11h^M~L&l>cg~(rf+>&3w%tzK1&qeyu@%8N&m~xYX`s+~@z|f9MAl z>#xO|mi`mj$>I;f|HNUByh^jo)%@OH#ft+2dvdCp0*pixDf>A`|CyuT{S17{(J%db z>X<5C3=BSEAr&fzZ|7gr%;6{+>~rR6pQXxM0CAar62diCTC zAze|vK|TB$ZQB=?1K4J5m35xkp~4h$jJkzmu+Z&KxYRL$ay2+jO<)8Df8+{e+^@xv zOpRB?EC;nq>J#;f`VO3WK3gHzBo4(y(qp5po)4tS~hEB8z4_ql)9)2N?JO8-Ram+K*NXR!Xj zQ_s!8Pfu`<^b7)?)(g53l=ZFpw{lmlp4)v$=Rm`M{q!u}pXV8=&t;`DQ7}FLp5>Ua z30PZmm@&YOS{YH8q8^B_6l`3gXDQs0Q2<<_K2^0tnT4c-Dj^TB0IHNEU>DU<3k8M%OoTA=?0tk76-H)uNn^_|Aa|&xc1=gjQ1o`6$X~XovT5 z#+S2xIry2g|8nTdzbXA(4*fEr_uaKU{GSGoDJ6z-Zai*b_67dMA4u5yKkx_4XBlER z75*R!H9YDp{-BzX^tU|i?9vLF%cu{9>!9#MeO^o=ceOS|>OSUsO|^1r9p#JaF0p%> zPaeN|E);UUzXeY9Uy0%_KMn$0dG3@}QVcYio6>J)Ou-scD?KUeE85R8rb}d(dWhou zN6J^s;gV^`zT5fA{ub@b325&NfA?!0@NK|u+S}@kwEvDGaHnUpw<6zd?ntr0?@d4N zVBwIqorcr64s2|@Vi`7yt5TMOLVy}&J4n0x{mMF#AKD!^)+j1Bi$pQsa;Mw`pci;SUNP!5`v{ zYfCQWMe)7)<(Q=W*s(U&^=*Kn^TG0Gx8i8``Tm5~Npn!_&&_u-R`$w=4D;xrcBzc*{0IGs8ZM&okgES`=GFH>0B1bbMohDgu!0VYVhO$kzWnde~I-) zjCdJXwD2wz*R7r7{qA50n4CU1Z9dQb=|IJZP8AM^>u%Ht6;*^7}GdeTAHcXZj+! z1C@hnI5?yk-+Qlk!-1Vme>0W(1*Ss`CdO>@r3p0anAeRRlouK{?`D1v;12I1lq1RS z#JU^5dILFhY92phO;!Z*TyJlKm&^}Fr)9o-u{Nd_TpDw_8w#jzJZ=Nyc zh+)i}YyM(@-?+UL6N4*e!OG%@Viwmf7;l1axs8T;KA_v!>qZf(pZ7e?oX&G|v~srJ zz^f$HC;)j1rFf6$f0EgLLvdRbKK^Kyi3N0i(<$&H>247hWgeR0=H0K05ew~>KifZMXA@R=Q$M2 zYe-{E2el@aAC|&76r7Z zF60W7N0vOl4ztl5%0qBBkoK~X)>#LX>36J1i62mpFb^0pt6L5Q1-VKAyisB1cQP+# zDx)~c^M3rhkNQFUpNuaI^?zreWcm`hzZr&Nk*_b%s?fh)uT$h=Ms6eq0~{1eS?7W* ze3g3eoUeI^yX`LU+v5Ub|Rx`;m=r+NTp&aEi7&0azbS^#L#vED5I{QS|yAug%0I;v?$YQhN%> z`AGPO_Mgs27+WfcLl@>2>wEca%VYKO}TGJkJ z>wlmBgpaEKIR9De_y0P+WsHdR19$dX^YcBc4jpF~w6q*43TXctANM}XzPV``d{{q= zzo&soGQMOC@Zh%=jJXMV!utJv{(HTC8^Nf?)HkPn=oq|s3#J|n@66VIAm>|g)G~E5 zRt3@jeyWg_Q6K`hl`!htnupD%JQs>XNS(jNM?HJtp*ZrGL$#WmgvH??!(0V4$dnKV zT+WwF;5%TStkbu&Hw^d~tPs32v?&;WHS;j^1E|HBx2=8IReOEDeT<5-IKN*ocga-~ zs*$!sm7%;Fda#`5^RPSi0Dl5+Y5|))G$!+2oXZ86CsM5zkyQOqk#SZAhFRzM>mEu*EnR1<@W*R6q!$(+cwL-g!g{2*Od$e0SO_W1$1T_cNKa&?-05-wLq2J z+vv`bfaU&6kqf>KI4)Nzg=`441;o(a2ICi_KL+YrF}LEog61D4j!p^ZdvhIS?0hF} zRL0u$Zy=*%aixSG`tSBX>sN_>Wtu#{$LjPRm_i91GLx zr$AS|_0(B!GWNfI#~WUpfdA9&I5VF0Bk{lK_>S%UZ7ExE=s}Yk^)}Ql4I6S*tW%Z! z*#jxLK>_gzuVZmfI>sT4hS+AC%EWt$ozkmS&~A-DB(NrCr+&Tej^N z;89z!$cvuujYHXofhF{fdLZPxpU*tGGRk(xG$n)7u6GWt+W|hW^XwkD0lC5+p|k+{ znfhDu)-(^~cx|E7f@>hRJtBTC>C&{lpC5R2jQSF{icBud_*zCZyHdLc>+w~{iU0?h zl)idc=OO#0VaX~@>&`{YDfueuX&3st+&#&+DE^5Hw%yvjob|eMcXn(#?O!OgJ>MnP zv7h&lag1>`CdWqtc(5s8k?%B?>6eT%+2DcS9^dA^wBuah$m2+&pVjR*?Ch_!U(yPs zA4J})@t{}CW%`jnq@`qjcw>8&qh+ZN>Jv~ZC>Azxt7O^IZ!_+zw$?V@-*SAHefK^5 znHrsc_i_sJzRCE|cAoEV^xiQ0@!WCeRr|Y;{b>O!B(h&G<}D(bH$7y!@X*ivmkYd` zJ3r*pk+wKf=-aniUqw45hJ;rP8^^e7J#Uhf4>{%htG1~h(f)4dRR{QkHt$cJ{ozV> zly)$_?A%!;-QcPH3=TQqoqeP27uyZm>j^!n!Mq$Wzk))uUu~ZWAMw_9&-PB_9MLH4 zYI{I_V7)yyPb=?L+PjwVaaJYX8^|5?7kO`qS5eBE+*8;=wCcJp7UhTk;t#LSe_2rcD%4kniUaxXepQ)v|9k!yLs|GA{r-ROza(v)d5QO(S|96N zSXmVixgII~oWn;v&wrV)6iQb|UE&#||+E?TY@a4I~T1#6u;0pE7 zI(N;1dm!&BMaDPy3=RY@)fM1sHJdiYLHd0Ke^w*-ue|q^Q|^4~-y|2n!C&rZ?Qjj) zG<*f;ldIbCCv#7iKb3y!5x?jB{Uc{U{Ub-6Gw6TKz?>Sh<7eN6P$+d<-3t%)UNky; zn=jp^ch;A9uC=PG_ta6UBEa|YO!Ppwi0sd$mjkxad+1<~{zAW?R{%!q%_U-AkI;AM zrGOcFZoLrjvi`P?!b1OCFRrHoZ|YZc8}Nw!NKXQm(NlEDkM<=mKIN9a}5NLgCgm*{Jf2)KyUuWfj|xvEl@SDp}Z2ZO#7;( zR>-=WD*}a8H0>Pbpu)iv#p66HQfL|~rEKaSpf*hdqS)m~A_|*4mf(M!SOyM!s3j{} zp2BiWkmoLFvn>VI297R=`A%UJWD{sN88saT^_=nLtX~d(=IlR=9y#>o--&)6`HMfi zRI^3Vg0d+NW1sM^{TMqm;&n2GKcK$0_yZBwN3B%2@CPwiafo=$A1?C#L!SQywIBQe z{t;#wl8*EHcFLVmn4@UZE~_^!hd3AN`qjL@6tzBzGDn!JxI~%-rsT>B97+9hTPK?s za9)^iilHWJR9ZwvI50A^iKUnuW+>Ci?~h33io%-KrzzwETd4h>crrUDIlDSh^tmSZ z5B9l9cg_T~@ke1xs(6=j1J5|F+cu*p?Bbm3m<1otpD`dC#eVjvtovDydA|&Il9z!?`7@%|W0kH`&vz5Sp2!cXnXopT&tph&Hh@gnmI&-VzJ z=}*7%oy^xWcLw&#*3E}DTaI07bJYutbJ5>8M==lWUC#R;;L2?5Ekz!y$BodR{ZTeg zg(fJ#Yp4384kTbT?$Po`w^6)aDN{c@f(+F4Zg0L5g~D`6YTc}7M3{Y+yJJXo&TR$D zIxA$Mc)wc6zm$Xd47ZhYFg{WYf9Y+_G+s~d_Gl^X&dI#>=1*7 z`*-)$?AnaW(0*`yje<1H@Y7c@pN(yu>>{spQ2q|SbJ;KU_hTqTFO|M~`gCp#{%gyw zZFK`h{PeK9(Zg9kxBuq693Y7g8s|nX&|F4>HUYWPpe_;vx`^M8YGb|6fs@EP zF_q_*nk9KhQ^6_$8A&3T*4buK9;@B7A+I52j1~i&I{phM)FiqfH^QyUrb{=q$<$GKe@QXn^ z+FrA@D4LCr%qC*^_q=0{&1C%E&Ypd81%TN@+v;V$@upwqBM0N3&0W4T`0h<_S~iA~ zN0**oKK~Kt;~lT2#SNkUWBsMbr=e$pSC?MMbVk{Xd}UadvTD7H`7t8Q)t*wcAMLH> z8BF`18biHrQ?6m8iQ$8Y$d9t$A)q=tA@M;oBKoU6`Ux>vOKF+n<86o-n0W1ILK{ts znR{v_1AziQG|yB-RyMm9O-tZ z$eBtE;|bIcW?%C@=07jbXzDlBRvdYFdO2&3VGDPg>y#fte=73(N2)g205lTCTJ%L# z!r(uS-+QALm;G{qIItLg-%FBS_O)uRy=^HHGFkn2es9MU^?|Q2@k_h4Q8eG;xtW-c zXYpQ740<9Dx5n{^dBND${+H4d(8OEE3$mBW#9}~`&2mfOHuDL510#2Sq8z!^j^L@2*;_)!26i;1ivd_ zk{=ChNviN{;9cGXPf-gkv<$3838H15aD1p_B7du9<}oGnb64}RABS_aeW+B|EVrI^QQ)ca>1NEU6 zex(yz4V^}Qc|E@W>U_lbtW0QskM9C*A+fZ#LF;QCqMb0#M-wT_`Hyu{h1nsD?_cM? z1}X|%kn>;Ozs`T_C36Gkzaf@EoAFuuLC$}SkMo~3KJ9;X{tMu_7C77;r`>RKF(*vT zJi@97KAuy9oY3k0&}hEgG-90tunh?Fci0#QIwNkkMCdW zca!}JzksKdwhTpO7=1a)EwoiUDH%UrkB^8{86R#S@mBwma7*DF3>MYJsHK_Pp)$}V z&a-bzWT`n9#~seWtX`tI0$2>gds*P&zs8q>niBj3=VQr5fc%1s3jBqmLZ{R;5)0e9zxfdZ)^Wqyh!FV1xKnF;B8D)hk>Es=?~!#e(YY|uY1#~COV-(h5<=H8uQd{fBHZV1;)Eh8SF`1PsK8AN z51w+_;9%+(vGWD*7ikYoX>S9&Y%g?qlWnNV%kO{KPS}P~e-by2p*&|A<%y1jevnyX zBl1XdhvvA3;WZ*)oFC_O-@}2qd=>J-`h1}wjL&^q@2_8iU-u1s;m>!Xrj&WJB;~?G z3eV5aygiA2e)mNq907_X1}5?R%fYWUrqGY1N=e<)0wD(r)9z%jUji2fX82bJe($$G zpbGpb5())9dQtSns8>J2Ki4^yfa6+)LFFPMKgR!LU&orQmlILmDLA75^ZImR(%>ho zud(0E&Y+#spIs<&=?)1tQY)Mlvf~rsU*E>}jQPe)vp@WFZuVu}7e9-^e^$BrA##Y% z)swf`Uja!0&22+8t#3fjklwu+&&T1f%GBol4}HVzi+O*cx1JmR?JQ^uaNTCUzK2e+ zep)^AGjL9`IcyPRInuPrYMB^fNyT2kQ;2yCv%l>)K>3OqW^W8%-D!06fH&qNTSpIk z#%GUxY@VaE|Bl&MQrtf{6#vcjg*XIkIQZKi@SR&9-7o)yc}TLSoKD)0J+p7V(RsEK zczrnhOvyvs^tkuPlauf9-8se*BTCbH4(mFw-FDdICw@bHpwBPfNKSi&yimMr;dX^b z;fGeNWT!F%Am20M1E21~ALaL7mTqHxA9j26)zg_c0yqjB3}wAC>SQ#_?nS>_+72k@ z+wrw+hn;ox`0KG=es_K4dfNegvd<^8LVNoExp%C$4&xhcR1kRrIk~MEVjo43BZ7Sv zd?+w0ARN1Mv#4XGuRw+~EA)$Y@qRDI5)l zYDIq?^IX3=w^07m_n{B_Ev0gL&Gbgt#g7Lk2O?($e-<3#KN*96-e>qoIt%o=`7<0I^ll4g-ld&iB1IE?e-oZYV--j8) zR6Oh2**?NIpukSesS@OFvHYH!FN2rb5GceSw7Z$B2I!X&Zp2s)Jq0<5#PE9`s2Jm6pEuS%1o~r+ z=XuSh_G|20?YKEn^nOS;AE=#_BjK`AUlRI}3n;FJ%sVnjD@*S;4URp7et$$}Q1Dar zMoy6D`oqOL1CPVCiUA|x6YM*TFSCRmQ2)e|W5yAOd6V)_P|!TU*7WQ&?XDRiIf+SXeR*G-W6zz<~4F5?@_9lmB1sN1Ts10c+}lx^JDo8*5SO zH_936v~?~$uQioG4DvniQ@?{}KVq?34I7JskNrS-HdlZF$x!nxPGc~1q++1B(Q~%u z$|xKJJitVx?N$6fnx4%BcHtB)zn>yU0OtZSJsyK|h#cEm_@SDwrfBc&K8`dTr_~(P z2FLdOq?<}Pz2vm(G<+iG@1HpXwtwcZb4LC5449wbp}G#iFB(t(<*tF|pJMV38PDN< zGs-UU%s(OuBl9D(rbH^4znPzzOR0aXO*E$iFPf_?#YHB`M=nxcg<9e$&kaPq@B!u0 zDA2Y6i<%`NM%)dENV)(xoy3l>fb-44<`|yeND`}4!0AK^Kp4%Ikizfyy(41o31D*s zW)41x|F7ixsyN60tCYJ#*hH|cr@4g?nuoMXsH!AoXn*StyUMBtDn^MUxN9jSifG47 z4jw4!PMI8R&Byo>swH5I@?7C`c2Q+Lqg-9Pg=$Gs>u#1v8~kSr zqBIpn`g2ql5|P+Z6e8woBC{R<527xu$@6#3ZB|OoL{g#^p#BVEw?X|J{GSoj&9BXH z%hRh3{hv#H&iHcHF9$z!_FoQt`S+%uC$u~gVTQ5fprnX2+riO-fIneQfj?mEzpnn` z4^~8OqN*r2 zi2<~Vbw{=rcY?Q2EWYXL?Z`mi<`=XgFcJmom%&p5;J1MtLaqfJr@e28VCTT$xbIL7 z8P^EoXGv{u6wqhu2PxMyn@WmUXIbYNM->!_D}9&vpt$vJ=U7Dj%b98ZX((>(gIu*7 z-vYy2KiT2O&Uvmc9q{4IH?uEiqUiFJ)$Vy{ze#+HQjOu;<8OYtXILK`o3}PR-HvuM ze>B!*`JkU$cx~a?$BZYpN)tt2{?++D3|)ajd))1dwl))}EG4F+Fcewdnh5`1b# zOg{64S!b0*pc9YZ**k^y@@VCC<^^nPZ7k)v5~vXi0>87(wdV$Ysp{KX@_V+nTRhRt z4^eMU06MkK#v%GS-8<5#!#G2{10*8JtmAcy0n(^S|6%X3QKMfqD# z3(3@EJoKItgQ(Zzxrgar^ENa>vyqRoZ|dV-Ohw^a&tts$6NP@Fqe7l^)-}XFFV7n3 z7xL3|-oIg#_0UfvuVHxl0vj6lyf=WsrduNWjS6NrgL(DzGyDLMKySZ1J!rqS+0*+o z{TpQrGd6**hir9S%&W~;?Fih$?<>4f4%&JC&a3&iDjm-_u}e>-&!{JFK{TJHy}M}~iTz08`7w>uGL z+nN7R?PrtqHV=6ZdiMeE;!eOm(}tV(%}&6xIQr0kj9HUKZdIMk+m^i88TnV%?*|N` zalm`*9_AGb)o@hMn%N1tnsr8rd`28MP#pN;@=C`s+J$)qL*=33uapZ=@Pe%q@y{ z%yd=w{XJszz!&ug)b2ORKA7o814|KyU4MEiuYL%l+Y zE(XiK#C8+efEvFN<>jd1y8|;&TuZqG()Ds+X?8j512%smhD}CQ6PEySvQSCfrj<*S{ZOf_2R>G7t#OZr+McJ}8`V6| z(JOyZw4 z#c;2Va2?*V1~!h!cu!@dnH&bqU27}E?3FY+=PnV&11P-J`;Q1dqj4&d}b zzgd;amLtSp>qmshDXZTzP&{4cIZX`{MU#10O|kH3h#GD=K6KNHscKd`A)I(L?xxRU zW!#A7ssS0_8|Dv|f%HxDq4i!zOs89Uu05t)55Ko0X}iqN6?IU~L3qftkW3D04>KbD zTf+Kx0scKcKSuUB<-gajhBi|P-si&r(i(V`^(zbf8$Z9%nn`2}*k8wT6p``0UcWQ& zMjTQ|?Jnc<*TxE6#kjsj3@mCaGXni!ZcAJ9Fo1YWYkc6<4(co8@WOl$U%HeP)kt6- zV;P#eYg>gMstI7B^y7UD$AW)E^LEC38&?b&S6v1Dz^qi89%$M?`dfsu;3v;(Q#e1e&%lef zxaQ?Q@N+D`T%i0ne)ebI9i}`N3NZy3h&#Y|V0AEzbzz@A$0#lawLR9lk7rkcM`Y>< zQy&5SQB}6*_xwEJpuATrkFx-DhxzFROu#{2#^(pV$ofUH`(*!B2S57&|HjX&;3ssJ z{tgj?n)(|2yGD67<9kSX@ay#hKTpei;11LjCpMY4&(zProwi|;xrj^vOaH>RLe{=- z!zq3p^?&cbhG4$z!+K!9%vVdk7(;onnQY3w>1#ZPvXkKkMeA(leFc{(O^D?{Kn;SMJ$258tRU%27Kn>%TCGPz(;GipSVrrjt*HJa#>0+zVQ-?D;c%U|!`FBbHC2AK^*UU-12+wZhhxWdBzDq+FNEz8HK@ zJpbhBZI*AB^XmZc&D`k;Fh_#c5jRiWKZzU_=AM}4hyT)u_FEdUzGVkQu83f~i(eL~AIsP}9rvQ79gYkN2s4#sH^Ll`ok%P?l zVDc$|??$?}kX+$Nc*ty#6FkA5bGq<{W!eH2PCI+ePOhRnx5)Ee<`=Y6*qo435i)qN za6*a6Y>U3#4-RD>+tq5B-QY8xA8OleFwXnY_m9Mam$%2?y>kyb(D2;`TJ>XyO`jIF zc<}cp+IQ<3da;WF=kI$~=JJlZ5 zdhjWZ7qtUIec4w9UKHGu>oA6VBk#kYwv6|c?>t0%GC1JVcc4edy@=->_{EB2i^4yyef^@X*P;%H#LFt=L{Y`6p=F=+7Kbz~hA`sK9b zvqQK#t)grq4=n{Ia-M4ktVS;$g7;OCIKV%1ms%|jqUibZi5?S&=GXecie{wMkEKxW zE4+6Y{UCe;$D%D3{d$N55k&dqU-FEc|L#-%`}|i6E#w5s1+)`nYaz1{eD^%<|E2$g z{#p7@?GWyK^xynh^%nL)lghT`|{i!TqtCmPt4BZpl<0u z*HF&Uf9B|SYk~(k`s*D1d@6Jy$G-Ak+mB8&PyS?XLjKcok*Rt(@C`Uw46r=fnsDH5 zZH*=OALHDM90<>fkurg*mVOX7{wZ>P!wPnucjcV7Ue0ys4Brqr7037tai}(@!YQs& z{}q(lq@BiEvVzazAp96taN!eIgNcxkRpYl|W z_din}jvp}sG-{y{7QwSYG8gcB4F7FS{ih_%`T&?sQe+O0B~1exZX0@+f=aS`TGP>pMIGu@KL89q}Z>jD$>t#s1-%<_Mk?RaqL$O#dDa{ zpn0tC`JuR_zc?rq6v10Xqt;6Ujw6{$Gu}I>{VI+FsITsdp+$R+TYwM0_vP-f>y*!_ znvi-T^B6~U>dP=|H>q!`23irJ5$0D`O3as<$4Uho!1y@GFur9u<71w3)-MM?bM{{j zeff8xpOxXJ25+fTM9HWZ#5Hu5Nu6Z&F+i=tm28bu5j z@Rc#1C_721`^YHD^Jnd=><_4)VEa`Z7K~8aB{{(Ol*QEFlJa6M5n4{a<9&PkbV5gj2&ZERFyg?uAK**q25sX5n(KgXI_1-+ot%}Cql2Vr%*}*BL0$mrsR8xf;8Px zKVT;F*~0r}YDL=n+WUinLeSU8dD5TxcAT;G_1(kx>)S>-KS1%Co%+Kw6u8&Q#UDPz zyxC6Kl=Bnpds`yccHB?9@d3xfHZq^-sHY1G#>Yg!6ayUL5vNv<%uNP2J$ zGv@%yFngYeAn?-|VU{DNN;CSHokYQoy6}<*xd&zEyOJRZ1N~}QZzIj9X*>bXN_pc` z7gJtrp3h+ZZ7)=qvl#Pz-!-Nn^RE6-=|0GFWF(j@hJG>z)FNtMGaH&c4UrE+&4TI} zFbhfa9?!j{)ir8Te#cmd_Y@j?@4>>%yvHAP)N@*CRqycomnx-;O)cf zPp_UYz&d@D7W6b@!6ayrNyHjE?ROQtbV#hSxLbr6D!60-%-j# z*}x>$i0AH;YW^MS$KsNttoP?w3gQRQo7t3WqF|8qDTmpP_2XOTEd@gZT3pV1wNV+L zrJdcR7%xP*ClPohc&O)5=Wp36vp#az$1y8km4VxfM`p z8_9u!g9E9@f1>=1lS4gVA4V_jA5}#q!V-DEx6Bjr|0e5v@Ct-%w01&`5^Ll#DeVrqo-y$lR9#i?J)e`S25>i_*$E;7k;0zW^&?z+DKehRM7j@Eq++yY0c4zQ6=-b$$$d z!m3IAC0wUue$RkyvOoIZ+7b-BMa-nk|3&&)8d#A`2V;SSng6e?`q+Ps5ByUHNTvJ@ z^^ab+LwwFUYkXC;ePTezh#k$C#Sm2)R|qnin4?)!_7Pt_MtwM#Y5;eebJb+vGF8k< zN#8^ZQ_y7`1AewL`owc`g#Qq!>EQYc9OT)ZPbv2?>q#aMc*5J(`o*cj80E1zlLy2D z+#seO;0VSiw4Yf^q`Z`v z`Y^ut3Ugcnneq9xJBsHH&>02sSs?KrFuYd2dN4S)s9(LpVTDge>3INU_@v7)fE?wILgJf!b}An=+_F-WM-ip~UU}n<^307!;{J-{5@G+^s^c`pel=FgRV<$O z$$|JM@R)m_&hy#ZvPQVsXMSA*s`-uvy*{|%`=9{i#pnJM*D1EoTe<25vVJ9pm+~Yt zj}^Wx_hTe{p!~Kn6H5(cnSQ?WDrO?j*NEvF*Nx##%bcV`U(HP8zVQNbDTj7%8UiplDP~+!ne$}Cqo(bNok_qI-m^;Pq zcg1h3>EeP5w&B2FYc{gc0-oCuU8gGL+?#@i7Q9G*LT$epFX`_&t)hE8-m);9|JsdFC z_e)LNK4x#ny%R_Tsp^$&z`o44EZ4%2){x7$PBlLQ-j}aZ`uoT-7|bV!M1}@1zLG8T z)}0A?s-J!%ndffpz0>G1`0X5O`^3=X}iq0N9Q(jF`nRn;9h~gh;O;md^6Zr z8BNkVXC9#claMEFI#dUzd%{7L~Y zgV;CqGHULq4nErs*b;H2(Xviv=GA}4apA^U-%HGU(u|kMX#;pZ*tdfp^I{BkKh+B` zZ%K}kl4&b@ROS!v27G5%&@?~R!EdRb$G<%I9O5bJmHzBB-xR(PxH8c6AC2GO)D$f( znq`@+kMOj|Vc~Ib$pH2R5sTTjtF&ikHOywcw4;uzww=7+)*a_PPC4DNNr7kC?X#w4 zpX56q2dwuGVxG=>{_y@tdyBGzbk^0m+dtN41aerQqoV_;K1g1g#=Lu~>Ep$*6rM3b z_PL6&4N`o^%xe@P+Y9S%21o#rdaAZt^tl3Dp6B3cn=Jj{VM37@CmBT&)j*zK3SWgj zz@JNt!zJeXCYFQNX{e_3|6N>i8v+YJh1ow^iV79^*IYo@P$=cAs-A)$;0#^Mirjx- zQCV?J>nH&~WEOZO@)W$I8ctP`;Uo~rj4yEdyT;MSjrwwSd)NBNQoZ-A5+UdhZg;a^9#!% z!7G%2`fErg*J!^R{GI)6CeXuN)QkQzf$|sp?x1{zFZ%;iae(*f6~2X&Tyrb4T%FeryU3>B za8}W9uS_#b^ZOYbv1?NvP3F0Mv{zEQZ@x?YQF0PY4qCx!c7%4swHmtr*t&v8gPZv9 zT+ZJ=at1^c$ocyR&wx1!)g%Wc)N(`A2d-Ht6CY{MgC3%A*~WqTC-Z>$p(ujQAhQ`s zSVb}Q`fjdYIi_!>yd582QP9*!+(h6tdvQ$c1dM>d{7ii&Vx<(g)FIq2i;pj(QL zil~67=O)8c>wxhwC;q?P~M>F4j5R~pezJNymB3w$O;_ydk^S4|Xp!XK;%l%w2~E91G3SqlEZ zA)!9eDq;w^XLDqMzhk}Q3UIb_k0?Mh3MOAjFG|10 z+HMHnQv=lob&B67*vH#ZJo#h@Xv3 zgXwRPta&e(PqTq{gBXO2)!qe8$~x z&4BbxBBoHjZm_v2Zy=c|>qkn}E10?jpKgkLMqKVhvI~(?AN3&kVN~?m4f=&K$WEGN zjfA};Eyd@~b+J*RoDvH7K;fa%6L)0gM(fWNGlw}E$u2YmB7yd~3q0Z%+h!B|pQE+)2#iDVdH{GELv0N30>@=9?RL?=6Ngjw ziI2!(<}I_f7x{{~rCi2Zeor$$cdnwnT3Tfn?_VuN3tg zP`|dep7&Ueu}|5J%r|E7U`y`ZiYrkA${mPwhF+)-QCEW>$h?Vi1ptxTn=HfSL(HG` zc&-p8wwKfoRG(N5MrASeN`Dv|zp!yJaPnPb&%9!oMH;o_de$Whf$myDZ%FQeXzIJ-P|Q4< zk8t6bKz#w+4#0C2iNfYh>K`cUu3n%3&O8t2$?mZ=m^&3Zp($ORyY+n`?c<@S&>W z63aZYc78;WV-Q!Pto9u28mZ@^1hWOt4drry4U|ieQW82$Uz2e05&ZbrGRVfE@E=XR zhgO*nzJ9$|{?avI`qJ9q*; z;mr3rGb{Te$TE~n0qZAHUZIX!j@4^%!I1G~a&r0;*a1WKAfPX;JqN~N+Iazd3D!wJ zE;BwCFp?)`0|zi_p^uMQ*EfKraHR+Y&S!T_1+Jst^MD@IZBKy($gt1=_!RCV>(L2n zCwNth@16r@F;ey|=Jikuk1o=hd|$=~A7GaRS_b=Ff32SbJnu^VbjJ5C@N-52-thhz z%TO~N6OEg4Iu2Ffk@}FNo~$oXpLebEu&&j!@Uu9%=wuz8T1#k~MAG|M^nRl11LenK zj2*ZN7{x5Dp`F9{WH3H6p7Y^Y>Qjkm-2-$XFO&slYqcaZ9ldjke4%Qx|IYLL0kF3w z^*xB?Z42B^CcV#r5^F2#{WpI8&iDj>(@+$$-`Ta6K^|w7L^-G97;p|J7XC+TWcAY* z#~Psr#nhLyfkU8qN69?fIIqk67Xts910C8^TouK!!fQpmj-@B^dls2Tb^{yoo&Gcn z4;MMe2r&zqUjF|O>*ogy<%HY|SO8ZfAP!Ex7Cr>w($BnL#HwOp;ASniYRz-YF~x!J z?76$v_}=7so$`EsZwYk5$9e$!L3t+wgSEWGY_WgUPU}2B7?*))>JQ=WRFC=(;b71+ z&l*p*SMbc2Rp&eEk;-kGNLkmC#i3KJvE>%JY&7)VmMaI`DOol*@0+W-+S_-VbT-uBV@ueW~BlO-}))X=P~9Jp7)%Fa2d^mZlx+b|U0rEM&UR z^A*o$K^}+I{HO~3tk^&5n{p|%7yqR3(=gc2R}Uf|wBUPYsiEN|>GxvK6%YMQn4Z-8 zSu@x~m8>pq@VMmi@ZUqMZ1BzTv#q3aF7tLnYvG`5&!-)>ea-KQ=FIHd@aO7A zOSw8yjZ^!@(5?+I7TayiYnmD9_LcXzfTSJbOZUi4HNpRipG-=L8c%|WPRs8u3KjpS zL;fe=4_Tw#x!h-wug>P~6f}x)Y|mJexk}SkuKaQJx`E6`*;+TF8Aqw8(zy$Li+uJ$ z+}2q3otX!GEeKj~DOkJ61MJ1Q-WlE&dT}K6M7N8~!_|#fPTi;eWr2i=$#2r1MaEXe zxN}|3-6j|u_D>9aJ9elP5(U_eumGgChe@1XL4pU`w*W<1;U$4bJQYx?gtYbzB4<_ObM4&L)oij!Kq% zI?KjBP=^Ahs5QJ-(072-A8}y)i%Lo0UCK`>L(}0Cu2korj{bbVnWLip2gZBT+1HL- z;{8~?(!b*U(`JS_obfLAHuNw*nF~@nra=$Ppsde-udQ)iU4aY`D z9UKMP2OalkUbAMTRnAxp-hJVYcC)U?iDk_woO^)3VlQm*d5*i z!ot52fjyyMLN6Rz&zYC4D^OOTXDv+mSGbUTFPHYxGQihDp4v?L5}cKFLMr;+lKW!dO5-TU z!3l&Pb)r@1Fe%C-L{G{yWh}nm8mu(#<@|o1mY}3;geUYy1z8v`osi zp~%cTSK8DUgA`6t9!q{{z}I~!-^NV=Izi-Vj{dVd^N^$8&Cy@8|8n&6Irf$Rsr_jG zU-qNwP+Ryr^tqZPcW))5HhfauQ@>f~k7;m?41T|^1__-dz+edfpEG!E)`(ftXXRrerlLR_UnACV zdXtUtont)RJ&irX(Qg9Z(v$Ql9BAr!y66x(y|`Y`Qx6!W-_Pqjd?*=B2p?GllwUng@QgiOz+H*hZi-qd$`a$5(0AKV-S z^+0{A9uIt>dvp$@-c8;sUIb+GG%=CyL*T*?)vHs!tA!8+Ck_B{qM#uH2kkHADyoSX zPI<~^5!`fq(Q{EQqsm%}zj*T;Bvn#%B_Rx>NKw=O8+-2^XH~I0`mSDk@5wnb#33_7 z&N&AK0RaV3KnZ3*KokQKRa6926h%Nq1i=UbO3pdw%)k(aIKvE+_w2pbdf#f`oZma| zy!+n$z4ynx$H5O4-FvNG-PP4q)m7Ei+F(%0ehW;9;Uroc7PhML5=pC)iH()#3RMqG z0djRf3|j!+BUQ^8pdVXV`iW7br0Ng6j@bf6Pd^6fKnRNy7cF^jnSGcX1%#lZu$Sa_ zrx7FV+Iftqka~}ON=_7Db_kY}8pY!)p1)%HEM9-b{N;a9{<&RM)RDQ>kq;P#?CYwn zTpdH6n~U1;H_BVNdm%C5hH}b0#zDfNM*APb{u!pOyi|2zoMQ)dq zOujMn7J03Xt6gkajF=5k6T>ky8spUwbq5Eo`*y-@3{F;Ew6PF*6hS1mB-;9^rbzU!iMVNgtXYpRq4-kbDjhM-l>qjBb(>X&Ac%5;tcFvxpCEE zz)!%@fM1*g$=Dg3bK%Y-_{Zi}SX8MaC)Qjwf33+zE7leCYq=2o9fN!?-cF_8z0HNV zgp7a@DmM(nWm?z#pi3A6v)%dUGKSNS){*n8;NZx9F0W(80`!#+iw64IV_02o-Uwja zjymQQzsJGXRnF>N!%%$X(vn@BQ5#gL0?nmi3YE<$jFoRU3W9k?5H3T*x@~ zo2M=GDAkWUw8gO3)OcGQt@c!Fx_35k8ZMh8e*X}?BZ2Z!qox_7@wr3v1aybDMZZu1 zs-_uAxvd>*v4ON!TGP>0#8K7VW<1t7>!J;QadJ%(da@STft;3-6PaJjNieQpim_xK zuK1e!9s(}s+K{Vnzgzbe#w zjT_dpz)k82-|w`0*BU7qly+WVDaGE1}lwHPNFL%*et?W`( zJ?dB3FB6ht*nzlOAH`Af>aBOqV356eF0<^}Ud-FE+=%oqQTdvMPpbG4C)P&!cP&xG zdws1sf#6S#QmY&T>CQf1hHo$ZZs%QCh#uOOXGxxKJYAWu*GM|f_*5Trswwk!1uCphzaM2p zz4`tnRxIPji8mEvFa(39Df7}wwbl;vd{PjxKg5es#W?=mgFqpr2{LRI|EWcM}gueo>QhpVO5!WX1+Mu!F9A!8xf!P~VYJ{U z9k_{eu7b}8>+b}2Z4aTmkQ2M$1HEgkWRy`A>@g(yhp}^lpK+4!qPYYzsQUI!D8k^B z>Yw`ggRB|Br(4EBtG9%YaBBAxSf(MI&$j=P&8ws5r z24nC)0tW)sEMUCa2L~|hE@XUwznN7X6HNF9?!%?z0x?`8T%(-uIp@c&ubiEzPj<90 zukw6>`Gr!Td2(<_0DL&^c*2pGrA%{Jjmt?qU)EhZBK`oMeKPxsq;5|MNWGp4pSjiZ zc84tJ;>OJDH8YT-R&Aq>1a^cB2vtD?5C`?bH->}fqAH$slDa@L+0S9z=aeEKoPY@{kMU2ow z!jaL(-PF%2`oUd_=Puiud}*+g$l$w9%8gFd9a`fC?DkZxyN%IvDtwl(q5}G%H``uY z$o#9(W^EJxVfV0075)UC=Bn5I;E#@FMhRDC>Yr5oJY&FbJmDkoSF^10xC(%k%n|AY z_0PdA-vE|2Vjax8qlsgZAN{+VefQ;b@VNZ#wQE*_x7C$*T-gTw1bz{?ChR15_|*NA z*q!ok<)6))%5?s5?a-b6#5w)ky;YkXh>PJ7g@F$dAetE&6V?cFJ5=FNrHkM(B=m_; z_=4{#?^z%CKlswIo~KuH6GfY~b#^a+tktb6ccF`@6ER;!bw%Az4Qm$s0^{A~o$H&) zc-_qIG3I@YBi{+$)oFW@xfpz3&3ib1Ja~H{I54ml?QixC@-Yu5QcgsjWPJta1?}tCz!sLWgRipUu;y@|tXpZ3!w*2^DHuiCxgTg1`))V^t#8At!1VIkwd zOMiPJ*LWBNvMe#dm)XS`W0vOolg%okKL*bW`7wA8>}z((=cNRP(1?1&$$nqd!z5^bv>lF@)y_ z@@skCKg!iVO(^$4i+~Q0=@s?5k6;}OpY02GW4@77+4^KXe3^IYp!&=n$$L+6vVQ~R z_Q=&gfFj?!0lO0t$$F#q4XtnGBhhP6&O+Nb4ZJVk;dNTR!yE6}2fL`Z^%WV(h7XsJ z*e?$loB2-1`|__Dfm?hR-d~B4fmqz~p93Gr_jLHG@b~B7OI3iYfn|Ypj5eAN|3m)K zZUprYD!J;Z*#3D2ycOGbi|yBi&_S_%zSzG4 zf8@gIfA2p^wkN3R@XrNmyzHmc%Up3Kdh$_rq?af!R>E6x>?1{xcZ6lVDf+5f#uZ`v zfGdn`%0>NW{A)htGw7A*Kk9cbkwY#cpWc##vRsrmOQElzcdjL?5-E__tuCPaBL`4h z1GlM_?DXY)|2l=bU!ghNNa0M-Q0anC(7z#YaS&A9JKvd#X7+Yyh$Q|g`kS;PDwi{8EhxJ&Fm(oY#Y}TwJ{S`zGd}I$+sD-~wJ4|I zjzAtDGcS^?R|41iYJquq@?7y!M8+;hBQ2n*v3@JZxMCvHuZepJV8?7<+t-p}Lgied77zh=&@l>7S4 z^uw^_y6gXxAM()qu=kF)8{M7atmVMa?r=LlG^gNiPRi|BFblEvVnG=H`93Hqgzuf6bGqvV zpBO@~CS7BrRpz-;NhN5Hn^CvDadM*;ZxnqkPzKM}s=_cWcnL;Eynz zr{&*0e+axiY1huHPyd^#3c+z0bVKv++{9p(SFZ5+Y#bPoD_sNQrTkst);uVKkuM z&QfM&*{GSh>Z)%H^YN+eB}QCQou2c3Ot}JQ%AhONSOrUfgj%Fb^C0zWRT;A%@SJu* zaH7essUy5oICoTm`ZQybH4-?7^P;DK+_@}wAzR;C8?6Df&&g4n@7lrkBi8G{Ap3?j z9XQ+`>6^!MUMtyq7r5VEX+amh(U!;k0x;OOJ?~xM8s7*vdY)scxiYW^_`2xY5?hsV z98kRiiG zr7D}~$6JR5Nm=q3n0 zt=86Qdjs{WIaw7Byi52Ebc1`{jj>t+IrA0$B^`XM7zi*v$7x5>1h(i-%GHdO=(1eC zjNV%XXrWc+0JGIFIeD&LgM%=SaDyOiFeq>D(vG|YE|Yqh`cKt6iuI2!UtT*HYhzwG zM>!BJc9?$eC(T!$9w84HPnl$u4tHZRW z9?KbrSNQ$}?KU;I$-S;NKzFj!MbD>yL)E7|{}qOfiNJbbYA>)2=WTBT3rSMmAJ`E$ z=##*nI1X9Y#$#$e|C0LWv?K2|IH%BCBBmDB1s64R7RRG{OjT3?)CZ{rI1LRIhr6H) z<<4-8N&5W&eNxy414}7qk)vZ3Fbe#@X9-@JrS~hHz+w~Lf0L8l!tcxQ-6-HK^{j&4 z2^MJ~8O#`8Jvr&89*nQ;z457W@aN}wz7B@|UHsmSME~$joSUcA8$cOfU+N#@yB||- zU`*5w;^{o6ng?0 zA2LP8*Gi`#jzoIFPcUTtM#wZCJzT{}`uhnA{>Gn&wSzc={a7dJpET-mpRiXW?$2R)#b^N!1$QAd-M0aYN$%`of1%AFfdLH(N{rqwX^ieQRtJkBY#RN=8xJ| zGJh(9OmH&3@9)!RZ4%2fpQ`#R_kRwe~eE%qB%z*Mg(IC9go-aU!K*lf^rYs$xVR)xSciv zABPHM{+{P~8DBiJ90ZIdFsYf5D)@t23^Qi(`{&TxILd@E#vEj)j-D6i4mLu4#OM?|1^}Esn!5`sE76XdqFi)rKmU69L?X*d%(Ql z@4@_4Bsfj}l+xq7%YJ+{B_Di1`0Pw_0hFMDUAV$qz{X&43h+zvMoa`sz|VH+AFI_vIg$(kd|$Q1;M;?8F!qD?z$S3N zvcN9P$9Z5T+5r68?#2}wvYyr%Z_0^a>jR%pR?7;*COb*;Iq7j1@LpX zC*SAp4!JGAo0w03^Nq}XxqSD^hzk|2UFOEcKQ?XM&Ue;_Z*8~}K4%9Qyj?gilaGSlPU;k!@x4qD**&bst&cRL`4;_rT%O}%?b(RTL{$YVnK+`II1 z=arp}k1(%)by=IfBMr6T$hF^Z#W9`NBM(G21@?=m8rB$jIokV$$B%ZOx2CwkbN!Wd ztV;WcZ;EdCZ1wL@EBl0A2q=Oq$Hq*HNyhxK{N{`K`!N^wTpzplA;Xy8=y(4gMsbk4 z)$v!hF@Cd*`M6|Ua2+$72MQhJsjpo0)BCmWeeXQ*)lY4gt9h8uZ_PsRx!BrkGoRK> z`%%wLp1a}u*bAP>NL1GMC$O)*!Z(8Fnp$6YzhJs%M)ZqhzWcQ6b**QrVZ2`L`6(}K zLEQ8yzc3#DmT@?1eFoo&dp`D5?1wCa*DIDT>p*S!ZO6-pePmk5+BN=m?d(Wz0yp*%27slteq#W=2TJ&M}uKXVhKLT%Oz56ZJY2jw~A@^Y7 zgZ5-s%wzu5HLPOHOGH6fNXWzVFE(^;;C0@g=bi0~gbofUORZxbkDCjaBe@!&w$9jb zol6Sf4Sr<}V7yDP>i~o22Bjfn)WNMQ#mnmxg9>qRuC&k^=Zx5`-p0>3y4nN;#gvCym z=ZgRQoiQLJRs82~i~$&0fTXxmuXCnoqO&CG>LnxFe=Np^Z$$wlT7S1r1=YY#BWfRu zI%W=GWKnEzk4xDe&65Vs{Ns|k-p)7n$Vn}XWjQ)UWs3ckTpeJ~vUkbJE)03K_51ZO z2$+-)!s1-O7+U)rxDrE?P$fCc#K31SB$-tSr;^G*(J(7T1nJBR*P zvtL0(V^-$YE!hZ+RIQ|4f(in(V!NGqRyOb$W21C?n;G3MexJ*D%kX>>Dt|oXT^Q~{ zDW_uqXaYQ`Vzi<^2UQw85ygE?427uCE`8_LcHR(1d;u(l0B@;}g<-SZxPcOG*tcM_ ziu$sodX@1NvEgVY&|aph0QVUi*ix`TAsN?p%7L7d+d;W_e8uxuOrOQ;ub995_xR^U z!>PoebQZpR1%6!G9<1fYRpXM}iLB)Va^`PT?~nbUA^Swj=9vVYd(d6bbXQO7Wom*)hQUP)JjK&IF@j~ zbP~h9on<|)uOc{L_7wRO7$52jLJo7+`logWo_o}6udXAWY7{=}{+V{mI-(pKso#rZ zZW!IF=FagT?^G9+uXxYDMev}&I~ee?%|}&r>{55aQ$mhl@Ls@G1^)Cq&;Cm?AViLe z94{y8^BWY7D$L;hqiVi_zXkOUI^d5%ve|*gc})kBd2?9L-=9jS-I)O=gI)v8f1F!( zWE<^g8C@mittYQ2)aS*}+%Yr%y0nvMF0hWV|M@ksS^{{xntHADwGpTj*->#J=n2go zwL%-M=Ro3#upSYQVaR>+R&rW-`ZG0jTgW5=33mCfjYhu+tm|wQRssX@q=LEEkkfZ> z7k+l@4vicE4vU^?V3Aj2j*~Enf7Kmd3Io@L?93--sbu;u+E7USVO`ZwYz=7jzcwG%qp;Y}+<|M9i7 z!ty85{(Q5i<4eYyt}2Hz?~WLAk>B^c_ZBC7p$9HZn&)`{qCj20i0_TD6AkdDCfG~N zpZNVFZkg={j3TvNJ$~P4H?!OG{9-%U2YxtJoUBGt|B6-7$?v{j>}YR)0yBDLtT^=+ zc&Lqq&{ez5No*T>Au^Pbs~smf>jE*8}SMp~cC zRTpAD}lnl|J0$RehIs6x#nK zes2TcyUO#!_^&dsG2=T9Jc?G`8#svHc@94IwRVuX*mwLwIRJCL4g6FM)DF5L!W24D zZpp0u0bCEYw+5P++`%tjm(XAwpX#RGH<-A-oAUjBjQcG8`MEU~kUy37(Vq7tb51?1WN*WihR9<^36a zsRHe7VQ0j=;Fhi~4pgHDxif^G{ne+kZ&9U@Ka+XxIM|ap7>~K<0I&e+kneVc8U()^ z_==3LGSnjVX`D#j2kfqn>Fqss%R@ zyj3;&t3|-uj4y}&v@+J(-_W0v^!^C-r7!?Ffmc+prn@LEje(wUpC{|}e45>qo9D0M zK7jA4TC_Bf=c91fU(w_9ii4Q;Vs$?!swbdbF<`lc4pk*6>l4cRNMXCHr=G>_3_grKqALkj&*86WN=ZOE&}Eu7W)y^%U8`&89{ zkrnX!F5Npr)wO>h7@qws*t^AZ)uD_2z?;l?U0@!G^&^0fv(B~y%NmcUg}`)HRUWXM zF&UQ&9AJ$e()3bBZIqLtm=oQ)d>faUd@tVq2q(H^ zA~+@cXucUMa)@joS2ca_VZ~jg+?oRiD}WWC@KV5|a6+N4XRt&FJseYI;W2o=!I0v! z8Bq0~lvi*uO)H)o${E}W)PHLa(sXx_=VYE6;venxs0@x`K)YQC1lC4-%*>PBnim4sp(`3d_&%^O!&q^p6d( zmM5%@$$(s(rL5SMkl$z=z(X;s6dc}onss(6`^66fwKZDu?)|qC*1aj zJ>IRlw;X)fJ*?=Lf|A&~FCA@v_9|p_Exc{oR)=AIj_(Lt;a>jQg3ZY=nyZ4B{u(C?|nCtMFeg*D6? z2z*>cSc32V%2up*GKNeQ{mFOI7A|4fNx0c*&+MTdcUDF1*;>O@4?Y`tBH~oYdSJ=0 zCxV!djjOgUJygJRYn|yX)@#YJrTWT=`n#WGW#!X;_Mp7UMK9CtOy8@%r@(h|RcF?- z;BRSYYAEZkR9f7!l6zTBQ`7#qO?|I#x)1Fb$qh`ec!sU+1zSCt=6Ab3%WPGd6(!)S zhSP3_#3In6sB+O@`gzDV(PkY4{~0nQxCiq-!PCG8-Ebj9UJ-Z+ZXfb^@G+Ksr?Q`y zJ;M8MVkbh*r1nhfafA6H-_y>+t-;sR>Px#T`sI+=6EU-hTkF(edOH{T?`i-jfcNaf1zqzQe~{s~${)Gq9OcdN!Qa(RYlzhVI()@79P6Kv|8D-e{8wqeveCk_ zps&QZT~R^wJG}5yxq89TKyM=>i|-ddDg6B*vAfE?VHl^|t@J ze(*!@5zjl`hV*m1Z;lT+;49-jYmL2 zdmZ%5oy%Qy#*P{sbs9za4%S2Fhqe0vxl6La?V1t|0iZ3FDG= zy9C!sI^{s#1h1Nx%lwWr{Pk6BICvy*0sW^CPQ-$Nld&GrNz$~PaX0!Tcty5^!?8s# znXWV26A_w=!~1f~g{?^BJrbV&4{q0%q7Vsp{g>C2YSsU zloq*-Zk?k2KjS$<`Y!8e9ht?@3(#p7i99fBs%z}cP>d+=21ap(S0OM4e-m<$y}-|E zEieadw*vUzM*ha?v^!tz((*&0uOi=&MOU?5=nErN%Z-Xig>n8S)`d)aGm;)VsNNra zPFp|mxA@QB5(7eV#ee?J7`S1#(Rcb~*%jPPj96QC_C15`8Oo zgoWEEGXmtaFqDLojZhmw3}KUvSZ_rPn-v#f$|8_p;<$$9Y>;}3@I@{CUL&_`i(ea||2`BQ` zF~ez@7=8+3^70Fwr=0}MmdI0A+7Th`Tb^HO#n%8JZy909{d+QnWgWw;6jPQ1x$o@HH z*+C3!0RsZh%gLgAU(T$86{ri=rSjLF0NtPZj(d^M4Se;=RmWhJb0;St>uL5){mXn9 z9|$_Y1sn|5i&{ir!M@Ks>8iU?dufA(IVbYvcZ3a&G& z);jop&Dijyx!C*4C%5Vzjvs4F#O#vbTV>;#Kz}e>s+~^ygM(qGIP~nY_CjwM<@b!r znEDL6iv6M`dQA?RmCT1}@RGE(a*5T zTRGMS;0`-Ur$F9j+YMeB)+#>z3F29jGyfBTykMpP3+aa)xiB$X&-c82U$sk zY#R1+nBg&GqWAQYD+fsJf0Sz}3@grBVntA%LMrO5e7BBopQ9(_zBsRJ`gI%^Wlq;z zUYWSBnJLpYu^viHj-Fom5$17qT&+gvO^&avp20T#eBSe+WIFJ;JmU&`QU22t?Cnf@ z^L!m7aMNA3sDlT3I_&_|o= zn=*&cp2s`Mqt`FUh-aKH;bz?fd=9tZ9Pr`9g*=7+x4jM0QenbKbI1?;-dJdc{0q8WIkks^qAH*x>5wrS0 zT#4dvE>g@tr_$oYK+fspw%U;$KtO|xe=IFz0Uri`oq(pAF9vqCidzk#BQ+0q;tlFM z8Sm+K#z5I(m>-D=pDAF^F+$!$EB=}pg;fpMgR#!FMo?@pdO zk0D&>_e=6F6ad?(x%AB-TfrCFzrA0r9mN&_}ie=vvQ5Ff1#^ef=ro2mZ=w>sZ9+7JQ&pP}#1 z=@WUw*+p2CM;kALF>zon5`zu~pJCdu?>18PeySqlG^zgpxzY_-UoGJOECe-H0&3Lv zWFQbc)hDk+3(BvnFEpKP!l6sQ*{wC!hW%LelA6Kqm5fSiAN84J92ifz5i_zG_=A>8 za`i}E?Wj0GGRY69Z^kYR{$)gS#f7Zf!;C=YOU5U75r>P+@BQ(qPv~h^%2SvZ<_nq| ztOKbB<7SVhJ_4>Z2xzh|@&G$C>T0z6Ik!ANNx2HoWdpOJ?DD`F?B+6ob;x_r3^-n| zCLATkGlF;I?nBxE9iaM&}A<8m7 z_#5ZQo9Yu+C!sef7ZI59DlmpC5s(w)9DkOODhUKJBqcTTm#m+Q#{8I1{c*xs;8S4f zqz(}22$zxZIn;OB!L<}jOFI*Apu!K)Ufxi@(#{v`%zIFNmr=z~e)RtMo~ECn)K|bo zKAQSyW{)!3M=2%qG?5lM0);+>uAR`Q^lc6tMdrByuqE(Qq#k%<7d}F-CkNER{4;CM z;_M~D>pp#6HwNj<5J{Z4UrsxJ^0vI!5N=iic%G!F&@;x)8}K5@kg-lX{$+gdE4ALJ zr*>1nm=K5@U<+KPzf=F|J^F;I?u~B@AtTc7k%U_$0;|~Lm`RbhPVGn>4)+myUIc#- zdP-y*>A(T%xY#q)O85Ujn^_pOL8C*2t;U`1Nal%;2Z2Fbn@oMus;QQ#@ z1H(?k-;e)(WYzg0pkw0s6_>x__eV2cNgu&-$*s}1;x8mBc%|XyB=}XtFJTRWZX*}R zx_|Qw2KFv|(M@~qmIbwP7qg6t-0yi3F<(qSwd4wTULCt`$DW;tfiDX3iWpzGJ7i4Q z=ct>5mV#~Ym^Iu3(8YE*nG;R}{wo9K}t^e^&c)Jq}I zS8l_c4Mp_(&x3!tcAO->!L{OtKcU+_A00WfgLRPcN7j-X%$M2C*cK#sO;_g~@$iMS zMtiNt``J~@Ui?1T=<2x6hbvkx1%A~iyNvZBcpC0Y^!x=rXL)NE(XR2G^{2(S3JW}A za?awg|0up@nSS^`H?=L(3A*j(>Aa|AI1c^o;rsonGF|yW!=3LT@~4%o9x3w>WIkiA zqTjD)M;HA`{7>NCJ|}s8XCPL&0ds*nc3PcYs!MHM}LVM{%VtfEEW zZ-!r4Klq6^-x?*ElGq=#_J`h_haDT7%!9A9ue${ud)_Fz=z+e~m&O{APxdhTmbC}? zg;`4RTyV9ppb+LgsBB27pl5jh55M`&q0L~wBHJ$N=!31O1w zKSxdrUkHa^Ko{_kmqiaVR*)M6dR7DMV-om>lx?RSIp^7liF~dJ2fGEVXYAKkeLCRW zhbfnZs|p|3$}S?B?=HlW`VH?#!%ea&KhBp*0Rk8iL*#CfhLHjB8uL%EKO^B%Q z`^c16A7~Pm$NEF=5!qJ@{`~z0a;d!1+a9KdQhxAB|RaYG~jH{JD#b)EKgZa7%<1<^0s0F(BE^#6VAq9 zqqm|51;KN}lk5qizMbc?2S&!Zs(_+Ge&18HMhxGcI8VN(J21oJSM&~Wx#z40#s+-Z zvk!RFQ$pJJE%hz6#xr3du*d_zo3Pn9pg)GGU+I5E7%9|juOe3UQR>~O@ocb26)Eh{ zu!q{yRbT$ml`8{iUmHj<3}A$*ED`=h_GW8CLOv{W8uf!QxO`0gezpyJd0tY4)dju{ z>kI~NLjm6pJde2&yugOHXhTB@eU3v?w`|phw1$|@UZuSfMl-ns6$4?QD9{*7=V}9i zhtPrYl*8DtTm&X#FaU2T|A(~!qIi78^H)rt#p|z_zx?<3XTW{=P~rb4`EU!>dm{X~ z5pv-f;C2-2t-${$`OwbTtQ`pw5FiF)M1f8%y~FrHJ9zStW}~USb#E=#OD= zW9W$>3|<9?vRCET!VuQXSKlWauCLVwwS`718^0l=#4*-PHXLS{8SfZGd4gR@9I-ei zOLG~I1k}AI84&_{1Qi6d#UT81?0b>rM9w+-nJbacrQ9DEH9_8X@~rXvMmxVdsyP}_ zo?3X+jp5OG!PUui8u)n7d;X7bz~sxoK>pvMO$`=KA0Yg}(Q79iG2-(Hr1fimFN z;06Ko$8VUUw_^_FMnP-*F-V4P4l{$;z&x>}SCz-V!%#V^-o>`aJ4ac^dB+_RHmFO@ zF7W*m30ISk@m#fp%6rNVMa-t8WL(BD=Go>sD0kC4hB+52401O*+@7C?UJ&s}Xoq0x zpQzscm3|+v-&$PMEN>r%%0s?@tiMnjb_cv3_5%I5;B`igj~cx z7IegUEYzldZCyJeH(~Fa?2f+s4EWyb`_{b{bzyPfv=Y2W#P!FH7WC)ZwJN)Z@&29k z5oaF5FY#IB@NSGRs(JX#a^S~tz-&vBXUSaD%=0Ap8SQw?(UNkibxcy#;(+*?`$)yn zG7R0oP?2UsjN6h~Vm@a{4V=RR#6e{*LvLx#?;Y4;_n^Loy<0o3el&Vo=vB;eckO7u zZ8gW`VAzfAju!a#%_QT6{QfS$KqmMw=wa~uTmmhvQPv>2!od55?}!_`dVBgR z<;X_$8N0e-{`|goC51BoYMP1;0FU-f`(yiU$`g!@b_3oYXTRqS1fH`4xr9I*Wk+1{ z+%_ZAH<5OR;o5tP-}9`h)(z@Uli0O6@Hu;kYd!Rw=U#dDA}0@b-W+-i{GM%fx%S=& z)VV>9k|EGRVo==f2K*k-+5erGQH7Qw*o`wVoD$1C@1$o3fJD?;b&O45pmJIjTg)T@^PlA23pZ z@8SOCIf8%};?k3oaN59)K3gF8g6DKcH|NB>3et|x3Q*E&o_m!f@5~p$Pjz&P@Y8U| z0hGrB!3(+yyCm+{rdg+G$tn-f?o<156hp9H+ge#QhQfuHWc zL}u_VP)>A1j~HRnw1K=XZe7+JVG`x#Lk+G8a5cxf^Hp zF7tkM!m1*H_va7(pT{?0EClLI1&%PDQeWwE7fm;RKqZ3bi98Vn+`-om0WU&d3BX~@ zCiKm>%IkKwv%@G$xjwE@#-~ad&9y`0G&>;HH8|R=*tap~P31(g3g<1>3yuekcTOH2 z)cc1Z^|20=u1vd5e&>oc^%~HRS-lGEh5OiI+`|arc#?Ly(vM5jM}r|Zu(^z$KTE(~ z8j$^wzUt-$E?M}L@;)%T2lzn#y#*CZ|E{q9Gk{spX)>@aD?{*~K+hHMzwVDlQI`FY z;OpP)kLbDVk0SK=lG%UVqKt!Gk1v3Lm|rMY1bZ^ys|bXV`F8V#Q@~yXp4hBnoM|~Lh>;jC%r1=H# z^u6)fmq>}~p)53>%U=g4n2rOJpbr_}emI)YR~qO4KLVELJ3?dI6=+AfhzKPOV( z3yibA)p0Hh4n` zx7F^5c!+S!RtNQo^b)t=!xuVSym)OY^7rkcF`jQA%iQp0!EI>o55Eu>^P`41Dmjry z`K$8GLgptXCOSS6dZ_Yxm1oQSiGBG-(~k9#*SBun*>RhB$cV@&pZNpv9RrdJ>u+F~ zYlBaRBR@_rzS#KG7%~HtKi=n9UC4gNnO`rQ1fB2s*LAWUio%M@c#$Io`|?*8Jq+8L zRkveR)w!NYs@%NBHKy=s3@bO}6NvX=k|ojvsO}W#r`! z*On2N^5VB2f4+xyzBg;TTGGx+`(+dS=4`yXv!EUAwRN0xPJv95EA&l(PX!MQ?i;{3 zkkSu1n2(Y*<44CcUsXRTpPoqnwh!Ok;J|M1J^j+98u%ubL+nb+vx3|?t~7*YlMCsx)3(A?o7{=6P%a1 zxqI1B@RdIGR&G`x`g~L8IS2i=yL#iij58!QY;DMO=I4ZGpBK7xoN#U?sLQDGW#tKp z(^($JLVJWL)YLByesY}lZZ5gK@D6kty+5*k1axD3Y)8vgH+H7yhE<9Fw)f2THAF10 z&p(j=2KAThc{%{}Ec}mkWVP@ulS~87H(eh)Uj>eH{NaFaxDxyy^MjteuXz(CKq3E1 zPOrSX;5);duh575e+ek%2R?ltcpF-i`Q4S9o<~28CkZcpk?*AG5L81Ys)ahU=3_<= zRgd5ES@AP~QG7iM_^!Gm^3#CJO2CovI@~;n@6<)&#!>Ez{3-)H%F2!aMl(~;9c{cV z0oLd#&9xjwf06waL1_2ndj?uG>j2*Mh}PG-z+Z*mb!A=G2cCfQ%Xf!xGIT!gmE}^S zdGz}=VWI-AzzWv3>qu*55z$mtz0J|6cwTx3O9De6>`~QOGB3dV6I5t@@}@3VEz58DA32 zO5iHLj-~vbIxhkBDudis-%!7UfUU)pUnXE1eMF^l;6ph3R&~W_qn}?b@1ym1P`^>X zNS#!o7p^7b6}?U!CZzu}>OUvEdkf`1RDD7c40S~9Mw6C<47CkkyN&Dvd_?)R-_w83 zu)VdYFaGnl!~m*X@t?ml20o}c#XSW^rK*~>%{Cp1tN^R2Rd=1s^SGyq=P@=YT|JGp zgFoNMU?0hK3=)2n>z;fwwVl$p)Yi zrVKU;oYp!Z5mNS70vR_&17u%uM zBK#Rs3fdMSe^e_BHx;SB!x?G}YsNg4jcMHA#^h7tFf=ckTg*o>tUXFn^>#cr-O9F+ z!>)mT>s%NLqoyRRh+nVJS@;0khR_kmGYTi_9s&^HJ#J7WY2aUk? z-y*M3-me%WR|Jjovk;%h9;?|Kx!tp7htGP^Kd;-GTEzQnXYC!k=3P#LN99f~zz~)g zUhd;k4^eKFRg^suJ>X?;jGXYkvp@5x^gf_5_s*Jh4A)Oo4C(X& z&;1oRrYw4t`M&wM^*ryNa!e6CTH81oUj_W-su|WT%40a!k6xqp+rK(q0$x;=^hv1q zRS3pD!~8|nFncf$J*+gFcI*+HAZ`Zy5(yeA?W1jg538e{Z1KK*(!Q>0Fb})z*$REc zDrJ4@10UR+d&K5>tEBIU=v(9gxI~P(!8HQc)Ystij4wQ|5^%IPFKZzE2v^IU(53&o z&Vw6^tZr$)q9tA{M4et6~Gc!IScyr4fI~YImJmY+u@u9-Id99+(uuz=DzdZS?K5D z;;Va7=1~87#%G0t@sqDS9iDy+_~nJ!*WRbyn?=Dze!M@F-truScxWOHx9t3 zbbSTZsu+w1%e=65_u|44hv^JLF|h1&<{U$`hZn=_4ot%6#TZ;$X-Db} zG{0$-<)&Qb1$TUqHXOHM70WtotiM9wDkp&7q}&e8F^lJJqa`vwIK@k8Lv1c@xGt36 z!PE+!@_T_cARpuEF4hU=hI(3mEWluWkl(NKtMs=A?+LVV1Tue&Hib3}H4eiqbd9n8 zF*H+gOdgiNBn%IW7x=Jx zUk7fj#X*opxw(;~n$muM^zMAV`wJR9^QfNX*89`^zSrKQMgTjg=QMqAx3IqYt09>X zpjWtghM|Bz;}8_O5JNcaaDuNYMi#-_z5PQf*lIw#6Tzm8?-08ap_4tF$!1@%v_G6Q~{`1SFXHo46zI0*mZ*O6aqn()$PGP802Lk@F;BJm1fltsVS32tknV z*90%sfe-dai{T^iE%rxE?(dIQ=>1VRZi;=>-{cCl>A*)x5kC=F4@!cc8namEeSnRS zezHGW2h~e|`mv7^co5fbMWF1DWPRz{rC#UYF}VOo|o~hf;uyR|8D;$JNpFIIaKNKcoCiLj&xh!z^kXYMi37F) z%Xfgku>%&qet>d2py)3$f5&mvF9yzHcGd&i@s#k*3(TU-M;2zr9>98FaXD}k{XEC_ z_wohshFk3kRiE-~yS+XkTb?Th%2D6ccvSY!ILp^)d^CmXf8+htoNpQcRM6E1;98Qt zOPT#USx;nMw@cs_0W961b>YWLWIUXrEvx$zZ=BEp6bDI)T0y@jp+9n>f0^Ew%J_sn zlbM})z^kmzKY_o(b&mt1aplu5xQd_9g?-rmO!$au!z$lQ`D^ee&u^jMK44|SMp^== zF|X)BXo&&(asgr9hR4)jesge%pNc9p3LOz>$3k+Ekd@O}FoyBhEPh8a1P-%q36 z%}4H^bVNADQ+~`$RPZPN4gP2R9>HNc$K`awhph$n5|W4;W*z_MoukknI|r{0gikci zXf&`!0_5;r)u$4v4{8<0 z&+RMwaFr+N&t>O=LNDUQKKJLv?TDq+TW4;+OWeoF8+UI%M)~vW)6>JzL!T;q)$-WZ=_kK=an2Jwcf?9@tmC_{`$Y#Z{;V2VvAIC^h{FBeZHSpS6Mu+bXc#rBCN-+u zAGY#M`uQ1v>W(g;LrZ7qoOIEc49|!z2Q9TM6Nje9BxN*et$pU z_aMfRJ~2HhGYkATKJS0wI)MNSD{L$UT~!IK{BtSBT{7|fs ztnci9Y&N7zYtmlbo*QV)j8U`ZO%2@(EjMcFgNq= zo>$Pm@N?*`o;Sfa3A)NB(?vaICkl5)1K?CHQ5r$}8Rk|8>%rB`@3t$7`cEBh6Z*}o zm0cmP5%aszanoE*`w@Nvo$2&*Y<49H__c3Z!|iy!f0*!7_@R9YPH>+0N1%-aQ-2J3 z1N{)F*hBNT3|97Ml$*gVSyx&=`-|tFR&59glEAKf1%1QIKGy--FTz!r5BOp;tv5|z zs##C)k{mP>Lro-yXD!NCkiJ!c>6#-L*cu%=z$*?;dXxJ7yp;>=3;&V+I^jSfPfS9t z2Gh;}eovw7P{ZM52HN~S(Q{NoHBvGSa3R>+|0n&dHQGIV@E`Wiq4et_bvBl%)>$h980~x4R~IDc~jFB)QWL^X$iRm#j74x@zqJp0(1fAAq&3i@wpoFRf|TaNq^2rWFY+ zw1$g=!a8Jau~q|@``)zz*%<7!CRj^>wr`8C60okXmxUs0H?%YKU9tz*PX9vh<1k_6 z0DUOVFgT1XuZjTxqe*pbhzUbgotnqaTDz_ zjJNEdaPyeJYnVYQ0_9AE#s9@@mkIm{Rs9Um%~SN7ZG>CjeYk`(1;Z!@szDez#n76g z4TQODGlo*W4jXR{RBRmBsG*K~w1O{6dJuoB!}vP@<&IAJ#`vDq8lxKi-hSYVluvVlz~&v1mA zn|Um%Ak9s^U5k{FBJZ}@U)nxk7IF;3AAx&Osv0m76B_-+Ikf;6f?>X9RyH5O4p%84 z#~&UQ^mX`|kXhK1dOLcX`*CP*$SGOy3C~S(jy4xyX#0{oJ2CY6;yo{U-{-mVj**U& zyx%&tS$=d`41B#py8PS?16lV@?Gt-^!~STV-N1u<-W7W0zz*cZwbIvXrypXUH7o2w zD00x#yr`My2H*GdE9)AC9j#49MCMW)`7h>O&2Eb!a$%*1k`Hj=>5Y0Vnm^6>z72@? zug>^a+O5sC7_e`-;}7YD1U1n^&f!x z@J&I>G{t-8TptBq;b3F^fJ-GY2wxkN-Y6AA>cQq0pZx1Ic#km7$;r7!e^y@F>lyH| zB`7Q5dFs0w(>*5`|8}#tuN~!jb~u+Ki2=VJMt^Y}Ylj|ph0k>`KSo8hQaj)t!XPIG z!6ID9^4x6WN7>+^ZFp5r-cM7@^-1s{>Rp_Hk~%ckNP#bix?j5dG-*y zKJ|TUKQYwXQ|%b*1lJ#9{k}2NjD?p` z4?zz*&5!)R8`{n5zCFOFxx1Qqw+>qsBz3y|0R~p+gY)2VT0eQiUZCbtKb)l1Y&@;i zzD7RE##EK>-v+~ezI%8c^p-O8%6Honfz2-$Z94`2ql3#thk%AROO0v{{e(Ro;H&s0 z&#x=GnGZgF{p{t1yJ#=U_qG^rtwP^22jlXN@p;8De)ZJt>ZhUWGn+5PAMAsFrhQuV zG%xd2B4tO;5&TrO_8-3ZD~^WKms4)OPX8_&3%DPio1W}OHgsx~L+g797=u|g5SW8| ziSc77?JkB@^ssxWx8=!X$}KSj^Br`VF5*B_H8`XHA?1>4mv%J85uyQ|VGcd69l)*8 zPnT1U#Emo&ScUbEei-q9z*Ds14~#bw#h?sT)Kt4EpT;#Mc(1QQRT{7f4y4Jzx=@nLXBo6k))9u^k(&R= zP1+{qcpUuD0hzKs)`1NU9HR7#gw-+HVO9&ncoEMvgiFc$k+@Txz$Tc;!LMpVD%o1V zZ)hh8_@}z8$9D%k_eGxbfw9uS{=8QJ>;ktGe8yl#?*XibD-OM#;D^ETJ^L!=g)o_`Ce{}3m_4w*DLTS&!6fO8l#Bu5ed=LQP!~kE1aw}%I zH_*jXtZy=Ee6Gp^XQ{3F;8uiQ)j+gg=nN-cqNb11s*0?CH5QZ37U~Zf z50jh2P+zG`^)PU;Jzm}A{ZQuRJ%RQqO)v3q1&i`U+zUbn0R%9}UgiLS$X6*}!F9M9 zSPN5q5U?Ux&IATvF6schi`0?ls&F!KHZXx+d;qLT|C#}taYDHya6hhrhk&U%%uUAK zQacc8^M!Eg3;8Q_nq+*Z9Uaf`bO7Zo%&YV}lJ^e*U*KPYk8QX^-vyRqrh5YG8$p~= z5qTP@=TYYGG0O4GkBo00)Sd&(XTJws(CZ={n3KV-YaivBoS%_(8_T?00{++WF~20Z ze?-anVwnX8Fb*n^cB^%1+$V*18sYv>>t%znDcH^Za{_!S=Z&*Z%zk>vdT;7@;(LN{JJgc zldlbyeK4t;58LnswF`<9e)5{}jy6Pwlb3||ReLa#PJMg0gWx@pRdoS4h~8EOZh)J~ zeECx&``gy+dOre|0z*+iLS;x!3WJ7HuIN&fmJcpwVj1nPqt@%l2_5Ss&w= zNfZJuGZ+Unjs4OEsiV~7&Iv<`)?Igy|OgA*kb7ooQzhGY7x3 zRvDeiE@U{DI!`$-!DbtKQ@t-zUa4lA-=jbMSeR7=eWx@^DRreYC-%ecEXc}Z7}p(X z!XM%z||!;CG@OP8SX3x`r)pE@7z17uWm22uA?`oLes-M-i}V6 zlle02wbN}Q1H8sX#14pUOFNZfN5uaId#RhVIV*%|u6?fjF`n~J4qWE<4$I=b*l!{y z6JW4D_F@e4<@(-PBzm2*iu0iP1K*oo@OTk=NY=sJ?{d&@)=%As_;j3%c^m_a+pu{$l;ZKaV~4f3Wx7@m&>7!}r-=O;1P&gdTeDy@T{# z6e&s(=~WR#5D_UVMFg?Z1*EEUDN2#vk=}a}LK4#Z?|065zjN?z*ZW-W=YBr#^ZapN zBz(wZPT8|NJ3BKwGrJ4BwJ+YSaB18Y#_hl>2JP`l&CaBK?sM+4F52&%mD5h_tAtC> z4cQ@z@OD+v2f=5o&BEg6+1O?MM}$*DI{Q2uTi$q(w$EG6gM2S7#g_Ipbn$gwbVNd@hR*g5=?^VycsBf#IX?5% z-0u*-uV)R-rk+8=g02Tbuh0)eJLPKu{T`?6N#9RmW$PrBGLHIA(nsh=XYCOio$uHtIhxviu@7rxE%xk#j%zq#vkCp*H+QJ) z(3#dY<6PD!e79!s_JCIK(eMKG@^9e%6KU^dQ16^QIldn1XHGFb6hE2XO+RIHgWdyO z1Dw(?b5(7TE8ZtwUoYcTb+mncq$JHH6j@=+7jdD47tTGmrW?oa9 zB@v6`L+WC=&|svk@Qp%}!M9pwZF4|u9;DR4CkGe*x!++Rk&ye^Jz!T7q@EKaK zi)DY-;`eRvVd!o8Ni}4LfgY5Pe^BHn+OrS$Bw@0;l@RAJXRE_pze)Q<&IZBpj5p-! zX*`&sN6K1q>Oc8ErGB*Ox<#i-wB|T?Y4ns0_>cb2wmhB0^DLh^hssIwHf zQCpqo|AY=NlYdwEKzJAa$9nkJ^Izw+Pv&oV@s$Ml`@b7MDueZi9_8nAG=`=wGn?xQ zj9|y7Ke_rL+G;cS8Fo;^9#Lh~1xWy4Kbd!|1o#`Ome{u{PPJl2jeaA+wJG--syPZh z#!CJG>!`d>iE>o|->NPu^n}uxd5e9#9{cb!@TaW&=X>f3ca{T>a>yBajJa7^t?#Cl z+Ez0_YjnDvuQQ?0U(;vk7`Da% zQVEzi8@G(FjC3kG+UzYW88Jg|T8`2Ih}lQr7cf_SfTu7|#n2y)DeVLv#&Ph%z%6Fo z55qxImFo_k7o+7cj+Q#WsyHknf%kFc$a`tz{v)6flnpIMgq@ASwu0YbiXQ}wXZ?UU z&>rJ@s0l2H!R`jimWv_4yKFXC0C)n|Re#`P6sXj*3s0s2U91d%ew2URde6Z+ieljH za1>+#cT*EFwEO=LZ(lO)bI}eDbj}a#3Oz*u{YLwk@coZ(-@khQ<+U$QzPya`6Nj(% z5c#x?Kst^x95O{My>O6B-e~XyR^Y7xrV<`#544j2@IpVF{a@;b2qd5ArzA9p$p8IJ zl+kCLcj>5g?<@Wf!Div+sX>B^w^XR zY5Dk0Z+EN{$Bj$*yyrq+=}+}q`umjA(JU?cQAN1c*x#dh8=Z;vqLlM>(5Hc`iL)H_ zo#?KM`g)qWF?}}WOm!`H;5ZKH6FMVg7wU6-#_aS@hz+%@GrtCQ!8<31MvtWa4L4T% ztXVtaW&0Cn#Qw$>sS#I>9ly$ZeSO-8cBkG64%2LlqjP8Go0+Fj)AMchZRpjjU);#O z`vwlrgPxsPTyNT=_9wM*v^6_kb6kUp)N6>3%7^{JemQTkwpa6(HnvFM?T$Nk;nf=~ zfJx1IJ(2zjspj)rK_+Acd;`jrC{FqBINT2OZN}IfpLq0*3wJcO|%9@x}O&tCgGFk^2FGZZqGv)LU2kjE{g*?b9Ss&T7Mzra$!C zj76{qSO`midYkW=KiGQ#4MGdFlc4P!D`5GGs%rZVd=r7d(%k=ojr%{Oya8%~IL6_z z-o^~z2?8(!xqsiNW3B?$H~)}8Hvd24R8fu^Fq35Ke>0!4SauXJE}K~rNW@NSZL|Pq zK$yRVu5+^Ac{UbxTvJa>noN6dINlE%PXASKc=AJM+b^n}ZKG9=zxPjIEi**U)+IP} z-!y@9&3>$1$d0g)6FGUi#`BP4^H2d zNxf{xY@aFm!~BL7r|$#_h}5iAq@2~4vifDP0rt6u^M-|;>H&IhUm z{WFYGK9Z=yH*Q)hYkjpIgf2DJoc4|9y2%2 z6~iS`lIPM05&aCjkMt-D{0vvQ^m_)}ApMZ3wUBmG_Fi$&SOL!rU@~%C?)N9r134%C zVeyAcJ8Z2~4psfs2(Fh@;ZUA{1BVH;1fR_z8q#f9uaM~&S7DCNI=C=P@)6DdI@wG7Ay`|3%}&2Mx%gX z>a)N5;|+^{Jk(3*lL~c&Zz`}cvV6aoHqWB>bJmVay>Br483#-waFR;<-{h+F=Qm11 z4e~RLn#gy~3$A)onhU(O#Z~YhnmGl00Q@TD-=u!DkJ*~X3KVx^{0hH4ruPZ~%b;!H zTiyz_O1MPerww>B=nLPWpC4K8$$V@-xDAf$BhS;m7C_mNqXh6D+t)yiv--UN=?#Us z|C~~!eYY9^Qol^TAoLi7i(KkonDHR=%-{*+61{U;0=l>jTU+zNGu)Bq=J32P@G-qr z1b9VFvD*0|f6I5SFk3ug-LGo3uK+2AG7gK;n_19d809lQ*+TxF=o`{h?pT2>&D_Tt zB}vq4q)r3BQvnto>|Bv?Go6*QlY!5frwP4^s-6}f72@E4V&FDvE92k^l=J}Opq}V~ zURtDu7bXQp+82TASoqW*I?K4~fCS72-cmEIfY>a`@d1BABF1ut|8dgfMQ?sWBFieS z4^f|+SAf?^FbMz_#v-c&lmkATz$=9DP5~Rhp_PGbXljkya6AHM!KGd;faO#Xi(k|D zQaNciEgQr286;kH2j(Z>J_lHV^-^1anQ)Pezhrt_>er7DsPI7v)>NGUhS5*R1H%7? z+6r8(wNo2`4=86b_Z!2v&<{Q=ZqZ{f{U_t=E)uvDa2TBA0NSYye6Jl)pGiUiWi+uI zw(G5QKS?}o52wgZ9b=JjvckAOGEowiT2muUAEZ~|SOm?SZPYZ7b0niQ5IU(z_BTW? z7$?2eWD9wpyRJyreb{?#2WxPfHv0k8hK7JBe~1?~VWV!Ai{gOX+Z#{G*bKu7zhLpF^{?xB0?=Lj| z%$bK+y%F$fKJ>O!9wb4ppk2Yw=?3k|ud!vHLZ^ar!l&h*$Z$9i-aKp}Y+NN`NjUwJF*4(3)?4fZ zzxZ0GI|?ySAoNBM_4gU{dB%^SM&V3F*S;R~b#~s80>I$Ne34wTDAL;is%WGgsw6$Ls~!ALf2d zV!=;gPeb0p4sRRS(Lady^<>|)ThRFP?Jgyq9eLYzzW!HP##B;HP+1O2#94zoFRI<|@?+8KbEQSTf&$|HKh0--Gwn z%7{IHv^ZgnHwR&J_#9rW{qO$4)!+kFcaan79dnbp0X$Xh6Zy%{B=#slP9SxJ-p{aC zo&ZzuKepugh1f^a!Tq%Nt?$HR-IN7C4|V8gq*enr==^7e{S-eG{;YK32};V&mI4^vB?BjZr!i6;|@!&3bRQ0l4G*fVVK@<^AJK>7#*_nKmJ?wLxU`j|3*+ z_UHwCig|wtxCYlMbj8W#ds;W7~X8twou@C>Fu6HuHv{~GN(g~NmiJ8k;cY~R0n z|K+tWPrkei^79~iArv`f!*z|r4u{DK%TeM&ev6|d5p7W#DEdL<^?&Gx#DAh6Y?dC` zkIQ5;&#%Lke+*cdHTuOd#3vK@iN#)D?l;3H1GwKW)~5~ewD8Qd92{Y}I;W)Luw!HY zuN4j>=Wcg>XF>30ws&mf(TAnYAT)($|I_h@eJte;GlLB#63+MCi=5?f+)ek}BaT;3 zR&I3<-^+Y2Js`6;4!Dopmt@75t1)MazDjvD2~CdzHqqt~s?q$a1}FJfMIX=C3^`&T z%o9F1v?tf=_}%c^N?^5Dx}F)tce=Q99H}@Q-ws(AjK0s`H2iP@=vTd6jj0uGuw(FV zz4{JXOFZI6_{Sx;uo5jk`kni~vt#lH=X)PSKL-^FXcmY*%$FO~CFCe^pgx(U()QDi zmF7K@axbpEeEj-k?yt!g8$6G=&rb~t)aSX1(@PgDJ%jrHX&ddFMZGx?$c`P6Rl|EN z{R-cC7&rG~BJ>IL^p`+ep}zhv4rJBO227aBRhqriAz0AZr8z!PRY z+m|?8S9u$FapY&*%pDosgmOMH`eyt{qDebfZV~J#S2stcd@FdqxVhSh`BBvK9X^Zoq;s+Du^)NvfncXmJSe&aBX_t3jt)5GcSve)j$Y$p&=`pJi} z?RoEv(Ll}&w#9OqFZT&nbtjaf*;?2pC{S}2`)5?fkSIn|A2eHz9oAejq$dbTmglc?vt}QN4>cgbkV+AZ*F(nuKt%y2|B$2J@n~TF$ z)ghCg@v4nix71PaKeV2*69F0iV;OU@g1DtP%8@PYWkOGQsJj&q4B{)4r@GQ=v~`na5Fr}|O??F7|M z;slVbxGPvIxpG(^2A%mSW(6({uKRMI0|=~u^Z+ee0_3VU0rZ7DrBpd9v0xY>$&=tU zsl^zem(Uj9rOh3*pMc}As9W%OSAOS$e~AMd`9`3Bm|TbJKamyFI6JCZb~IsNB%<_3 zO{<)*ag4tLu4|Em>?8r9l;xlvjN~24dmrOypUd@cRiq-|qt>V`+5+(QY>{6Re5u+f zM^0$zDp?%?>V!#4bA22s28bAIiY8F0=Kn*9ksra@PV_w ztF`i?x@s$dJOT%A3LmkuxhyNO0Eo3rBr@Em1uKpU&Fm z)4*W0(+Z>}qgNjB{7rf?8`zTYb8FyKt-mSXHK0@!Fp&FMz^lwkngPFJ#&Z>TnR-V8 zpFy2q;3KqC0!iuS41s+u^& zw4v&ul5zAQF0miM-yvN5A^3GDz7D((fn3HrE2dk?_+SUNs*>oTZnEM9IZwJx2bIqC zx79i67u7&rwEFD|v$8VaYpK^o?$1=^#W6>q_J|d5k7ozAKH&eI_Q|+CkG2;+ZAuDv zOUj)Hb)-JC=(&+V5A()o;2r`xk=$QSAY17A3UVR@_=GeMU*KK1F9z6xmdJQJ0Ef;8 zRzWJr`&qo{0JdeMiJYlS4-Ny~;Hx6nLMe9+uqQK0_#N3)0*Q`cfz?qRc#nY83ZPt( z`o4mk844_c-nmS@hNwkWU^`Be9uc~)BH4l?O-M*#e5sCFE5@|OR-?_WdVW9)`*Yt( zJClHCSOG8nH3KPe1pF1aRoa)sEKv9+1vw*fvMtYx{M^dyS>)Va5`mC&aC=n=C^0&I zw7$O#*XcE`5B|IV$|JoBbN?VEQ!a0n$Ff2WAh$aMFOdEd1WbWLg}yCtiADj7FjBez zpL2A;Ufy#c$3;#IL;Bp{`e4FmpMZDbu#axQ971e1-~z1xK|_%j-9;}TUH4h?{u91} zJl8I$^J35Oy+CW+bfo8z1L*oM)K^?@h@AWs*aFTF{!T<1OZy{v-wq6f9x@KEt3#5Y z#kE4_`#>LaJ~D>!tLmBKfz!0rG7gOM*nRuJubaosONi6&&AsLWpd5q%G(OZ{HwN*2 z5(@aH=zTw<2X9RL8r{HN+7=iaZxN z6MdhQGb?Ks^*QwXe)1H`sh00TXcP4J54P3zMVL|T?s%?zj9cZK)W*+gPl@J5jzn_Z zce3kafB4tzYCe-AAI#}SiUjC2(s^vl1)4_{^;St*0(sU5(*F~?-$Xl0Ce>Ab5C}|w^i3ys8DtUbg*X} z&cr@Ezw*Mn*IKYaNY}DwOKyd}Y0mBT{k)$U|5YOV>Qlt$Yd7P}8S47k(FQo#S?oD)h6k5+RiLG&E*;+$s9+v3H0Oj-9?Xe@;F(-(8*2 zGjk{9ruh8o@?ytt2(B7vgD&&*(faqi=ZFoBhcBEX?1k*;0Y@iQ*^V4DN|?*U-^W2p zl{^zr`?-3U`5JV{F7R4tC%)tKUiQ3;nEE{Xe(q}Cn`Dkr$Td%h=ZGBfpgC>*Z7ZPb zY5#1$D)h$_!WY8#quFOby^Ddz}>YkGhe*xRT*{L|U?y*dZIY#f>ezp8!OmyB&H zjud?r_?s4K$=|TQ82g(NCoq6tOk*YlR ztyYD@tE7Hc)M{Wc*yJpbp* z;or^k59av?#jeVWul(2e(Q~Zze#qNwbqI@HQz>erDh-_~;}LovygJr4^CA|0oVVsl zh0rJsz;o3Ed`KGW_w!@X%6zaBGj6eu|CGc}(wc{>d+4)HXbrJ*gW2)#0r%H&{|tCB zELHR+`{bRn5?p7gmFh3>blwzwyNpD!?{}7VR&YI13$6~1+V|vnR5H8N<^8;r6u?x> z`+1ou(5gzSm9NZoRK$$_-uR<_8fV3xH_LUYZfwORTNJ<97>)wKkk&`(BlRH|*oTct z)>i%@#toxB_;C^eFa(Xetn}XuJZ7Zp1Aqo={f`2F(%&*30RxN-1H)A>XsnYh)bvqC zDFcJn_|e#C>;T?04jCBelolfi0Qxk2o&GiWuX+b@*fTv?Yi$*nhWY73k7Zk~4cSsL z%`!kMGqtb8^-SC?V$dGM70?8D6W6;qNK!D+`98+DujOEzgE(Zuf)lEQIP@^CFItYc zftb++!OLOZ_Xlpr=YY~CX^Mx(P(GKk}E;43(2=pbu zp;@cOPjl-@4@PB&y-o*7Yf_D6? zw(noP|MJ?GCtv=R<>xKJD8rFYQMg;8fi-ZWVsI142(uhHIjrA`1kXm=#{vhSAHn=c7vR#ri{ZTnjlg>ZPy6DW*PON8^&RLLpKSlBKFe@y)yS#q!Qq+X z&DxXQp6Aopx(mmR{k&s=I0A9{&JxG9UclSId!7lv*Y>WqpMihb94d=)O=G#)0|#UO zz@PniUpsELl${E)wr6b1e1-DmmLs1DrE(%$;}oJAMJ*=a}PQqT(v&-^=$a z-pjFlDh~dlm5bXWJ_n!4%H~|ksg!j#J3-2|cd`|tUhi;}0FLs|(P4`U^kxCzk?!+4 zg%N+N++<^imBb~FhkO?Cl!YlfAD_RC-aB9XPOI|~#0zuF->Z>Lds}+$%2xSLYQ`0Q z!bHb0hQx-esEHO)i=Q0eeZTN>!HqPn%Wo}~w%vlGHz=Y&(RH-<4O^mpF5jQ6UG>4f zc=kMf=i@ZgeB#rq=ZoN&-Dl6tkNr@vn15*Trj&cw`A{Zc<}T&yVZ!9;VSJVGoa#K>fvKst1La(C#-<5!ix+ z1KeZ!I@aQl{lc7{1Ak;x%P#(8JLL^h>ud{fxGwP7QiSh0%Gtm4r=8|QElj6eoS=nd z=LPdebC|Wlzogm8n85eSnQxjSxb8Ko$cjSqv{{ZrD{vIT9rT;g-HdkBhh2g_YtnGc zo?mgb<|e)udp*AD734*Mfc&S*!uLgY7ns&yKJS})uoH)5)@NBg(uUI9^`5@jd`I7< z5BJOj`s;U$YLwf`nC^I+e!ZWV@8N62<4atz?W6wp?2j@YFixKY-RzMvo;YOO%~3H` zFzdcc-kkbk0$f!BPq-z!Fg4~wq}@CMii1|{H$U8}vw?|FTI{USScPSQ4;jx1z{)so z%LB`BxIs-|4=hRAuO_ioJp7AImW)j)D_c8YnTexwkd@Hch|x~JGMO)C1v-nUFDyKb zk@F7Mr)uw;^C+hZi9A1pk7twT7~pnXsFQ)En6P&Mmc=?`{16s*6VlVvV?ymqq1$zB z!Hbppgzcm~UaVI5ngCjem56iueyWd0{Ub9mZHUE(%|-u0?Nts$SZ1V z$7xOG4{z4s`44cY(?1+V&`)t+ZD)-Fm%X1 zSnz$&QRV}05XhJQyTy0M04r?ksD)qdn&LXGYg?d z1fhSSSCDFL?F`VAG>Ihemb}lnrQI{Ffaz8EgmKN+9+?w?2JQTqdJm&F>390Mp|t}3 zCpM3VZmIw)#ODAr%p5BrsMSVN86=fH=3$YSIl zCF}yuC4pfIFw^v?Ux5ER?L(eGrGv8NCv(dIlF$X8^duA`f%sk$m?apKYdP%vu^gly zo6rl?Q;k9XPX?a|_g4Z2FbYcogP29u1GZrnnFXvxfT%VwmmZgXKF{+Vfze2?_kadH zX#hu(=rRbnoCI{n3H?})Ahd>TN!Dz@0Z5T>U^PlWPQyoOmSZs=`boxN49|-klmqm< z(CHUSrCe17xwRVn5MlIJ!8fuZN#w>DHO`U)q5KX6*WpAzU~@+Db)Nf}Jp_>d1hAi2 z_EeGiGdB%>oMjp&#F@qu>Wd{b7z_ z5P9^W^am@?zp}>LEL!+F_wQg~_5nU7O+olGKa|T4oXnmXj92r0zTSlQ25WA3Q~Vb% zj%L}0rk8eK&|=jLuHQx4idF^00s ziI#gkyI}54^!L83@3NcY{>Vu0lL7xfz7jPlb}iRy=GZ-zNuXR@s7!w7;=bwr)>()0 zYQwGchj+C8Tvnjx49bo396|g|bKi3_{&PZeM|!EpgW-=hM!|2X-=?)pK@K!}TsxtD z2=-a3|Jp#>YqT`p@%BS()haNp5cTmH<(}-z_%3`id`uYMPraS`OUhpIo2LBr^^JVg z|CDWj^JD7MAi^6l4mI)qvq{m=;Y7J}-=Ds~u=y=ui>n3YWrclLEKS<4B5iL;w-`%3 zjA!jFElQ5?Q8%@7j;6rnT6=Yj>-pI6svzweU>x@%w{2@xZ&z#DyUcyny$AcnRdA0S z37~q~ay%92wziM*Kl~N?wb%bL=s&*$KJWX*V>Z+;bF#!P?8GZzZm;<``sCLCPM!{QQc!BQJ*Di zy;<2r4AcvX55SJN>$n^9Xd!e@@qVGBzmdsK(F5i+W4Ww^clL2bIgi8Men|sU7{8tX z&+A^sx1+Z6l9PUjJ0JV)^OJn{>jK;I!MUcee4pPJghI$k!t8OUIV- zDeC3ELqBYECgG!H#@y^x&^3^~qq;%2o1Qbdjj2~ZcXjs#`n{>oX&>aWQ5vmPi}t+F z{-7)2qw^WdvfwY{m>Fn7cbC)WlKUdxJDKbD3<3`Frg{5PZb>%7pUU^`woPK^)8S>T z1wjGkAJ%vuX||UHT6>P;v12Rc@6%qF9R|!Y#y|~vVtq9<=|}8@fg*=VEc?_-6#5hi zN&S&OB`td-#Ih1Jl6wbRiAlSWL}R$WRSgxp4Jk63JxU}2si-9f^CR`?ckK{%8hp>u z0;R2ltjg>$b&&6@W0U4JTtCC;y$KA&itz&`Qxfu)fO?Yl5I78JIRSW=goLkv$<)UQ zoTpk?_D~6G(S!1qA=#I4-AOBG59vWy)lu-?+NboP)d6yU5!a%x;GOuc%tqe7_*xM&vVzZ(FVT&qvJEk)!zVMvFdS zwzEV<0HaiS^)`GH!EC4u_;vQ~y9C~Y9Ru$J6*E=jG>HV~RU_c9tnueNq=RM3{6?MO z$N~BO`)Gj;T%XQfg6I`?syd;5*tcKHF@7}fT)G*3GJ0|JOo_1N{k)76u)mCr&TI33 zP@wGxZPVITa(2MI{77HamW^1-L-6T~ z^?3#p24kS{u2BM5n9S{;xZXjZV=y7q>zKv#nbg;00zr99ihEjydA2qK5hxDcT+1|9 zIC#r}Kf-|(2;4|szD&?pvk@2uKkmL6gxtl@zh*h?b9qh--6FW$z69>KQUFNx*2du? z7RO2#%VF$?DVqRZhAAWy6b$Km{1XT86U#t|rG)xPm5QfK4X_fq_TzvT1N=D-_*~#W>W6aX+m_tFtXfzr z-+T#~{(-^$7i)dz@_eXv+e9DPFWE=iXQLkK`|ft5FTA5Y#l5eRU(h4No&6Ap)*Raf z2|U^-J3h5D5zt@NYa6rBi%Y$R-ksTbjjnK~JdUfZQ&|;r6>9p&=mJj$QolHRe$G|a z9;HVucrp%hPb%gujKi<>$)<5l!3*pOE)a$T+g-v|$Ok$QmL1@rf38%0eCZr|K4C%n zl&DnvO_@HKg|Syc$NTIm)E@fj_BArWqP0F*Gw97RWw!2jLL)wlku1CQdhc zJ_$ZTy_~D<8{9Z@>ozL;OP9}CIFlGqGAtkU8?Bal8uETAGsdXS_dVt;?@H+ZIeMGt z2@_nDLdA-as_nXp>v9?d$I3E1EyEBTEgdVGG9egJ8{4#UAIOuWQZLxL`2r-qmn(N02 zuu*T$0`6>vazBC<*{8vGv%;_g@QG%#m7@PwnpV5xYzZZ9LZ=hKf35!|j{XUDZcvqp zpROojG-KQZ-w!xbt`PM~$PLvS3x9dqXZ=8XE_y3jJ3p1zH`?xUeZ9B1rw!MK=$m6p`^ zggHx1f}RgZP*@JWUz@CAfq{gBp7GoeS|)Z`5$r$cNx-m{)eo2GIp~J-@)c{sJf6U5 zBIWEL>^P6>1{wE!AL(<|8uvq4f6RAq^RBZ3_X${n)PwcywPnSo3SgqW7rY7OKLEc; zCVnLN5bRO-0o&8Zatv1@;jIDlS+!RBKDOCU?u#uM-*ASrwog_YVL*p~GO^cTVK-c}&6rkZFu zoX1ncgS^)U_rwZd94iE-@?350cSZf7Rv0yu0DmRsr(11yUBq2{lOagW3KQ&ZaajH3fS2HFXiF>inxpIHeNrJ+3KK#SQ{`6XXC%vDnY z>5OYOHe6x_!hR!tAQ^ls(y=qomm);>4fq&J9s*vHz@@;+*zH1}s+_3^J^6beZ6|U> zKc@kE^1jfg2I2oVf%oaXU|@bWdv6F#)YjrkkpNwHLUNiqO28R;17H0h^fzbG^7`B_ zX700An4d?w9|FIs1)1;}G@Ylu17`BwZ-HTyp86?!wGm zu7Q^%<$?a7P61XwU9;Bs%S!#OYC6}4@Rzi+Flj{4N6VyU(!L_hE*Rgct$ITOUD_2M9Q8(GkMz$nh&72DUV5MhM@Tt&O#$8?ZK#=n96X+LugrWZ~fUkiM zn}O$JbL82;R^}n~2iF%9PJRt|jxY)1P0b=a*N^*mNFms4z2A<$)>JHRtSR83+EUg` z3xDQU=quc0g|rsH8|DJ`T#zGnA_$rAJp!*%k5g)<34IBYUbn`VGME3Is4&E&zu6vo zx_pn;P5Iw$--~{}C4pA;BdHwHzCv)PKd=W0Ng`j$u;o5toS92P%^(Q^$b$4lx^1>ixRf6VjyppNw46C}FG7as!A`+@1~ z`yg_>Jfo5Jk%(@S}9VewGn}97d0BRtngh*);SesFY%D<-O9}FMNX>?rSar%6PiL zcNb&Nl>j$L4=4(pgFF&`Y=g@bJBzUxWzDmz(atB}lh`kz8n7I*{D-{%gL)MUP>y~n zOU)#qqqEgN#aWXj^zNp9B2|YSP1I$JPPw$R1Gt_3xC`vV_!$8VVs8@ZhYMOD8vjN6 z=r^_y|H+c)9kdpf!*>gu@E+Iik^nWD=W_9K`~vu*%Pt~XV2yNRmLfk zb~rC{{3+tQw<-hw=rkn?R$705eA zKfta7nolnu&HV_wV@T?nv}o9~r?;w({4lc3w`C{Dn}u(@diM)p)SX#(DKEHh$j^bF zBaVUsKlVqCxYAs~?(T@2w?n@SXIvz_p8VHy%BgOz_@FFwEqk(*qwF@~q_OA2F0SS~ zp47JKi($uMr9O{rg4?B~Z-yH>Y<~Ldscpyy$8x`WnS)6f{i0}gFpQbq(o}aK+J>kv57>Btla_V{JLGLZu z$#Qf+>ep$%rqli>WnxZ0MNa!S_3!OZe?(R*`fE|jFVVT!t%8MV|7!_5;u~WBnCA~) zJBu1FRQ=D&ouSidS2@Q(7MOQ_x<5XQ{(X%N?8AWv(F3)qzwUWgM?d&}=9}h*PT5h} zTFxJ|Z6MlC939$1cJA~p@eDQ=&_6R&8%dPSs+LtXyDatJV4Em`apw;AF{cANaD!1# zj;zv@G1-LgazD*3mGcVa9WwmP8C;*N`OD5!>Z&c((sShqebb*t3nSY=-lk)P`oZIA4$DCarWK`PG=;Rvm@z$4h-J$Qa99=WRApJIM1 zdJFkFi9K61tOZHIAyEsvNyhU4EG^oHbyCc-2TPj$CBc#e*@0sBF>aqDF-3k1vgB(4 zq#txaOI=11ik-Ji0_5g)?4-@$i;yd?a(@ta<^41u{ZAP^tpu)Mq-894Rnn70E@UFl zp&Mg82+1ZpisopMz#g=VdXa2c)6&;Jp`qa~!ag%B{WpogeR=MWH0Ow(Q)|^ANoYn_ znyMdA=C>Z;KhAH{mRJEhrlj;#RV)y?L__+MI*z%T(jl*KcPdOe>cxR zkKV|OKOk+fOOP-B>hYBq@-rR1EPDC|31irSDx1Vm^bqs%t&(u1yrkG~2d-1KExFkj z%VP<6ES9s#a2Y5jHP@OpZ19tT@6?86tbVl@d(VMKLEIgd|^RiLE{<1YV zuf6}30&TQ5k!>!!ry1*v-;GHv-ZEF2>&?q89T?)r^*$KR8;nZEYT!L%ixr@6t$$!3 zc#Q3=%*TM&Kh%dyVt_GKAFg)=-%A3(Z{TB%JNnPypX#%WAHX~6y~Tm9?=$A=7}RWhIQ1y0mC$y7#gcM>uWZ3E{$0$4zeAX)0Bg74Yma5 zQ``kv;8BRg?|}oreSm?ykp^6atC4bX&_!9eiEE@1cyCPe3}9!b?7e|sBTy08DC)!3 z{cWhL7T}_`#BeW#p&tbNlc_lqF&t!L#KD9EZ>Z%!zDX^_LGy?e9AdcdA}_WOa2{?^ z41B(Q%yJkUCLikrxNO;qpvT$J-*Uth#iTzEz75k^CLnq3%j>^9{>&SHdGh7|w)`w% z8P4Z%Ys4dWW6&x%k~sVIG7?!F9DQYFsQEPxH*qkXP%ZvJKm29shY|#c{CRFOQoTLT z%_2Ne9(;nf&k9sbV#OAY6BR*#ax~?BfR>2DF*Z53gZC8pB2JJWK_In(-!vZ_QRuId zsyO%ScxKC1?gU0^ivChH?b~hW3FELgLNA5`wy}Q^U-WcvNcgi5u4jj5{GQbfhhK-R zbJ^%M<9B_%@sxU$Fd zztxWW6_1o`SF$JS?~Koeu#ME$qt$WZSTnxW=9qsG2p;n6tLwFi3q{5qyxN@S^M!Y= zM|}czI-lh;upiR2sToXo*cQKY&J*a_$@#foH5`m1?Pv0RPe8dxO8G}Tf27dOpSMEK zpo#%~!)M}X-CgQJ`(ZrS-;wI*1-;%fOXL>e`$yTFm*LbBg7bN_G^Kha5?9CXn?j%aYTJC%Bgo z7qB(_Q3IICT7a*4t|Ot;523?a^Ly5qvb8IrA(2-hDuE!Ze6K2&t=N~VtN>9Z+`7fV zuM=1jM{PMmb^C!&%+IaB(j5XzLEsnR#!kRGSlI@!2(_&W%&%q;`sYkr+!77J*FZh^ zl%Q!pmBRIpSmVDOxLd2D9s#{1P!!_+N32Bk0iL8qZGj^QyGi-aXqEI!5!}S|uj<5I z`a_#SLc+J;d)PN)1nrw?PF8P#&tuJT9QT7rQGoAAqV*xgLc^L~XxY={33DUQp!OeD zpwmmBd=dClwxB-;Yzo~Ifftw;bOhF5#Twt`1oC!PK-I0}G<_ZqWL9>EH0ii<*wF&0>>#RF|3%E*KL+XYk7^hf)>kEwjf#AWoyyUyhp;~3& zIo8XM1QsQ1a2~jVFu?o3TtdeEfL?lSIPfMjfM$GWt(s>AN+o^ZJ+7Z+hHz1yBhZT6 z)MCwr=2@O=#Pi4-q|a(~61=LmiI9e-eWGevEAdR~3%%HSf3wUtI3i)2B#f9F&5i0X z_&^Rv_=x8}XH)*&;De}*4*o7X(RhIk*%@Om*B{awt-#Nq&u#+);M38-Cak}&0KB6b zTY=wdtYxA7<~-9%NMZkguh6IreE4@L*DsJVjT}cB`djm;K*An&dEfj~dk73*hlP?n zcbSm=Z#=h)omZl{z6U--E)jkXlK?n6yC^ghetOM>|2QIG6QLDZp?(k@Dgoqu64)Vm z#FQ=d(bB;R`K3}-yfvR2#$WI+LEzdF!1@2tz9s4ka@PF2Sc9m$YFrP;6l;=C26Yv-PQdW~g z8)S!$MC-_bU=lvyV`iPBqe5 zO7s9@V5TGhFox#HIE4ShtOT8#lye09FjOx``4v>K<){pyH=JC*ZyvP*>(4pORMDQ_ z&B5kso@+puU&c`vIQk4Ql7uGVn}=La271(Rjswsb+dCMclEB~0>c8&@j8_9sMGJ-k zOR^=ujOTt(I~({K2{8A84b=<&yszC-)DONsA^b||V`WDY^mAoPJ~YRL+MnwU&v`hW0sLb1oI0vPhZXbf#!7upL3kk8p{stWLy!yih05)ZWiSE z`2-k|o1Rmw!Jdcs809UiLwCK7_oO5QngjJ-#wyr%bo$y%`XRP@)GrA?qM!fF8ku{P ze642121hUsGaqJL$)Z0vYM`F+fNuP;#Gt}Au^0WaM&&S$Qoc$)m_~c`Q)Veij4J3T z_*p*q>FLGT4hazq)3b3)6Y0mISBk`i_oCk8wGZ_-utR^dRkiP7SRc=SuFww1HqpJ^ zwVrtC@WO%NeaZLP9Qs)h{di&c)xgV%v?q%rUXV9wUDCT`@!c#lbFKF(;`R4Sr>-Mc zQxdY8CnIOe)F^SI*mLaNmHq`nJM&&Ot&qDS?H!;+dyxa4S>6vbFVUVAwwktk*mckC zXB{!r=T)`J!FL^l?ekrX7oSYmXje3OEB!;R7ivTzz_O}Y^^k9|yHZO(#4d_n7B@Wl zQ@Z*0#FHsA5a*9`l5%o*zIwq9A<)y|vLCQDWO~u;>9Ba@)2;dUKEDs26&g`!ZrCH- zOJj?yEJ!)U-e=Dw`a5PueCzlmP1_x~d&ZFqm@|#TpM_S1E?eC*Tw9=Loo5rCe~aBc z*KyMRJ?-%I?9DAi|E$e;GZXpk*FB)UUs3G9AwF-pDaUWM-zp!*iMP79ypF!O+WG3X zyU43(Z%pGj=u|eaOh9qQLFD}s`wG|N`^BDah@FbMeJ{9c0DKg?A};CqW9anAh>#sp z99L{%cKXe;)pNlMADR6~g>8zMuxHwGw$ZPFB*wJ{HfCl0b?(a$k*`SkYXB=dMXOVb~Trd~ucMTWMUq$m!uBTaUvssk4EjsErt(<<`yi|%_zT^+UdE68ZTnf9PJ1pI*CbJ! z^u4bv{guYoBdN!G>{#?A*M}lWk=sbyHJ1FS1qaD^I*S(X2mFjgD!v0v$|?E*DNo8A zur&4-{K%LaPY7S^pBPJSXP}K3H^|o^Vn3jVKfzL=LrExvp4ck2#$cz&_=>}hlXmYyAHk1#{?9!BZk~UhagY~(csb%LZM4Id|KShpy)MYbqs+(F z08`NlVyA}M-a|u>D62JP0UpXmqo_*2+MW2x-e?aDm>P=FDrvZ;K!{5*K zMWn#-9)3uNHDCNxt8C4`#xpA|!Sz|{mO`(wgTO;(crtGrWy$9~q{fZ1t`E|_Kha8s zur#UEDJ|-8^u_1}o>e@V_wy1`!2S}pIj_n8i2`lQv^mfw&3A`6$xJZMw;5qPHHVtR z8u=QJ^>3WIqII|IxD1{zgmerkC5hrrRt8g>Iil=b_ibwBXY z`Wr?7_+}$O_Xq0c1bqx}qQ21#=l&~tUHuneCw+h<5*VjA1b~UAv7Ss`3~3|3@wwpx zEN&Dte&Rzm1leR@Bmw+5V0&ByTY>&e+baVlus#-;9}~I&Fqx437N8#)ugSo&nCXHa zq@-uSINS+^fwvKnQhqe1EfXic5g~>!Ms!DO0`7~8Net>h%y%a+m2y7@`s1#u4*Z?3 zV|Z(a)NeS$m`LDSYzmAepmZAej2bY}v9e*M-j{h^u6JaeMl)a))Wvq7Wcm&S7R7-m zhQ74#S72WI^7=22Kl8?4o_rCR{eND5`r`UcqTCSRcfg;~7C5Xd{ZIsW>xF(`VH7}{T`wU%CL`oi%;;6KY+0hpi7lsj2^t*i?A)Vp^shle7$Za1pbqQYWkyRe75?& z>ywIF8tr}EJCy6!1E>22@}26A!8Y`~^0#-F3}u(cmFS{g&$m9mnh0I$1lO8gC5(m3 zG0hiuq~7j#9k2S|giI|Sj=hWiJ$d3<`(x;1_6P75c#ZeslDnp$$KxI+k4-{f=O%c@ zdl#cdE2P#*1g~DTLRNWy{QBNPdyD*xgK~!2l7&6tT4|r=yOR2+n4kFECJ@{#;fL!r z@zX!JU-!gB0{=4$9ItU4b-T-5UlJem-?aznOsvr5D~)$~zl_}{I6v)R-J+`mi3$1Q z3;)GL>RLq9qZ83Mw4LEo%YT6*_j$>T&cE|~PkT$}1L)RX?aq<+hG<>&T*}*r>#hs< z2kNOCX!`(gK##x9R@h1X*gCe7IPjUYJ(WNfdiq^!1?MuXhfJPNP@8PCxh^4#vf$GR zH2wy*%XG_1cftwlu^lwCm{~$rY#Q5)?~Ddq|C*CQdEZ>iS@unUg-mQO?l&?j*y;ic zn~fy!meViy!n21sTx;Z1et4Mvoa~$&LBIGMaxIORK|K%S&JN^#kMXL$mU7RTAL+ro zpKG=jelw<%fFR@PFdJ*P0RKoUVv57AnVGE0^WFyYt{poib9UCT6daSMPh4*I^IrPl z`zPDa^rqiFD*H`~Lzq>yB3CuxI)R0&>}jB-RZriLJe>ZRWZdy^-E68y$`+)?Ui}0q zG@5SeS$bQZo0|DoJmc$rdUU0Wymx!XgDJNID1Yy@pX0{DPyH|7j5&%KH#DhJ3KM8{ zni_919#nAxk$(cWvJ$HTaFv>EO_-~*4wHIge;vYAD}hNDL2NP$=)HqHcTZil0=t{E zj@HVtC4^cgaeWFaFbDHoN!GFE17;EU9}gUloAzyBdmNeb5x&t1GWhe6CE$qfvU?T3-2;h&LP0qL`u=Fl@tY~pv7`}Ns0Tll#>Qsfox*JFieHSl$Wgld7GVa*!-sEsA9=sfszm0<}Z-NG^RekX4sy+AEI$b+tt^jYrR=LtIgVbLVNaAZJEC+lGs6LMC1?k~zpbc01 zC<`xc1?Zfl)yVjy|EzX3BmgINb`GV{ud2KHNx^r>;*L;M0<`;-jNi8j&4+^TXG`?o zt@7GS|KP-aB!O~hpJdGsZcz(6co<>U*1%M(`vbrLdiO4HF7|vqU@$IsA7BYhS^eD* z`@SZ)A1-(JoxdycgdBm;!U}wzfvdmc`>R5 zRlRLqQ8$2xnQbth2s{rIN08b`U^W%Jr&dRH5W#KqyCimLo!FZJdeOFtRvg6 z(OJJd-?|@!<|Lt-dC^b2-;w!MLGZ#zz4qYmF)oD9(q!6QaDTPdhc_fqrKS})S_Ajf ze~SKtVv>+k+?ubZ(j%q7`w#{%E#)&F5A)n?^po)68UpT&1O88kMzZ3+r8p32Z3RO9 z!lyPX&~y?>w4V3Q!Y>hAUq$OW0~J~dK4i?jIPwD(YW2%sYLg@ik;HP{(uaqT5N*H> z6@wF0v%_7&hdQkhzDzn6JXIe=f0Feh?I>QM_=ftm~b8_|o0f&bQj$$z&` zF>5|cInzjF0Fsa&B8gY;MFD>#4FJ7^7J89*^^2OR7`HfQ z8$&6LG=ut<9EU$G|3za)Cwd*Lu%PHE+CLUr3!lGl1tNRm-t}_dtIAmPPf%wqzJ1IH z-NE(wXt4%BowNaw1aAl8-#`vovhDR?tjC#A@+yi>gC#rxV@Y+M~;ljt(+U`MV|*R489il1Nk~j z6WS!hKX)tLYZ<+m;gRA6B@MfmyAwYZe~@JZ^MS?(0*kkm1ufIOER zlQKAs>-o2b?G8eJvR~#d<2}rP1xb(78Y6xVJ$)Qk8#`_sdw|i->ZL2Jt3bU=yNj17 zhTghf=T^(xt6=-c(+e*j$L!n|HYVt6=+nAz!-CM`d8@>*~=#H{<0&-G_~$=5v3EdxOs<%FA%wcb28!Hs<`2(}nLn zGU83-r>l``yyHvgR5!3kz_;*215ZhB9CROP(`Dg{;V|YCa@XuDY^`kdVCNggbz?Wz ztDDUwfyN%GhTCRPZUxUN4|Gu7ZRJHzn|F+blIX?n+N6rf&KF~4y!-SG_`vrr@7e99 zoqFp7yjQ%GW9~5`Wrr8-mYJVDSv2!cW2wxawEjo~=tE*wHDr<4H%qLe0A3;79}nJF z%^`JABTTXd%`5k-5&D)RYrK|TD1ihQdFZmNI>w=@Ov;1U18dD0K;$Q>e(dyxv}K%Y zqTdSwOK2^SF#q8n9Hu@KeUFs-+BB%&EwzK0sD^GhX(c*UL7wj9`5Y`rv3tt#{S;th z5~!ryU`BH_;3e#{8Nel^=D{DdZJ}jn6hzv7M0sMr)ZuzFBsb$$8%07MbWwe@`mzH8 zad9nv8_I49xR? z=J|Ik!v}f(`Mmf8^8F=2VS*DO%Dt4adTkXU?>>!r01#co%{)O^MK3;)Fj+6;MKt+mwv zPsA?7u22P;JtC(`AaE%33Tf+8RdevO%1*W~|1fi^$4{k6>`zjw^dIE3HqG#|tKwvk@@Eu)YTWgxhW7`>Ch1i{GQG_|T2 z)`VYAh-4k@UXGhNapz|dKV)X=r$JU z)w!N%gc*;3r;X#rW8f3xYXgS@I}a%9xw`sFV;|SA7*4~F3h%;{b^)D#I|i2Eph^O- zj;nwP0B)#wvQ3$hV#Wpn4|826$`wfn@BkgSHKqWY;D}@*!MdCS;+P@R*h5qvWyE$zVllw~h6Wvdxx85Km6gl|WQiamWxbylJf%TWZ=RpkjV$2AJLOAGv&X zx_?Kl9d^xiq9=0JX3o#9jwA0|&wB4>-m?+dK@XZs2$R4Ewg$FXn?HV^Dfx@%`w@Jg zql0Z2@r>(c*$+96l%d zji_cM5-hFuW7VO|Tii7xY91vZyxdbZDT8@RUEd>eR=x4mXl=K#qjTG*T`yE(Vdw|Z zjjrR^tC&^jdQBW!p>zFq7D&Nf_}HwRDUPPv>X}-Pzp9-xAfzqr8)l~2O7ounZ1Wi> zeyD0G^&g!e&N034x;n>*=Y3JETeD%%ufNSJ0c0~kIjo%+9%-K&pHbdv&EGzm`*l=7 zC+%17GBH~XK7$Y#j%v@|!Y46mB(OFK31WAAP5_dA zVP)ZZ1>do5rmXOfQP$jU**W7l5{c*7Vt<#Mt!kWyJIeF?KrNpg2ejcb4j4@EvNz^T zh#iQ7_p_Xt@$j4PH217R&*{g`t`FpFT-8PUP*yh^(MEW%MPco8a;w)Q__5psx5GVx<;eSn@`-Zib*n_M*{?kkc zuc!JexgU-j7kXvX!R=UkZl0URM$@l=m!T)&Ppv#V4fF!mks_tA2qhqAV5w@V4gsi*z-+Z%B>^{Lv5LKCMW9rr&q<+Ivr)VTPP%$T{bXL`jk)h@c<>B8W&5!GI(|k)#9xm7t=M z5hUlFbB38AGfaR9OxR(q`>h6^^StN2@8_O#@A>1NbbmEf*l1?k2z?QwMj3fg;Kv7n0}?*7jTqk>U1xy7Z+D06?~^U;rKlR z&c+dUo!`CGQ2uAiBG(6FO);{Q*77 zI3w=^`cwicmgoSGZg56>(hew%93=c_zFt?fX1l-e-7Xe}NA<17sXn14qaMFonRBAHl`AB9p;M^or2?0IL|OZ%$ki$Q5`f z9p}8EzGA&3>nGW#FLcijzysn`H~G5&BQ|DYYyD|nA_ zixZ`x-oY;Wru|5_BDAC?n1eT#gQ1K9r*g37e&S z%&vtc;17fB1s%}Sj2T4e@(t$LB48kLH4;1nRa%3uFd7&qP~i*VKYDkH)I)t@9}~HN z*|?N;EY>i}>+#2lF`8b3v0Uuo_sP_=CAj5z|EW=G1UzG?+s0tVp1l76+H=wf^YBP8 z2jlk<^#6*D8}IYkHoo3`3CFATwe=;xA0?y?dFeXnn&MDd{$5&N=dX@Lny;CjtCfpTXM|O;d#pMVN>5h3MEa_3oNy=&CEbc50 zo8HVB5qc1P;rFzpv|$W~sSnR26of2|1MUYQzunDz7u{TS&88HTN!v+;6eByoNTkhq1$-Oi4B=Wn_ z*SyH(lZM|8N9Dj7(DiroSI0WU#3SG2v^KP>pJS}+7W!<1a=Gf{#P3-!rD7c8`qAvf z0&&mCch=L69yc6*<{!>)$zRIZ*6{=18Iv`@%Q(-hni-t+7&cy+82V%s<@beKD7R|+ zGTtgCY}ch!&f<62K$`PvpUcRJl%Z*DGG~*oj^jP)-@prjl>)}nKbxP;%lv@npQ-xH zRE96oThRwUr7ud)o5_6QSfM_4&~9%9Uoi_g5Yi;{KnVTnEa+U|fPWrzdbBs5eogQs zG?EhN`1M$CEOAKR9S}AadQ6+gZZN zcf6y05kB5epO;ee*;>TnW@k%tC*=&uaXhpreAn4-DST+(vWi=8(I08Ua*V?^Ehp<=j6yK@nsBH4PQPQre z^|>U%1ZI&gVRzSZcDY)t^};*MY%_WO36k|JI237D39Qb@{}`OaIPONdF}T@Xq~kdX zIutDMLVhm7(kJ6Kl9>xWL%NP6s7my~_DbYnC-g)3PJQ-5zVtwz<)FTw7+>i?r>~J_ zB42+*;xga!UAGYh`hx{PH_{*i+=X1O1?D4!nek45!E7}NJj~e!yLkSAz03X;>}3QJ zf+Tu&sK{GRrg3qqnPHD%h0=laBaW*6o!|e>{;2~KUf4g?Fe92EBr>vsE(LNzgKl4S zbrngNZU4-+?`GTQJ&c2F|HD7SzjB8e;raaVzHA)eTs@_jm(Wq0%eoUs{!xWoRApF~ zp5l2E{6YP|Zs_O8bJm4J)Q_a!Q#r(5#7TJDIW52-Sbw&P=XoXNkbqR4{vmp(DyF)s zH+X)N`Dg>_^K3Md`EdXt!pzH@Z`E1?$J9wK3u3-CcCc;buZyp;26Yi-JLyg zI6l-t`)jZ_M<_M~%di&=g9!&9uhs^jPVBAd!1EyWj^1%PfQa6|F(@7ic*Xx)3{^4y zYh0C?PWoi_mK`+SGw$`?{=C1$Y^k<^ZOlwmeu?-kqDo@8%MsB!6vLFWs$V|mJ*J5c zNkvmA$2xC~w~kO=2mfk*EEoa@2Ynte5xdZP!2@Kcj@d?SFmv%<-hc?#4W0)E*YPhy zz2eP-><(c&hhve0dgol0vv&>*V+q}p%RG4k94TC58izw%!c z?pr4k!{*T3dCN_P9=nX6#ek|#o3p)r=zrTT=!Fl|BlaS*&@xM@4$kU4&uxDpJL65- zYGbHnzjUbi2LEoxEM5mpV#oe!e&48eh@sQi2?q@YSE=#h;5QzsN(w#3_}uQRSO6LW z?8D-Sv%A{*2za{^8q_J$t=XqX{}Fy!0jGw+K7FTI1AVBRZ-mc+jPu+>?iItJ?C`cq z`;7LE_b*Y{M?Ws}8<>;d(Hd6BNlolv-)>Lf{bhD<3;o6JYunUS6N)S3KbZ87b;4KNdIbJr?GeYlXQH=q)-K{}#>H*9L_M$9 zj{WHDBu-+Oc5c#b`03ow$G*Hn|2|57P$8)!^!=MXNgH*0o@jlmELzc7o=;L&(C{dT zowM+>I)$U-7U>Yov=_nLm>sjgJmil8qu8fO{m{Z{F?Pkle@89ly-~O;pexRw^4gJF z3s+o8(%sSaWIR{Htt5Jwmw&-8eCrGCsGQFG^t<|4EtZ{UIE_jw>V>ni4U2e*?7xo_ zM;zhZ^^W@rI9{HT?^VpXH=)l8LNY!l{qRMAKqTf`7x~}gKj@1?dY0aqoQwm$FX<6R z9tj{)(^Rm&JAE@dP8ajMI$vr87Q=xW1xok=;~K~KY1#2fkkLp@x3Q{%v=iQYqE_*H zU)=xnGw-(5hGE(-24ft6xwQd%B5$F0tBII#PmpgBt~mHr?Refk3=1Q5z)Lj@8E@-y zJi7k1aK#-Xy#|MP0N53mUqMiI5GV2ec=qNpUey|<#Ia~!AW%Tc?Smta-?>^Kzdk7; z3`2Dq>52w>|G|Pt&HSKE?WA8f@fCk?CiG*x;BL*M4a{ZHoZ(~DpV|vwtYyc$7}9&H z8At|kpgt1=yc&a}jPamcj8Eu5J1sd8g1)Y+9fb8LCxvt|s0JV6WV=U5h&Vb&;o6gc zi>+Eue}x{GdW6$Lk#BuikvPCe+><@Q%Ur0?5^RM5zXxd1i-*Ac&`tOw9BwPb_a~_5 zj*PKzQYz1@*axW-M`A-A#+e$-x~2s9mwi|GmYwvqbpXaPb}arty0@Cm-{Ls9tm)j7 zTf)ompqmKE(0QIz+WeycQ|ONC;fMoF@VELrOC+*oUXD*9BUW_>uLIX z)F`7b>2vI1#WBTx&qp@$oU2m~sHVKv0u$pUa2TO&wZIViJsHda*TJ7~MNR&eK&FD4 zu4|BvHtEk9XUKED9;)R|Db}w!Nw=aDX!3K;wt%^)btafe?HphZLSIBKe1$e2sM@;UJ|xUS=7AhY zcOTE2BYB1IeuC?eH&_?k&_?K(q8)j+XaKJFYhApKpvJ zO6s?a+7AY+G3pP3-$K7QurA*bI%lZiS|7cxuFL#I0M9*f)YGTu32~CZfp=vdQ*|7zb`gv>F|+P^V+C>N>NO`j03O}(z}lb&f@6nUbaphZ=rzO56S70w+0^yO2_L$XGW)t2m!99LzdiKZ(}Sr668gdRbt4zcPI=#J-ftuz?r}=e zfQQ{-|3yf(t zzx$^7eib`jP@&*cfzTzO6#s2zJuyG6L`EOz8S(hzMEb!s+1b?bIm0?%rxhP#%RdiW;#X8CBR^N8PvuCDOsH^|RB z)c;F+mNkmskFWuWdD7U4=dA`})qnS8e^@@S~soHUxa&Ka=u3?tb1A z{658cTOYCi9dPFg_ zmK;3a!t7lNT*|s}0r{E#)KLBWzLq~dxO#+fWpqZ*SOkV5b!ve!Klp>WsKqHz!hG9; zAxOeYU@!qxKY?Cmo*tBUK<(5UrG}$-@;l*|ZE1moS6xAJ8uot9l7JsMwPmoLA082o zxsLa-e{xy|9fo9~zmb0b!T!nB4BGywVpMf~g3}dds=4GV%B&j#zMS8)?Vs89UF06x zLAHJOGIYqc@BW`<-`$J$&b)?{zl&yS;O4KPPmpTI8EqR#pX6wM^dPnm@Ne?fU_DEI zHJDI!=37-=O%-`;L~*&*P11MN_oC;q5om;d-p?4Su903s3Cve2ORZFU$zOq)=QGk} z2?72PY{?ZavacA$OV{_FK>);a5lwXk#}>v!v#)doeoBmc3m;?CG^)@ZO0 zX9QFRA9EWR3a9Tw-%{TU(o3yayAJ71zCOMm!J)pE60vKI@U;@hzi&UG0Y^x$w9Z=m zZuzX^R#~3EWBK{kgZ->9Yd6pL`@XUAfIs?r$PPB&W_uYXafxuRpcUHN7$hX^L2*6| zMx*{FfX7&tR|Gd;#<>7C#5@-S-XP~d&|;6KDEw8_I&G+$f%#`7>Aw-wEDTVfkLt+T z@|c!Tlu_2>v?1DqVI&{vrhL5*C}zq9AO}`*Yndqi*Odvzv!qAS$I$%zj>Sk!R_#cC zhFUFxK9mK_BQOm^N-OZR{oF9<#pS@lfaO32LSG`e@@PdryM5XHm(8Ct&a=l~wtV?t zAwTnJN7qISwK6}{N9qQEdySh)&d##6gz^-67w?&c9g~^wG=T6f5<}KdK75r)t2JxU)VTXb+ z^gSEJ*{SPzZdVlj?Wy-J3)uTN(E1#oX25h zf{IhV*X*R0^4_bCx@LdY<81;i`D1{4G%0>c{43azJ}H!xzZNG${?xzOhreNnjVso< zL`^nYe|_oUQOdhl@p0$-EAVGDjEpRjY#6aW-kyDz@-hl#Ue3UP9C$GxrymQIq%uzy zKe^9_&oZU$vgB(su>SDoaZJazluG*xyOPiIk(c@3HBrqB`Gje_^v&kG#$#CAmfGP_ zBOLb~^KGvEI|kOwz>qKur0%iytW zIXc!3v)B9JJ8O^iHBK1xAiJ8C29C5seS^VET)uRGd`pZ!oh;n!C3bro{p(p==CC_w z`MtaSs_gIzeAEBu!ksXThB`hCp&ayzM!rqp38S$65g3IYw3YXx`HBmC$^o!FdH$=> zi&G>Ft1WxyTk`yXRojJrhB0)k37w*I-K()5zP^xhz1z-Q*zJ}UXj6G6cDwNMi(5fA zKd{z2I=@r~jPQT-nx+w-zv_TFr1$jsBl)Q_{u^H(Sm>r>~s%t1&_!=|SLGAFd zjc}Dkx;c(J=!uRu0^Lg-`TezHClZsb(9s1IeZ2n@rf2$(?{87)vE;3<-gSkA8I^+q?uLy1UeS(AR6zOK*ZLkW5+b;tnxdRtNFE^S`#K??+ z0Y{UE=f7ZJNCg{HegUvF8t7P%LxZ(LY!Um6BS_Wc&UoSXFH~K<6Kp&6gMXkwX{|>~ z`-K0`;rLAk-$3&%3x*L$@dKEfGX(1LelOfjG9GKwPt*rD{x~G4!OpW!)nV`g2I>hs z4`U~?w4))l=>fh?y*>rc@-?ALV|xB3SPd7~a?rw^_8IRF!%#8^yu#7=4xSg{==33^ zf2G9}NOSYHHk^EBuh;a;r<~N0z!3VeCYTPznu0};{|~_czK{pJ2|bZ7^wDYU@azeH z$oNgf{VenJ>*JdT^(U=|A)Q6pbS(*x+z=8bi(U$(pO?frIL`sCu z=z}}tXGFqO={?-em8Q(*>dT2J!*Q&Kb zKYN~iQ#;gOMH11UxU;YEzXbH;(|3zbq}KaLPgeWXQt%>jM#lAPlncKi6WrSIQ_#2z zXTcwsFUOOw7dw<*2mgR?FY-K`{uBN$p`Q1j$cc8Oui|Dp19B3FIM^ANwsxpLWBfHC zofB%@0%Q4JB6y8<3xDoFY777MQS)D{^Ybe|(&OQ@B7Aon<3Z?H!5E27CUP)|kSm!7 zI%)Y<1If~tbO7Ae8q7;=tAh>MPxS_PhVKb~)*#H`JeZFfzy}20l%gb##zvmrA-%+S zEOL<2P7$nRa8b!IV;q=bG}ZkV&mLUz@s*)k&IIzWGNc`RsXZ8ibe3^^k9rHgUEyf$ z?exoDO#ZQy-s-a6{v8=OvH z8ujCfkBbs$fb{FH;>jodhkSuI0`yMYNH|FNVu|V`a$fZ`he#k0yPCafDAnI+cOe+i zuzdtN(jL2zone2@^NyA+CpTK@zIAd^5dB?L{nXP%>{542Kj`>~ z`lUKE>m^ZMo_cu?%gIsUV?$CSytH$V5$B>`xS-;mYE6C5WJRXDNByHbTQaUu@9}=K zLQ0YEHOF9Ie(E>hJnCtSIQ%krark}2-{3-j6h-eyEt&No>1)JtWcrk+xlj|%W)}8Q zuk?iURheU`&j@#-cO%1VcGha|BHkY!wlORV_2#GenQ`!UR*0uY`Vp@3y<6>iwJy{r zz?|Y6LLkJ2)D5XycyFC|jm^9kSS9Fs0P;`~@WdW$Bh`}}{VX4PTQ~n$|8VLTWv%s1 zp?^O`FB|~gFz(yKD8FpRsjP7f&#J+11VR_)t{F1F++B5d#Xa&HOVmIK6mfmxOmx@` z+no^?!w17&+l$;RfV_A(|M9m8ZzHDG#??zaz_1GsnB%{l_a+eFI)U%>_w}|O(_c%y z^Sxi9P9)lMeT^w^nZt6`5I7OO;&K*5aR)XIRfZs=ONcx zC-nE^buW@~jjnck1Nz#dm?xZ+AEkbg08__cXSCxP&$n?^0Piukj3*e%uy1k_&Rfvu z1UvLId42=APk%9{I%s*>2x;;r>Fww-3&0r!4pt!lEJ6`U;hGgancEBFK%!blC~JQ zk~3&TZgoH_U|cYZxOJeB$C#x90XCuc-RAiss2DzlEX zKimG9ZNCnJ53=p^+5QLSDUmP#NBCFDAvb%_9=V8Z2l8;gvh~jUIcyC=Pr|BqO#D@B z13IMFyQ~`Zi80&IK$)-D=ChX5=;Q>oCVGD9#2Ga|@%$YXto7rT9Oe(bSQB?u3qekL z6g{5c;5JxL49;o0r>{agjiuxV((5==Y8Ut;XSw*uH<|fk+3jPjN-C)qo4>d}=l-Po zDQR*1lKtl&NCC$`u+7;`{x=G=%xan3GA&1Q+k=TZwC9)BO^%|y66J6CTOk$;DfaI> ztYGxuF}`j-7A)3#)(RhrHwXS_Xhl21+Gl;t@6D~N`mWl;#NtNrdm%yu;4a_$)^9!* zDAp}s7ulia%i0Y8sH^Dfwor#-<`%smJ^mcKK!LF|2gy-IoBEQ7eEio zeHl%Y_}&4~#e(@yFq3lS`%$RnV#qj;nOU%=`amMTFvw{K6Uy>ZZ74`J6smQ0`?C8l zn?JM1U$%Vt{~|vz2x0DDtrhZ%sN~~G%Z@&I-qkoIJ9#+}eZE$(2dKB$?I8x?&N!(s zu(G3!`9b|`Wa*>KW7v@<^UPbAY!rB&9bDglnaFuDv<%|Vt%c-&VBRzLk}ie;oB4|a zTerwgV83pGMf`0HOjn)v9T?cN8l<<$#8BdOa@7B8uZ{!#AsOzQb$huk3=JteyD-5U5Y zR_DAL23?c7J^A)&9~|&AqddjS(+#=nrqsi*lr}Z%uY?v%uPZXcGC{kcccX*yPFIO- ze2955<7B4ZDOPDz!8Hw;UqhY*oDZb^8NI!gl4M>SZk`p#YRC-NlJH@?zu#D7zCryS zJUo7Q0@Lt$-_g{zeD`v}DYXmWH|iD^Uvwt#HB<9^vM>v=3t7y&zA5(dEb6T);~>HC zXMSq*aJ?n{t$uWh0nW4w*z~h{s^*zrlm13k(fm=6exV&WglnkoU?sD(EZ~fMb~4vJ z&@gIugy)0RI`a(myKg`6t>k;Td{uq4j~(}au^Z2@O4`*hJ{nej`?B4E_hzUmPV_1} z!ailu@6Qfq-ijT>@9EZ%^dcB&GXlm(9)y4XG#dv^rFwYx^(E)z`qBe*0>+GKfVFK zimSw=7CbLpIp3yguVJ8mH|v;%K{T^*=Hv8Iy!X5Jte5)P33e@047X41ieh+l+a7=S z&-mMRJPnSX#YX6q7{B9l*{~mfyULwy^iQ|53*x%KrU_SXJ*Y!JoyuzC$)Y|TIlP+o ztB-I%Z6y7Qax3&fwSXh9SAid5f^W_9K8$3c_lN9dT|l}r?vh7fI3b+y9ZuVHeUyGM z4)CGm8$@J0^v4m?UK=KVWhXY{0EhT)J^t(9FuFxL60u;#$1sD z91xvXeGb0GGCLPoimyRe^uoCkP=J~DmcH}84Er*nmrpLv=ZbmO-&^Jkd)lEC~p$QW-(%?jFqn*;s1Ch1sy4Fkh)dwmFYh1v~3e?}Ah zNqA-?hjJ&@{KcbfzFGgDe3>e>N<)gJDd^iTC(xihi zXG_1u(F!S(;GL$1dja=#AT<9-!{7j{5tjhc-^eBeo@^7#XZUO0+m;ILl zMdp!qF!FYRK{y;LgC}upc)$b#Av%EV_@z2%vcic40|>AQ2WL|s=k zsngFoP-P^JTH&*jjO4e$JmgvhR%E3n>$Z66aUXnwYx+CT`9Ih`C#yZFM>uY588=Iy z+6}NUu9f{@1T7N!(tq%-@Lgp+&f7DBT9Ll=fG330*)30a+?NaF~Nlq3*_6!zBFaahr=~?6gH4}b+M*k#%7mRc{@d&v# zQTJQ0y+I6<+<|@*4T=6^CqCnxi%z}~d5ojEjmQ&dGK`TSbX#x3m+*N3J)T_9P3T$) zF3SzZyy(ByaC0-#SJW8Yt~yX8k+hARlRq&?&L?13q`_b?7%pJ`A)IK9map%zE|%|1 zqSYc#Jt|cPwv=b*`vcNb)ktG8xJG@Y<zpz09siQyh8877xeaL z=xVZa-=UwoaoJrZT>!2^E+Q3vka3P4SXJZ{`d_@rVZyO4G8PG7A=HTZ=U?+fGo)xc zp7(}hbAmx|K`^~Ac9)}F4}yK+w!@&wymcF_ z#wf1DcWhOdAP!CdvK!j)88RhN#;JKoMVo(+&qoL%^N`)lZbjWiA9-MHXSm0k9&;0) ztLxq(j&9FM_eKwLT2+JJESi_c(Rlv?19$6+&&t&*yO-f_D1f;vV`SYG5uj8Z>I&xymL~A{2NAaMM5%5dt*vRNo zwQ1)8`_?n~>F)A}eh;P-fA(&nxrOHQy_led0gP+MYnce$@QdinRkd4o$*-VyYc zUs-1ybpPcpO8u8OK6UYZd!nzT?0|O`a|TL#2voje!$+A7vKq+MW1QppuJhx@7QO>r-z%?-|sf)RL2ne8)5qAGxCFr=0ZO@qzC#!Vodt99h@L9dn7f--Mq$4c)-OY~Y`8UXk7Ch&Gb4H@bxq-+Q2~c}i>unv7y6`Rb$L)IE7@A71j29WuvvHDo+x%>FyanjlL&usg3Y4{-9KA-J>U_M3m{qN~txnsDr z|41wk^=QX_v|HvetQO0Rrr-wl=;s1+u!DaLzqdl(GhZ|N{HZRJ?!tPG`IfNur($dwd zq6AJ7HgL;m^Pmd$$-?T<`s4S%y8pxdOQ{#6sO&%gJPI&nXaD&pQ=rB97DrnA7Ey|2 z{%~V(pA_H6?CD=$u(q|v_pZf4#WFYx0ENwZW|g)IgX?_n$en1u>m1I160C2QzmEfh)40jO;GfhpEy>poc3I%*|?e-e5y z6THj`1be~9z5x~rt~HMc-(K`k3=0R!!}t`S7wnC-nkz@5KEE4sAN$a{@w*ShiWqi1 z7#c&s^W3TV9e9bmG{=G~F}Pd<`=h!igYlRPoS=KiTce?!RpQ%pQN)^5uU{em2FxAsTXCc5a|B za|C0YD8%d=8!z%2w{I<6L@3;9t)3scF$2i_P{#g9AN74#?Xn+|j>mLQIXGyR>gTiA z13r!CU2*Zs{IUbH)Vmm#nz2)b`OLA+ImMj7^Gf!Y>I*S!`fYH|z`i{*bY#%0_=f{h ze@kzH0qTi+h<7OYn%ZKDHdGCBrs|7fu9B;qYczJ6PXiAIRAw6aGk3@EhD>X2Z$sa0 z40V4$8=c8~=o%LAz(sxSb2yYYV&Iwpj6ie0#bKbI;4k|qctOA0`}Ohf=ZeKJcq?spdI!p1=$@H1hxDRn=Tb&s z_-s*hSQPZBf3WJ&YHtx>FvOkjaU<$G&mQR8%QV>`XhL4*;d`IPe|_g;-e2IJp8h-j znA?R))%l8Pa+lxxes9y>FTF#Pi%{>J<^k(BM)ETpr4j|kLAMqU=6j|aJLwA@d z4h?&`{m`0?{xaGwZJhVX$iqE-*To%=_qFU%6XhSRS1DFnTWe z3i|%^@;tajV4f%(ZI0q*au5b%}+KPc33-%*1*s;&wV~U>h7Poue;r;Sf z3*T@s$V%Xn0>cisr#SwEPTq{<6!`D*h3J?K4*GNClbTmBG*|K#pV*-ZhUdhP98pK% z(+24(3;ytI_gI--;MWemWpc-_)!J8GiQ)1MJEu4bv+{Z7WDF9&-Tk)58CN%V+{<;j zD}JO3593qelk=629l3poz>$j4?zpM6bB@>RgPt6KcM}&0J92Tc_<^5u6#8|K#$hsAHt4UzK1!pDKYvIalb`&@S{hL*N&k~w9;*)BXD8X0B_Roq2!xS4N8Ga z)tjmw&&zX2p9v0UFaKlEPyJ}pAGBkcI3(FY|A#ml)e*H!97Sl2!P=l3rZ#E%ji&yz zlN(!uwF9;iwPxH&ffk&%{ z$WA)y<`udT=&)KG;e^#rk-KxD+jkORzz*NCHI?7t$_rv(p?}7xz2FcI zZ7j*S+sc^*eM$eIymC}Kb@o@kP!4kivIc>PTb1<_Jeh}E$vJWuNwS-?$UOPpdyD|& z0M5h$I&dP4q2nf92uGugo2qbQIwJbq5@R`Mx699tUI?c$2^aER#1^+=#Mz{R*u zw}QDT9lGG$xS<`{H<{gJJ=1`G-v{Pkwk!=sz;7nlSC0t!euOxzdDqLz?0Nf;YqTjv zx33sEs*_%h`}<}4DnYN8?dxOwDvl;{kEf3e`mnBEr(L21%>|3@#PA)}tOiuqGqACAbD|N(A4)9aIo(NB;>Q+~krKXD!E22e_Vokp3G?Z)Sln`!9ffp8ZI-=R}P|U>0|ZLQng+(heo$ zkkdx4s7aiNG?wRwskiXiK;v5-aMP6DV%!pVGgMUuf5GvNUZ66KQH%hQf918~YdZZW z{8#`}zVu&Rv?`HHHQa#=8C(H(Q@<@dAI;7w=?B`!;i>FkSD{+&%uoX~-<@J) zDSnp#&Zl54YP|!Df^@o08JlSA^}5TIsl_NrJUmTNb?x3 zO2)k%uE%>L!gxxMZf8t|vqg>`*YiY6qk_$Rf%{U^$r@|@Vk4J)fxg*ta--WX>#X|` z)w<%I;^~S0w8~zkE|A}WiF*lpLJ>^Zv(YD}g#Qvszxsdg^16_F$}r2C)HgLbrDl3} zvY$7HO8&5_Ild2W%;(F+Z+{FuQ=g{PNa;%q`F6}R{IPd!0@NH4Qe5DG# zPx*JkPKKv3Z7nz#al92`V{3uC`SueJm8)^z7RZ^1T{&|2)1T){UtD{cc_@8<+P7JZ zivr^cxO3B=#%8rnj>LDaaMn}_bkod>m(T7&PpX|YJAE3{U2w^?iqYuPHBYy#Y#jJ8cKeD2OO+ae zy5>!YzxxCAS$?kdjc&ZR(m71@sjwrVwL`*T$JC5(vMMnxj7+PT8ALt5$u}qu<)41< zZ2pU1QQnFmD`+|5^h}NXHQUp^AH!M(4x>AZ+{}NQ^3$7Sbjd_M1X_WI1IX{2V+DDC zr@k|pzqip(zd1%ZWqyppo<=Qi1op*CAJ=Ij6fixUNx8Wu}cM2loxrd%A180<4x5v7s>eYWq9`Z z-sC%p)(9JU<7^ku*;N4kT;r|ei{U$`nYrLAJIMOMhVF#>t&;JjhH_~_CeIt#V{FF1 zd6boe$?p#d5O~1zZwXNqdEXg1m_`1Qgw*XLofl~m0`6g!Nde=T*Y5Ir4c3YE!KLU2 zfAc&Ly-MWkV1AW(JBFE4p1+(QDk4vzyOy8G3#82%k#huAER=OA=R|#?=aGi?PH_}5 zCluC!VlVBViO3C$=Z^?e_yfF()RXVMl%GRMi(K*|?ec)OY9{L%zBdRRm-&V(pXPzB z2{c4L61ZgB+rhbnh%qjZ1<&oDZyH|`+=$-ByxSC{(%%w|G+k9H~HTvFulyQ$J2He%V}A5UaQRJ zmYy!2$=>PZe)qiX>EuCC^Ef?TZ(a1nN#2Pb7FwPlPf1U6aDk_hXB>FJlkC~W?=?J4 z<%lnLoF~D9Ao50f2f0xUJ>@)c-giNlr=bUffTx&es|Ur?^BZXdnkUe+!xO9DtK!~7 zdXp#GlM8(4UgOzJIa%I3UKWgY4;=7g5M-|-3TM!OIHQsM3YdevKI3(RZlKJP0W}=K z$WFg?+K^LLb<+mHNEF@!JYR(2=?0h=!_TXfdla)w30@n(f_X2`_hUXe2M)rp_BJ@( z-oj!1aujqmRBprmi<|h4lfNR%^cbF3z??V;e1MtY9$1}yL@eA;v^!E4R;rkVL@)^HU^g<_CQQ`@iM~c6_(y`7wK^IO;Hrl+%F% zYcZo@Aj5F~ml!O}56#z1419s@LmvjgugcGS)m(z%$>H7W#h~Vz<2vL*(f2;~%mx9+&y{{wohHT~e4s)W({P}cX+$lL? zldE_>v%kPFHaGB)Yd+;3b0^5%^#Qwr_5~JYTJh%3%0)YaJ_?EpNXD>NCw4I>)`K|c6a1ZPL=M4at|sB=3x|4KzPuH>dMkMgdOnBEI~J;0ZvpjOV_uXK5v)q~ zrx|=V=zxDkIl|SI=6KbACiGkE2~Hc!N#r-9ckfvNoimjy<1X(XDpIR%B7T?6IhIy{ zzUr=V#vul#K}HP^^Q-T&l{2$9?Y}^P2kmmqRgIlJ$@ej)zWd;xxLuc${~(84&LLkr z+fdBo9QeOgmEieDxJ=P6?6vm0b|C3SXdQ3ydjO#!IY~c)EBf=h$4as3KVNB%T&2GD zG;5VjeeFzpkT}}xD7&0E7|i>N<_@uVz4aaq^kB z)F|gV%0H|!toL{?x4qBW4}I=h>n-SR7bbv2^p8`RvcpLCu+~TbfX`zz_m(E@w2S#J z(fwu9PA4Qlw@a7LZ>ftxx0LVIWcZ`f>@rc002M$NklLsg*>1t~EP5@FDa`?)h|WDhpUU(l%}BkM1moi-Gx|S1a1v&6uRxkgj4xpaU7u z&R-lvsu2t34|$$|J|^RE3i@9xn8};|{5}lLN$Bwd`y9U`%?>BEl=KzefDeeAUx=n9 zj+g-HPjthMT0iWE)+PFv>^m(4w#5a@I926wd&QHMeNlZtxiMMZ&x0FP7T}67PO0}` zW3TMEG^%n!!89J%Ha=pHI|Gt>N`7(3I>ENo<72Qc``4xX>Xd}Ohf}4tcFZ??(Y|!P z+L?S4&~C4R&&cN@|0lTfJ|h1XT*dUi>JI-c(C?>cJ+TV>2j4Q9!*sxcjcdq5x<8J| zhTtaLY3R}axAs{`Gx`N8Ue}KBN*IX4N#DgJ?EpXJjzIKqzR}ul2Uf&g#CK`CR}}{R z?W5utqTScjyQCZNdqw@bOAHz~>jJbxuOV)*VWdx^bt7lc*(ZtNoHk~NgHMfwzC&rx zMzrWZN!KTk=5x}!*lTxze3jWB5zF(AxRhp)PNSaiC4IL}?=)VCn^)*u4a0Fxung|5 zN8mkNa4o>Rd3fIoS7UE)ang~@9Q&!?k8oft>Ab3pcKDrP){y>vpWFv|9&ZHM!Y?0klsNqPuk}Ms z<`aAi2GL?E&hZ@7d$IK`~A6Y*HgUhr#RlfUt{ouz)Ji>DiwdoC(AF(7+49{WeixJzl9DD z;rqMb%l0j&ri%2x&+kV`|NHvr3TZdJ8x8(j`?Lcg!FWZkGT{n@7wzLvYiXYshvqpj z7*0ga!F%0wztev@K;&in^1?B%^83s7$@*^(=>znNygv|bV?M{ZyF~}C)Pd7j@3Q}X zy^Kf3Rxi!(72$@#q-Fj0AKUkQ{r9r}UatS-M)wBff4TlkVcbc7oMT)(1v{{EoB}?= z;oD5-Yp459(JNlk|5f{<7!AUIQ>c&dNjg%pEO?wy^^)L!Ti?d&6BzpNh3-6mxxTHz zSvbfmdp#Ez@jj{N>)Ud0*Ff^^M$YU2%d>+OIbp9*owU3w$jM0P4Te%3WqhDx*-Gdh zVw~0E_=5ddJGM^HI+3sNr2dF2bXH9`Ckqtm-f9}zu$$6Wn4$$nvMYTz;9c@ z`Ea?&NsIbap&o0YS0JywLx1ez`BvngwEwN=?X&yYvt%bS4!Gy-TV?Fm_4H>x*haqm zjK*%DTmf^!$IT$ zr0b~WT+$&&pzqY}tV!)Fk)FVb9%I2maAa@t&&AF5J87lb>42l$_CYyehkMNZ^Gxugq z%en{o8a!!}GLvptnJ*#NNV^FxsE?KWO$_7P;Xlp&9y%rzkE`oW5q8YmB|rV)UYl7Us}EQ= ztClZ>Y2eplvkGiS|9vfUPx?np%L_~PDcJ*cYWHh3J9R>Ana8dj?ZN>hfB(F8CG9G> zv`~{m3mGQI*pc0W_I1td@BN1Iu6g2p(3MgBv77|pzt4ZDAMdA@PR*Sq!Y2(SC+X!|rj+_;pcU|ya}Gx^>4 z%DU)lNd9R1L(%)pXy-zQP5qB%EO#$PJ!+adGL3QrUI~07fN|oB^PQ6`M?GcSdp-2K z@0R6pWJ@3}Ia{(LXD`PBS{cDBU6_&xdG^qlf~pwl$2yr7?~ z1mEk{9_ld!O=u(ex0p}NEZTqB@3=FJ{5QSDg?`pi-$hIGq-Z3>1uzCnVrjm63wL%Z z@AV+CRmSyDB-kIIiPlUzR6mZ=uTQ>p>P-pkK~~<=fnHV7Z$Or{+hBCGb3u^NH$!eQ-NL59awC1Y16z-*15V zkv?s}6h=Psf!2;fHW|pY7|Ewz*gx$>NCX-GeT`n?SVHQYwa*J0zvlN_Xp5Ukx8+PP^f+~tK&;g~--s|BNqV}mUfn0Xl`~hqApI*Enw0Z7A-T+h zDh+>FS$?0d;$=P|p#GVBm$0@e`pT*0tj302>4_iBXEbfMJl)7F-lVhy(5uo~`sAH(lg-VRaWo{FB=JT1}B`g+QGr=c*{^AvTn z&>>abQxpvL6!jvoJYzhQJSe`N@}4pt7WkeN&t*?L^7mlRAn&_Jc?!Ab^SqVE_T-du zyg9rzcz)iK)4h-9vF=qKxX|PE?DU`zdJ-uC0q8mCS?MW8{+izW-WGJwIZR=L<~MGf7{6dnw=u`Fb0bOIX5O9!BbjME{|D4 z%HL@pM0I6FsEUYT&faUsD+D+PcJ|bYtRE(a6a0P}RlEY&j|H?Wtb4Kao<+Xqn4PIV zMhTCu=T21LHKZ+Lhk{;4B>Q{vgL$bbiZkE7sJ|P8m@Y)RIkB}H!ObYdQqS!6W%r*f zbVKd5>- zP=NUXryCO#Z!@o`Nc*{(|&Lj+I^8<(a&!f1Zp-+N;!9esj zC)}X$yNjgH$Q%gzXEt(&@&0=p&K<}5%L(vWOSykK&Wb|r{J`1Rc^JItdhEnN82U=i z89A6QRTnc(cD7`$PFeJ}6 zhDGXQ07@GFY-sYw7%s;>zL09NL3i?v#usgN)}Jf6vhW7%Tm@skxJS8(!;;oN!BAJ= zN`Y}XZy~0Zl-wDGA*f9A!ud;~4?Mk|)jr`g>z_8Rl6me>pI5!L+}|*bkBupLgn2yk z%k&_*0-IA24(s{mo>&9-|I}fv+c(iuMB&*u_>MYQ5m=y z-QYR(qaTJl<-iR86~C{sOQ{LashK&|!S6J3pqZVzt8?Zl%`V6%e zo6xm%eAK2EQ^mk;Cg!62)B|ZziO?}?Pgdph`;_z8zC;j(VFhr`6BY9%c~^^`l5ryI zdOGdDyDNUg8NPFEP4u=a)HAli^&*J}NjEubJjfv5`ixtdLWjdp{VG@ve!U0Au^fFB ztYXj64%<6wusEo2_V?C~6VZJ3@OxqOq8wlc{)Vr}o2bWgL+#!chiP8f!N$SK?{Ul> zjA(>oe5E1Wi47gtk5yO}%s9^)NWcqNTI+4iDUWsVQ@uhyX&-!vxj0b= zI#fd+ZN__V;5ItSd;8FU2a;}$UhE~`L)`wEU@fDC{S>rt`3qgb;pXFD66TC*;M>Si zX?F(tHTpXa&U<3`fOg^9L3$8Jo9M$W*@Gb8KhBZ;WBJZVz6L)*kLUgO7zd*8hm^&* z!r)y3r>peO^PTS9jVL*B1&3rY-EKczwxvmz$BWMQ zise?lcwWfZD-IB}_zKeBI6NDPL!ID;f(p521T#O7pFZy>4tdULcvU;}v)HM>MZb4K zJ2YQ4f^a^vF5oJUtzZ=Nilv-A|5`uv(Cdd#D1M&buQ99C0sG-jlYab+{@Vz4q<4|C ztRLKZ{jvksIC7b1J0xI))Qx|wA7V%+u-a)3266QW?K4`i-%-l>>&5z^B)xf!bR2IA z-{gf#^8FZcHwFXQ+LuXxC4%MnQVVb=-1#e5obv91dujbJ z@Ll6S)z5Hrq-)0`>m#|Fp1LOL@$ql#Bh3%2k0_D({a@|N`=70kSYPS&5l+kq%@?vh zy1;wUv^NR-@7osy2OZ@3A*js!&fouW{g?G({a5bg`j4EykuM5r-v;->4gXpDPCZ}$ zslEgb(SNf3(~jBZ)FPGls{Rl6-+Aremm6T8@m@nh4OW3K*MCLm9ckZ1dP?L)C{pGa z7y~}_g@j#&(S>pfwx zYL?`GIsQ~Ip$71uy_bNKv*gQ9xPr9zEbeKMCr&~Zc7eMz_laXZHzQm4^7&5LBM zbR@l158Odo_|wakP4eD0?zkQVzJ=pWY4;;x z7dvIElV9ZBbw>YbP}`2P~Q7lDTvM|DPZ^8;p`+L#{&|-z( zQyGm?P9tW{bWj3K-UL79OB2C@Tyivr_m*(fY)i^3W1o;fFitbtB7r}g2y^?E8`HqZT%{wXFfD$ab8y%Uifw9CkrN<8s|N20g73^VW`=nk$L_lH^O@&R zufNQ3H<)?B(ZxB$`~^L-=F`h*v5-WgD+r{^foj;rN6veFB{8z}fZVZ=Z9@AP* zJ2v3foJO3(`Fhi<4Q86%k$5Ia5fAa((cY(^SC^sfSGTLcjWILYhrLcedPn$*KI=od zBc0XF>b#%Fyk`}qJ$=k_j6uUy$5GZlANeZT12dOE-WHj$PvFPEX^w39g6mf=v?`Kur8sj{g`i?Y<` zO0CPsu0p4lM_Mdt1V1!=Sod!2H`u>2^zgLf$;AJ8x9#2M;JsSTOMdWAaE?G%5d8k^ zbn?~I#?Wu0`;;4cxaYe6a93scf8qC?{}b5urj_A6MEyT78=KLrJNl)SNoQUR-4(nm zcq8p=szxYuleF3y=Q5U3{#I8nM@goiGEIN2H4t_^Q)FY_35by@M?X48zN=-fwu^;d z9ABGdoNmgGbWibhgzjI7xtQpQ6<`_p^`u&o(xsUdn z#~nimA~2pUPHvP|k8;Lkee0p0%#w~Uvj^{gkEHC$^H}$031BcL*yHW{y#KmY(`rY) zNv6|eUh^9pnAg8D&wbWTeFEv34LW2Izng!tQNv0%O!bCGubrtHs&iOZ#UA^LspL^cVe|Tg)=>7o|t)`F+oe z`LDm8ZAHJ<8V^H&emTEu`=`o_ zq=&Ddf>Rt})Lv=5{1r7@%gdMZU$*@-+y0pVe`MP~|5N=M8ItXPSOb6j|INP=sfrk} z)awi(lAnQh2`8U`TwKf9ZX?0*oE3Ks3^smNqG!3(A#ERctmf+g%mn! ztwgIrE_0RMRCNH{%Sl($!3D;rGB2_1Ltm-G1z=4S`Un^K?NZG9tY+Sqz+tX3-KBm2 z_o=h2H4SvR{VF>4DfX+5>irh;j~73-{n+(emrQR@6;1#s&kPmeh&mi(_89w|{V@!!hAO9CrM>_vFN(V1!MzX*%utiH^L%U<^3egU>L4igBh`h`WR!eR;=a7%i@#| z;x3>bM@W=@KFEF_<&}s8A%ZJFa#&@D9L~KAC5rzk9NVIRcfg^5!9x3K+7V*mPB=vQ zB>DSdXq@Ht-jlTZF)rz$lwY$?NT4Bae0lSiN1v>Ny!DsIU;f|m&ty2RVZUl0VfTkP zP!yb59OAcg9V6@HC|i*aS`p8c4>)F%Ier!SU~BoHq6rsRv~vbmyYThR>{+}4jKiV* z3~-hj$}R8&)*UMxU2xbn&z~Jugz_V&ljGlw3>ya(4iNXU>>=*Sv{S|IB|FB|2ab=` zAfBu42y-BJR5nMk(ysHm>wDK`;y8_hHwK=-LHJq7&A?yq)9lVHl3kK^nr45LL;vgr zII=$C{R7SbM-SdFsk%8v@ZS0G)Zjksh*=o?Yarh@T-IiL66MJO=|0HioEgr5-b#0H!VZo=O*j{MauMQd&gmiNw-NXI zvF?Cs=MZ0QlUk(UusPo7RI>|B3AhIqS`%CbaW|#r!%8?%tE?^ExoinOf8W(0@D=Q` zVcyr|uFk})v`G)>@Ab%pq;u!#m&101;*cw_HfUb4jyQPV%kFZ!9(agzOt9wR7b;q? zb@^3{qlmHJu^LBZBlpDQONiMLcYZz~?VnbuemL4{4=+8S1BOySS)WnCBA>U|H@(jR zzc%JcVxec2w_mmxT#IO_7465XX1=8;pJYe>5#W2qR)r(bE@!;y7)f~sAzqpHm)MIU zhS$p+36A`BIN7n&i~8EeVR7)Hb$lrfO1nH#yENl0Wq)9SKkK^ph!qF?+WO9F!te9Z z2G9fSsrGPn3fR}UB7xOx(;Sw@_)}YD9J|&V`){plO7J7dwRqrys9UuEvfWIdFffC# zSt##C**+3jR-g!l3PQ5hhW6RvWa-tn@={4i>c+{xn=buihMZDs|`2shXPxm=@jJpx- zR|7i(fsx4ZLclV(JAHuu+-V0L6C5m}1C)i)t}>~268@@h3yYinCX>)CAqp_3dKEY zq`U_YJ1;vt+25Q+5RZ%-dI`Og4AD~R!O8bV3*dD^$G3s?Xf2<9-d`L9hM!uf6D)dz z0p?4Ok9pGLllhnNF`vd2T*|CVE*K~TMhuQazM}&}%~T!rF7-|5zx3Oe17Pm~3$W9X z=SWg5r@xzxgPQljQ7o7chh4ZH*CXm>UUT5KtP@&Jm4F#(n9k#D({kk`lBI64HpXGoazJCCC8+W=3 zc#QxZ@3B)NKOs89e%&}_PoTa!m>3GopKCt^8@u$pTk10fU8_b=OU&?3c|_pl$*Imi@hULGn5^CavM)s^I3yw+!8`WRj(@k0uzTXEk0T5y>j(Qt98w%v z*@Gkz>?3=zp5I*kjPS(cJMaIrkLZPr?>y`B3Q%SYx)%FL2S)zu`2J=eDQzDaX9?$? z)BHKEH_fnxu$KEZXa{bziv$5#fqzJ~e)Ms^7J2>TEI%{t4q|Ne*Z ztx>&nU|PnP#q)n3UkokC_kv06DGvPqJHB%qrYz%|1crwJUt|700M;VpE#tE|J0SxY zPk2|xk%=Ar7Vuy8?c@2ogQgNeeGnn*1mN|*jV}dPB=iXt^wNnDG4PMSj_)w5Z7;u9 zWA&y3Gwh*YO76C9tLes~m$J@FkwV!Ic!Jfu9{9w*P2sH&$~OG_u>7v~u*wM^^JN~? z2c&)t;O8NnAo32s7iZ=502U(QN8a<2Sa%J0!FYva9qET#C)CJ!E=P3zO3hcI`PZU+ zgx<({Is*5X{yrJsH>54dyf2UQa)1n0S z)KkU_>LBotqk`z)`HFNDB325~stb=e7j3=9r<-anP*UiMrS zzf#)njCq-lI0^kscwESE)Bqo!YEJN-8kpKSlkxbi^egEL9c2y8KA+tT@lwn^(GC9V z%&yb8D)d_2Q}u(g&~?k3O`1Rb9c+10T6oe{+TZ0JpE-ejA``>EjVOp;{=(gu_|a^v zJGXi4j_Kqe zSBdFgsiuYVoM%B!AHR1&clPfSmd@tJh2y(_IY3ZPv)DXh97mcCf;Y*vqk= zJM+L_)TO?0(cs~F&Rr9{bDqFSm$Vz?@AP9{ynC!RR#W;p%JGf*ocH^wzBc1mg^Uhz zC64_$iM7a`oKl%vvZ2e26{*!TI@6z$0UP`#^Zaqu%CQmlm_L0(MopggvwDksvjV*S zUg+0Rk4p`v(q5bzEj!R12VH5-k5L;g2hNg{&@vjOC1&tl&qDVclm0;WwT(rR=+@X=`uM0UQ&3NB&%_ zEZ&2+H$h^{i8vn!pTzE{FM2J!yNFJF@@GY#p)8t0H2>a%@7@Cb2w(jaIEj+rr-RXg zyaRRa7Tj#?tZ~%GXI73jl_q zjlIo#N8wL58E+eVspcOd|AhbTM{foH*haOr9IcIh(~@#5T#o1AaZmJ@KmDJQfIvSo z(53#(|0#(`*xen)zJW)_!s$2(g3E2d6#mcScvkcLpZ}8YdH(A_O+R`5`8}+QKlSUp z_``o>d}TEpl6k^MvqtPswE=DKOW^D3I+i_p7~0XVls9pSA#$B~q+?c~d{G@! z`GI?QV-xkuvDiiaeXFWE3fv!V)WF&w{ZyMHoeuwSc-ECJJdyYJ55$1_2aY*!$bZ4W z)GJdx)50r^wL9BQtagXe*zw=ptRMD;XNG68`5J~zEwi?#E3h$vehe(LC;x*%5!kQi z`3!}5xu>$OcbVa41M1J1esVOdS=fBWgdv$7c<(guiWy;Ih;V_xcjjBb?dBj8g^w!+ z)|)su%wKr|#o4{b{i%sU=xOU2D4D#T*SUP)c{=6(;J1=-fG`ei(!1#5uNdr&)U0WrvrEImDG+c*&!p(-@u_D3VLfJ zQg-;*GjKQygZ&IQh#1f(P?yV6|FfMX20iZJYM9D$lxZ=!yOVE~AomM{oPDCRfZNqX zc2CGosI|Bf4f_P`!H|vnxFRqxxf5oJ-kFuguiytIJRPTp>?B&RcVgs?FK_-t@y(;p zy!DsIU;a1#>C*hu7Z-44`0NUMwm3pjh?}c2lrM4EI*uIG3db;tG<^8qk-3#}&1YxLTI~Sf6AZq1E%9 z4nm&TFM4ON%gQiMoA=C~z-0H=CjD^kaQx&DId#T(pnBkd>KXExKm7`NFJIlD0sLOx zxy`wQ`b?h#a@3{Y`GCg0$i+v`Ja8ml#BV>ge8rNHc$FT$5Z`kqexp7$RyQud?}svi zl5s#DZFBbG860!hj^7@5i|y_R-&$f=LP!xZ~cjuU_U z^b)7q_~Te!9q>v)^ald7GcEK73u&Cw5V7yH&Stj;PgihkkHYVE$Z_0H^oKzEc{v(i zIXUwG1Ij1uxiTL{1+|Vtg!!QT*ugcyh;@8L`H(8Dci5f6Y`RVPDfY@-WgZ)HS2Fmr z23rRu5MZse{k_nQwT}di#f-m|^}Fm$wyN8g2`Lb;x5qfpgL1B$CDYJn67OcbbqW2z zud{1y{^RVd3J7>UY8lUcsLFX)0i&!nI+wI4YjC=p)M|;3i(H|o6)g4N+Unq=sd#LNMnZM=WMIxYbsF0Sx*U!vWq#wsWCm9``E(W5t61|@D4J3f|^k|VBE zyt z%c`0>P;rfvfPIv^^L#My5n;kxz%hhqBY|OPWHCTrQh6M-Hw>-s3jJHFDk=0k)>9=N z0L|v@4m>A!p_T!bVQ(aX9}M2+IzV#EXu;ld*0sHeci6egyib!dE4vOb3GJ>fFp#iE zb>904|KUNX{M}0bis={Lw^MBOOe-3J6rDF(%leycu(Kh&T;A9}0w%hz@*#%ByuO*sr(?((jz_5fEH(K?`gpZ{*o z|Ig76=8+_V9yoPr2Mn`*pv?xV0q|+u*1rO85w@8CEYI6Tfhh!zWSqI}dcXy2?;+|kiGpt}z(copTg1d46o z4e)o7c0T}{tYd<0wc)J>PVt)(&|-%|Z3%d?qcv3hMENJ468y-~xwV12na2V^86k8< zu)hu=G=o+v>F+i$=8~*nz@sOzhgJe$+vvNDw}{5eeG&_Vjy`6G#dAP6dyPeY`jUW9 z8F-O|+p~PPFJ})xr|jEbC-#Ht%^3*U{C*h3wGNDs-?isNWl0RcpUpd=yD|vc`r~AAbkjY3wo9Q z#S+qO1T2js=nLR>W~etX*M8uAr_eJv-mTjii(9ERZ|hx_7U=BhYsKlWnP4y6M!XvPwb=q%*PqZj86iEPwXS) zs0NpH`bA~gMFmn8~?J8NZfW(#y*mO5&EvSkD#{4_K_TgF5?R& z98nvX0QDf3R4ZoeDcZeYuh;XJYyS~G*r(?sj;}B0c{#FuKJdx-&O*GN?O}9Ll#3hVSlfF3{%ZgIWqdzt zePSeXMdn-VKf&jd`8%FFe=*E{Q|d1gW?uws1HXX&XlI|cS3Ly6cTq-vA(SucypgtF zw_+);r~G7mPv-BS-Xkvw4WasEx*6;}g6^3|U%kEstK}%KW);Xfc7m})VD9{}-im6x z+U%)+o%-LQQ{ji<(3cB*ZP%-m05a$9n9R=z^>}>g{K|S`O=bzdv-fji0DMhVK_e?m z`GCDhc96mqYH7Z38tEr|!-Bqq|1p1R0kD8kQSP{hoBg5#XwUOIavvTMt?~Y3{Z(Rh z$$ZOpfv+xl-(3X4ggJO%7u**5 zLKg=$FW?AK>UQJb#Omjo@d-FDgRwqXV3W@PKj?W=rsO z9S#E@WOO@qKQ^=4q&+`XjDJNgyd(VG=%n7!^6oeZSH(eNnxl>IX-8$pBBu{}soOWk zhwRZfiRQ_q?o^8HrOb>I2VALz>b`v{BvpnT5RH&;f{oLT$=l+m>U(Q*dH3+dZ+n$yWx=tUTJtldZVJr$*Gk6(tao?2# zBaY1>px>|H*r>YnbGa2OdS_~_)aZjET+@=z-tHJ-S8z?>D*sVG{UE@uv1mdxqtG%<+-~d|TJd?jFXUIxX#sj9Bpc zbMSCH8W;O37K# zRm}Ahcsk%5tC$yWC%Y6$vxd(U-}ii58_=s=$!>ri{dUev+3-!XOy*Gc7@lk5`NPA! zyJCI%_<|QR#(l~?9kIVF+sdIo-q$=%(W}*Q$5a&${amwJ$Q34jjmV@w`hT7jJiC6R&uKj~f^b%@1Ajjoyl-{MQqi&0utMN%2|4*NDK zo9N-Hot9f}_#g8C|9GsQ{at@~kCq?t`|hVr|dA@DoT%`>{IElhs_M@@Bu@&bPSfZx}_iN%f> zi+=tN^CSxQa};JGC8Hf5J5zo!f^*eqwFXuE$kQ{>1oPerq&rz7>P z&CB{~fJ}FiN7}pZX=h?snLh4uZVXQM3bTS)8b!R0x3m|70K+$2?>I>}67-I^H&L%X0`Vh63hl zs2auZCGC|s*~IY6kMqMoq0NtLMB4Y^5G%PuI!&Qis}{HnQTPqnF~+#5t)t%N_wS7J z+94^5th66vU(`nn%3d=J0Hr`$za-o(BQ-;>WiQkH+Q$y1ro1=J9xF#ka)-;S?0%9R z26s?_Im$P0e0lQ+9puqx-ulbqFaI6?Y^nL@6wZXf;lOT-*5Z)mK<^V|%i>6Y>(&RB zvG?MlkU-WZj!oC{K_MRu zub#)z)zkHn%O5{ZYiF2q0qxz**D3gKtL!Tg+++tEb|Zq9M?TB<&KNCm9vdp!C}^!<+`X)M znHk{eBlU+9$DhMrmGiB^bMx&5HhPJ&jd15;>MuDwj%eDggiE|6aGd?Sy2^8tjon(V zZ{Wy(`iXO@yEwp+{Xr5~bJSMJ1n&&*1Z%1dp6wOZczvZrZR57h{8}ZgkF5aev#f_! zZ_0!0hptc1XWDpnrlUtB_s%?W3H%l$Sz0EzBc3Rtg?IpC+!ir#Tr(w1= zFMHm@q1z@WEO|BYr>k)<9UV;^G3?yCSBFrxPDI4tCk{8~-mi&%^s9$=q!)Tr(JD;Q z9|BqrbbuxU^R^H5uVV<$qCSOv!(P54|I!aNN=-9(pD_JOV;69^O3*vGKPQASg>o}O z%HIRel2|F@Zfl(8USfl^o;s==8)#v}B{PDMHF**EeU^R@AcXW1zk2Rp(D};}V0DEyjU^4J7c$5AOCM?8wdA^B0s=5e)(1o}0xPwr%OQ9iA@h+}{ofIg+y+J$BkJDla>W9*6p@Rm-EWNWfhCRX2l;0)fHA2_7&~j!!j&h-c zlF+e?qZrh!fQOiUUtnn*=N9mp*bX$*d8A6E63-0@k zI$$c}gHBmbFX@1B4OZJC#@j`mBxtGU&zQjPnD&h z_K_YR_K{wXEtqk6?oa!O!zSOR{1^Mk7(hAZ@8jdbHO7ZLUMhM&tMKvsJsIEogax0B zkNITBXYTm^2l?KPvsA9r9`>KHnCJhr|7>mlu@li89j1bMosARzmBg}?)dB{^Orlm+PULfPRmie_dX{^d<^^ms*>>~ zveMTB|Lgp1`rG{F+PB6uJ%2L3(%@~BJ&-*|k`VCk^A`jckZ}}7o_Tn;*5_mz$~Mm@Flq4TnVVcEnYcR(378ANI>>qx{mj6{Y%+u0t+iymuyzN-2?Y++ z@@qfga#=Uqz=wP<6z(JAK1I4r9Pj-OJp}Nb7D%V_)Cb!A*)l+a3TF?pp6r+HZu$fS ziP0n_n z8yxm9B#H0rGggvH!j%tp8S87Fd(qR`E{t9o8Zj`8d1|z#{;XQalc!^wrZwF~|BgGZ z+sM^NG3jd)@-aQ70=|;xg1mlKNN@5@=5Gt%+KZ$4^R54RYchJ}L(ZSv1iQXh{e|+Z z^Y}0CoJlwgy0*skxj}#19&g#W(QAe=vf{ud!{DRNp^n8aAKu$-`(=z|+!GwT+~qM} z3};n8C*sV{k?wyQJ*%wwN%m5fP4%4h9>(h*=$jGDI!kyj$#HKk@z*7bXMEF)>Dzd@ z#*H|`2=j&}WM zKfjB<$9;#<&w$LNO!R^5N$!E3qx9nyE;NE)1>X!>7x*dP3v-TeGB2r*((9!~vCN{X zg_d0iJ2i`Ri-?6y_D?&V*%5W%;)UVYhcmx#Cmv4`z7lOVuzmnfSGnz)ez_hxn>ya2 z{czuft}K4P;%Mkdp#9N~fe!HM?dW~kbBTU_V}^Lan`cPorkqLO_k!AhYq2$(-#@kb+QP498?6cL8$Jbny86sx88!|n6o?)}lHoBd zK_h!d=KU<>hx=?!l(~xfhS&$tm)#C4@;Ulb+j-Y91pM3xT<$OU@9I9HuP}|x`PMCb z>a_if$X8V!zaH~p@8Tq(_bK=0vL5P6vm z_BPVBSZ|yZ@GeP5l1Sy${PhuOg3nW~hSnkLH&_2eH;7b~DfdOoP3OIy_8Q?=TK)+? znqp}ElQHzxd~qLE4s?qC@>sstBN3q}3`4v0%-veIrG&B8Bbo-cPJt%l)bF-$| z1lY?gZQ=;wfd3AjnZS-_E%#+0mr$8D@PN5Tpl5=aX`+z2*O^s4VsIpQo^@k5nNIg4 z&t%|Uv!HtcaJRY6d;{3XylO_H@c-d0;zc30SJ|KA%o9iAHf@L=F{o1SL z{P+B`8}1DhUykoDsvRbGNd3AEUyU&~i37yW<(~w~a^(YRIPw8E=D*1Y+^b9Uj)Q}^ zYmqOwF)xY&kMsK|P7nz&_Ljh%@1TJ3zO)~>Amn85XacHJvl?gD#Zgoz{Zv*k&+Wu* z{1LDcp}T#+Yq>k!@;OE+aTq;vPIRsY&hb6ra|}nrKY-jfxe;J@8n_!ayX9Bh{GYC-XB5zht79#mKoK9N`BRR z9N!BJJ{Z-Wc^zu6%iIBeuBheW81(M57rHA^Ze^@-yvTcp)jn4r%C(Fs9Nx>0M*FHG zi2B!5W7jj3gE(5V2<-Q)>vbQ964&=uv&R}-g?^h z)%~qx2RJ8DE@eHG9ZlEXxKn;N&)-S7zxt1p`0vXUUmEiXaghDtpHv%)BlP|36&c_u zqhIC1qK^y?GNW%4|%zDd8lUm*K`xp%3G%YOi;IR^v*B zE|i}G8v>ufY=%x*yRXa6BMjrGIh@@8h5(kEajf7FLB_|kPs6j&PpK8W{zP~}`n8o%7Z7K0J00*3(0i!=HNHZspAI}f zuy1NTcY=z}9Y<%qvn-rIAoyZmS*-5&X~J^9Qf@_m9|8~YUwwhS35h#_H%V8Ze{kQ^ z8m}e+tys!(cWp`fQ-eUXALZeM()$C~LkGfN3p2_VKnXBM0Xyl|#3A2P2f|+f1EQy! z=WRb>vxpIM|fw~gdhZ?dZ0Ir4-X6s1ViQW#r z!D76ovsStK^+7_5(x0xXI3pLlwa^I$3#y zv5r}1dVK8A)%FoPDWmmW;~w@E%lgP2pV&vzkAK-mMy`Ei3V%kWHtql zOPP&#f#;2ec2}T}JzaL_+b2{}jlT=_F?|<(M@CeG-%mj)0l*YmO9s|MdKUw3W|Z@R z`N8jI;1cjE_;|AZRw9LhDPO@GupJnW(u+gk(qzzr>-GR`Vr75;Eq51GU#5Y6%4&QJibd@@EdPG z&jxQ0&ky5_73jk`*JqeK@Bd1`GXDu$Ux~5kfA)i%NOw8d`f?5p?~It<;1}zY1-@1} zpOS>UkS#$~Lq_ra@;(7h{xB4PW}3oRwDkA>%9P7Zy?& zIh%StC#;);D0xrS+&^6>XZ&{@LbM;9J4 z|Mh3qu25+cH$W7B;b`A4crK{m;L=6tFLE#y`gb3-+Gjq+IKFr74FC_>BXXP>KQO*- zj+=pBG5$`*x4u2;*Bh>aevy3d*u9Zgp|1r!e%kas_&dAsV#3XXs7sTIrWXH$d6`V= z(lW+h)3ZwCSmuZ6fmy6qb=UsL)0%kimn~{H0B@J7#1_2X1NL&-Gs1!{?+m!z<9-|7 z+wUGN2?U<`-k&_slbXhrX3R&Y?ag=rVNn%cL=is&>$bs}u=`%8r zYdO2jy>95lucD)c^IPWem7weZC-1$)R-U2Y{aQeXFR;n!29>Mz2d@Lu*JXl#wcPwuzSGb<-kVB)3Yq&oRha+&&d(j|Y5zvx_W^UjQ>N>tgYmi= zxe}Z?u%|+vF5cz9kF%eW#0C3B&s*M|jAJ%>Ciu&~n>kyqh%jGs?{q^4Rtqa1Nxz0S zj;kgLQ7&Pxo}M0jJrKHeUULPz0%_+?=4aW=XZESgO<7HN zFU*={)uDg2Ncd?9dnn~w-37j!vwfEPOrrlOZkNe?dm}xCyjSQ~K0J^g(61HfQ6}%j zssReT!PSRod+}-b*;&f@Si!MCAD-IC^B=OF2T(4c`r&gFJ{_$2VSCP6sY&?(`p7Y0 zJJz4@opWg2!+|5H5&m!r4p0|}{-GELXfG+o@ccT1TN2&?;`(% zuh(TYR|h@^r$NtzOLo-(`oH-<(N3T~{uUu6m`8YMFZCnvuF;NEG~vVlyL?a2mG55U zAM1}at5XX4h0k^2)Sbuj;RbkIr7EZv{QfulJ&Joz+=Y1~^kCi+-qf8!-;# z5$OZBB~cYSaiS#FDnl(H<=#+>)qU|lsimASumt#!%X3ci+*|4b8k~WTXtmx5Fr4Qc zlr21G%W^8JAw<23)tY_y++p{T9v9m2M&92)5CbCG^Zx$97_g@k5I2;K()l6x{Y_Sb z*h{|^g)0HG{S6Q^h<`@|huiy&3Dk2G7s{n5hJA5`i6OrUqgNc3A@&QvWE{Ekftj$s z(!g}~=>!4KVHS&`syHODEASeIFNPULYoLw6hGXVuZFraEX$&8X%uMZ=D1uAtHf6%~ z5;(Vi;Bb_WD5n@j6$X}3oCA|Z!M(uYg1dmHajS?T{i4c`^I3LQ`sp1@wK#OPKA&%; z28!bgXYyI?xGZ7c);pl8plG+E{;+C_D^e8Ap|bM}B{oD!|I6|P3^4K4VW4o}Z8e-u)T1KHdnb)gY*BDm z;LhAlxfq9yjtAzAFK_-l&_N!3=B>Xx{_-#W>BHf%&*^os(>@9eAWZxR@c3i?NuXko zEpkZ`1FpkY*TJntp}v5$*#Rtq6hb}_t}h^R!;Zm?Dsn22f5lF4!* z8^>E@ct$?tsBwQql@~L_RBbmsv|g>wp({R$Wf2K73!^wBlG)$zvah~Qfg{+ zxe`3rXlcXi4HWGa3EU90i;cJYPDh``L33?ET>e`w66^PrSQoh+adRZ)jdbL$_jNng zT8(2fF2PQGkvLYnG6jl95toQ+;$QD;9DU{di}=BpvSX~7=2YM(=IWd{0{^ewt99cp zc>3OFZV2+<@tnO=j-=08>kUq&e_>Mtzb@npnlHH?gcb!arJWUgZy`R9=FET4h3PAE zqxmijmbAv1$C8qWJJzgLwa+t*Cnn@r(XYU30eipuCh&qPMv#Dbm6@K-cU-kx2Yi1A zKhx|;C;i27JkyI_V83p6bZn7+7`2>U>fb|;7)X6X@bEq zDwF4%IU0!rjO4=cvg46p>4!G-X15?TAbQ4VYmS%k7$4ZLsuR?I$D!{SXjX{TmY{-R zUi36eqds|P`pipl;G?I{$8uFcK#b3|2$T2x>?-axz!S9Q7oDx4zzUkGMg`y>=&7U~kSVoxcr^ z!iaPEuU6r?%)yzjroRL}Q`2vyHiA9|n`t?{>F0RwHSZ1JUL#oyOYcyxkJXv^ACcbo z-T>5*S1(OCGKTo);48foz}x9xj@-LWztf}AN@hN!pViUQGQit-BU%hgd$#JJLzuDl zS6aW@j{(X&qMgOyI+P>Sm+F9lY2VWU^Aw(k9@)3rOiclfFtTMH&?Y;fLm6mctwlc~ z++pfxbUFDT%B&ms{&VnFj|!0OenC9}8Z7$8FFa z=tBR8ZotzHJwA&N`vJ;9{NhKuvsDw(i|sz_oUcKB3%HYvdnkb<))zs#Se}%`j2ykw ztsmi_cPMX%%XR}s^PT0uvGlGmun+V78L$TSLMPx(@W}ibwMoSQuc|cIlJ8x!N9w@g zS-wz;`r&E@ZLkxReXnPLp{lOdpSu%oVjW-)e9B?l9JLKL;jifb-IaXT359mnzlUhL zcu2_aB;{K?8w>2qPT)TLcNbjp<$13@|K_7U2JA?`cBrYk-+y4&ETi0y9rY7{TL}2U z&!CDP1oLHQ@MLTU39sGP4u2<4Ri(UxuLc7n*s+lg%tvcNXC07c(}9PudY%HtfSHBB zIPiH1SOz!zm-MFv)))A;PpVHe{j9PF>7&OtLtpR@u^)6Iv5EYvOZgGw+X1}I>bu7K zqmYEfdA~DI=D8M_r))G;^l z_R(uV6O5e&?q-FD03$hnL&leDACb24#6G$%@8Rt4Ww+ux2k5`MCIJsJUysMv!-l`<@hzaf5vvZm zf;-+5KFa(})QKNY?7vOyw;#`Qf1f`mGbroYkC3vg<6*RpTwtz)WZgrPl3FAQJJ0I* z{vOVfL-`)z;c398{37dm9bp!sj}X@5GT1=wN~=-c1$_#?tjwyE`HO}= zg+5*)foDFjIQfYZ4dPsoOLJb)xnYb z&jRl->#`0~NK85hJf{jvKpVbQRQRCX!CtP=m)Ifu6l=g>&6be$1MQvD^m)i0X3P8+ zCe7zn;8J_P5_&GFy6BVtKDWo~@wQ^z!e3lqU)J#j@F?SMMY@z1*bMAReHvl@S-?W@ z_eH?w>K#dJQ-i6IaRzdjtIXpD==B6}i}98urV%LbuGio1q!e_c{t@j5mJ} zBJGLXfe*p!8{k6$e^A08U<3iTqI_qZ`ku2|IEe!dmU+gjk!%m)_x!4o4#0iO;o7w* zS6A(1N3tj4Yj{w4Y9Kr{)l{rYCh3^ zoSzBj@1UH-S6PqHLqqu;{(4C8#JASy_Rrb#^!TnLzl83Vk-B9A^YQyVpcl<4p7+Kw z&Ijlj9&4i&L;X@!OyrTh)+{S04rR`xeHfKA3b)Xb@SFMIdtnFil@Fds9^vJrS*g%NT935tnXjSe+_66P zRHof8vU-~LX}5UjFTt5C#}-Ey9UsH*KRfQbq33qznonpo9JDVD-%<1$`c1El{Yl&D zcc8UY^v2@ti*GNa$PdfvniDgD<&d`X`jIruipD4FUqS8^92>bO{5)h*!4N z%LQ&)_uQ7Pq;4Lic&U`uNwEHgo_p^RO*EIr~`4{kK{;b~i zJd5>`lfo6A<*2{P5&cm-cL{G=X~bx)n-Al``-Qe)(}R~1{|K#hsp88l^OK4GslOsF zRzwdd)|JGDDi>VWHuK%leqNuqc)yc7RZc$3DVvs>v61iW_WtN)e*7;4mI-(tyk()k zzryog=Paim{hQ+SRq#dE9OodPFv@9J**b9{zpK6D9oWq4o*53FQ*QN}$aOXnZFW2j z9Ah>0!oRs_pej6(li!R+oHl0UOv@?beiJpP0cQi$<-0inOZ>0UZn(R*yFUGyYesOo zkm2VD9OPG?=QA_k&zV8La#SgHu^X<&K2==c-+tRlkVGM_0(;wX@w!=IX@u zy=-k5O8xVyrurQC0I9Kv-^ZcH?}r`^+V99oTbzt`UF>nS&bXwI*TzxhulfNWlA?)R zL;pO_mWRpc+iN+S&8DUU|IhpThhjiTF7NN3jDc*_yiLyUaLg^lgkOZa)<-)~igWl> z9}wfhFue@S-~g(Hz$Qj}T)i+9jt&(C^r7w4NAP8%Dq;Z8MmL=iei^lT1?71#o>nlZ zvM7r3{71AXhI1_HCyG4Zx~h-#+>Hyvhw?$x<5Zr%fyucQ_#y|#VTj-WG;!66p`45f zDn~?RX$7-8nc|hG|J{BcmmGo-#c>1Aw;&L7i24t4xZD61GOj2LPTsk%k7j;{9V&@D zKND56JN5VN?ppB-!fjZRawHDFalody7VptsU7i<(dJjjc_)>q=eh(pR;3T-Ono~c( z9-<%s``28UcjKb8%V+)Ex?zB&8N8<&s>(6uTxvf+fc>cWc z<;@@TkVl_+>o1SL{EL4+g3A_xKkl-x>YWUCas1;rGQO~*wOr}{7yem*`oG8r*->Zb z$_E_xH6Gv5g~M3h-+_w+1sbPIZDf_9ZaPY;ZxDMW*b$O~WA1vu3w|Ht-|3q5aSo0W zk8K(r9BX!AtDrR<=&@6EfHEEz|6$nrb1r`;4mzKzev5oj&>c@Ze{x{3S(hH~xXzxJ zF+4ki=lZx8J8?i+S>7vN94~(N{D=6JCEy(|9DTlrel4+!%T74w zH_kgQ9ESe8{LlH)5BCmpjfun3mFT<1!MJ^U26S>xW5@ro&c!-6CZB9-;h)M3=0x6* z3u_Ky0H|G>$$gSpc zpPE$%gC#SiUz>oYE>JpC-`SmX;{Ghc9@O{JfE@10+h09V*uxJTAd`7nQ;r~Dd@{vzNzzr9paA#q<1kVFB3CQ0J> zvcj$2;()e-twd`w@TzUf&MO>v2Sl&1;;h|{rTE1@GFPP{|1&yd9l5a)2VzsdeZ>zl zuiyC|F9d#Zzfbml0$i$gu=@`Bu?s5jjC=nyu6n}`Aizs|?AZHQ9E|oByPV??uf1#k zYPrEzqwKE}(Qjg>-dedE2U$jF=E%efICyK9Pw51Gg?|~+tTc3#wL9y}jQVtMMrv#_ zdWZk;c9@=~lV(l0_D{V`c^hF71uTdb+?MvgBtZnc5Z9itp*HLZ_G@~a*P%-S3+Uj_s^LJ> zKBae3g%h}9{i1hg7%hMs>4%Iv1?@T-_$pX?4cL@cWIir4zkHW-2UZYrBQR&rReShO zOAUsaS5~j!Pe_K!61qd_9n`;2{e5P zj06vY4;;q?qe!&C?NJbT6YL%2`-`~vfqCOj_8(Lo>MwH??|xu{(GuMnJ)0DhE&Oh( zjv7x7kp{ukJ8>9a2P$sMtOs~iRmRmWfw1zV@`!x6C-jW9GD&~;cV?3H4fg6mnJ_HE zrKfpsHi;EmDYpktLN6V0bN>SD&in8&=BSWPY)FG@M4kjePiery#vxngu8DD8(^+l8 zmNJhUc{>nz0ZT~6@f}pn_?W{n+M(VJ`npFslUbJeC`e#-3h)sw{06`?SV!QMd%!R2 z=N8zNu^HGg$vEc%^OGno<2peL#p!={^}(O=PukgOR3~MF04lD3`Tiw(DRf=%apD4Z z`0f+?h*>$5YafL(R&5^NQfB+#6Hq^sY_Z+Wy-&()yy@KGTH@i%-?-I ze*=sUbbxvZoaBjp1YcnHR-zu?IaYKz-iwE_z0`jWhVD>a_~-asY98fq!!3a(`1#%3 z@qO~=_&%q8x6xRSZ*A`QE;1UKuYBOEE&chwHNNvu-;?oKjPDa>=qml1W0%a;Uvfxg zc|3nIpMRgfZP>SB|L%G`KEscMh24~2VSIwm!e9pbmP8?sjF+Srxs~3)omfZ`Dxk@i zH60g%J_)?QvrcNey9t5j(UfZw)~Nveg;2f|c$Abmq31=k(*oE7Yds%O=2+U53qO#f z1c8t1`3uRV&lD(C=yWkV%hv#puwft^I6yfyeTKs47gIjOOv^g&W=xg&V_$4Elp_JJ zA^PNz3-BZ49&N`!3kFG0_ZXY3zbt)~i=6LsmvK%pg5=1462B&Bz3!NCmTRC4RRR71 zK5(9%(vHvi@SPamUyaoD13J+A_5#16R2tZg@0!38U@8_UCu_*~T+q#%Knamm<=+c% zo*I zHZYzc%}?;`WxXP|nS+b6BVT=MT$K}c>_Ms|oK5KV3w>430k%%WQLeznf$%fNd|dlq z%F)w8y#E=!zYok*`ic@xA8alD6MLThhW2yJHV(^`StMmt()y2&aZ+D(5NwEKly!9! zo-8~1^@$Yr3A-ynJVVuhD+*oasEv|nhqiQC%g0(8gK||tKfyPvyepgpdE%&~^`}sd zUKjp81WwltIG;2o_=EkdHN$$uuy1>Qu;3Hv$w@;}UqoM-;O_5!0kq%oStvXA%_Y|F zRx0egsS)5ffc{r8?Yvy|V;lbC0%T+4U?kFybJ2#h%EDg&9YDsd(3& zDI921ZsGU^#USUpTxt0=`u6TdHygjlPIxnQQ_5uY=bJ%igKfxVvEPmm`sew=-ly-j zA8l{TJWGFHvqt(4C$JwFz9{rV(C6*>$}8wopKM8d`?EnL+}5}oa<3$K+UmR=3Vr4j z&Mt07L)Ld3`5ov(Iqs}uHuQMsQT()oK76l0V7CC)MUk+`3x&q9f&B1;>50!EMicKZ zOPmXyBeHtCchO!oBU}7w9F$+cssUc!@$2DxndPuGrM%3KQNwx30iA}d40<=P7WG+~ zt1_9VjDnfzIj``Y@gZY^T7bVpemnfbNYJ?TpkVSb!-y+!y=DBjs0quWdKLZ^{C7x< zNLI}Mvv#@{J;(gsd)K_q_kZ!Rd>Fs)h=7m%n3t@iw4<5e%VGJna#e>8?)a_uxx_p- z&Qaz=@b;5uo`oDyTYYM{Xy5yiXP}qwxku(?xEoO~X{F%JT;PuMKvzh~N=SS|&2lVI zTcE?%)^FA(-dpKe&1oWrPcPqIB%2zU2QrFht^^M|ywj~V^lOGsq^ls$A4dwJcUt4k z_Fnj}%XDpa4(9!zJUhJ?X}+wpumk#W9B~zM&=2cbv%N*X)LF+;2Y5ms^S8l|y`7Cm zw2SUBMaxl>#IFvNlhLO9`TbqeaiCYu=_n=o9zpOFMZZb3>;eyw6FLrSdD4h==b~H) zEpj68E4--Ncz*%ui#sXbuqWv&4<2!833S8BrcFg}P;Zdnm%wvZNSGA-^|hZTGz5Vo zG58&AuB_GrYGFrBrCbAU&AjvNT=|ZC*h+a3tuzM;{|p5-L`rM`M&L*Nj`pu2&!CY3nNz`wk>3>QZ_I@?nDCq33`#PQH^8Wsj7;yX}N1Zq5e_`Nv%*FGr z`S$1b4totO>H`u2P{i5CFNzR?FtHklgJ3d-*t;Aid!f@kZl{U|9S1(D%JmH-D;#g&e=|J@%R73^Q- zdnyKZ?m_CyvgcnE>qKHpdE?8QzdZWPTYq`{<-hXJ-{GIaC(EKXi$frW4BP#W`KNYF z{DpsNhxP;le!22N^YJJ0;UKE9@ZAU2LHQkL5LzQEBcUUp76GF@LfdhmWL-?1(fb>*wP1d|M_ zh_|(sghT3w?2g$pdH)xCwcHWU5s{C)Q*bzq@>}G?dqGcyjtdFpy);K5krQ@D??c)7 zqUJaXEBYI-GpMLPj-kMYK>>lZZdd@IhFg@n1ti+=$%7}mx;@6xp6EW`RFa|4e@yJKkqC% ztpxJDa_P#$%JDs9+`HBp9FZ@DjtdKBe37mluJY{MX?45t(HC))g=G1-F+57{&mUW9 z1LMr_PR_#M2|w*$DFTO6z_+eBA#d{D3FYv?A?Ph)*S5;y|F3dBYJElQqFN6Z-44LP z+OFKoor)Sp^nwB#>Q7_*C9Obl*x9x1t6qzC&sb{`!6R1>Q4Me` zr~anxm3e3PTouP^=5;-nGW<^c+l0F-P%dNqWH$h=bR2g$d9IJ+LsgjaDdRhC^^_f_ zGbKR8Ne*SKQuMQr_Zv@p%3oXMy&nPRTT|r@RSc;RJA`rx?~m#xc%A3@IWr1JrBBZK z`?r}-r>kzk`}A*^@7EDcXg}Wg#9JH~L(cY7y!Wj=PN4@`gY0M6OKMnk?fMqqw>DYf z-d6lR!+udtjzAx~=0v~9T$JgT=pryb?q=N56+g` z4gSw>JU8qX^mDx8{?S)1(cYR2S2p?+Zn|f*UUZ2NI`eH0=Fm+UPZVy+#o%KecgHUS zMx%c%q@GW)XOe(`{fk1+!cDnc?;tMA-uq3I(^~cPD3%w)pEWIy-|LPpb`X1IA{WJBw;7;%AKq0wBFXncv|jarGL%2ja80D ztx1SXc5t7<`GkWNjdm%pu`yDgNRq}1Pov!3uBCZ}$6tEp*vC)N$dTZD2a0yC+=ZMazH0Jk$k4}pDg z6ri`FdrGLwPzw~dSyQ&y0s1uXDtMD|yh4BrItI?rfuiT>;b6*@8NpA$B1R$hG_!?} znXAh@9AvNUbjtZvYuWKlnhMm$-!pbO*&z?s%PH0kz0o^H2}fPm^D~_m2T|V)j6MS9 zC&i#DFonQi8(=cO$anH{2rcU!d{)&72^~16AeqmsMf0A__m7h(Abh%p8YVmWaVJN~ zJd+e+89im=?TNgsv|DZxia5e*VrX5xno? zgs_3YU@$#V*H_YX-w`W9+F1wXv0jJTI2EV7>_jG`+ zJe*CwC-UzA5c}az`8S+0_R;?;|ITs1{yCn*KGMP5C-zY`t0945evMVey5Qt9r|>JI z0%=o^$Jg|4;|te`Qhr8%^*+z7fC6sF^U%jp;JY}Yo~4~{2pAlqTn4;c1m1)Ce1I?W zUuS`bIX{m1WcTz(`g;wL>hMoBij*6n^WH`mNo2t?tz|C;l4hV2uzo{|v!3h=g!{jt zzBUQp`*=Q(dia@Q{uJ}4UZ7Q($6yi^ES_J+jF$rr_&+qhKbRfp5o>p>J}IvP;S0fo z%-=!aU*~TG<^O5^yie@g-1#ep7E+z(tF!7^r;M$ntQ&an8oeGS5`KkW8!xL_nx2Xg z*pcT_NxOLlC;{#-fyIn5gs6p|RMzy_7>bLdJcAH_f8clU>tjHfKcRzrq#C$@FEdLr ze^18ugg&bfI=)K%2)I)cus7UK=&}s6^*ykh5l!kQC&wU#@=<=W{(Rvd^Qgaww?Oa{ zWxMqWCw){iY-zdcy0dmxp0MMDpK>K%MFk%v;cO^|zw)4Y@H#)1l%l4B;nl^Zy6@{}Jn3@SF%84+FMh z#Y#K9NEl*$s-~)r&^hka8e0BUFv_Tflz+n8A#@hUc7P>3SBF%M{nTeL|MGq}5<*`A z_R6Kt;_ySk$7Q}Me03@-k8#6eXK21qN{v;bPh949*P1|I!uQL8^^w0n15Y76pnIsw z&ZW14n!lcBRr}D+(`vl%S5=d)BwEM}U(+*IM$S9gKnSk&6@by^Ud)pBy>m2OvQgM*G*L~6x ziI}YJ80UBiwBGrD*n1BsD~fgRyKBeE12Y2*d4`;G2FX!DqGXjIDh5yl14uA{f+%J{ zQBe>D6%m!3K_tVFGec&Wz%X&o-n+ZM-&4T3=iIQ)eb;yIdhdH3So)`WcUM<<>d94| zH^TQG*Rz9fquCptYtCPsIuW&SR+OW_H>w_;Oo!+qsQjAsgh~2j_Hp z2(p-zaU=_V?;YzJ>No(|K3t}ES?Y;B7BjwB8fwt^T!H%Q{Feo)(gRmHyUp*YMD<5=kXC#xk#=)5F?$S?AYFU)z%k9Um=!?4dJ{ zQGWHtGwFptrxYd7=on5US# z3HMhGer`=;yr<+>68$jrw~#MFwxB-6xPp?PkvlC{<;pt0ZQm#KleH}4qwJojXX#0e zPQyp`w?4S@7~@xUaJ6p}q38O4)U8&9@r|e&{<9bP6YvFk`76@zhk{drdl;9~j>Vj& zWCUvFPc9hCJWcSK{tMK9$uV2_sVCfh*hM=C;tvo##&I~Ly=N=GD^_=BFokeBl@Z>+ZFC)EE)K}KLU<@crZnFbP4OZwmTQfcAWY^$p#k$JQq$w$MLV=pQWf55h+a;}8Es<13rc zm}22me_#a)-%?nQufoTcs5rghvjXR9qo0uO%|`>d{t;(t3tvsd(m4iRf?HZVHWd9k ziSnOlRw5TfzefJT1vhH_bufwDq8GKrD(nUN46iLZYY1Bq&?Ah6*oleYI%A$a4fLw! zBwvbsv4P~!3dRi*JIlD_O5vZs@CZ2m!hJ97x{N?9YT8lnlPGcTkV9J6{tlO)D1`el zMC_GEA$W<3C(Kw!U&#~w%;z$$P!X6`1g z=i$&TMR_)?P8<$ZaWp1V{scq(F!+Q0mi;={P1qcYBpLhb^v;HNQT%wHy;miQ;R@Rf zXh&iuU)#m7!x36f34`5_dW~X@JL`tL2er8p%nye>W^o+&BkqV4?mKJ0hLcqk=$6{R zZGoBW;r>Jnm7QF_e5Zc(1pB49jv-EB1h@uA#Rs5=eR&vOBr5FDj-<~}Y@>L-1&+#r z{CY87CAsb^{VFQdyy`G$3Kxrcljr7=R5MD&K>!H z8yWe4Tj8d5i1alagxYWfsV`BMC4+pH$TxSCd%XJ^4w?b(y6)CEc*6Zx0-xgl*p^q{ zCvxf=>l(5|9Mg++BHS`nlu(v|6Xa2GsJf#(bv-El&K!>R=Xvvbu)uWUw-{~SZ#L)o zadvS@WFuQH(K(O%;vCZ)$b+!O5k`0$#LK5)vEE;}?-e7#u7V!FApCeJ^B+~SNR3G3 zYDA~Vfsr_J3NHH63SP#)HZSX&oG*yGJrwUR-iMQUGX~j1sv>5-thJ&g4y?2Xvm?`~ z@A&e|wT{f;2+lWZY^}Xsc0eV$PcA0jGI?Lk{h7S)NvoaNg?zj#)n7=Uo`kODUMaN_ zKU7Y|61Dd6{_dgAh2cmFE(p9XjdN$hc2F}p# z>(2Qh)Z^Xd85%Nxe&RahHp7~vJd)UVa^BUGe>_bb^yJM|r*H&zjH&zmP_8GM#cDLT zN)V1(H{$4LYscuYezY&vbEDWB zv}dXsVYTEvML1OZBXE{+*6}&@q}sdf65wF_B?~@pDSM#RopOR*-Ss2b*0}9b#2!*I z>zu^Fcrxz%3u~4^z7H4NkRx!b_OGzH%LR5=eHFf^QUUKDm>-?@9{xvHcKq#*v};x1 zNC3yL`6oN?sL$Un&^S;R+!Z`52I%FDH&-0{8b4v&nFsf6AwE9ySlbNxaj41e(r0h; z9xJzJ9{psOVw&E=7=^i+O#9oD9uWb?;ozhFI3!O@yQ;jX)YD~+UT#q6_;sg5s z0{abR-DMgBbbz7+;RzX!FPNF#;Cezw%pW>kISE8!9LLMddzx->-lv;>lQ-(jktfZ(L=YxO;4ytt>KJ@QQUW#0ANM1B(szO zKFI;DGTz1bTM~@qKbgk@0<4w58|=ds{kfD;l8R{I2YC+{XW5t z{uN*|?z#ak#_FIx0?|!`-q5eZw0?bs9TA5pf69px?YJ*n)yFkrs3Po#fNzj4F+kUw zVP|N^_#Uj3Bh+(Dw?YDBv05Jwp&hfhf2``G1Mrsdm?Y$|JHMo!uQzD-Pm~wXYt|?K zKBMXS3;}8xr~M=Z#DdMB4i{J+y4(Tg>hPVMaCTEWUb2xhx42IKu$1of?>hJ`^d{qe z*M6Y3a9lrPmmJe})m{66bdb1200+LU%Wz&N^UUEVTr%CFxeOK7x#f7nJ_V^&I+_2__jE^p55P z`0GK+OYZjXH)g&X<-Sni^WY6A;27inky=2gQ%<5Nr%#s1hhy)fJcK>sa(^O!t1xe; zNT<0(`53EIQC`mq6gti#v@{;-r{` z!5z4Vyk6&op)FujW>n~41FKZ#e=m2o0SC~&m%!_^i}@q{;0rx}$5^GZ?#~#% z+P{EJ7*!{Di@!PGJ`$tk{^Qi<0M|i5>7dEqsm#-4!rPxvzPJ90!$(?ieU54^Cwdsa zsvTk(6LdbW$K@tyxH zcpc|@2v~$qt$av7Ud0jo5#<~;h#C>t$SvX5UlDE>`L~;Iv9<^IF)s34IEfZQpLqn( zTY}FRZQ%_hb`hWzx^eJUc((eEH*}^P!srYCxxo1{Yrrmq`DGk_px^LYCCIqi6p zugHEz*(Tv)Jy-$$AnjU7?PoPUq!R=(;9e~#M=-j5z>COr>O*=AkVFaVV{4@K0A$gh zgx6>J-NPqER?)l_AuoB{(rxv=13GbZ;;XptasOmhRp!xo-BHXjhk8z12kl($4{?3u zd=qhgEp&?;dUQ>6RdaNq8<#>J_N-=Eoyhz-`*o&mS;3}2KJPi8K6XBYdXkYf!MBG% z{lh`!-EwK8_yMB&f_!QFpEu-5kUFn{KZCTcgXc`N;f!xlf~qb*wV8EaP6k zblsP#oJJqdx;;9V_E^u-lS!Bfoy$F6dW-ZoxW#o0w!iJC9X~N2yXGhD+6|p~w|cHb zF@EZ@niRn}2Vb>9f@K-bF^+C_I5@&ecGag}L)D@v)`LIYjL(Dq<5tA1iakZ1;$tKG zJ_Fw<{Xu+#;`Hlwz3iBrMXaQn4=bR+)P3Q*3X;ake;qXKI zhBZ`9cFD`R{djh3-gCe=)PIonO>?dmec$;q6is`K29BSVN&ml#d?TE7Uz`hAom}3>{axOZA(pruB=1mqvj=WsJTFIKzcykR?BGr_TY8% z8?GNUKenJVb-?k2)MuUymJE*Mc^~|49rO5JpoJ;?aE@xGa%q3_koDfLxWA|4J9UO} zIqfO!{uZq1Ipdi}`&K&Ro$qu1HTc{La4COD>uD#EC_-!7Fe(;Ui;MztjuQeoOQFAJEXQ>-#2Xy>AJDsWi%U;3|IFRl#m8{!e=g zsaLE6xHA9e5ByV3w&NtSJO0lAp3on-J`F7;3tYgeE(+rRL`I0*(f&_l{i|B8&quE~ z&GiFlJd7LqQfoMr$cO)|{&GjYZ-LK4KX#gZ0bNhzdu2%^EA;Oc`mYQ9^Q?!$_`^Tg zNB;xjD+h&dv&mBq+ZTOnzP7(CEEwVI)3FEh!B5aGLz%xuh6_tnPM+JR{hTjzK21fQ z-vWPDT)%{6fZW3e(M|MQ?94o^=N%wz0RBw6XnQm};lD+7;#IT}qZ6h2p#7nz!2Me* z^cZwGhuHrlyVa&mvfW~^{XKEN!aslM5fBn9{PPzcff<8m44Ls>+Xt-O))DK|bqxa_ z2R;hacfS_w8K@Wh8O1+3SU1oMY#*2%xB`v}z|u1{wvsgIxl*0vHg1 zsKBEE6v{wWpj@B?SUPYu5ClU4*?~kbJWwVu1k4VU3OK<^fsO$bL$cjpAx;Hj`wBoPEUb>7$@b)XKR`SV>G~G0@NA=n%*0eW<_~M407^}fo zKbvu1zR{cGNKx2ToK|$RsaF(TFJ?6T!;MjrT}a|E358kXm{!9%bewuV0So(AIDduoS-AfGy8IFP;DjY%2J0Ef2c=Xl{pAwRQ-(_{c87zt1d6GRiwZN}AgdIN4j?oXSFrKT< zSKHyws+;2*g(F1$uG*;E_$|+SyL#GCPIG*~A=$_=|GRzwa9X`j`= znoUUA@O_tJ3`YNt|5gbvjxR4X>LEKl)*}#FE3N0UcFywu9(szr^j>3U;%bSs% zh1|LIN#>HQH~IaC@QERV`GjnzF8l6lVHhLF3}`p39`Ak993YPBFSxm!>i#v}XkIRiOGNLN|!`Zzyh&&GI>wU_NJdT>{tyye0 z{ki2@#S?S3K1ckd`~;}YAcf3vD3 z4mP`mJzf$y2-~dG4yzTMrNn$&dDfe90)m}u*D|3$w1N{39H!P4JIdP*$LRU=yrc!h zy#}3``14^lNcV_dTcH&G^$*HiZT%zjZh60nryY5H^G4=uA-?chny;Au+qkNtj85+G8Q*n}gk-LOiG;0&fvMb&9*GX;(DyedEIfwm3AkG;YQz-^wjumI z5VTZ7=_iiU5M5s?TAuW`BYR3ufgNytN`Dz@(f0`Zch`>GQ2S2*GSMR+<=1!uvaBxx zIScrgJXfAzjyRSF=-(R?HtI+@fpPm4%;9Y{!EgecS=3vLmUN+9kM}dbjC&Ow;B7@H zN$ROWO{2igj2d*M*97a6SuC%l-e_hb2J~{4#zN|=t11W|gIAW<{PjUNY&7K^w6q4; zinN#{unO~81XT1^)>T_l6=eJ`qFFx&UZ5pH2M%_0%lc|#f2_XY_EsDsPe0j>m!Jd1 zRXO~>3BQM_kvah(pPK7Z7Wv0GstN?~M1L;Ak=P@6?h4eF&GmZ5qoSunOG(rzCwE=b z_xC|lugY~dfin0CeQQdHO71%)J09u99sA)7j#T&{!76P(U>n@EAJEWuay<;qJqNUK z=adB7-MxN=RDc&KV?Qv%VrP6MflZ{!>pEc=`$5y|w!8L2bwaC6`5pT~0@k$hj{T5H zJ*O#$-nAdXdCNc84+I3^FT6iV2h`-~$8D555#W^e+>?KK%tHa?0B#}HJ+d;4wIJ&! zLHEBW(p2UN9-`;D{@s2=P)5E{{|QZ>ZSP#iKH9Gn>BK&2!SC5>y0(uv7+Bj!O*x!+ zG2?x&e_|gs=RO}NwCMi*%|1FH^MOuZ@GO?CxHKV z*+*5;27aR5*BD2k$N#SWU1$9BsK27>t548N;j8OHXDjIEcJS}|r`H$u-(CIXj{PU$ z>_)nML9O@HWYr4a{TWA|(DA>_pF_V#5-Eyu{jPlrSNNNKtLx7u-9^@aWdh1?fkL0M zZvM``)pY!?=`)LZ&TwBSvseKPr=3Ed=UEr&U_6e=cfoXHi%w<#f)Km#o6dLbTcJ;( zANgS49s2xt?OWFNRce%@O&vTT0j@SWu_lx2LO&@`y$g&qqVz@r=8xnqNz9Xvp-7

3&^~y#_pNjvz$?Gi^K_ib@6b?roqO^kglB7y6_s9}zgH1pZKhx4%; zvdd;4%bUS-E>9hIeZ#j+mRM>>)f`>Yfg(?>uwHkGX9s6lP%e4zq0h|o?D>9h&Lmj3PU~= z9b5GI=>4cof1EvZ9y*Pg7+W@sdLB-As7c=+$Ul0x&YAYmK_J{7l?^`)5-82_^S)-* zHqjS7yHxkcmGmdqn&<4z_;$5R3;mKPIK=^7AF6P&&4I?8WboS3CztwY-ygw`9Obz0 zfcdNiKgb@FeIoZu^uu@ZHwTyV+=Gtk4(KxOVr+}(@0k3U+O4ZrLN9#enCo;bWN;(v zR4)Ap3=Vu{X3&l+zRCVqY1av>Er-M#)^O_pZ8RLOq4|G8|K~Y8GLEhVuE$+GXT8O0 z9Kr7a=Ux}@bq?Z8nEBLKkJO7O>KmsP*h6XG1&2>Qb<}Te&*wYSulP0b#fw#?oiF)b z&aXlqlC^dJo+QX`#Kn%+zd%22p1aYvj^}ITA1T<%`}TW2bC+cPUW{#BWD)Nf?&{=b z9D`$nF9zVRXxVe*#E9Ui{5;pPIz&q)71}9bjvb zfep7>A=mI@#S1@G%Sb6j-mtCVEi^$xnd%#L1pGrCSLh4sCv{jMuSi1s6wiaf)m`c~ zIA85k*T4+p6FJexc$O2Cjs~XUHydNL?Yw_?!R{lww`}>HUkd;H1xLW~7wmIkm;ZhQ zGBG5Bq22AT__AXV7vp3Y42;Hp(^zJI#f>3kf?_D(44Ww5*dtGA1tz|n2(UqR(AaMn zS++2iL%8x@1DE0m%LiAZ?%Ckan2jcAVcx=sP_CD02kCC(5$%YbgQ6;oZ8JN=_JYY6 zqN3;~vDcFK@f|#`{uWhv+-hE~?^i?FW6uwmpvS?Y7|<|Mdpl~Zv~L@#X&&f9O|1t; zaENFExQuTA58=KAD4rN7n8VlTiyR@`-#$#)&yiXfJQ&m0#o@AtD0c@# z*&%leyp2g9{h@!Vuz!W~S4f|Q>#vZ%;IT30X#Tm9gRP3eA1@Hd$^wfaO?rSK$PMA2 znMi=U{PT`{KwZ8gAJVh~BuF?xU& zaAvq$dMdb(H(^!6YlR|rQ^%zZzBLUoaeuaz(;9I%KIY@7MVyE`vGyZXkW*c*ww=(? zhd<^!Z$dcQ+?8`_pIn?yJtJL%ob>C$l#4%Iei`w+>e#vCO{71hE!>YIvTo5K5${mn zydv+1)6b9wA)P~!=P|P*qoW_fAv!Aijnp@3?>@)=u(xnv*0@^s1lLRYBhOU8kz^jQ z=9@Uy{JpJnxvwK;lRQIXm?y^)#|?4x5LoXoJE$&JPjehm;PsToNfG#?zAPVTIfXom z=gR)rj{d1B&clJujB9P9i0l9}uUO+GVWPlcCf-hDes(&_gjS^9)ApA#Pexz+6@+n-*kP34z~_izWe>A6#o)5y&2)mXhE>8YkWVcHD_Q;WaHtg+{^mDnxARBUF`WCn zWjsYnFuzFi0-+1Lh1J2PzxECL4&Js)TJqiuYn1F9;>*LM=El`q$&)@KmD+A6};%ZI}vr=RoFT zg0x#cwqy{N9itNjBI)0^lsmv3GQa@T^eFdNw^x%yYT(qYrC#NF7u?&76T!1VS}%PN zK7!%S7pE7f?c7(Nc0kX3Ked$Bb7fC;Hos?}6+#C*cSd$T6V&$Uo#mY@)nFwU$I$c4ya?1YZ@4OB?>ci4beGV>*(P5gy^b&Z>c`%X6ps zDz&`FVb9b0c#5%H0!gILekX|*su?YNjaPrxDs+)L`i>!8>p$H2Sx z17kHr><6rvNbOMeBQaPvBpK&wx|Z<7m1Z}x)@$YDR|b-0PvCUTz(t|oNRh7o)b zY+y`-w&e)-mb%`qgaYeR&f~yj_yBWRj#LKOdFR!EdVJ?R71xjTP5 zjPp96`WdTe7}wWv=U&G1e$`JW#@z&( zf0@5SI`PHE(TH3kDePMva4Jr^i?qKr0j3GyJ^GBN2TLg@61EpPyYKJT-*L+K*55X+ z{|*gpVSS<3(Dwz}ey_nQ6M21*eoX`AXzF*tBt8+qcnEzGTo$=AUGu4ma42~`T{YGT zYiHSM-h^^bBuEnX$8&i8Ym`fq8t@pH%V>7y`onN(;gb^Dp2z>opf&yp-o%5^15{{_ z#lR|XYvD&Val=CQtik5`lQ3<$zZ&JEXc@D>guC`(gx?0HxIMKCWSR~j+f;o#roA5vT7Uyf?4{h{(E_AaXtUy1#L7DnD2DeeZvi=~r1O7Z1bgxy&FTnPLI*U34NEC7{5GjBiCJZmQ^}P`-TJ;S?j?J^QyfYa?3AyykrsB z;|r;u<_+V-0pBv;j~>K{XV(L(&e5KW1t){try7_ag+cxF z{i=)cb9x=u;l)ONoNuQ8chrmTZm65kUs#E-E+M07??BE3yMR8wFrsZJM!TZeNCgbK<>}cbtPrG+G8q3af z$HSg=F4ncPr}MPqH094-3!Ohw9_BpYWPaVVJQLg>a{v8~0*B}a#nGoeqrKaLm4nP* zFg{Q&DC6+H<88+%@F3L7dUCnkecWBC=SSBDX93SeAxSSnCy(-1`rqHbD!}*@l+CYK zaDe;UdFHx#uWIAG<$%7-cLFT~+jxG7YlyoVcEksvM?!AUzPipXPTq$cZ6qfcu|_ks zyox7nP*Gl?+Hg9P=)c!xoO$CRG-LRpdQ|2an>kbKX&LAXB5zXB25fLPp{7>gS6rbV zaFZm>2U;1O(RoA|2KYgSO2_c zAKAac6(ivLl{nL73H8glRDHobB2jI?bTqX4K#3pK1TWaf^e5>QNB0ZA-N}cXgs(dB z6BGw)sW`2-mQ_z_|9~IO@B^;bw7*27hd-+^>Ju;mYYI7yo|mQlplL+Skk4Fe2oLUv zE}m=j3+>L8!aslE5fGER@Xud)1adJ;-wNvm<6CWy#g%pz1rb9M2g&RDX#Ll5ke-1- z9zmdjWcQ!f2I;RP3LK@pnLPV%_nIMaob%1 zSHSLs5#7d3u?0LyKxaF6-9CeJT)sK_zGV2@J1{qVT;GZ#wmP_!+|@t8Q2QC35O59# zH%sHt;7ABVs$0Pm-aA%Hr&R}@S8a~$zyDOaZ# z!eDpaVc0k-uIYO7$$Z_xeV_5`cl^GR>n50kVgC-egC}nD`#IEO3|b>qy`y)E)#Ttm zSzTqQMHKh4mV4?kD-2eu7n5XgNR>}D^tn}ONJCcX|W0n-}< zm-+OJy+#7QRtu}2g#wN{%xmGG;Y*^ItzOjE&^pG~dJS*ekj~x^0`(P&oi0KkEHpgg zxv=+e49#%A?P8uijl8SeI5t8XhII;k2Kyk-x54)?f!>qZ74mQ>Mt@YKT;x-9qhDpK z{2=`LWxMQsU?O5GqFh=n+Lt;$=auvqX;+wYs6870_}ar~PMx6p>4&Q(;iz|nINLdq zuhwtYnXr_=o`Q8{nN9I-Y45U_Y7N7=O;%h=!#v@!p3W&pJQlJsq9LoR}4vQF(7%XB?8V zQr|uh!~uGx*og}3h%4rnD%G?T_kC-8=zE*?gxU=X2#^GmY*jE5F}>A%A}x(^8K=rH zT!w2XW=bpSKaY#29vCpD;zE}|+{ex@!O!f0h8HYr_p~d51CYikV3K`WB{2^h2z|W3 zb6W|&CUE_R@tjqe_hy?P5#l%OP4-4PD&KmI&_6$$KU$$yYs$|XWrGdDx2@I%IQYym z!T0^lv*R^&HM~9Z^{jhFao(#AJIXsMP|p*5^?eH!tS}mDhgxT-6Me__vj3BQSySzj zlIUm6vqs4L*yB`h5956;I&ID2lc+I^FE?BZ9hRI^{A?52mH1?t?(KQbS;FZJ{|0)f zpVu~T74$GV=T?@*c)b{?6GR^~yP2VO4eFa`r#qOh;8fFJK)aLAT~FCxnn3^A3-QMo zx62XdYi82U(_b9@y`D;yF{OSjW1Lek>6R8j60_ZM5411n4mpvxE@YOq$lV#%BitnWA0*J zNEU4%>z{D(GunaJUp=86nkl$aS(k(%vI$1Wdyh+innaNlf>nmSJ4Z%Qm#rG$p+e$ zrCRB70{3m_eXrA-qWpfhe*~E#q<@5s&gsNhKc5tl`M5>MSKeEm@n${f@nwA2?^;O* zj$>72{YesW4b}9l$}<*8uE5c&8fgZAS_E;omnUfJ?yqAju05AZ}xs z@8hbMTvrX~pUj_+_D%pT^wuHZ00KX8;0w&?8Zd#@2|Xl12@An0aM(TII7ZF^N+A3q zxY$ln;)t%sUGn^1v|X8Bi_$2t0(|-w{ra8Ny$#&RPB!SozDK{;klTgUUzhSyRYBVsp(K*MLis#* z4h7|?{+{4q+E<)<-$c$xKbmo$(B~CK_g63+idzl}b;4H}_1&x(N&Ap9W~6^#LlY99 z)D9oU<*xohpiSh`zt|7g>AS2quih#A2Udv8#|2#6kzg9G`pRHU%0h4T*h44vcBJ<* z?y;mToYA;j2VO7Zwvu(Y9ryk@up)`fLYF=)ju*kJyh-SxGE(dk*oV93f?lZTb8sj9 z`2@`4tcB0OLi^~>{QXzypQWKt`BR#8{yvzGWxWOTlfF_N+{G*ieSSxX9R6T|3nfij-@xzGR)3gf69B@&S=A z!8pQ4LWlBc!A{^+QecF?oFv?x4Yt7ET?Q6`bH4}PTYuHi+pe6a!F2VWUVrsC_Xj>iD|^v1;iC=oc-LTs%6j#{-DPHznT;;s zPNS_}U(Ml@(my|(`(^MdD@)d22M&To!60qUtH1f$1oI$h`GM~jbac~{KPsax% ze6ks%>Ib(OUXs!b!u*GrS*h=SO*a{E2lya-_KKDdoG&7QN75H7>kSDBNGjnM>BuqK zuM&(*ooEnFI;6B`n7X3pe=uBLr(h1gwTYL>7 z|8#=jJ^A+#91gjr=8#%(46Lh0X?hN-din%{Z28(P&rPNM!so^^YxTk2YNuix)MaC< zqW_%x(;8>EVUM&%+bDq6Eo+3844FRROZJaM-T3giUM=p1M===$NcWT_$82F<92j8E5=rf~S+9#RM z@?5z3LN>ZY7m1z_ISY3D?#0eG64=MmW#cQGALjX|)clYcEMJGOPu^Sj@egO@U1vV3 zZ>V^*4(|!C96H#`yqAb7{(Rg-re##(*3!~$kI!GQ8S;9``$Y)j7=A7Eosh0f`-_)8 z$moVxTE61OO5S(UlRW9zbG)a%bDxL$9oy}YaO(Bd_n*pR-287_S94hhfidPc1t#N@ zXWt60Wtp^gJrY(GGCf`ELeq~?*J4|jYW*AIl47YK?=7fYkXHcRc^i8kcCp?>B&c&iP5_V6M#E62$zIJ~2Oa9c{o%t^MJ1V4B=Cv4#548S=J=O$M*7xTg@>#X(HR%!wsK*JhjJ!%D{J^s=D`zbFA9+XcAI6HcF zxt`|wir~HgznfXX{=rzrD=DmRDE!>D*7dq8nfhkhb+vxA)}GB8M~{MwZvsjtzq_ZuY(zrH{O1|#WF3qDQ`U$5t9?WG7$-30NkRzT(M{UkI)Bgqc~?G&{}Vc( zZFOY6spq;*z`c&A6F$u7tt2JzAN-$p{JVGcvwQyCQ+NHl)zSN}aeq5F{tWOuHNODv zU?x8V3;nx={%h#7&_7=oe-JzJpT<|tqs=_beS6SWtAb_h$8>`9LvYT|DF0W)kD5_W zHgUBr@X2{-#G-Hhj0T6?L5FnO$bH)Shu+9^4eb*Bg#`RLO4?IbtwhHZKSEm_SE|Ub z^C`=jxafIs_dhlN7yYv%n#|hm1*TzDAJb6*LlAJ)2+Q$U$CSVW4;a+vo>2O#@1=8v?;jR zu5J$I`8j5H3&v+YZa!yXSm0nEV@{{slf!VPqB6!g_r$&N1an-#9Mxhc2n;S@C0|p6fzcI=u-+;z! z$8g{P!_Rb}-@`tz6w1HgsQ45`dXCYCT~`MGep5RfQmGAt1*5-`cBIrr;r*C$CgyY# z?(dB`okzJmY#4)sKXZ7q1fHBYYs7IQUp*CvNpJ0_xkP?_BIQB!1H%-DVOwnwOaFeR zT-d+D`75N)!u9u8<1Y!gQG|bH7_S-&L5p-T6MR~8;XC|uKjnM;b1<$*99blF_0w-P$OeP-HQ766s2IYMKY)E5-#TzaIlH=Nx?bb=(bmWIQC@Q*7-yE__i#rU z$9ZsuW2|EYj-f&7dHGg1PRRpyG|xr59&q8nbFOjBcf|92aY9yp@LlVi-idJ#X;PBk zEmsrg*I>TuTX86boDF*{6b0Rt;ojw%3V!dDUE_vlXjpG=IOXSEmtEtir@KATzK{Gk zZ)Ve+D#Q&Il@1hZMYmrrv#P{o{O7G3zu3AICssexq@Wq~UK@V9&BcktjRxKxeQOIE?xLaTn#LBcl?^B1hfb zoTuCv8Fs9?V%0&8H@2Jm7ZG^gdu_l@(FeXV9u0nvqjYtd!>#CdMBk7xMaFP{V^z|} zI2#M>nl_H2JTp)yy#eDrS*19c-;ghyy(75K(b7nCU!?u}nAIKNZ)%y-&vUhntqvSf z#xB)d2QCWOq53N2lIoKB6zr(h>cIbA!no&n&SyladA#>oJ713I?j=FkW zpJ#VK!;qsxE1A-dbaRleGuM4qM~nBV0V+0(_PQ23jj+m;OQt0zJ=6LmGONubNe~y>hz}MoZlW`r&F)E@%0Yk zIBpi-sAWs;yJVyUdCu2?9YoAq_S|e=b{2lKNdF7|QnYik)x)Np#&NrtqXzY783h*C zuO{DYbclYPYX2eRu-|`_ z)}MaEG?aQHjRa#0*qu+cAAL0E zP67ms6NYq*4&46CA^x2xN3(}M2K1;K4HgLp?`2 z#RbYe)OY%1fgFyKAI$Yl#u)Y-8tM>^%(~!wwM2c*ef=RQjMXKhor1nFt`l`2t{W)`VxT)%@$LD2Hmw$Z@Sd_I;XwK6x&)te`keA@HBNoS{auYT6$U!gC%PXk&;%Rt`?t6so&m=) z$}(=1nSoI7Dj^`omE-^Fa}nfirp;#8vgg7O~(qGVkA;-GB;M)U1-^mO)@KC1&m zEvQi(59L`QILuXl)tIwVMI!J0JKZ5OH?#7}eN?Rd&9)prt*Q@=2C%$J(0Mr*oyl9`6C?e921xC+;2 zs84i&E74GDA=gDdD6Y@v6J2|G&sC$69A#@ftmcwxD*?}T>Q``>(Gi&-fy1_h!o(5o z(Bm0Tm`vzCg(sf|4`4M2e~Ba{+7ldr{UGx#_Jh#J-`fwXDc`dnR-oS(r~EE-IU0?qP!`s(p%&T44Nb@^}&>y}j%ujSf!-YWD_f_6#& zE;9NF;6Uc*NAM67UmxrVbu964sjs%z;YFgH@0&Ey3fgRQNkdQyq2UK_q)#4^SRp-_-WIQopli*=WGgrozV# zli09@-=pCx*wX9nji2wlC*%s&Sn;Y6lY;0R;5f_@z7 zj(v{%n9tqjbE2OicS_4iGJ#Hkqrn^ezMj}@7P?gZkO^+;583Jc+Vd>zdAZ7VG)G_h zE&En3Wq*ah`k(95w_j0fU_eGZ27n6tnR!?2G82{|mSw{*gfZzEG zZg)W(+~*x6^iXbM@zRx&8LyM})6O^0kJlCq$(lv^p#8jq=j|`;hWMxqYpi|J4;`A{ zS`PC?^z@%1Goqhlx%Vp@EDb&FesF*4e#Wz4vo9>jeNhEb_2g4tW~RS<5c;w*gX>J> zS?otKFBKUH`>?{sd%vc=kLIrTIr#l^wAZDe$Nb)0$+-7Z^_0lPL5?d9>a+Kn!>xIg zzcEHzjFXujj12Z+9zxa24%+YO6}r%?n77jACi7|PdBz%ML7$HINOxhptOiK*|j=icRXObC?My{Z9BUs(F#qlfcj0zqOrqS-@oXd3v zY(ZLFjL?TC*8LREKkj&0^lGcX{J{8Bl+;^VDf)mtt*?FH+=r`SN zEuT8DEOQ9S!*UYJH^wyTd5k0Uc^~U)yR1`34b?;F)6*{GF)#DN&in3SpE^c5Mk?-i zeBdnMU_OFh28ssT@ZOe2FY8tAJH{DE!dK8=G@lp#30f&UX*OA^4_YQE<{lkPzIX*>ncA8lx+^`5`i&zf@mo_<#7-!1fC z-_c(R{qu>ei@$FCAx6iKQqb?PGvWPlXmWSrN5c31i{eLUb7gfxZhQ5hB;2c_9NvE$ zL{^Ev1HG(_Lciogf~CcdM`koq7Uhd*ma(9z&MM){J=9Uf{rC~qik_slb5fu1)25uU z_YU|coSF9X;Y&$17rSz@f^W0&Wsk}LL)AOtKR^!jRHv=e_)7|m(W?htJ#lsU)i0A? z=a<4if58zDGA#V_7aoBbwP&=SQTC}{gYO654W9bI8T>BzV(=ALx4`p(-hm+~q8)?d zg8k7`>jioQP_zQI1Em5mu;AB$*@2HJ=LM?6C4db7s!{Llz}sWW`P1Q7-$$k z&;U{;9&8iHd}91Im;v=}TH-=FEDPzzAFg`xJvBo>3^8T&nioj9~p zah^$HK)yD#8{_`-Q(lT7xC-W?TFU!IQVWa~LpD+G2$CZf>rxpL*Ul8nD6;GWU|0|a7G+?@Xr>KnDuY?ryDn| zDDJ~?BUJ{s+SLfo7<_tVA)J%|9d6!m%UKHV<*bCa4@*6mu-^SsR*t-nIZ(9yzve$$@CeTpq1Mx|?QQ4LvZkgd>e9H%HDR7T|8@<7H1IAYX*}twUgu`Q`Hs zUHNO62ZM9XJoJir_EVCO2<>D_pxNqZmN99++1c!z-<`67uf=o$8SK1T33=&-7=>(b!hwo2wq?(d~aDdv&={tej^Z&)jFc``oMRlAY|6tX_c z`Skh%;vs#K7q7wr7WG1S{j!Wp#S&$2x82QivDWiYs*!K!hi89?<9f>NV9r$N??hll z?oob!$8zG#GJ;kx+m`;P+KH+x@#bs3pRN~2UHS7^w{6hL$&LGaT?l0bd6 z^VSr6K>I(2g1OJQ%)Y`KU@`$2o@Xaestz2~V?>|}s60dOZ0v6|R7v%Z zreLPlq&&<1Q5=>8H0sFCBfe}}mNhSWSRb8ukcysFjdCojZz;cjWxOK+Y$K@pivGrt zmp2Hm8iXt2bOJycfzlj)FQ;D80i~gQ>>-TbyQ;Q|@#gIpbinCx+O>#sGwy=kcxQLL zquR?i_{wvC4X!he9KBapI~Io$t|~*>g%&FMU|m9J%nL_Rcar{bQr0S&e^o(isT|>~ z&g%Q;vJ>`A?tg}Se$i7Ka+P&VxJY_00ik%USH_`HF5~(4)Ficr>n9o4wT)hm=+5I9;5-r|l#`JI!w+@PBT z0cn8*aD*QEFndDx8N5~cS(Gr~Z(w`gBG2t5FgOwnq7R<}`>8Fofs+B)r#lQB2LEP# zF^eTsEX`}i>_Hdon9E`BnCRE*MXy#PSt+w+y!iM+64$rbrx-y=2#BJ`?2J%VMDMmQ zFf-62)Otrgl!Sie&o&Z6q9|wsIU%Y34osmYcc|< zU>dU~^yMY>$M{+x-3v`;`ZQ|IB{KF*exrk1%8}^)7gXR_OjQTJ(=L7SWFH z9XfSF?1zSWJ$Cq4{oC<(_QUf$M;bt({lGXB+DHG)zP)+ZzGY7&sm4M94tc*}_5i8?Ia5Iz_0NeAg+u%L>b_e&Dqbwf~7^kl{ z|9ADX|Ezsmk=leW{z@$mfH@>^$-1dVn!V^5Ph#iyhY$3ieV4c%LTl2&dhlcE=TfxC z>tH?!nYw?fr#MF8R|%YYz^8WZ^iK^VoK8RNrADLaD3iNfVzHxe4)m+XsvQxc7E5~mo+T+g-7YyK(+%(VHD*SHO*Mv_HVr^d}o^z_x;mUUeoe1mXlaS?yMugN&oF@>PfwmO~K_aaDA5jEN>S1`h#v) z0^XGU+}95--Hz+!?M=F#6h`0xRRf0hU|`s?w_rhQ58Cfh>k3X-j15} z$j#kp^tWrpPQzNQK2TK;Eecw0-tMeof98GJN-i7y_&#UB{#*}Qk?at~Bo3B^0X$2bkv`n@Q0}PHTMAb64ZxBLg#sRGhSzW zz5M7Ift~(_!AU&Oj8sJ4uU4vBqF<;O0x|_WH`zVIMLBd@#9LvkOY00DJ)6LL9}9+? zvl;(r&m*pfsIPxP-2nabpY-JwFu(Q{`%^y4dglZp=o6j6(t+=a1i*JuZi9U+di+)P z@V^Siz>S~}KA5^o_&Lc5>vaIXAKU;sfghCCe#?i@f1wN7n4|ew0{<61aU3T{eGZCz zpnueQQ76ozU#o`Hx7Dbk6Ms)rlJV-w`K5WhuQv%*%_;A)cWOS+2rck=%96(TDR>Gk zH4Kb_zt#kc!WEi;(fsQI_qKp%7orm(ax8VFt1VmL}e)C2^$MuR?EQsg!aV` zF_py5lay1{9!E9sh|0k>HwfczR>)7h4#R}+s|_lRWC4S8uLBa`$D8t#)WeAY3q^jZ z9jdbuxpi5cS39}>A{M(hxEk+5f8(98=I$B0f7{*T(;oa%_~$P;0?xl+p9{PEcO%dt ztwT|f#h71C$n`V;>2zn#BaGp?Qm; zm!oAzpcoHPmmCk^Fj^M6*mI8ZbVnZta*ur6qoj@*R=;=p2u={$0BWrtk{cF!4R z9IijP?-yGAH2AaOAUMZ2xz)D{zV99r;_$@b=y^7LUFZTFMax6J_2S@h?DxdE;Qs|9 z{2_roy!S1>&DjY5@tUZz;mz=aOpY87k%U-Ej(skQ_XOTBKQTWvjJ9W6pKPAU{V%zz zyXjxRHn*5Kj*{0V7fC^$?oBy-A<4sgySTn|hV#DrJ=ZWjj9V*jr)L%?Kkcpi_BHK* zAErm_idf{Ev!>$<*_jeL&(k%k2K`A4?6qp+pRq5mIXn#iQtil*2{?ePM}kijY{kKr z>mQJTnGjnv;;DEy&!2Fta&7|;I43(NL8ilR%{`-V(78{QS%=(jGN5W$59EERwMBe2 znNPc_UAy2Q<5QQB-Hs#iLw`&faxWu1Z|y}4mzZv$M@o;T{q-El(Kx=;K0;%Q7;i5Q zcaSfM|7j1Q{38yHJnE}Uc%TH=Pq5b&d2Gj$0s>-SITdnSjo>ivF5G|8_=vsthV_DV z)Lh7YJ*=0k{ak;>dP)MSoZ#S=qsBRBC^|TZ`{v@nUBd72%J9sko&#z#(I7$q06+jq zL_t)WD@fU=wpxrIN2XU+pK$+J`?yuW{gdqB`Uu}X_8CGUhJDE{VKe^dH&2<+1HqF- z_fZ_Jso9M#`-rt&YAAFk~Yqw8yzrlDNnzwh* zS?creD=6c~ai~u6_Z;o1ig}m;#^ECC1bWbKo&|AiYKLG+4t#e|zJ-RueX5h{rUL|- z?D=_&>oLY9g#*@@rjleQCQl91>&e2+7(%&$S|B^l38K8Fs`LB`b`CNAysI5+MFIm5 z4gbB4feXFh5UeThS1+kYFnSI3IgZmZTpvVk{4TI30TB9$w)HxDhYU4=knamrf0*=- zvXl=JfMK1fEd*$KaNj8QHPr^o(y|9Zw=rE#a$>d?F>1&3^|D&u>qJiflaw>qw-^uBBO&E^?*Bt=k)4(7`~N`N z!zTrbZt7w9M>6Hj{2mGp!aZ7^ z>mQ@vCQ=?iz@IXDYZ#iN&{seWjQ+3z+?G@4^E^vLdTBG&j6!guZ{O*S{ z%6+47L!1W_jie;~v&_A5=;d*_nM2n|?%wOZX z^A)c9*MN4)^?z$W*m~WG{cwWc@7WLKj6(Zi9ph1GAL+pKf5N`a{0IA1*57`{@9*u~ zCX^Sl@@2i&WW@-7wpjmRU_(}vtiK={QZ4Wze?^YAg*x3}4)iO0LlTg7f})>&5B^8` z*}t)G3DMlKZ@2uz`in&#+0?TGZgdqK3WpJXd6;ut9%ua@;=QsiB017p^tt**qFM-s z6Pnr$Zehi@lPBAP6KU2+& zSM-khTkxep-1h)n@=x$9Qp&18{yQP(iZGx;Wx^;eBJs*uz^}6J7<}n$93XV z2Q^#?U%QWl0^xVRGE){&45EB)z*JQR6#gBp*31Jrss zSwJPKMY?~Z(Q40fJryn{a%LzR`Ef9dQI&SxfZHyO1}S_tEBD3p#eCxZigT zLDl%&HP|_m=X%>+C6O_tNNDAdn&_W9^4jFnK7TKN-@tne-|q0vp~z|fFM;2JNr;X4 zrc+M1EIFxE%M$+|d+z~eRk7{+&YCN9PUK9JbC#TgfJhQ0s0av%fJ6~RKtTjV1to|A zB0)gK2$BRu$vNjt10Be8(|N5m=X}3W?sWIL=YHqBx9@xRZCCR(qpN1kniWU=Ym8B& z7~e6~W1o*<-8p*v*rd}XX{IXoa~{x-Rclq)QYjLcI?eO0`3&EGmdhxrqi8=~@XdmQ zF>pPV`BmDBz|g6eU%0df`;e7I@)u48Mt+jK?GEj0S#~))jPe~f&$;Lyvyb_XXAF4p zdPejk>KD=?q`h}MaCCg$mx`32J>D=*i(zW^$=p}dYJ%U7ddJ7Kr<(_76-$2)Sn|xH z?{8C&@Hp=`(f4T1JViE_DN4P{SaCVLf1}Oyo=G0+S3P@LU>4pW(3b_w zJ)6_WSCMwh?^az=mEf#Bp>q8fWoTmM+hIH~=rJjS`FSzKh!1~+?0WV?G%xkWr z-@Y=BxxjCkuI%q~-=Up;N&e_InhWHL*wT9B8POPdZJx(s8K`D*BpLlNDC0Ce&b04x( zG%!}Fr18snCte$F#;Gh-fpmS1BM$S_PIZ#R(8XNnQsyGU-`hK_&GbX@+(at1yE;|(YZc%%hU61_5EJ^y~D4c`(!;g;&KLXc8Y5wv%C?|pMzk~@*GVh%?s;J?F zORDMO!XIwv)qJoPo(Yx_E(w>FdH*~7su8pdT(dm%S4>ms7uEpHKfzuBQLL+-m}yFT zjWk|XC83GNK#-=Ps?$;<2)n^$qxkM6wF9hbsHMi=@k=wF-!ryrzSahQw2g2qe4ciM zdnW37#r`Mj!+$2<-Hd@zXWnlLKi&;7T>kR3@!>d7HhS^NTfYb%Yl|W)5@`vNNf$;C5tUiwuPQ?QN^;Hqd ztsX%evc6q&L>juY!kTL}fUdGfYeRthcDM~dw0-u+wt`l-mRM}K ztXkFts~>c@J;tg*{8l@~o&sHIEw$c)Cfl2F3vAeqJ<^6jk^5=;5Og;iw<5fE#-3vX z46PXJlmKEjx~H|kp*HOF3BqUDB5r^dKMdQ4FsFy z-`SFaNKp&HNZDq$&;k1E^32DmGI9afBc>^u~5I z+xiQH#dYW}(60Pd0QwCZdHGHlz`G%|7>+*&K{F`jU1*B&vldV&V0H@Ob%+635VTNI z%2fryM;oZrPs$doU$Fmz8uxb%O8 zeny9 z0;=<2<3a$+fw8$~efy#L{aeH{jXlwBh)~sVuXDF@J;{5KI8~%v_8lu#4AtP`*{IkI zRm=Qb!N=WAy=6Qnp}+j$`rq@acdCc=%sVe~R_ILfjj|m3J<^A>Xih~Dd?b2cIO|K; zjF3U02zWw14}IKA{_cd3KnQ}nc3WD;x2Z(+4wY$KiqF}-tqPCYkH+E^RCX4Hi#Sy}^yhCDkBhTuPe(QcQmcbXJ zM-*aRavoV*T)cnc%(Z+6`cu9sfhxIq(0lHR{I&2S1d$taN8OTg<$4k$vXH+&71}r7 z7Ruo>rix+2{mbdCZqfdS=UxukvWR8$QXjKsOn&mbC9d*72RtpUuxq+RNKihHae^2B)J-GRx-l57LcGC4_xDk2}@3J9S0 zlHqg&a8DC1jMIPeL0Dfv3j;dOJgSjK`{lmwe?5nMe7-?cA!ziZ`v_k$Yf=Fr5Fw_Tipq-gr zgP?`LWsDQm6czeH!le=GKZNGxBzYHTK2otEMl9J)ErB*g?N9&U;D4u*d|GgoKs+=Z zMFi~we#+vEBMJpIjF~Q$@b8P_M$JHU*p~RVaFLeKp{Nif-DMO-w1-hrRhIgyspx5O84K4p{709B{qF>)g7C9L!X$DK!IBZ}&7T47%Wvm6mnUZlp1 z8^p^K)FSeELEY2>-G->Xr94s8rUSGH=1WqZeuxahpvMs`%e;sr=R>?V2Bf)(aj_A< z0n+YelqCijYA^F)wxpvUm^a*sxR;Sjebr^%{-?;9@q*&Ku*SKKX>s@l0@_U)Pd^4n zjwS3v46zSt5^koSH`C*&IEGMj2$w-{8>gQa)8itqYNq*a3ocJ!ykjgeR6MQVpzt@D zCzy4-EhiN4y5ZG$*mTC}`m{rgp33uYSr;xsvkgU^#K0q03~uo3vOzq7;Ljja9f7vR zyg=$F1_x4)zvJu18egv`_6qTg0ge0rj<3J`U*hZY>;!06>Qcv;47J!lyaKi91!@00 zs1Vbk>nJtjmf9z4!D3TJ)Oo@kNLd*wuD=^YP1YUhFWEmx{et_4;QlCuaqxfI{;%!- zWPiJvat?sgRDkY)&k7$aW{lN_R(Y7&vQFG){3k$@;dYIn_~hVkxkALJ75G;f{o#aV z{t3Q2%Sp}kP}$$gJo;bl-@ysYrtR85O*oG^e!)g_8>7549Yp%qyH zMBw^7-xEG+a1(X_(*7^}<*)P4XSf{d2haAa&q%ccSNrf6wMb>CPSAzUNOc1`h4pD1 z?+?Q1z0@xkzR(Zaj`cv+wM_UzDQHDnp#}63D*0ca;i$c(pTpt&=b(`&s@g&SDxb(t z>E9!qP3l3=0%FntjYm+dpzB%fszZ+(-L!$(M)j+BNmtRVU67Y7)9YFx4BVt20vHa+`kE?;2|9{ZZU8hyRg?9U^}f5T+q7F= zZ9q~MQwlfjEvr=-;$K98_ztwL!r3{r8ioVXpI2$g;?N}LJKc`wRT~+9C@~d22v|Qc z_$Wenyz{Evo?_Gqv=`4*sG%F1PXNnb@J|oOIx`x>x>ex(a^52z%Iln?y|PJ{YQASC z^4^!$R~F+lU-$gq#ZHI4-_NrorUt`$&FwDtDQC*oluLI1&dhUr4e=$dZ+(pG z{QKfA6=1w7r4G9f6YsXZkSn6xlU0`xs1@xyX@f^NG*iY`89K|kZBovZ&388@{Z9J0 zzzuvi8F%{L>U*E{(RML1^*@J1%!QMq4@Phx@z+Jh*(`~wrm`xR zCDnaEdvFdGC(-wZ?aM zoq4F54cA+qEI0GaJ{lNjQBO`_?#GD70G%urykk#xKDC<=9?R7n1Ie!q=H$l+Z)E3E zn|!nF^Ws4aO!cJ_J~R_^XXZm-MBsJ+{1}++nI*M0oK4P2;lFAL3Zb_N|HvD(tLg&s z7d}=E{<$Ao*+|g*I*pkx{3i+plV?m=<^Ap$P%R?ApD@7(e;O0P zztxCu;C!s-bp|G}uMnSt$>&z6g3F|kp7qb6-4(k5G0cH`{o#q^EOONKg#WwpPgx%# zc@O@n<-XfMYxM}9#C%uQnV@_(D8C-YI0(w;gZhX6JN)Ipt3Q&fyGk?O3cx+V2dX%H ze=l?@C-LR`J&b-@o>Ws^08w+L9sIjJ^t#%n=J9;G(OdTG>Ky#EGjx~wSh4PI~b$hRU4tmCV z$_6-FZ&>TCSD_Q_i?$ou%YIE<6I->d>-IrtTl*txC+U}4BdxB4``aH|Y`8hE*U%QW zbO(WfY3ap7CFWn&tw zU$Fo5hAuchgY)m-k-ywSJSl>NBiP^y-%CZjm;m(}>4;po#2zmE-{GG)yAr_}!i+p3 z5WyXMTYci_9-`6_#9zbt|7_@C_`e8p>La2lgU{TAt4Ux9%fy=PhqtWa`wJ0l z4JZGap@tcJTu?4sywgJG(NI5?P7jpei)#goF9zu1G_1AeP=YcT^hHi zq{p8dOuHBJj*UFU{^;4r0%d=v9;4NES1;Q8q7!c+sB)Gf8vc^_QJm~A1l_C7N;yzc z)>iq*{|zjiM-g6)x}Wu3g&=AZ0a$N!&72Hfk1JUOPWCrW2JU_hC)D}E=|{Mky~?59 zcDl2{PuS`V{-EESH=NU^mwbLv?Op4j1JoqSAnUrR$I)8jd&viZSbko|lK{7{G9I|` zvZ%P+bV?b=93Stw)xHo57_lSk$Ggjc4GGu1n_AM|%k$2zz&I&hq~O^GBB+WJ=jRA6 zt7p%VyJ^#Brte6*!T9*Wf71_+a@IMmMUd@uFc!#(e-zkP-KnJ8m$~i%0>VqZZ$=$| zjpp*7DShKMdV=Z)qOYAnpRh8scn;+;CYilO@XdBHpLVgHVw}?odAAgv-vD^26SUc1 z3@2-WO6n0dnJ3MlxAB2689GCC7F>k=`Btq^sm$3x`Umy2Un@YXfP`A{e1>{WF>g4Z zK2;R97&d&(oEDr@4&{vCsLJ{z_abIJ_z}Muf4Gd#v(rK(;<+~|JXIZU`VPV>T`DFPQV=ys`Mr$TraGU~k}oHvzSr;dLN!DCtEX|&x`*d3 z)G6}42+UA-Li3WEd5_q25kgEcgt(~{F>>;s@j=-iYQeyI^@=J-J}VHTe@)l=)R*cp zo_9vfn?(E!kaGy(S2$VEd_+jvSSwz_ct4(S70~SC(7u@0MnSiOa0Af6jC}YMp%NnS zhHFP@Lyalq7Eibv_?B^v__&zH*LnDQRl>2DRYb2$#*_1i7FtMntn(u( z9mWOiE%Vu>vJoDOLg%0unqWY4!?{j)oO4^XC;b`}P4R@Q;~Ro`$#^QP$I~7ZTbBv{ ztMK(+1U51+Cxggk{G6fx;ajv$HCZ=kgDrY|<)>$uhwK)1>E}PuX=l}Qn+Z?^WO<=aOj=|eJ%ov&H)is{$g9=>MmV$^eYeG$)ux`UM zj|zXs_@^mxhVd0@Fqz-I;dlwqW5fy{|L@oD=fCaW;oq3+Cusg0;%wI^OEc9rI4rCkap8S@6ud%|I5 zy%-1ICmo8mE_xje0msJk{CRfp5yZElB$BQ_wMu~=Mg2|wd_9pGi^qU~# z&+ZO-M>-YoO5UC!9QxyoOU^4a*)Q16r>WhGSBPvXT+K&?a1=ruB zzuy-5L?1y~@!ZzEa3Z@_*I1Eeb_-nDsv|&qEPq zk6}4{**R#Bq#Onwo$5k2*gffM!>)|Wckr)$(`v!xNrqL}Ur3fW`$?y|!*~oW683l~ z_3(Y}PYTp$xO^A>aVXyj>lE5FWGThyneEQWVR?W5X3ymOyx0ZtI_q(v7YiNESIsaI zZeHz`G=pj0;b!NXe79t)5Mq_N>Xgl8 zT`GC5(3mpxk5%5T6&Mbj>*~z(4deZ8_6!%}C@{}T@i(WQ*X>;a>K~HotrAH&GD~L7 ze(){z?HhPLFc`-Uu@6>f@P1fYXm-dJ-W%rl#3O+dhoN#T%X2R?-29ks*cCd%OMG^l zj9uCEtD0<16oqcsu!sR+;1Tx|?m4b_zBkV)0d|yq)JD4t?Jy&(YUn_|=g*m$+kt$G zs;clr-sdug2C#_&<_{`{@80qa_cN{op@DS)>TMMbbhZe4=eizqt)jg?x)FEdWrlIh z5+#bh3tPF8uT<<$>}xt)J$QXOxVegTD)1Kl(-dC{$9O-Fud|$_|{H}7+l!j1?tDxs-KNz&|AbS$~6T;&KC&x1dYQVF$nld z?~i?Of>Oi>kU;(ny~eC(Jd@`{V&whv>~usy;^4*~Pnt30T&x1TdXosNw;eU_*C>74rmi8?K z_ud2l+h~+mtcz*~xRiC1J!5qhO*l(cWrvG_nd+ffM>qy?M+rbF3t}c6>BGenDIRPL zbs3t5kIN{YKgIr%^>^hSkmLX}w7J3;Qj(^9%?7H|^h`muYMlLfmITuK`WH(1Sp{v(TCd=-!6@jA};!V@bru0Di=b?`Z*DZ6h8bt_Xt5=#Aty z#HRpzek)?rDuUkKVwuIpSezt^A}YTYupVNgD;x4lm=5fRmIsi^^AddZDd+{3%(Bov zz-cxb!IW@7`_9@t}lwO@bsu{;jCAo z{lXK&HUN|hnv={H2;yGNxRTwN@D@)u7vD1nnO~XY=X9|@6h*)P7yqdM>yqB;V5e)ek*yn8WDSxg)q4KORt-oo}wh85qUK;gQK?Gw3bE5_pf1UMrxhm#ZBtNiS#Lm_~oQWKopryvNPP4@;?pY zVEwmeSoKB0k6X@yf)|`dxZXqn3$8frP;Yyny;clII32fAp9o)%YO@I6nQ5GI?h-!- zbkPX90Y4Ibc;D+pnj#>(qYi0B_)zD4hjw6nB`!xM3^u?#^M4 z=lcNoP^@6*=JYp~-MQ)lg478)71J4q>6O!?9)TM8>7jF+;GWkh3eG3Q$O{U@PoPQ_jJqJIrP*!CK@>d{weyE9%S|2I)Y(?9%-(^pfh z!7ze$L@k3P8tO+J)*d|Z$M6V+J7VX}d=bcqc!9|D<8>_>_$Es{4e4rS)P+r8CaeTv2 z&+&xvqJAhR3U#$uZP)emP~R-#ea!#D(5@gXsoy+Q6<S2dwhzqferx=~3R6kMa{s}bJ@M9KW-ce%!e&ytVSyDn$HMnZ3> zj&ipju8&ViyK}m2q-rY9F_d7QF*=)QoSKMOmT^h0k2F8*Nd2T7^{C}wsLgljKQ)nl z`G#;hCJ{-Dm&>Tv=_l32*)Q`3*R+=%!4ddJQ7KR#iZ;Q-+pPLfI>JQ2?bd!aeFl$LgQ2ui;O z-3z`>hE@haOZmaq|Fk~*m+&?B*6RHOeU~Hsh#Kk;ZOR?sm=8UIhOl11HxS;e*Mj3R zsMq=uKFaukKU3?5+)6GA&9izPD?{pIgdaf5L4*5;;QomDE#vdwa{njuuR8S`$P5_) zRdCHg&>a2}KDHiTc*UXOALS_YIObO+ph?1spl9Jjto!gqOYg&1qCih098W5}f9G7h ztVgwtWqKa|PxkMcud#pEist{Ye}})wckaUNB)_`&J`su;zPdtjN5Sv6K+mDDb`yWx z$l&z9fj1u)N_6;_vr_oFx~5vm-SY@#bA+E#rW~AwV~FIe&<2Sgz_B|Lt_e4CpijY{ zv!Tb}w3ncA707JpZl710L@%EC#7zZ`-}JC%USdoAJAd7#CR zPY8c>Be!S>Jr1&+1MPsz@lBMk9{Eu&P9F~tyg(h|)EdGA;AE1|^KiFA&@=GS45;*< zjL*SFM@RU!52xM2U&Z6$aoS;|@Oq>bYFNqi3ELeEnYSx4F#(MtH> zW>(DiNdFb<3GK@|7b5*f8A`~>UQS+@Rl*+|87oAQNuB;M6nT=-0S@IY%|G!Avy#5z zpakoPtPl4PnRnLDTeAkq)dtDhv%mo(U5|?ytkSiK--N*;{l%e;S%UwKPdQi3I%~JG zezsl!Ki0>TH%C z8;+0d8c93Ec8Z!Cxf;B-HD_|pKEzNHubDTRfZHp2hIznkd7sMnPQEg1L(;DAPiDPM zj!mw2vo+1^srYUAMZCX1YE5i9@9*?|oW*#{Q#o(D81PGRf8ke(GG5Lvzc~JUWtO>x z4-cik2HxKk^;y(*`k@MHgYSWvb?@9s9nO0zz#wEHzDIfQheu0FO&eNIJ`FVLL$=4w))Eqc6bSHUyJTPpnZ!( z?}%K>=j-|V`U--R3+437nTFxO7WZH;^CIhw%wMx;Z}ptoVXo(UTU?`DEuf!!9(tmQ zuWkBG#&zWQsO=Gt`2HySYnyhk&j%V=+o2b%Gl4HDUmBN=4W%9_jJHy>M~e4pFZD+; zQwQTvLk+_qXA;Z|C9AW%%tK-A8wSA zD@Nd=8`U&uWojeiI~UGc9eNN>5)KW@cZ2flA&i5dd_Jgua2N;wYW>j#_^0sA`beEF z!OybrL|BpV29PD|gxLbVJe+U?bs3XVIjC_}ZHI;%?`p$~o7z#8~ zli`P>&n!%QnDk!KJL_1Pf${;np#&_dh?){+(NE+f{kl-eXO- zFQ_*5ZhNWy7(jEJUDytR4!1^GqX4A)>{0euXayAd-H3k@%h>$T*X)gAv1@g)UD_h{ zJv%{r23U(B0Du?+0-yE}@SVNIE=jt#?M8Ma@oVj$?O&lu_7NKZj2a|Wr(efkY)BEb zrD6>#!21Yiy%4|xKfO?K0RE}wp!l7~IM@jvmUX~|SRorK z3QYJv=ed7&c;9T``pWeQ@%eBD8V^l_qYHm7hGYI;ppA`#4gxKAynB}$K}SSROryvg z*nULj;+#EXw?Ai0?qt4qTwOtFWvHL{Ze!T}SAi1NZ0K?;N7gB4G3#JMim}mk#5IY0 zKfy6%6TW5^-P||jPR|Ql63yeR->R&WZcgXqf8ChgWoQ@sMdM*E<#U~JACNmPBZhdI z$MW5bsNApzZF%pzl(qX2(8V+lFP*PE`Hm@Cw@D1&YwmWt9p10cZ2?87R|6x=p(#GxG= zxAVIeK)r?mz(bzz;#N(0|0`!d$^t{*G^44rMzw%m z;{+k&)yeBT?~vYk!uZg$7D3}T*@Yfuagce|jpo0CH=H?+*WU;>cD!hZR#4YU*N@(o zjDs$@N3v2_=KH62O)HCF`f}iezaHsFGky`&*~RTQ5S|&%W_y@7s|`C;h^3-Yw^^;xR@YCH_A6(Fd)IIJ73TB%;%5&?AUZ zZ;|d@BVG%(Cm8*mHN+QX3(dwGgM^N{J==i?bBXUs>3TxN5h>#XUggz>1&@pe&Sb)o zY{O^ro`oQQ{&PM-vGP3Og(xyw@H|eVQ!zLxpmAw4J;4t=E z-dln=migp-j~bi)Qyp*}*p%=AO3{n(=Li%8AB!Ia5e!u33+144;X`5QPoQRL*FMyK zFf@ek{|XJI{>7mI6yJaJ_d|_yD}t{p5}%1$b3gPZrV#z0!;K-%L}&n%4TX!&5(5%v zvie5tBYqq+bs2P&bHqsCc|Tk}FMu9F#lyH@_PwPQR`c-lsZHe!==gukZ# zIfPe&t_u+UkL$xfaSvbZ3!dKY`3!Zb}vU#B<92!0bPnMmr;Qb_i#q-Q%&4+&d z`~KY-NId&@H5|(R9o}I$Q<#74;R|p4W&fV~Py6>$&UW_J^oJS*j}^W*R1{@+!zm?- zb<|$TVz9uP6)pT-HRMX5?+GsjXI3LTTb&d|yR*~TmrXAbnEaee_oE+u}n>dNV3_!S;t=JI?YQiT@@ zm*x9e&?<1LlF-e@UL|}h64h`N^aSO;3U$HXZQh&6$`(exTkwU@o$xc{4F05kCyjV$ zIP1Tx4{6k_5VQ=OPwG<<6ea6JDl5Y-=pNo~O+IPHD{|L4=`RTGP%rbA*Kz6_Iy|a?PvPnOK z{2judIU}j7;e6msb(TZN+1>1wD1?(k%6prFQx|)>y92yG#`sOG<-L}!7tMZDf1>B2 z`zPY((-PpD%=wu&vk$@EL$jLY6h++geN;lkCf@%EWW56WgoJzk2bI8m&t?tE$;5u8 zMyUhw!&nwW6WiT-AH8VGosW_j-<7+UA5!r=%j)}{HW6>5f1G*t$bR~xdfSQ}EB6G) z4=(go(W)%dTh0zY$NC$1CNM*AX=GOTop8oy*3hg9Igi+{6v#cA+m_}p6*@H{2|aJd z?bmP7PTVPfKW7=REW2plGSmY@*#QA?Tjubbt&bX0?*cAg#AeF9!yV-r0?bH@DV;x_ z{YnXUPr*0I*YB200Y}`vdjIt_+RN1vznxpiC&&64Y-{){`{MkE!KJp{-zI<0!jMg# z0_=-s_zze!n8vMKr_4`zKE|0N^T}H8?-eLxfG3=WP73e8?DPAnXXx~>k)f?fx6nB+ zxID1U-!V{$&Q4Sn(cag?J~&V}5o z)a$yFE3W75Z#jAI=X+Z)%$D!H=05IvjAH*1b|jQ`%YD-~&^Hox_I_4!F5?Fby8{j* zC)6wJD}myTr7D4X4GnJ{1|I^m9&%#H?+Jf{0AaJWYn|DE@3e7T&LzHk!+zDK|ILTy zUQ_1Z%NVTk9s3IM&hpR-R!{Ma%05(k=Hfzu_fg`&XYdiQh`EK$*%Xj~_K8zq;Pa{OH4-?@`ltT@M)5Q{RgC`$A`y)wa-muo}5d0mK?*`@9 zvhKk3gYx+~v_nw8@~_b!l`{UopF2QbmEc3CF%%n1`^KwObsahy=^^U_o)a%7uma}niO*xapmsuwldc%_7CsR-JGb$~RZV?y`3cnjA0#Gz4{{3r{25!o^=GVfu+D#P zf!2dtKihhA-~K4*Gwo~T`dZy=xAl}NVlT0}+pED%xpr3@24W}LY4&}9=mczHKPTMV zZfLQQw~N}5THzmK#mn8awvVd=0G{?N>j`Tl^f9gqmC2To%4x5mcOjJ_Pn!&=j`i z)1gs_9R=W4K#W1T*~nfq5};zwe*@Z`jdnCN98vRLXkmah8$~wJZoVOc#7){oUlfp& zcA@NLwZO!MeZ6dOFGH(9qgXHm2nXvI?7!gn49>rQAO2GCf8d`d*?6;_7z9#(Hc?~D^tq8y3K4|vh`-9BYCITY&XYQk} zACYhDi6|Jh16nU^mA4J+n8&$<5Y#Zsy0)1Jx;$$;bPe|PgvOE0tiAfYWO=o8p`sNxcAIX*11?uZ24#enVBzSP0AU;N$}6l z-M@GS{Z+HN9qMd1j5_P9Y_Ehs?qNv!$VXqJsLu=^8igQ3&&A!y6InKLIXg6~8nSK6~1ftSZ~ zF{~4w3Yb?>s|(t3vh_c!%_p zJ>z44<2(DEH!WGG-#}1HKdS_D1MVpe;|r(1C=wYHU^E}$A<*bhjFlcG04HSO1*O;`IuY&)v9O`koT~g?w`5fTea4zOtU559~qo?mOJ|E`4 z|G}e^lCJOD2u5&K11NY(y{X>U z^XqE_>1<3Hea891~ zL18jp1c0Ot*C$siVMwx=^tV7=FGA}ej%2)0*LM2*H5fHs!f!Gz_VHe0HBuWSTmm0T zJ<6d@l6Fr(u@VO@%fAlLd?=1vKo3wl(y8-^a+gCBP|1UbL8saJq}^A>4z-JLZP4o~ zXfk-!4-KQf1);f!2?{|Q(j$d=zqvC(E1t)ru164nkkhMAa^)f@u0y&)#x>_O@vAW4 zXic~tty~#;9B%mpG#_G!p3p9oobk%D;UH!Pq#C3ZQH9C<5#c8A-;U5+RNUpEmuc-N zXcf*9bcNaw&wzSPhL3@h-ZFpTf+Q}2kv$eF-7zG^&o5HNosXAN7O+y53VAium z)(aGNtJoQc0ZRpq<304JjQ6WXJI=<5hlM}(4?{SSF`lrTj}<(>4~2d%)WggW{&JJ~ z6ax+JAA5PNm{_kJ4zx`eBZ=1k1!42%Or)mBq^AEhh8j)8U?4NgLF%wbjvib<$ zEpL3P;M?$4kKQj&1b_WVc#^Y7_&40+1#aa=Va=)re}aFE65ME{sN>>VTl!D<6M$ua z?8DV46vW%5WYt4!f*2#mqgL8end4V{4c+n%gQK*Y2~#dF4`!=ie=!(rIhe* z6lZ&Mc?PmVts`6zE;@?m-PI5|5e)YXL%AddA$ncaytV@2+r|>DKwD4k&yxNZ)~9!f zKLbCy2_1|SVG-2ectZ*PF9#P~0F{dh_*ZgEgT=SWShmiy3ZHXe6mRLkFUA2j^m5xl_q{k;3{6 z?m!9uyqXN%L<`U_a8a-E9aQ8m5217cWfen>FJ*mp_Fz7Ei}YWpXq5~-1BV_!{5TN2 z@L7fbos!VM)^7)^-#Nlw&_!P84Ykn$Z*U%NmKczzs^r#?@K_`~l#e>x*5$lTjrkty z(mJ{F2@JDRdv@uB4~14lzrN{UM#ldZP zT=`vdsmBYM&t&h{5MJh2002M$Nkl-FC-+*<2De4}Xn$_tC*t$DPOGY zWy=Lfsd=r#-6)-*xtjp|z)$r@x2(b!PhCvKUXGg=4BV>8^Xeb^9pa zBd^Q56u6X=(&biR3;|}Pd_cDv#z00xNPI{}069^klgjQ=d_I zRLvltL0pD5k91!1qVN^Ft^K(35&66ew`f8+pK^cTrk$COZ8=Qu-6F;5Pa}Ujd~{JtnDJn*T)O#Cjz01)V=UI%P786A3p(vBh|WXY_$H zq1b|(3V$gHS8N9TtNc?g6kxo=AFFarilO?cx=sS&chz2ko5Y7x&D7~LKE z*Epd*#27$u?VIoz!P{Nnq`~z)D8CNM=Vd((>L32K`lDnxqwtp!Xr*lUkl(1U!U%Uj z^70GeXm-pyp^fky10QA8_)*paHJ)ARXQUgAVJCbRDa;bZI;S3S$)(`1pSWaF@aAa# zhfk_QYMm+seaK~oN1#CA1j=fF2YMplaTO$CN zzuGPT;@W?Q%|^>^iwOW5NUOEg&jD!LQ|&(X6=;kdVLycKw%@Rc$B#gky$2deiRLF8=Nq$sO}&SsOD;JPGp^kwC>tl$Y+`p?}$RDBm^Zac{03@+$*VGVxAsRF)TZgdHO^lnDbU~UGdD~d*5Yu z$eGOdW*O~m${&6@JT{~wg1V)}dl$NaoMdX`gh&qt08cmgq3(8;f%giXF9;lWA2B`- zWu4md zxtoW4yN28k`-ytj^xh3c0CcC@{fW28_txwO?_DQd(ckj*iZ6(PKu&}e#|8qo`K<$d zCw#s)G@Aa%&f|MMllq+4cm2U{2jfjqLlPq&@SsUqrZ>(gfw+WM}7`1=;|6yzQ#y%nFwGIOJ{Q?n7i(c_r$fXy6H?+ zZk|`?yczj8GZ2V3A-)IB*RKo?wm6T;~I3D76|=-NO>UfA4ck>xfmuhd}RU_uC8hB52J0H0P6ybHIcO#hbREzM;w9 zU!qsjU#)$$a={M|Rz5s%kN(OyleItf7Uf8>&scNG=e!keQm?@Fz;EUR=uqoWpc4Jn z;o;`9LvZ=})QN{@^U)7&_gzg)rhnhQ`%&5{%01MW;2_Xc+d2FFB=wFp@*yfSI3+m* zCAZ-mR>Rdb!b=dW2YB8J!G3k<6;6sX4w-`=A^MZ|JX$a|&G?ejcLw(5J$3jV;uB^JL=W8ZsfX)-&5Vgcz=rz;@&qE&r->~sz1KLl~Zm6}# ziiZIF5$u)s)M?HoH6UD{jd54#JQOU&hw$7#PV^fuGfJK&{*gMRW<%Q<%MkU+$$BqsC5n%lDvk6LsGrn3iBTi%CO0}C zfYxOc(C(Lfem3IVHBcX@^da=- zAN@yJ+H2go6_v?S;)_x1W6;guY6J}^X)0)Z9EMqm^ury_VHM}Q{ps&0!XK$W@O4RN z8MnO3ovp)kJrF!tl_LIm+L3W<6n2(i+=ak`c4HjkPouNiPW)(4FXNT^d@lGJ z1Ef-duTg9b6MXHAa$eH-x+m*{eCI{XbKsw>86NGbd$Wqy<1d<#=OX?R=4A=cC<9d; z^c3}%_Z}m6DaS^hNIQ$?32EPC6k*xWMD`C0puzn^aDP;paS+6x|L@?>zs|oDxcBF@ z`)Jfb#hCxH+QSbppx=V(R`%hJ4i~~z^Im&KC)Z~SGRdgX8Yr9Jugg1>BrzQp;}u22+aS|GbhMLFOc@Jf5$Fz`}R zLMzN4BCvgzbQ|ylBz&Vd+*0^wUE@PJS)do&Om+Qzc&VxzmhHu^NZflZ+SBo`WXg)!XGn`N`OmP!&d10Ox~uv z&W}!a1Dt|Efu#*6H)Ha*k#sSfFMAf6{1^Gl147FQzsV}iy2EL-;@a?c6$;3v#2<$% zOMTx{Z;1d~^}?KDEc6f_m%0;Q6y&}L+Km3@ePcWIqdmbY=aqb?j`4%`bkI?KFZiGE z7e+tmbD$LA=Xt>q>ClO)CUJ(-1l;o_@4u$ni6Mbq+KLuW0D(!qs($KY53p}LhX_C6 z3;1c@+^4eJz?BtFNT=C zQU|BCAs?UB&d>M_|0JwhDESq7P`GA6AGodVDc>2|IXh3z;jHbzp~ov#tohg!rBTh=aFgc+R`3|rLi=jwZLTE`Z?PRqGB>KV4 zxs7ta&1GE0#>K6T1y>}5Bp0~$GjQij+%3WH@!rC>s?qM*Q*uhBlAras(>HL1eMzFb zSjaT!TGy3uaC_kz1>A+IvoHAV?zNkrF%0fz_sB`%y9ceCuGO^1o2Zz-qdh;hE&CDQ z?VIi7vTpjLea(G~*{5uBA9GzL{9f(`Kl9JIWLFiJ_wF+8Ywn)3|77DX-WiOvw;z0# z_8adX$o)^|9hH*FR?y3ij)08Lg)x1mno4?+uMC0*H$J0fUi#DNs>s+(0f+uXn z(WbW4tE=z2pY||cHD}5R{FuhEffx_*I|3I2j2mwokKq|Z`n+5gv6b&WQqK#2@#YQL z=;eFc9kdN7CY!6Ec{e<%84}1||yg0p`J92fIl36D>Z9KW=fu@6w*r&ilJ?sR8 z4?U&vT_uoF9m3y%5-aoGQ{bk$#sx}(jnXQRVKeUeeeSKcll>^W2*X& z?=8fTf$uuGtOty5^|H}TJRump)n_uE)JK2FcPXcq@8YTCZ}MF+D8TUcxUOGE{yIlK zt3di?i9d-!EcIi3KWemq2Iad!`8DH(`4N=QAEh0F`jvl;{^%$8Of%Zk1le7O?__d- zAc1fhE?3+^I2}GI{HU{Pttvpb;^Mz3&v(Mlsu5m*2dV({HP#>aI|c&UBNDvvBW+mv zy!y->N&FXZRDL*6l&K8>mf~T6eDL2mUhUz*3#6BHN#>K#AE#UeS);U<^e`tYLS6e;b>e`UD_Fa3k{bkXz_S^Pcdy494-@&PW zMF9C|d$lzhyf)D`EH)H&3)^qQkZ@I7*ZPp>Z&~AQfVw^2o^QLM9($vHo{m$07!l|H zi&*&pV4pc}TL4_^4eKY#7x8xot2yBw)^NMFP<#xq0NI-`B={QY*u%wQmhI$PwuJ1o zfUEor{Q34ni{ZFVWBiJ;O4^@_NgpKxr|0pxsri9Vqi&fit2mss$#abh#E zP78P=QAjl8SrQwVz0hPz*ae!!nTKTPF-p4++7?ly0P%|C`y(_Nu|scYuztb*3y#m= z{QGy|FCnM}g>Tw$+f4XmPwxI3L^#4w;&PQcu)cAGe{SVAQ`QlLxOYW>%IVI5TEGy4 zi*I=^FD}km57d{Oq(2GY{n(1sf|AX~0!R4&>*}5;E&;ZtDDFa5gnt%#7q(qK`qhXh z5S&cN-sJNjfU1ynDu?oz?ac4wM2IsRfz>+R8)#;V!qi%9-?k~2dx`g&yD-n^u(HLI zeyHnt7XlE^p3t{F?~rb{zpWUmMXZYL5s9EDbV1A)5q=coWy9VHN5GIfCTB!0f}lVP zr=hsG56_4^9fm-r%qOLDOI$^;xG;8B3<9`%wQEhR>qUQBspiaD{V4Y?b4Ub&p;J#> z+jOob2QyY3U3OIJ9m6Wfx?)dvW{RtP*JO8HQKYuN*({>Te9F_&DwNxZdXBed=FUM7 z_)0{Fyc4L$Lgz6r?UOL}ZljB}C}%D2fzVtIxJ5^=EiCKz7{{H%_q|zeR~Y@^qV>b4 zBCqXz{nEB~X;+sy&%Xh|<-_@4T^2NErKfdcV=7&8x6;B z`sc2KHsLJWtHjrEwGN|SQIj?`DK|I2*Ed;FIY&}LpV%sen2L2@l}8}VR)%vx?{P}c zK#iJDETE0SC{o8jCn7#t0iCMk%s40e?Q(Jg4p+dhvyvP3yLOoUDd7*fTU_Fg8ixgE zI9u(3P9^9myPY$F_r=jIE=f8m7wWj}| zFB`#X=d6Wk?P%{;>{9*&^2=xMb2E;uN0?bMZvB4$2>)Zi3-_aL*H!{cu3qkRk@_60 zxwGZPdK?_wmGa!Z_LO%4D$ZTJKLs@^;F&Ll4rx6{ zX$k3`Lj}-}dY#AOo$;sEpimgVbBpiofyQyZlJTIbBcQwpU5;A0F!Aj{VU!=7*jexx zYKKmm*5C_W3Exw_MFG$Dc$lKyz+9KLE7Zb_JHdAaD8VFXFby9s9o|A9%(F*anERt z(2AG=l#hDRCMPuxj{f5$8OEkpw4l{Xtc>$`XdLt+B{~6Z%ZEl`Eux#Kk&7h{y*YtHG=w0Bz>sih#=Q^Ty4P3vw_)2Gc5@DoGU>N z5T3=jxzF?4tR&r_xv(A?XD1M2^BkjrU$o+>GNoKY_!a{1KF}b(4&v)1#zAoZ_kZU8 zHZP8);dAidQhI+M4qt;G!<~~*JsHMU=6wn@1uiV>$tC#c3itso@bx+|f|ZAOl#8;u z37)*q*;a52@&Ne#-}oo^S`*- z#J6&O7XFK&LV2)_Vf5wP>NC)SaN7aIKaQb4IMVolbz0WlBN%WAU#o1~)E-GhabAjW z7#!;Xv2HnDMGQSzDWaXjhd|mkSMOF~jg}=!s6e|(yFnry`$|>tXO->CPs*f?t z5q_Va6{tS+X|+=fXw}cEvN0Jt9aDii#J|nz+!y+Y)_x9}PV3x+Cc-r@Lm$U*tr6+h zq6U=p36C_2a>9deBP)R)!&j?8XM;j&@Vo~u^`}82F{_vbO{d3X-7-N3(qGNs&wEJU zo|RA9G0iBVJq9Hkt+hvo&a@Nj1j@bgS|K@>uS>f`(fc7#F9r?{bhxt!UpR*9g7e%# zgliyuk@cz!C*)Xn%)6?DS%7eNV%PC}4u*NMURFd6m=|0&#NMS(VowPeBDX?0+lIC^ z%nY-#`8Ln52F_TFHx#LR<%&yZwfUR0bIeOIGos-exz}@yT*{y4Zd`J7L2&TT)_WH1 zg;0O8;IZuPIpcF^_t3{eCxm!`4auJSAuSO5+&lipDcV6zGdtoe->B5T+{Oyvp74R8 zGoyb8pU+5{a^)~Mu$r|bm-u393YRQIf27t+JM`!V`L4_wne#XXxQUg@mi-ynQ0~Ev zNAypgqH*)1pQU~^)jTnTOFx|P$)i)izWOE2vdw{e9b?ZI1^;B!%UXB$F4G{?%yPG; z+$#dZ0@Ne&ZfJHK<2L;9h?XIY$MfdJj^`M6f&BJse)9FF1)j6grG1=cYCqrcIm>O@ z+X*=N^i^1GoqOUDE$YXZK9OTlhF<-}qV)uId#_k)84{w=}RbKF_ zQ^vk**8yJc35>SD3$EH+nbd-KKL(Wv^xv1E$Go(g(-`DBm2q*@Ss)$*tegIBf!Cm4 zSkXopnON&%iOViw@e_xx=Ol}aFt`g{u}t7 zU>xGZ0@VoX1cFp)m@1y>LKk@Xj0ZA+;_nGZ((p%ud%24583H+Z7 zlIsG^BBx~NHW2p)Xiq#d(GFn3>e>)E3Ea&%fR~NXo&Yve>Ui> z(hgpY8kqn=`EF2t&3FjP=Y#r{JdA_?r}`t=VYA+8sS_s?2?cq?sMRge;1S{_3O+22sFEM?waN^+4 zpRfg7f5JKktNafw(5iT=Dy>=$U(JcM26oAU;r1p@_Y?q`lT!Y76l1$40IYbS^M35o*Kks_JlM zEdZcc$nQT^JLj>J4cQTiXt#>&`ZrasxiE$jJi45;wE}b`|FV zgi+^14J8$qJMCmaDa`kz9_IkOY_!-$pVj5>f_OcI@CTf8y95o^FW7&<@fnK)Yg2w1Sxza=i*5w{i> z1u_avpNZhW+-l}E@9=!C{hSDH>=n3tpUd;!=2Y3}!<(DQ9o_El+{@iL2)53~c8`LO zKoHfzQ3tToVt>H z$BAG#bZg{?p{#$1zZQyXc~`RgjynZC-u(hI#09>0qvyOQAL$ENpNRk@ca%R`Tu*wZ zd-J+KMNpR>w=en$@_*G+^#5Y-J)o>AwtnALYlqI=G)*@dnw)da8AU*XBvG<}f*^>1 zBBFv~P8bkGMU*6BAV`u7l5@^EG@*0ed#}2`S#r*IzVYrI_uMz$8}Iv`yB&vlYVBIJ zD$e@PP_yhPe8_;#Ca&yPcMYk!p!SdR7tD`V1H>d2AC3~Z7iOhQ~y(0N`x@LxsLNPSPc10t2 z+$xxS<@#&rTVKw;e4l=ge=H`U_+a$TpL@!Me?$4Nx1}MkkcHO@GGgMiRnazol)5dbx63b&Ue55h|;R{NCEy??=(( z`cb{?Sx$e1amVBk`ZLQ~V3V|ta9P7l=zA#I$MU=^zRHWBix7mCK{HjhLSd$lq1coC zaG08bNtce{e;$7Ciwgn$&AGG@&Jmt{2|oCW^aTC5D1!A5m>cI%kNezNN=#~~UkM~FGNOw)_ z5<0coX>di-o5_v#mcZV;zSzpPL9FOQF>+fgK3Q4pz9~^ z{>FPt2nY(!;y%^z{p7^b4YhkqZI4b>V-oN>rov7^|U5k1JuL(rS6lB!Tth4mizMiC@V@F zcC0#tva3W|&aHzZG2-W2m!Y9}t24fwXB?=Ck#^ME8UY!j!F*BpssfmGyYu`wD6lBB z5hbx+F#tu0AWh(Xsi7E36sk@MrMyknKSJ~qs6s{Y7iA0`Ev(C`8R;pA+&g&hW007v z=W&=Ns29$mTV?)HPCH{-YD%vrkgv5Bs+cF%=T?{~ATieuH3ptrobHiv};~1$F=s)mv7PvWtbO!V8UFb;+Ox<|@Q5-?$lXeqQFrV~BLSQpr)foMUiJWoT z3K7Qv#`{0ZlrHuY>Xfd^jlo{mtdu%AAFIoOzIAy(Wfa{kEPWd_v`-@N7)_D>%hv zet%9UVXP%2J^UY>2#+230Dle@KCd6h@0^cnrlyg8z+co{{g?dSk07j+PqdR~g#QrV zWHw(0aB-nYKd4Jp;tV=zPI3){R%d3Ggyz7f;b){;gMIMuWasGuZAvX8q1t*~LiuAT zzk|_J_}U6%xI4oRDEnbk^akKM>O5T(yLu#FV_(Bbt+H_dSc!v~?ElL0t7Fi9l4w~DeiQmEu@;IzC-dGx=%5Gt)f)C=+Czk*mpR*u!=IjI@Zlg(#Tz_dq~@w! zRIjz?lDjkYOW`u7bI>EsR_!8xG0Q}Dv=^ukZQ7x0>T-hboR<1sCls3Dyl6s&|HufW zvpp8{Z-^PbMX;k%6f>W=fB=(%D71$xJ@7iC|{ZkbDcQ_G~bOQ$~W8tyrsB^b7M z=6~!1FQt@9shG|_7nv+u>%9dJJPGV5ANidPUaB^!@`~E@qdUiy z9&!!5H|)-dYvc<*9=2J^iyu&EVI0rXJ7%8Bna}pw?&_(V%**ii!-hr7WV+d|CbF-( zL%c7zJ&0rQ*+0|8I5Qym81ypGF*vnu=lnSAV>(EA4*U zxvrTnu?J$Fh%HOGUqti>VLqqV$vvOei0O7EFI$%)|M2`f{!{du@;&d!zWb+sR6D%C zC$L6BuDjcNvOO}-XREZL#%0*aNZQLpMJh;bv+rY3*K?_Xq2%w(QKu8I63 ziglP>FMU=v?RCv{&vMZp>PK})U8KKua3JX`+EvoM*fpQ_H}>}lP+yPsgvhvX62C$| z`CVRD85irCXl4sE>&H3npCMNnSe3OF#|7=unVPh2uFHsA!9^2W#SzhNhQsY(@-+e7 zwk6%gE@U?+UlqMt99*3Doz~(QZ~d+x6NhEo9!oPbE&EZsjf?*FAMpR?7dfI(pm$&r z_4+C##=C;_Sm&ZcKk5%SSS9Q4dAJzk2&VtZ@Q>2q-x$&v;Ll~y9L)FhGixr&L|U2x z{-htR2}EAnOMRbaWmclRRjj)yJbx93)+0PWYh4C`iX3sz;Al76*~0}>hUVQlYc z^sx6?=_N>u{ zV>|!pbD_GfN+mybJ^A|f>s!8*9KpZ8VGqcZ z3jY0_dtehMWjcl}uvTI2h(OK#ll8v-1%T8*|EWb_oe$)j2u-ny=r05a5(*grisa~tMY7uaI16Nx?Z&IqC=MTYt?vg&i(>>5Nc=|sCO|W(wK`#P$ zw~};WRMi4F52NzkO?sYgYEG~v=q*-j(p^b6Fh_PlI;uA0ISC!(kppqdK|mijz~d15*A24eO-=w^Vjc&u?56uIh`k;SMjtG|NKH zXbktB(mhJe{FNHuK#>#2MWQ(Y4tBp zr5-oK+ZV{9+v<9KG0)SkM_k{^iT#l0+zTV9Z}NrAlD9jvZGCq5;LoWXl-^b?q#gTX z(T%ayD)K$&sxF_0B8n3-cLVI_u9xi%UiLw~mC&%U{C>nLD+*Fhpj~lyU|j03Lwca& zbz{YT$6c_i)KjE~v&(%+x{R_E-%-N|>AQ(?mgz#~F7|!a+l~lAcskqkyOqXSnHuEV zjf23ad?$^o2C~WjhSQSpl9n@1Uv((QSxQWRaZqnyvx5Ln$p1$JuS)$eEz++l1T;YZ z*`@87_D7VviV=U3e3$eFy^MTM>ic>=v?s`ge$Xkpp?r^8rMbIdpr4(5`uJxA5cocM z;U4;<=$VArriU2U7EzB>+QvMn$Jv`n=+i#T$;|4FLh`+=5jh)pe#!r(nGc6`arZUK zd(1hY=qEkRY3sX9zjw>%c5@2{4{9FibcFQ&-8-9|eFlGrzDa4xi^w<7+HI~vny%-` z$!p>d^fHEnIMh`lAa+*GF-?f!`)z!|B}C^MQ7Cf4x{92n0T&H2II5bp$QU9@Sv58D zfB?IXsLIeHxJ^hue}V$<26QvA7AO}3L@`;%oCi!*q0s(nFiLeap9FPBumtlYqC8~5 z;c5^A9mom)fqeHPf;UONlUAnc2^|VjWS#KuDdPxPhVgDlzC`5_hYiN&6>~MfVs0BA zN%{#)?n1+<59=2X_R*LEEKU(cW89V#ipfgx{7q)X2c#SE)?sLS+=fa*i>u;>FI>iS zQH*pvhVTG%Ip-E_=tr1kwm>t$Z{wl0xD+7-+FSK7cPGC@ka^m{+^ufjQ}jqA=_d3@ zd%pWThW6hXhtnY5UOZpKS$P!DY6$K}bxF78IceU%VQ^gyzIc%IC)A#LQ|m`$K2p~Z zX+LYCmp(!|U3C>jCPs@R1~(3UfN$Tjp44Z^SCw+$U!cgQreF7fzcuM_(4~y$M>r^4 zht8qLw?emqJmov#eD@f%7HWvy(4xHe5cDwh%!OXj{Y^dNQQg)e-IY=O7^?Zcq*JId z>m0X-yG8*U#nmjL7|FMi%seWNig*RtUnZUh^MyNbcbj=7_Ic%*(M}}OUpLqvqM!|t?%7}9W5>-^M5FcZ zvX8-gYp_#_;Xl(v5Q)$`;JsqdDna;vOFfX^^%_;m$nO^@ErZ`*w%UP6F=*-QdM)pN z#nZw(w=s{&cdBDz0ynV-lovkA?sLfO3t{LTej)v-waMJ!{WR_h7fBaKb0Y7x1C6YM zo@E!k4BbZV<`i=b7q3C2Z?FqZgDMof@_Q8RY6-o?uY6x6uv?saaiDj9#9{p<lp?C>p-BY{C<8|SqFuhbt_?X4zPENlCa0|z$u z;XAeb&x&5C)w~u98uI>~sN1t|4F@l7IQ-mrso@x^@}pU3N?$SY z7dMXE3~nlOv1H$3t8iR69y1{toST2j|7reN>hW8FmI;S(U>J~A;~xE#H6i1V4EnKd zi)yZr zW}Sw{MwXJR%ly@Kz?sYMb-0r49rB;^&k}>SreNAOa;z!wg$NNsR^41EjJOginF<(cIMfBMq>8~pT(=gqJ&A=KahO76yd z>g{P0qP>@?XMd-5USsCTjKEMXA)^4andhJlaU7ZkeG3=mZ+V>Js;{n+?`!7;(Qn$@ zU9;5|(t~sz6B%!TwZs`lc@3Ql4&U*m=hgEaqJE>?HC?o3@&VpBM{pZPt2*Kci}C9e z&Y==fD%;@j(r5!#lWxW8W;|GxAw&UzPvAIJg!H$pPT?hRhqYD zeHVn|v2Ij-eNwJyvl8GuKU2=Xi$6DlKV@7Bv-*X947bV}hbXT#SH+Re{+>Nx|2>->Z1q2T;0&t3M?+`p zTil|%599g?#7zJ`OlZ9bc)GiuY=GNrpb`QEg6KvK@V3rzgW7WFpBT7rLkl3V_JD2% zMj|Ndje3cG9-4z#TZ!kl@U_1QO#=3{gjVKDt@(WhqVY-6-(b!cfcGcrL}64nfOMy! zn-NHpp@lIUxu6?So%V)$5R)%L55hk8L4SqCwWGWbsqJpkMNp6f6k((L#A8&|M6?9B z17MScAp>Ub5l+t{T;88XVSEljwHUuQN3iV+jZm?a4N^u$PzTx#p!^E-Hn}%KldQ}7 zeP{&&uMMI6($rf3WLZL`z7Flpd(!>|C?c{*ZzD)vIP@N^1ps4im|+ZJ!S)5mFE~F1 zm;~3~{~!2E6I5=!;Bz+swo44XqKd#DyIVz!0tEhP41P)aqVP{u*{V*sPPzO21S(Yw zm}`^ z6Q14j77Fbl3a^-Q(LYDfo>GO%o+;hYvRb!pv#&LZx>}!9>{R0}-ji>&P( z@-J1Z&HQ*r&yo{s);7*?%eeH%K+FEB`{_j}t1R8YX~yX{hF*th#z#l!S~?S2p0i$0 zL0ef{&B>qzoam}adM~a3@K=nL2km3fSM_V$N-1}Xw~-K;dX8QyA;t9O+ThLOOLzAcW*qB)L>f!UP|k5{S%CQ!7#ir4 z?PXlbX0ORjV7_GJ`<-)?b5FIa z91;$LuLK9R=J$HgNE`*|XVBq!V;HH!uV+YKCTQ=kwC`K;GtT_2Mr@VP#ZAP40-V;? zl-m-60_%ipBO0l1pmKgam)}ccme>Xz!S@$HA5j+scdB7(xZ*jhx}y4)=jUl3ihREE zo@xy(MXN?YYanCbW?(&^C0P`o||{f%>9ZfFwdwJtP<-VuD8 z#W_^wA#PWdP5ldlpItoPKx-d^7NX_%pn1#`))%OCh&X8AWOP#BAzi{sW5ruIO&EL) zdY>nTCc)Rn00zG1EGPJyK-%d%KgXHTqO_|S_38tSrPVS&D_Dufp=2CqBIc8h1ZQ7{ zq8O2NixT6c7`jvtUkBv}nV&)VFDT!xg|Ga(@_PWzUYmLI1O_e)iT~s;k(QFs^NRhC zy$(L4)~aIAGMv16jpw~_MPgsE7O4Kl(PnXUAhdz+Ip` zm(EDYRTBI!6ZbjU56WeT*DItPabx2X;i z!9d9#^AhP@>Q&Ku=zFwU+9ekOw1s-WWqHtRnB)pVOM?ESzL(f(UW8U@Kn& zoFwwiGnBJ}-J5*_Cx9qbo#$R0_BKO%T3bwc4cSjFlHS8kF_-+Z|HYGLe^;$|Z;&dc zq`q}=zGh)ZHL;sNq4g*(*_H-;(3a82k!a~2f#_W z=Q2OcrQh#GC6&2(DvS$=$J^1JTZ;WmAox;!l` zgM7DYCH1{W{q=8plSBXIKAc@G@HErm=vQwoVjix0X4A5D?2~byL^qA4{Y^ipHK`f6 zB7JvO$#mw;&1Y_nzj1)(7l|wqu@HC?R-;7u50M`lgg)+}AJe<0FUoqD_PH%DZLmTo zhj)pfKUSUoW6@UR43(wd4khh4`ndE@#=cx9xgXngvjWM5YSB+^i8RJ{Q$NUVc;gE8 z>W6A|Y0MlhU9QB~!rN)z53Xmy5SDjd_@z+hr*~P{!q8i^=Pf+|-Ja!S1;$`Hwmi+e zB|OPEHe88r9`P*WvYG$N(I1UGNpkgo>!@d=JA(J-XsrcTonm#(p?=G(LE^aKx~?kN z^LhX0(2qi!QQm>jnjvNQ-gWPfjpmMzpXNO9;eFN z9awg6WL(K*9&hWgbN}8H`e{~3nr9U-EVJ8^hePTA9=V5e)>HnauwCHEqTz z^mN#iP;k9EqRxQDE$2u5fyg7E&9Z_sT|3-|L{8LubSJ@GdYF|e^B9Ncvx2XQI2r|~ z!9iVpVDR2&RvdFt3_XiPPwK=6mN>NAI*g%+`Cxx-d)0X(&xZR8GcRZ59rcO4uowC2 zE6GQEhgT^t%z7UQPH<`fBrT!NJDc*W@vD6QC>Na-fR<+cHih1Xt1W|SI6__CZ)JHU zVjKC+QxoBTSM^8GD7{6|AMnnfSlNO28sHf<6 znfGwzXrnhiYn@P0yk8&A`4sdHTxSzBsNW6hui=Amub};5AN>%tul%3ck0L=2!fzc= zPaXJ8R}O@}O!_8~M%F{`T9?fMh6*@B4kleqt%vtw7sEmEE$9=(Iut$15`JYr0WS>z zF^OC(2uvI+jr`8@1_oy~=JyIbKL#fp4J`^E6Z~|AoM)itt*1nO=aB6=u1{%<)+E#_ z7CM_J!N0$857>X>wg;O|51{z|%3AK72Z$HNY#m@*k^yq1VI0DM@=(O>f^NmnQh?Ey zKtKct?o~Q%?ti|aD@1sbP3G;`TX8fzhe}%zXPD3 zqx|x~Xao*~pDj7yvE2iq4Mzu^1~uD}0|{3RVeb_PFs;kRFM z=V2W50CzS%1I@q$bQ$_JZej3GPGHqHe7Lasi+=|6H|8$*vbbYK@cTyfyZ~T*#cG1< znWdi(c%72a>COf@fu%Fp0Sc1u9y?VQd{o(6Y!o+h`-9^ZN$O znq_$Jw*OLK6bhfhVG-Vk@eeB;)g){MimKm2XNJ7NHa6Mc!henYFS2X?iejxoY=vk5 zd*y^`PF42HGSkWpuFx0-VQjg^<*8Tn=8z}Dd!aXd_Cn~P0D6w7vo9Zg6UAQX>US!l zpoo|i@pKppv|Fo^&fG(h({4rceU0hAf-MS6iK&Tx;>Wb5*XdtRyh`)ZKKEYdqKEzF zc*ezA2j_BdzD8!zl<%?QeIIoof&DSIPvj4UP~dpZxo(C%O}|XDPutIP;{22Ie{7hJ zUHX@6s}8rvj##9|SC2JBu@>tI@$RABU+eY056Qn1^Wr|*H_%SE5A%GFvp2AieyoQY zNcw$=vq}uosHIwH+1IA&zIHq4XmCSu=pr>-6wAa}n5jM>Jq5=(zHhzEPFMvRf?<+= zA;6hS6nTh2F^X|wH-AG6{*EWG&ne3LeGzmjlb%lq*X^WxqSB;4F#lf_hX77)92JEX z3i3Sma89JFFuNeM4V_&N^iTAUtB;NqW{i8r;UO4kDG&?J62#UC?fng5qL}C64 zrdca6!2g+V91a0P{O9udPWH{b+gU3u>uSFnzn=dS|HWQ=SDr40p1sVajdx1%-fC-r zR@5U21!8}`lTR!O#)DRmGa)?7TYc0={63Bo>+7Ina8D`1?;ciz;Fyd2kN3cG^^F5g z9uX9hC}%UMNpMWK)t57O;t_5l9MBhcjp4Qt0elOQZ>qIIJkn7xhDk_V90CrQ@u`Gq z`w^b!(fcBZzGaP*lV6lK0wg7&elx|Oj2WbeQ6$EwA7$O4_PdX7I*uchlnU*N8lHY- zwLfJH+-WHIB;O0HLgu0Bsor4CS={hDnvtY^lwO$MOJY!`pD~2k1lJS8_@jbHRV;2q z4(ZdBmJ3Z_J%0-A%E?dWCzvZ8;a4IA%oWEIJ)VMuR{+2Hcs0zTj*coal|;5j$}SeVb3(KzXfua&e_{=6f!@HCVLr4gs@6KtD9kOg zo(@sMEod7|II>Q6sK4-a4SM7V>3XzY`kBiU=yUE`hGP#SSz1L2|HZv^smv$qVbwtI zgg(I6oWuGjzE-dCysv6v&bVO z+79M<0tPVY*C4(Q$`7)Rg7Q&NJ`%q8e=fg|G5g*;xYr;5!e0tm(XuaDvHGGIpwuIr zL9N8^Gq^BF_}f_wZ=cD$(?4M>A_GSYD)9Ti$nRW>V}U`8{La2`Lhm7;OJA0VEyTka zCwxz})~AdER~?mT^cOwUN!lQh2#1;dZ?&~pPS9e+e@65QoY;RDle`=tXe1|v*+0t| z!wOV$XXXrLx@hec0^d;kf6$P5NPi#mDR!s` zo)hR>kLP)191FLTu1e$r+5gY;bw9KWI7s@X2QGa}p(SxgJ5K(_xOadbbu}l>M5=7* zly+PUN1A=s#rWC-UA63&$iGovH|@^>xHqYkxiAb_%eldj29Gcs{myY*f@bw%e4b zOQA);(Uzy6i*`6u{O|bNV_#g7bmu-eDxzD&rtqqi)73XyuF70o?59_MTMizde0aX# z@bJbVZKL`CD}HVE10WQ7eyb$;RI6Nq)Ov+I5 zzY}2_cyyb*sXcr9e75r z6!4A7U+kkl0!#fD{ImIey**gvFkYk7KINhR-?0N~I&h(^JIsBUd`#jVAMNEtkF#8Ta2e>% zcOD8!4Ed1sVAoa`{pOzGSs{+Yo|Yl?y@J;=^+CBZ&iRGdW#j|XZUigJy6YhhH(YJE z+sLKg1^PJe^1Kkp4t!=`cD-YtC;cIE@@8m`wHg`Pa=zn|)g{p0frbwAA9t2ey@lVi zK<1Zt-ZfA(zt4#8gk#EJ=ytn+aU33i$v%vHzrbzgLD!;L ztp`2AY7_kR0Dq#h{aGAiFyB`I>%oVq*K<&9?FX4!I$I0U#DPY?iKHL~!`nt)QE(&r zozz6LzK_8@?vZYX+f5hfPiRNaKm!~nV;%DAC9pjDT@Z9O^dI`&B5R79_)l@t6G0;infx zub}(lF!l$}w^~_(H_&DMCVCmr(QKn9IR&>Ce%Ay24Y&mNrP>zzBiQwdIEbN}$uRsi z7d?RBxGu=`gP>(p9ML*0tAY9h4L}`q2UDzcuWz)47Z1`HG0f|y z0jl#!H{z{yXawT8hQ^`}5XRin@)!^Fd~2$@M*c;rfe7r@OX_>$aXy&bhe)rdzSP^= zs>+BbF6R8ZdOr1iSiNFAT|;5iaXfz+M}R13E~-uh1Ju2@jfa0hRR>=zi_=i6A2?hD z+ZP-DskfHDbo4e8F&@C2sH=%lThCOBo=F}Q4V)tBZ=+aC=w9%60{cL*FT)=$ zejUBa&g`~npQG?Qes;lO_L;C{QKdpCCwE283sTO>oo5m*pzsNc2`TPnKP^mhTqvR~LtcLL;MpD)tKf zFg;1XJzq4g;#<7`k##a}8tn|h z&`cV$pt-WgiP6J+D3Ul&k}C>YZnA9SLgB<7dtU8?E_8AUhHlvt)OVs_QajWfS5N3_ z^|DPluJZPGaa8q$_Z{@Vn4coW-PGgxGym4F8x1 zZklqsVq$MYx<3B$^d~5-zM>ydK+aZl45K$rF>kD8>V$Dzm;p{rCw&%Gq9``+;I_be z#L+WU*14KMtcgZEuY{ok#W3xQH|;A4x=bS7g4W%HO2}ak`A2j9s66SWL@;4p=`*T? z!NJ`zp-8{C1tH&n`f+!f39Sfesxa1 zCqf@$guW!-V^&dd@BlAPQCG;n1{Z}&q<_I7MC$bh3R@Y!O{DHaw^O^r(0Xt!>6aI* zAB{ssagez5|C^Llgy%W*$98B*dSo}W2nd++)f>b#Xb$a843>U8-v(Y!gEl7qRgB5M zPu87v8+`pd)TjS5z7A271Yhf~RJ3u>0$+>aO7#NY7l5|G<%9X7s&Q8|e2CeWU?O~k zTJI{pqZg|BVz>$7>!AD)l#f^sLHYJS^Oyg(<@YTr(a7&jxKsRR_|)TY-;(g7@)){m zk^TlQCi_GHuD=Ibg0r}PLSuP)mf!zHeiuB*-u<8T6Q!*KM)7(}_cjp)Li9}Dm6P`! zIIG3JO`IL!`wKwHn;8Gmn9vrGZumey5e9bzFR=f|#-%03x_5<*obq6;E_CLzYmaC57#xuCH)oJi5mP~h~1q1fOe+| zKIf$DlSYp+l-d{Pc|Fw0&7gbHZsbAxu$#%g-=5v}7PJ%>KQ4wAA!Y&Pa+sonl5*>T z`{&TE4a8tPMfxL-oa}-QKo27OQWsD_YiNBOci)7*NCcsk&|;jxcSFysXCL5~y~Z&3 zA+?qMxJQjk2n9FfLSLkIqEAZ2adZva@h;zn0PXj^lz-4i|G386m+Z#i(qr~3Hv6>t z#!d$FSoX&%&+ZIP>+M7uN7Dt)B8i~o>z8l&*uQ+wD~ZW+hV3A5^-c zUVbOgEf5JT+8bLTWtY` z&U83Iz1Pg#mbRMtQ{>Iq_Y*_JA1`Cjy>W;$7gM+0{E7M$4gaEWE993UuH7ESsl<~d z8kc51wf?>7^cH_mo^8)p;0|Y->qFH7hk{-O?nHqT{A+!!{jYCgt^7j* zHIb*j^VIQ71itw5C;7Yb-sf)HRg`}BdRDndu`G`SY6obC_hI)gPkqL-rG3)Xn&%7s zuloCtzo7NA>ooo@@izzGrN4fev!d~O`nhmgLW{!SpxBEArbb?X7K>9c-%y{+s+RpW z^|f;M=93OA_wDr4FX~AQ#m~{sv(7$;b>u$bR_?E<=S98I`JDN2*xT9Ni*~O-dwHAu zz3sjB4xUeSzvG%tx+faL-|3Ipda#`6hr0|D+^KF_9|@k-H=WsBzbc0JrQ%5M`p7lV zb(r?-ws+dB3sq1Jk%&v^9`lfGEN8uAIm}yqNw?R7c|J++mvy1yng1ntejdr|7s?sr zDrd89k&9Z3!-W1A#{=e{>rZ!#OUkQ;126r8Tk&$Ef9=dVe~WY)jzwpnuYm$nDR(N; zl&s5+#AaPhIScgxeH^-#)qarQOIjD?V3%sAUcyw*f|!y)rj zjEOuY_;Wt>Xhbg~2dD_RqzF*xcjk*pJqMwnX2s2aU)H zXl;&~E`&ac#4dbO_-8zHD$>5BvD1lz6>j@EqAvyeG&A~u$8lH{KHeHG)*8BpH#4Di z`F|16MjRl4&tm?cfzC}FCci|VP!+vGBdZy9zu@2Bx(C$Xy7j@vTQg?PcxA?tj7t8~ z{%!urkyCsPeB*o*tbzW?{vm!8dA@4Cf?+f2j9}LA;#oyM4VBufp>*Rl*^j%*8 z-$iIQ-($Wnp&nnTAI9Zx=%1MnkoUFndHmC$@xBheLeLJr)4tNsb-q460I9En?;v>8 z@_F%v7Qt(;FVBbI=x^>Ae5CBr4)AV5xa)jz-@35mrCqBf*s@C1L^4CFau zh*E?A7KR$Be^E`KmGP4*4lQlj0wfWaGVun*;DnF)!=!iPZ&i-;aDw7(A-#=TQIC)w zNr=-yq~GC0>+7T&;x+XW>6I|ny`-zFiKsXQKwl)Ugm!6YJYDPJVINMu?>Rfg>cX>Y zj{%^eh|CC(eA_mHbOG4-N#4shXAs0QIM)EyVEcmO7o4BL_4i+vzu=jA9=;YwgqHU3 zvjzCJqIlq>X$NzM>2?I?qx}9){%INh33oL76D7hPOkWc6bfR(Os?KfXPm?~Qs+g0x zHUL%l;v!V9@O!H$=NR13TufuXL))pr5}Mr8$5X`J3IlDMqE6gg*yzaIck@vgqz$^e zI+guE=jtm?ck-`6B~l&AnIW9bEW0t63zR|+F*@{yw>SGtlCOw=73Dn^(!^7fdhh1! z`DUIU&u{AoXuC&yS9;*vG0Wmx7x-3ic=Y>W>=$+_?gB$F{3d4J&03DaZ*j zdnor#%l-*erQEIRCfjxPpUdlt7|9JM0uEZ8d*fJ2dodEEn)W?`Nk#gxBd$oye^nTcB;S3AbM&R52k?Lw zzR-$ZlXOi)ZRr<<8D|Kz7UvhUO}dD|bpb}C1V4{v1mHiMta?f_|0%NvJtd9|+De2L zL2Y`^&<&>EeXQ5jL%dg+V5$2VkNF_JtNdOY)A&&6P0s$e;djBGn@R7*m6dhP>41Fo z40IK_e}Xn;G+u$OK?QseT9^9YfOf~d;(cf)vw(3&ow?K~1QuXwm%oyjVx&K#aA-IH z{YU%Cae)WxiAVF4p%r35mkh;A`6Xh+KKX-vfM&%J&4RNYuFSC!8n- zDEOh^YcX``<;+g{Q8iJWan_Jfx0_h=(!M99eL;L3lplif5$h!=|NYmO-);C-3HIRt z0a``kbD6Kg@`;2hJw?a&kqZQw&td$Pe7 zWihosPWlr2k<>qfopT`cCTG^JLbEXFftR_akiS9ll`+n`3rge2p1ut( zotJj&#zp-5I^0XSS(}4`N8@K#?u0y*>MNGDllr{wTb)P0=iJDN$zgwA{>;i6tDnTb zWYFHM6RXMpd7`(pk9=jbYvqPf@0H1g(q_WmsurA7fPT*FonJ2RGyHLf-=1|JeCYHD zyyEnvKV#kVT-&MVZl|JVeFf$^(*rB$-w-`Y92$|M281&{-rpng!scSXts3`ebSSXv zO3a1mM%25&-Ga%zD1T*EcZta0T4t|vQ9sv8_Yij->OaIk*AG6kmEFZ&gg7wUm*|^E zg!=Xse=1du=~&wHnSG1#yAq|tz>_yyBt<4u?)`SR=B0i_zrB*WH2odEe$QbHb z(20;M*z~7JDYVmj(R)C~+urIK=VHC&&&luZe~b3D@lEs><$K<|zIpV&`%O=hC!cYd zl+`2WbDqcKj~6{BPCieHW30EBH^f7^o>3u#JdAJPfdAb9d?e7yKQ3^Ed28DX<-~p9 zi2qPvFYonp<`{?dC#*Md(zdv`>Wrd3*0;KW$Wge*qY8a)YV%eUie1km-3IN+}_S@<2Zc`N2qJOKif)FA49(er!#&Wjv8pz*Ba2L z;LmQjEx|`X|9->oZeeo2&UadHl~n@e9YiZzgLEZa8bZkDCC14vsKe?Meo+%{nF;k_ z-ns-mirmAzgomv)I6i@O`yAddTH>kgU zlX(`jKLqV7@R$Fk{b)GI2^;}hIbsg99#nf2d|pjgbL1d|%2Tb?k5H{v;cQ976#n0G z_;R>4%{V4KuS&~7Q*}>$q4q&fa5dE&@~u!+&%!Ff26ORe>lMSGk@d!4Wvi;qWq=!y{)gR+)?Gb zPI;%GF5fQW5K!26%oju2;|uq}@cecC3F2U2jX>Q$8QNWa%V|*wksBw1Aj;USMlijA zIG0SiuvL-Ut|SC*2~qG{;rP;TgF)}W13nZ6*oC`e<-J@&%&jr>fKk*|d5tS*xTW{g{Skh0aeg;r`O)ygNxWB5Z@%|9R=C0)HZxxrjusGj!-zZRPacgxe zDG#4`7&>olKsdKx=Vz4&)(KW&f)0ve`?!1;Wmu|FD6~_vOo**u`-0;aoS(t<_g|I2 zY%$0a zk0r`Pjwynwb2s@aa>AiL^mi1N3!!Vn6CtvveT22s6fL^voiX)g>qEd{(aiU$w{)YDw!#af&hdrO?BzZi) zrz)Q+gQ6wfH%Crx+t*w#+3W+6`(i(fs)Av>NKDi4J3_Fp}xE4@dA@K?Hxd5rp$dM)nrasd77eeRsUmWsae%#yBz_T!d;D!J_M>}SO#qJTA8C%a4Y z-S_O`q4byDuUh-^cs`pm-naSQTI-q%#S`b=Cfn@$s<*mei$ZCH^#|emX_)?1PB^RI z)FKIeiP`dyC|ofUPj$V@_wNRFIrNKDP&*F$BLOa-b=a4=J2jO1O~oPL6>A31Uvlnq zi>9T2*SXFq(na)eQP80_o-K}j)=#>p5tzc%c&8%i##SUsWQ%k7zlwnsXM!+ML=!Ue z7n|>2I(wt>_SNX^FD3tcn0ZrqQ<-ObUS*!uh=?mpKLy$Zy5uB*XGZ7c`w^t>k4(OL zmw6Hx>i=0z?CFb63415y4b=Vovv}SuFi0F2?run}dGk5OXZOLiYv007@Yb%UuP1W_ zz~+k$ZZD-?H&KX5KeRzX=7%cI^9_a8M}>C}dId!o-&cgxHHO*Z{BnlpB~h2fLA&v< zjLT)r!8@Q&paukQAn27+-}2rJWlrj&$f{uQNj@qy){mM`FgWI?e%WenPQFY-Jy?_X zhH|!WJoJLqS_Dx3^&3OO0Mv+#AE)5k8PD=^7~rSydt2UnjORm9tRJDgY)1Sm(j94) zkKe;+4+==$9bpUs0o)%4lI}nx00)|*rWr%>SnCNjm2@+5%6uwC`(!+yprw^I|Eg)@xpNbl80O=O2I%-$%{ zyR3ARf0(p}zev=@^e?> zCS9BVa-nx|K$Y);KUHz+*UmEdQ?FJJ@HMeHj`I7z;A?a8;36h5DLhs0E8gJiJdpBz(p7@^TKKu_e_R$8lplifQBeMSoOSTuPkujV z*E-5xC|8#35yO+JL5O(vJ-w2%y6mr|Xh70$MT~)K8{Bmq>8Bpx>kxLu{iIh>pMua% zaPIohN7*N4KwDzI2Y28&77&Ls_CynDiK`Fin0-2d+QslZ3Oo+JVQ)Gn`x5Qj`_J~x zBfZ4(qDzr|Xtoyoas{kj~b%l>l`N6ybF=e+(+ z485up$fXJCS+wqDez)0&w?jWiza#roN08e!=mqPf!C61*&5Hg~Evau6-mA;Wxsm){ zi8MG6XT^KwI}6w^KO=n&2LtwV91<3o{d$xdMITst2rdB9&p#8fU<>8Nb5?!^={(#n zzkz&c4fE6v4 ze7>|8^3X9?&4{j)UnjIrSYiASUrT=`%S-s&=#vnlsAL|8A8cf7xP z=yxkM@Uit0?~e}j^JgI!`FtM*c&>W|qD9{ElnU(}@;q|S26s6Z?Z{5eIhmVEdZJgk z+~E7$l?IieU+aHWN7wM-@4E5ayt7ql-^MfEOU%R63b(hVouS+zdE@ei(l6&j=SYMH z=EY08GUd$l{NnkL`n8WN81XjwnmLJLu+Q0@9WM?EffYF~<^RZgUxj@VNSVuD7U{^+LS4*Ca%J z?yl?$xxZ3gGkr#!3Y`rqj%4j^$c50H%#75%R#sCsg+LA9YY>F*$Z3E~&l53?|6 ze_;Iu?JNHa`F0aAQJ;lxyn;4`eUb>&pNWH}`bMpk2tcZ?+6nfNNKPqg82dmWjvc|b zRDjq}5j_7`Wyrw-4E3K8YtvGhY6TihOC=H0={M*WwMg_?T(EddY2Gh}o6=jP-&MC% zEq+hqy@k+iDn|_eDxjh``e3O|Dqxo&*3^0213u1azPr@!H+J_958=(=-`}tY)Zehp z!6yGp4@}RU5jGdS|g_uY^a|Nck(rThSL zUy5&{?-I19&o&MPw}~YHFz0!=4*|em!~d3q{L+mH@{b@381)HY)MND_{R$1J4L}_Q zjl+L99O~mXQUoW=N^eN0Qq`D<0ozHZ0NV~g@8Lcm0Jk*H1C(J#1|3#Hb_` z7ZG_Y@~fh#%Dw+ddOfmF;PQYO0OBmyOnVsb|7@MZ7%1=mEFrqxV?71jtgnz~!W)Oy zl;W*8&Qw^sgHwXwh?eio+(EwAVDEDa#L5Zx(85tag`+U^u8Vlhi-OPfx>{_XLh;l; zkQn%d@_)C#R@YDjg-6~B=Xt5j(j$tpZ`As*#;iK;5q98o`)WPe=OT)Q770hev1r4Z z(u+nDDzxLBGxw-R^ttG65$&*(E)6Z>j>d35ufp=u^rLTvzivL|-Ku}L;f?ty>~bso zTyZlBp;ugW)j7Ivo_l#b``oFMm#d#(zqxlIbIJ8?bldduA#H!4KAS=w4uOWWbuWoL zkN))XorL`_;easi{@l~GIN*P#><=C3k3tENUl&C{2w=9$?m$14v6{Ns$8eLgt=2s6 zpnvmsrhg*TXAXjk`)R!}v;n_2R_}|0hqX^F^JUTBgH?nFL$Hq08TJ~U$Ki0t_XrW6 zOAvHIrxPNUdLxjWQEAYxtvmKW^6j@iHFuw{RR!(Z^lJ}d53Pe%)=PACXlZ@HK@cJi zK#ZIe()s#^L%a1Q=XK7UTe^vL%ozoJ+1jG!Q|>%M-v1`g^}FIQsMk9a4Sh&=kdXMg z7UokFL)HfUyA8g$H!sz5elB+1+^b_&GapNrD;m|7dK8{ta9lO!mH%H$LPP~A0KLU^<&;UM!?T5(ACy=)(U7j(1QnB+WJX6#I48i;J!k85cL%6NLCC+RB~9GG96w>Rx0;P8Bm1Plmb zxPq3UHTh6!-#5_Pl(P@o-*TIiLh*$7XPi|z)mY!>dAik26{I~EtexU8LYUS<;!t3{ zfrwqs%-&EtxF_ zdwpL$E$vjSeOwvjqO75US9N=9fhayzcY2NW%IxT93@igMh?FNCj=7{5^aRhP-ExLX z)@@@@>ttv;cOoB$j-r1bho<0A5CH{$n)P-Bla=)QaX9%Qz61W0lcJo^JcLqL90e-K zd}kg16JMiz#Uf!De8#ATN+@hqNmViPHVsu!S$+@V>!AD)&OG?9hhKv7UtiX1P`>?t zR(?N%8yWms4Z#(XeM&!z>*G}Be+9TR`zCvFvf=ju+_5f^&VIl@(N73Y{tx{G_!s=~ z5B>?Sy=!pw6>cS9#u?-K9A6pZJ>nP4Y*=#ihL8F^zA4 zW`M5uL(5v1%|3h7dKzZ|!P_~;@$DR6%qD#bbhLnW?6xX!W*?k_e&QhC9iVs1{)H3g zT4Qie1O@cx_k8xlbm%=$1N{J+@1%L&Rk3~rG9YL{wj z44M1&Mg@N7qL|vok)kZ}!AX8Egf<48KzQ7Of=iGd^5i70zD`^K!4E5mjI$fxt;0GQ}Le`VBgLO&6}P*7Jrao)fQGB&NS-Q zplQPyOvg9VpS;_U_I+iy45Po?=j@j~f;Zx=Zv-Ev_sQ9tK{>mBJkWiY1B`xdUFkg` zv}0%f6XGBo@lnXO&_1Lm z?*}Pcc`I^4L@(*@o!3YV=XNY7wC9-LLfX5>dfBGvuU)_PL&P zukrAnr&WmG%lhyP4axLw=le5!zxi1|c?U8B*~~wjWlo6$OGtz8vm$S1oKE>Ho&32v z=apQaDL24%WR`b#$ae1@8u1%oFOS{SgzQ?1(7dADr?@Mt91u zl{YOfk@7vDRm!pUt7Ya2sP(v-HXxm=w-X#%RG-s z<}d!KT5{DF;{$)I$WZ_~kkx$~nt?mhcxV$^MSt^dis5Sx98%V4ck~hTGt$y+;S<*7 z2mBNLu0$I9-|8pYsK2iGYM>Y6#7sTaAT4r`gT@wqBvMOIzsooU_18iBgRH-xedT{{ zKWYPi1fLLBQHjGLT9GwLz*yV%84q*8)M`PzA|nX%Ln zYr1h9xJCrjAsjw+5hLI$UyWV@+lB0kk3Wx}TfTE%TCIk~f1wkdLh)7hC zB!UP^&XOdDA*UfTOb$=%zQ5h#3GaLEch-0By6c{Gea3azr~0X`uBxuy`@d6_%QoQ7 zMC1sA6f-D*l1$XomMCH!^)NdU^)Q+I{iz?MwUYeKSx#C2axKi0FhUHT+a(i6*HL9` zMfoGN!R2D0SCIqb;CI8Vk8DTQ6jbv+_-CYS zVa>Us>utHQQu2TJy9b}efP)b+2MHjKufbdqMgJ??5ZH%204X=sXY5R$)ttVLp}_Hk z>x2_SDjRYZH@-wK|2nwP#DJn723iY$jU?ZwAmtd1afIidMV=v_jF4lR;~@++k3}4C z)#UfyjwmrCnWF*#mq2L0FNuRXFvGhxKs&CX5l7r*F~}T@df1b~b6UB&I!<%HsZ9s+ z&3M*$Sr6g<(vs)Q?~Rgo^|YA6a0v8W%;jq$!M8W)L$R@5wp>KJ9ZTP+pc;#Gw_R! zy8X;a#+|f0`KSHNL&G7JzV7rUhQO3Ke`(~9`KwhVhxQ0!_{MmK@wL$nT(oDd3r!Ei zFu+8s8=IoC#im~C7KP#^{gnd;sJYYX;#*AlHy!1q-^_ceL*#4>TTzbT`bN#>@-jiz zXk88ZH3r}1&^OcxV;Hm%Pek6c;p9hJ^PakxCr#Qn7qXwuE0kMdZhDLINi$1-1&t!d zY9iMiRy4*~0`IIhO%Koc6qRQIG)iALuF~FB>kG+n$GFr|wxCxYJyi#(A8S4=j$qwe zmv;`t;5Fa7A-xXsGw^2g-b47$lM^mBJWl3Q+4$|%n4e$~v$dRMYUbzg01VH$GjiJd z*708U=lQtDk+y8KndBz6+|G8yACAE(tC>sk7#$zZ|o1Xjt>80z(KJ=Qj$ z?FYr6II=HBQ~Udf#r#)>vWv{R@1PUFVZ1M08^rS)<;|dxbI>@#2nIlpUAux5K2|ku6D^LF=leghcXQbVX|>G=(iQk3jDbVnDlKzOQh7D>YrU1(Qkrju`1B zAhVt#Tk*Pt1y0xLl>B%ihMSP~|_ zTs;^+~uvXVn^N^2X%;UaWe)Z=4`YEtx-gv#cYwhTfrf&~Bkyb#+V3p* z4_;E!4y35hDBwrrm3$-)4gIpJq?tDYm@3$T3Wu4;DwLNKJ{p1=7-COAM>2D#p}XLp zWuVp7T-^(Lj=))Y4;yJ=J{0^Zff+XbybB$MdA2b$9v791{_VPtKM((bukA2{V*GxJ zysWv<_PBMfLXW6r9Lgew+)9e}*_mUZjF0zU&c^v+d>z&w4A#Sc8h#1ue_{Rh|BU|b zw)OXD!pF+8?jOcIk9^RLbvI2vm^8EF-1MetOAc6Le6T%ly|EuT$cH%;ca?8;*WY*TUu=o`jfb;9@jHz5j+KdIVk}Z$p722!vI(% z&Bi4ctVittzoOs|KlLuLcLMT>AifA{D76J^k<3#&OyosxEP!Tea}(M7=?p&kKCN-JkDP|Nz4E}}`PUR^8F!D@a4|5&&pf)Px@x8tH62%I?< znc@OZHByby1*7*RIj7vd%N~^9lV?bZ2&^`r!BL zhn{CV2SW8^UR%7>D7vu=zfSAS3CWG<@6o6=C4OXns#_fde>-~^?+AXWceuvsnqJyD zXti*+X2YS1+kZLm9OE{ux{|09s+v2|2VRLxjvD3>T$p6tl}zf$LXqCcS81=l`L^W* z9(Kun$$N)(+8NP;+t}UVdBN}ICbe01_zH~mU-5&h{hxVj`FMWxvHTy!GEQTKW37Yr z7d0#Dt%wHPH&DHlcO9JmB0CB!VE$%%pZ4=yE5=vU58iWhbo4fO-_g{UozQL{yItvS z1nTU!2d*C;ie9w4eq2rP-j!|FCSL>RW;RYw$*oVj%R=+S9_pK&+e>y32qgvYhDOl- z%0Q72_(+dJ(_(#aEJaOOXRfNeN8VrbbNMdBpnv5>W%bPYocme?*Murif3vY#0?3t5 z?+3#x^_A5OL{5JLuLj4{P8oBC*e@fNN2WzE&fu89M*;A@^#9&pGQe=HnxstaEA4j}=t#Uz{$W_wxJC9HL6QpvfO4@H12)v^VrMw0~f6uoCaH zuY0@e1@5nf1vQ&_e}O~Pme9|`#x{d_GcIssYDcd353NKKkU*ndb|(CFGVgz*{2p4! z4d_Mm5!NwU%^W)*_O_KHe2jfJ_F5ls-OqQ-dIj?@m35|H#Qw>5X5Uo`y893Osb{hy z74IFio?y3kg6~)Ee-W<3eejDF;`oApdTjfFoTa^ha$^E|_^!~R_Hl>oOwfQ}DhZ4} zX$MGMQMUa1xAuu;>_rmdzJO|HouHqe&;nF#(Sz)E!uH*;{kk*jBJ6(%`&Wb?{m=YI zUDe07g|NRaY5R``t6%kc>c7W6r{ERz>W;Sk^f^3Yr&&)mNQpqc;4SNd?u~qaznz2b zhl?Vw32(eA_5DcQO7dvgo$hk(%paAOLT!9TCGP!1R(tVxk1zuz^w#1#KR`J77B- zpqt2X6NPgsj-eIQ`$Uzt1dtZ7Cfh*GL0)|v46r#lM<#TfDvOg{6v#~5;WX76VLJ|- z)(xV@6-q)80nFA)PGb_F=jYVno|M0`e$~%FXA^-c^cs!E`3p)-vk}di+Q9p`^ z)W`UJkFFsKGQho*tq@+}iG?U{07ed?ULKHD0PGa#S?E{FBWDmZ_8V;KkAg@HBmf7mLE6>!Fgu@agXbaaia+#FG|*F2Eoy?f7UnjrvpBN_9Eq@c6>- zFT6g(-`{^4e|b{*gkQr`N7_e}Tvgc`eg+SgP0-}5&C<35>puU4=l@IoiCG{5Ng@jQ zQG09uVmK;AzWfR-eG|F_d4Yifm^(xa71n3gO365|KD0_&p8|LXIf@uvq4^yy4S)6C z$h+jjaO2I(c{vYxV}4;xmp}k(5?gV4C~r5{neWry=j1QJr=8EZqMffGmfmv>ckQP< z&NbTE7d`)^*Xx6yTiPmQ!iOWOMK6iOu;M!CDc~AQy_&AWPWZTW!s=@Qup{zCZI8%9 z?7S1G6wFV;*2&WSi(??EwXt4YO&12Vry3V)iJ_)ot+>940QI~it3gf!3}nOijodK= zgW7`y>c>1wyUDJC28N-6p74J-c%DuZY#zX^yn2#MK=yGO8GcoXUMlmaL2+ z#3_vSQUmpDkl?^F5i zA2<3LjMI2fmp6`5|BzKPZw>SD9Gg3K;(7~pP}B(hAzSR%f$kwvIg0yhsrtGMvEzoqQ7 zdRQXI3s?>8vneNAt+OJcf zb*(m5TYj&qTN$h){~N(id^NEPcfRrAdiw7f?~0B1jkvvpLIWB;!uS@Mi~JbCtk&kH zU_Z(^p&um!Ebk@n+C0tmV%G1*0-pPp)kp$DLajsb<|)=uYAD0cd|s({yWU9@xTKpW zPh47xp7+h+J;~P*_vL+CeLLt!8NPs3(D9guKZHhO?quHCo^qG%SpJ10jNa!t_jEsu zPn;PCURuoWpA#W|1zH_5zI-otKviDoe%(%YftDkrLKMGbRY*^Pmf}g7&=rIY_@SFY zA?~zftb5NN77C+ za&ziJ*WqH9_gV+HK?piX_pvhx5A&oWlpCV*%KP{ooHPr%MU|Jp8}bsr(9Ao55G8~k z=tbnFrc&;p`e@!0wY%E^5PL8Mib2rBytROaa5Yn(yGq%adaL0-;s9WL*?!eci{`>Ic4NX(u0f3(rA+ z^?6$9f*9_anLf1 zQ0DV0xJ$;HkKSFPKhvxyB=etxCJ)#<1gv56b#R5X#yT#U&7Aprlet#G>HZ5r@K3<- zrTZ6hy+mkCXgkx?Eb?7r zxZjcDo@{Vm%!0@n5hZ~)oxLvxW)LuNBdd4rtKjic!CU4<0(M_bXjQN>xc*4svev-4 zi#s0JLp#P6<3;yx)LZR$%MMv(o0qCpa3TT0T`KP?wT)#q?pAK<0otv^o?_sQ%vKqX zWz+A-!BOu->?E$CdBXIfZ!^7BcR#R)`bP^UIZnJm;J~Q(4HbEQ+~ugB3!uNS?`A*( zC0rF;*Bl4YU)mb|ovf!aRZDFx#JDQIUg^D3tf#C-*`H)Hub!tO_Pd8N?rz?u{-exi zWA<18hdPIOB3&z(rvsk#uDSGoxRGs?;(avI9~wOVa3E;3Uhl+-HzW!0& z)Wvx2HCGweIm$1qy$18LrPJ1?J1-FMur}?x4AxJ}g^gP^Pv!F(SNMg51m5rax^%E3 z_q`u+!M%m&1fBVv)N_`0e&>*NS_}8iE6jhdP!&ReRj_s7l^}Y7bGB=k6Z}c<7aNS* z`>MBrWc&wO`B(T&+S%(!lK=*s;;Z;pl)2KJY4RTY4(|;g-<4~7L{0an%;Wl~84)iq zkB{b-&+Enfyo?@Pko%XqdOC}9e}3Io0@AHFIr^+MG?qQPc(3X;cC;{{kE^cMNcu5_ zuafrz3Qm>p+N@^o5xvWieh#qs(#@CgZiT(6%7DtZook73mipcktVf!xg6t-W7 z{SSP9VgJg%@gGeF-<{!m`JD6x!DDsdGMk|t2}}}w{ZIeV0<2!!DF39|5`u{#5yZ9^ zS_3V=G4v@?QcE-6>)B@&J_=SVXyc!6NOM6hfnRcMdq6V+tqxH>0rCNNfMNRB0h>Qz zrGbCLTN9zt;F}rxpsmSMDg5_WodNw<9e8-$f6T!6N5{7q|FC&AcsA$?Uet+!PW(4a zMFb`V>IFIg1lI;P1?K}??*w`V@=<;_*fuzUa_>O5z+mW@K<5AeK5#YoTyQzR4+?Y$ zv3NO z8Ce1ZvoZ4mtWox^14_|wV3Pn)f(Lqt0iEo*;y^sv@^qW6D3(MmE=;*9Y9R`OHHjS4 zVbt%+X0Q7w2ibYxD!w3emn^Ml<6y2K8pt@$ED1^rset+Th$#)R`{{A!gOC}jh0wk{jNq0dH5n*~6S_F4d z1{D5D%&G(e6epDIZ~PPU-JksPvhYu`jUJUV3gM?gI{@G;uK(?n=i#DsLTA7U1t9O> zMDEX_D}laMSqJetS{#~gXGA$S{5mo(_CVyLWO`J|yp+pz-w)m|{6ACwDvm$+vHl8^ z>uBgpMx1_!`+oJzb6@1X(s4JV8&WT*dRujoGaY@s0v}+QIi9o3I~4=mhPmFT3JE!>2sz)S&`QD0aKI||#cWYl*h=Hk$F5;Sn;cZlr zPwRX^|F1`!h?~c_eh*a1sl#)N9yq#)=N7w@kS_s4Orsf9`u9Mdxj%6|?;b+`A25e_ zW&UT7Q(9QATkW06nh6XvX73x0oJ$44B7 zexl!SUg!4&^@zs6WDT=CCk$(D!-KW ztaxj^eLV+LkTTyVH+d7~$H)ki@-g^xo^cM&`-DSoP4my@BQ$XCU1ElX#irs`<>}&uhEa`xQN?Br(wW)9eR%05yfo)hHajU zvesQP=QxSBvu&8pV{?AmQSs!ZXF)6J31A>zDEHTb_C>X09qZRH^$nwZK|f}n+5QlR zAdRHl7=wNT=m5Ud1n5>h5#yT}KHCyyE{4Zqc82|S90iprZ)3&9LhsPtPG}iD!J13` z#%jIZPq_;D-d<=;@DJY;hW(PXt#o-kLO)6Q0a|MY9mq&JL*2NIZ}R&v+}iQH$0Pb7 zvgwunh|T-!@p~q%^@3Kw0pWtyQB!QWSBetrmtclioCQj~%G-PadXg}J1JH)3xjdI= zJSK)oX3N5qD4B($^=Fia-ydITzI2(_otOdPBRDAHY(xJs^|)S4IU5HC>q1qxe$ae> z9DwkYtw7)5wBWC3XB2@CGOl=@)&=SS!5Pq(L7P3Hr$BGA-X3M%WL|diKA+&e$8ekn z>Cb8Fl4d@%><<;0v%en=B>H^S0*6;RF1FtZD&r|*l)G6yV5iZ*bT7s~uL}*p@ z2mhP|pOyFA6@!Bq28!`i4c&y9vMh8ut0WovErAfDq1V)X{HgwiKW&E;_!CpE!fo?6 z{K-3IL4%9wH?z0ab_ktgg)lETT53oj1wj*a?47qd>hiYX_7I0(vHrsNI;=m0^&`HM zu>Kd;Z~d%;|7xFThCI6p|6BoA6?xSW{WgvA7LH6Q1OMEu-m$*7`DiygknX?OC;l70 zjz>~Ar+#T9LlJ02<$a9V$Aw|xg1!0O@JIMIK*GR2PJQMlL-M>TY=vsn52nx-#UZw0o(umymAbF+V zCV_Q7L7zkZbcGfHy?hSMWwtJG-vru6uIk~|Q8{vlqoCy29FfNrEs>)sO7>2Yc`hKC z(xhp8X(7Kk&@o9DrM#bw?HLdI67Ax60FnE-TIOXdBbyJshP!_n^R+4&GBB+UJx^{-|#GPsH^BMy&AEbamuJ zf%4S~l?GSbtC%(=>j50lnRh#z{RVeR?9vMx0 zxeasgN&v6gj{_Ti4iy#$cWT|#fhqNsTC`~Sr4=1`y+u<$_U)m+Hv+Gk z<$*uFqZ&mppRR7MRnGmi_f`69S>W)z1v%|xw4Z+^xfDuqIiGNxdrOQfj?Y}JoVkzc@MQYYo7iD$9VJnA5wp^(G^6a zg6B9AfcdxTg(Bt1BG+f0qpo<`zu;)^*vb4~$Y`BW2Q}hS{q@z*qx$ykvHzi5)Qi?9 z(@#H5K*X>l^E5ay^QE*;vIc|azq--r9=|(t9M>HqdCn{5F7svjy~cCPeSq;M=o+}- zRaA|LXCfGPsD=5m1TbeEOx>OF3G*^Ct6t8FJb$RMK^*kKhJlBJjE`?@gy_?{HUYi7 zx7efkH^(tg?(vc3J&ZT&CTBR&Ur%dK4R>$G^DT*3e8)NInLlTza(^Lx#6IJ`p&CGt zj?!h}4BXGRQp^56GuZhwiTehyw;KI5HX2imS@d&i^czvkyK}3jyzGpZQ#0eY><#pL zL}*6{oNo+tEHw&nUvqP#=wXD{&$b;@m((_#>};VxSV1l59MI!E=q9+_RA>xH`6Fl{ zHCv7bqufWnFSubq9O>{iG)kyfbrc+_>l2>Adr+Ox+Js`?g>%UcfDY?2bgim~?NxTX z>JIkhdxgWbf~J!?a15G>gX{#f2ppptv^6dBeWN8jVh0faJNv}n@pWmj@6yX8u`6TK zu=%In1s*TN_*WBX&H5r7wu3bQ8n*9-?boc6u>T?KU->8hQSm?c1zL2DZ6__p4lT%c zU0M6t-B9TxdZiXVI9g5Am*BI5;4nW^epfqmdVwy2WkRuYsi_Zw?UsOC z3RrjdT;6kP&-cv-E06iTV8v-!1 z4Vcs3a&#@HLjjC&Gu6-lUzMSY+L>M@V7pP2lThQfKwA(|j$%k;TDd6vJy7 zVo(L}ZpVcY3(Y2qDw}pE01thXFIju;XRhU<3M=xx;;E^u-JJNhmER*Vw=!QC3U1o< z4(rJ_&|GE>vK?aK@rB=CczuSyzyB2e@(BDBzK(hDamjGP@a44~Yv1CM1JDZpv@>=d zQ1|&K>bUUF&baUvL%(Cs7x*CRyn*u;iw`Qi3%VJ%zwp(d`qJjZUt*AqhA%GVbgXso z%jf^t>f80b>s{Bo7%JjpdqiP?(CNly<1Gv#%TvF~yoj8bYTgQsrk$M_0#8FP;6CmI zK7swm>W-NX3~!ztk<~r$eeWy&a{iO}L5J5^P%)s?Lyvb|(DeY~rC^0s z4ccL_+3QS>!hmx2(Dfy!-e-r^y1y;n_A$?WBhXMXJc`B?E?*Ep?)cf+Rbg0(DIC?q zO?wqrmm64G`c)@)TNd)QP|CE4-&cVZ8##g#x!SBig z=wp|9>f}c*x(r8;2n&16#iXS_BZrQDc6Q`mp4Vqh=VmXD#V+NID^Yp~2CL`H*K_kR zzsId4UrG8g-tj_20&+Js&@*=+*MsVu`5o8qId(c*GT&cV%^b(M{+rd*yvw|f(%-p% zg6>hjI#5i=e*VE=UFbDPwH}nqb4u--)E}p=BMFt>&StU^T>qFo7M63pkGW6eo!Jmm zCW?;P#k@sUg|hCN%j~mYi*x!p^Jz}8yk=Y8?=iD?a5%rOGe0K^TIrTL+So*UK}^64 zxt^;Auqm#x4D&QAh=2j)>P~)dZbjQ?v;0T^v-UDG$U@Cx4&du$T({Sl!SApbv|hEA0{n@f!cm2B#%Diu zF!UTdJdLE@AU)DH`0wR3JW-Tiz%9b~R7dt0;5)~0(b&!`OkkfW);H>QTRY$(9WyBW z$2yIgT!HJuL4Gps+pL>w(6aRFIq1X8Bj1w>z>VUdYph|mLA5-y_CDnZ%#^v%HiRtI zfu6@@vE44$5{D1-(^605IRgnxsn7L#ghJ51?t~+R{xa*SVo=10nJR%V7zzgQWJPa} z+WfO4;St|bp38fWhIV7c?1wfZys8AQvxVqTbL_v3H)6SHhK*Y|K$-eU)FaDM1+ zzKX5T2v%EnXggwxq+XJqVP`&_gio{X;q!IWSZIzeWd{_L!(}4xF^Tsf-<{xVS*Kxq z9o8T0^%>TW!L<`A`HrEPS^nd99bA?Qz_Ry!dzc8vDc_@`3M9 z?Rg*Ku8r}-Wv(~WVkarsD~|V)m{E9Z0px12Va10QeV-%T z8y|=`K7e{-f`<7XFf0^sn0|2h)QE{LFV7w5{@#JyH-9k`%y|S198di|a|iWX8DE)e zh(oOT(1-2!03%K{-PjD=8JQ5ZTy`=$*W&u)m+AlcA1-viaU46_qTuWhcqRGctA(y# zqu%Ie-}zXL|75dAfRN3-Oo3pD8mK{#Nqc;-#vr1}0reao@U* z<2*67N%9@^qfd>d?s)o}5lS?vcPjCGwmeuWJg0DWL^kU;W5}I+Y4lsYsgHs=mHA$!g{=E`3b33{_c6reax3H&YR~0pW#??^2I9MOph|Yr`?aO z%9fjUSA+bxPi?c>Ss-wBZe1YXqp`-AqggMpt@BriE6#l$98IOse}2hqoF(5yRgQk0 z$bFZrM?~+is##MkaKF`$lquTNRrPM0U(DiP`L1umH-b<<0ZN^qUHJMFp!?uBJE64* znSGu6o83&|36I@HOuzY~KyruWfxj?0*RRSC9+pPybN|5;7K1UJX8ipX&4a2T3T< zA^nDi?~$l+vGbd*L>%r2w4iK6+44cr#e z`T@#ofuE(Q_ZwT=enX4pwOu$hOMq<~m1akvmgXeb z6O{Ytv8Z?g@cd$MRoloL8$i3?=^-|ttD_pp)@3M;=k0nWi1Z%KeWN&=>IJUfAU3rZ z^tRQ@R-mKRLfLVE@Pb#=7_K)XBMYFe>!B`Iqr8KyYF*HS#F}p4`rD|dl&wBw8Ww^s zBJTAfzi%Z%s{!Tnj=%ai9 zGu}Gr9`ZK2LWh#)QxtwV&uk)rT2>b7{(~5fo(y&g-sAV5FpE5l!Nv?3p%?kxO+Z2; z3{?%CbDayJpE{Bp7zoIYsf`grX-n@Tr$D*ZxcfT+!oelJQ-N2xziDW+?cmOK-E_3T zK$G89*@5BEQP!0!)GDr%CDS(I&4`Ph<`pF>gjn+aC!EK0>*=fe5pJ4x;_C2u+!$BZ96fg(z{E4Aw1LN?=?mgZ4^bOjJ z$hwg>9Xn_F!YdOQw=drB$?b!|riN8Qj;x6|;f{}bna`qQ+SAE9Fo2ECt95S$Vr)ok zyF?6v?p=;j&QtVvmU**#@wkq4z>OKe$JI;f>__tNJ^~SBxjw9Osa6)luDyU%0MSZ`WIt z*@?%2L5{a~Rns0s{yovw=p=bAIS3T^#x*?%{`^?W|0b z_e);Oqd1e5`Lku2Pg4HU8f8|eJk}cLU>)RC$(wq&9q=ONYVXbO5P&f+{>?^{F|2(S zu`2N(^}Uuywr15isA4HRe{C>Ua6_n8po@hZx2BrIBy&2nG_*c21^TM_U|=BY;JUAJ z?g-k=y>jZ*b(ZnsUA;~)@8>^1UFRC@m?3Mh7*JJ$wMf4NeS}ed0ev3auo>D61JXU7 zJC+UYi$Djfa(WfdJEtC!oh!gLQ*A^2Lwr}@5gf%4stomNgF~i5=W%B446buFuYLm> z0?k~2He=QW7xloD{xjvr)n&ma`bBcwhfuzQ+Q>YE(K^@$@=drNI`G`r2(YXVJ&z-Y z{$QBT$GMwWGI2|shbCaAe*hZI>A0<-`}n;C^hd5%gigeCF5mxatPG)T8E+oXox>_= zNO=IYmq9aebhGYRJ&Ond632E=8*lZ(jAx?kJ&gam2if0{QfEo81J&vF;F@s|OOe5U|UJy+!^V z&FQ+7*QwX-EtYGMYxn{6su4mF!S$Z-LHGkT*HXXN{W*XC#D1dksQhXnr?wb#*o{0Yvs@h80ZZ}`(5&pAxLE2*D>gGI)1k~bO& z{hYUY8@ko%Y}b2SMc4*{u8azP3fGrY1qENfXXAIl*ORy&#@Au}0e%$LkKijCn1`_b z7uIiuum7*wCz`0%BF`}r->@BLm2jZFPx&qED#M}IkxV_Ix9{WYzsZMxuut6L_d0O) zD$w7ME+V&1fwy);@5?{r#~>^$a-`!V8z*c5C5XH|OL(2ghjjWOaz6v33#^>#m7pkWApSVW*Q&kMZe4+Ov;-Z?oc zk9iM#q~4x!8~j?ke&r8pSnL$Gpn|_tAu`yFltXFYqC)c$n<{Z)!_{Z+?`2;R?^1-^N1F*q{wNN&;81mNJ*h)7RY`ghUo^*o0D z6z58DRYYHD=h+g+_{wiCwYtiaT<_y*;X)sAo`~w_1V=g@uJKOvh%{Hqo2iq4U6JIN zQ$ON`h?hL9FVBRCI-Y?%*C1n&_mo{LbFVns*+=P|WO_$Vi>VNm$^3rd&GymHjEyO~ z(!iB^Wgr-$UDun=(GFfzW{y_gN`|XHqQIL*Q!YRH!lS^!42h zfV+%>p+j;MNA8?#pKlkiyZDK<7f%7R4qUHslX``RB#g{ogSf{fO`_|+$ujyZ@n9_D zvho{W$~tj*Jj%^F@aOX?AM4EDC1<4fF8#lfk(tSQ_EqxM_VT{44VGYKDQBeX1?O4H z2XMmh{*BvCk8=>^7x`+zf7T`InmEjZUxvO56{o!dxxe^dWxlGj3O=OXTUIm4;LnT9 zNz7$D!Ct|Q!8ObSP8@^xOE~pSz652qw~Clv+Do)@&F;{_<|$LYuK+9i3FqD?p)T3RfVVXn{&K#ld~N4k8rTsr__(fW#pv1RcG6LYAr`UeM`9)9EbbV zBcy7<5BMI(OC~D#a)pgEPeGSbz6Bo75B&!Hh2PonV+Wj0?4M=qdQCX=>usJ>h*YVU zpv6>@eH83{`>t+DDDFqxHyfPF`XDpEpB>OzhkXQOUjDT2iXD0s<(_bl&d?)#wXLDD zKg0p(zv7>_xn7T6RfFz;8}hyWY2Rg+g;XhXFh~#EzWWX74l>@beK%~shVO>`4`Kfb zJn+BUuZw_Z#&Es)AO0h3E+=fgeJfTF2mIuyF3GXlN{`3h0ba9)Ae~AZzLT6g1K#a;-VAa)$v@@7Z{lDjWE9^=Op=H5MB4=I)eF;D7 z54w}zPa_RS;^+IBf6v%G=VJKpuQ~(9Uv=Q&ao?YTR3fE4s8G9L6KUjVtwQk>h3+L_ zn;!-vOlA}1o4`BD1VN^3bA@TGH>D!|tYuzkrZ zTp!OC!~9v_u?gHz`G2aOHJyKx2ulzET8!L4fIDsGm#uPnW=S!)vtPgu5*bQdxknU6 zPQZQAW|X&piSjjw`UgYh9O^{7-DvfU9!Gfv2I}klUX8pnfIeXLBqmn?Dd7;mbG;HK z?(@(T>cP;-jJ%6;LkzV|MS)fu*_^Hsv?``yVay$g+}a1N6dqsr{jmoffQ{U`C4 zMDnq#ur3Y4yCR^u07y}k@AFT-+57xc{WJgEhM^Hd1OZ-#ZMa+lzX#Z2pxa6ms~dnxxCyz_dMXFbzLFqP$ib*GpO-3qN-~>^|?>3%`BAIBe{|AmYkf;H%2- zZ<9lWoZ_U&84e64>X@#M@j*G7J8L`m9mC9gH+V5$zbq zPsT-@*s5vedYk!K#m?uK?KdL zC=5Pvt0UUQ`54z8;~{q?3^?mU-9i{Vj{JCGz-HuI!t42dOyGItPL|wVkNGs(7>zN0 zC~Kk>?WaBcosM=4q5Ok&MFJ#pUk$#LHi&XVRok(c`WeQY2*#oJBN^Lpy^!@Z#$U-4 ze#{U#UInT44cC9s_pGMWo2}}bC@T6{yv^R?oL3z#__cPntS^3#3 zM_B{aLfJyi+HJP54Z>wPDkzz9l=+$CdD!Wwz>mJ7YzR?1dFa+Jc)!~sUrG3t`FXQI zpsJg8W34YST9e6QmNFZ&u3GvB`;q%O7jsr*55Pe9q3@j6M}O0Uje?X-zqQEZJ%_HF z$r3ntJ(SY(%v=nHPoL~@xh4j~vgg~SJNbQ8uwqcw;}r~PUqcUqVgxs79KEBVPQDA) z8Ai4Fw&J$~NA?8D<;lQjJp8(yf5}9%3fUPny)j$&rCbKZ4IE*e1z|i1okRxy(=_`O zZsZvGU5~=~FACmhF>rGnMjdkUm2RM?Y1S9GrlxJc?gPpTQQio?nMS=|FpJ%xJctPM zBGfOfyA!G?0X`;90T#X*Xm8aSBom8u#xw8P8R5^MsLFRdnBKe&Eyr6}!0#nhQ7y=9 zmvzd(dRKoU>k>zQJ^cZ+0d5Bw-$4Qq5i{v;;`4L+5Ks8S-4IM!k?rrGrNVr(&M)K{&K2e>cxz0mhhW}?!y)XIK!zgQ& z&jL@Nr`qzt`hd_=2j!iZppnD6G>0t-K0IT6fngauLU<>56Fk_AF#@PDUk9LTkY5d; zAK#bnb+NZxp&UziVkPJ){ibcVNhYvyJnc5r)Ab7E(t0dGnf%^~u*?+bJY4y&L3a@9 zkPB@~NC9|F7v>PihS2dk#+Gk6AQteQx{Uk-HzEC#?0$^EekAR+Ct#sBG=lm35IUdh z$V=-%>tWe4-pV8(ay{28qRj{$$qoP`Xg`vp0*V6HmK)dgVy;(5f+E+0U4v;MH+XWL z@A&}uDReB@O^&=onkS3B$-lzq3w#XDd_SXc4*l6Pb9c*w_i*etFZf9Ts+C++^5J5Y z!JqBS@qtU={$FGFMqTAOo6QIbeDycajq^QAcjrdbc8%eNNAyqPu#Gwxp(2ZcOH*(3 zxW#+k_WN(We*@Qe{W%8$XE48|6m=J8ofUQ$G-HQ@XB(Q6&8N{{KFRlMTw(N=cT?U- zy?`V53*UDCi@>cY=jZO5z>xUM*|*TgmakbG`9lul8E0kb89c9n=f#4oUvoWsJiNs` zUChx%zWvMdy8Egzj-pW~ zB4f~J$~!tZSf`GXj_How% z1KyMVgS2}V)W`UBO%jE8pUx7FcO4({+?$TJ+G75Gca|_d=l9-(vmfF;wF&kMz5-42 zpAi@oS0SoOG~-JB>E7zJxk_#MY5UY&3z`3iV_u12d~s{z=Eo%gzZO-wRgrP$+n(=y z>@?=9r@wt*3+*O3Mmv6jc2##QzS~e#a8a-jpUYieKR!H{d;?f4pPsCzap;rzy)$VjH0qR5AHpZJV;#cW-k< z)&S`r0ZtdWzSw%7-2nLmrB8<($0~3f8jUu|_~0sW|7hR+n|A!t(EbPyU7N^I`wPP`O;D`m8m)Ehs+=Cv5ZwWBii^ZW0 zv^{7p6FNjsw2u^Q00(SM+2Rn)Q8qs>rjbkV?M-&T=MDm)CQ**(&iT+>P>dWN*wl&! zahB&w`0p<~1IAx??BSuy44eXH9l+%11*#n(adIzk4d6*RRScZUMD1G+y~tGnFqu#5 z?a1zv>a4Bs{S4$<&2W~AeTf>e(97Hx2FLT)j@ znEqdb<1on73Jx?&Lz@IP$Hw@veiibCBzGC-?bl2hsy$ zuuHT_znb*|8z0;$?n<1Dov}m9XPRKBDCRF*zXZQ~;@$CXX|;b;5lZk5Xu zF)XzW&J@S7Z?SKS4?{`bNN>xW+U$UsTw`V>3@~v^VyDM2ZxJZ}L<@jV zQRR$%*ek}RY`B2oCFhuLU^?SZs1^G_(JwJb-EfVF@ zc^LSb-mCUTkAAd&IdV|~2}Zno#fY?z@87i#q(y< z-b!;S*)YmHZ=H}#rO-EKC$d)5y-ArjZXmZ$ZaVwW$=gtw2#sBDKVs~3mi%`2U-+#(46`URTnTA3LkHP zc@s=#2M`uezt{%1kbYhq;5f~j*oLpiQ5A#Ki$a-WT@gN6m@kQi{f8lre4>MPrg(4g zj^L;xpdRjL?w=CFn;xyZ+74}xim|e}zK{rY<{zc=5j#MqBCY$W_p}~r?;Ma1gB!Sq ze>J8?dCm|!K=Ue!t31D%ZX!D-=rPQK;4+sQrg=X&7pjQ?UVn|F|2O7mKbzvox;Thg zPH>Td8M`Yqh+9MY_Y8=z4)wbbs+mT)0QZ-m{4w~c2IWW$i!SITkk+rz`WWDop}$}V z=KIIZ@U--c*=nl!9>K3yZT|ALUaUJ&PN5ZPCk33j4LX>-#k$Z4{)>SogI80aF4a#0 zd1%jN2T%l*-+G7g8|n%D3G|+R)OOI{$DjHwT@4c}23)<*ZtpCklX<$v;S@EYf8x(p zsN~U(8vJS%fUs%5tx%7Wpe_z8B z((UK`vwV00=~R*HU6F?(|1=0e#eX66Rs#DP z*p7IULxR9};IHns{LTiq`zRk%yX`Z(FCmBJxwq*L@)x-qVF&c}AoLDgX&pztg0pbr z#p+gc^S*os^Q^J$EbGw(z}LvH)povFGN@7H^W$g}B9FR~=po~G&^qJNvq1t)DaTTd zr2R|QAM(8c=w9UeT*3=v{9BRB;5u}OdTK1xq=mcC`Ix@DKr^^5zkApz@*MPO4ne8~ zElIm&p_$mT0?=rFX$0*|&hi+Z|E#e~Uj~j`F(QpruJ6LW^FFjP@>p=y?;tnEW5udO zi|Yhh)>UPwA8*uf@Lc`0k!LL8dQt0T%R;O#_wNr1UKm3r`UL9DFp5i{Wx(Z22=pXQ z^zBRauHOO2Z#a4Q>^Jx`w^f^1aUj1x5NsK20j^w-kw1(6`V)eiIHFT!T}U0Afu0k1 z)cZhSA$rbBp2F@QP|v=8P<0w4GA4)OEeVqS{w9dn9hF)8cA951k^pJ%W00bt&_eBZ@q5?^`d zNYWX`v8~4bJDZu$s3|c+qLxA5^puPhT=S+{W6+Kx-Z@$v(#_U17}}^j@TOkb;-!lN z>#l_c1P6n^vs|~FCNSi~oLRZ(0fe0tl)&w%nArN!2Phx39M6NYX2(D@xv@=`%B9x?i=iwD_buc6&*fDQQH6BvEFeSeIYfNDf(vc zGykwaJCMM>pC#OyC0ZrrQ`RB=DLY zEq2J!d){if$MbmK{!6~GzT3R_F%iq%M|kd1N5H|lu-c$+Fiz0lv!ag?wsOJ(=b9gx zPg=*g{sZowJm?bUTh{dhI-h)B9MScp1Wtq1=df9|^6&G;`F~|ye4Z1`r5|cE8q7=3 z{OU_AmdbjL@Pt(*B|H0VBR=xZG*k#^-wZPLhL1j{$E9q?Xy z5v2Shng;6=yJ>a)6`cEr{pU~n?xWauU!dKSy1DJp`a8a^YUAs-LEqWjw+cxuIQ=U6 zsI>GazHSRTj-?!FRb=ewSs>q=(Ba_vIOxBT@BfW|Zi9QSsI$`t0?+Lp{p6Le(S$vNBFTjOqM^Dt# zZQODe?-laI*5408yMvyPC*T}GI{#=oMCJKld zZ`KO|3_~x3UJC(4t@5EE<^rx?GoLmon;B+|>4DZY`^nj1ro*&M0He9o%oc;bl}3)@ zcwS6D)mS320ds|I#(0s;Cm5+JgQD06nuRJSK(7L@R20eYfVb_SHDHJ9q2*9(1@OB0 zAHWskav=raN69^8x3`u}ZM#ywreCu&qkOd7l5#^}ssQJEv}{025#Z1dS`oEb0Hq(c z`y{jrY`!A22v7ip1$NsNXTM|sTDIa_mB{wHlnqp*23y`M%Df%c|YJ16!6~l7@ z+dv#1U-f8$2 z2z6f!iznf909E}h|DAyrfGbbu`Y^cm0ccAMpe13C$Bc^x1}Vo(=N^ZY+d1|d1EE(O zPUB5zPe-QVLY_1WEey>^PV5TxFn{9ySh)0Z+8t*2jf)sqo^TIziUFb>-1j{ULruI{ ze)zRn%PeDl2R@!0Qz>dTw>=WyHnuL$EAL8ojDVJL<{22!Ji{W2OXf>fx167H%Mfpt z>)q=^?)BN+GuUmgQmvC(1zK!oN5`jPeknQ$JLAOk-8~cBTQN9Q zj_DqmgaN2oV7Px2{W+l{OaOT2)(?bQB8%=F&NkAJb7=)Lo=QbdmO50hd0G0K(=4Y` z&MSzIgLjXfjpjM?@@*_$A91lJU;k29>EG+l=vWlbtMhKxIMtm1;RzR7Y;4Qt;;oYJ z!D`qyUnw-RN^j(6K6X<0p65Poy%=J?jj6hhvkv|F-2C1DAmZzmImJsm?wZCEQ8BbT z%4i(fh~M?0(uYETUniq)6hv)G070toAfcghTkjdW#W+MX;Z}U zt#`8;YEdpm-qry6amT7D4p6g^wN5hptte}=#m_kO17e6%KVz_~%J0{Bo6Ii`>(L_D z*_~jB{ucUwp~4dyFCro4PRzK{Z}Xg0W}iS4=3#%ZQ;6qgznj%68#Tx~!Z*-+jP^dZCYj8q zIe^R)41AopswJ>7FV<&dfNM@1y|8otoy2-$aMZ6z<`dz71?{ufGcZFPrQ89-8Sk5X`B^fLdIN?r@DQuHgieR{WRzE+ zoynUi0Ifm&iPRgRf3q{vOQ=jue=v$S0a@T6*9&Z1RGpmp4C)QkRmizvYfv>@(@(HU ztQcBpua_N&*^sxzdPhmjCmC>hD;aHPmkZ?p06+jqL_t)o_`N82=>upv&cbcV^@sEa z_E!8&RBgS5>sK+~%Q(Bi8TvyzvUkBM>OH7?YH?IJ;Q&uVd#Z+d3bY2OrjuPyGXrAq zFDQYVx`*D3OG4@EyzRPny~;L58%h|&M#`l?cFZ&VuA@glBQdi!r2ag$TVJF6gerqc zPa@m9Ff)t|vj;!M}P!%TY_l5rwi`8!8+B^Imi_klU-2N2-xBPW=sTc*f7ReiQY1%J;_y zHY_56NgUNM*2bT$F>QZNIZ|EJJE2F&AAA<-B=eAY#5Ka!n_!up>9UkK~B|9Sem z$iEqO^OKx#XXf?IKc07i_1tM1;WfanG3L%IUp&P&--Jt=b?Dx<=^@l37hMffb zw?^;}fd$?51%CftH?;9(3OhnQMEN-V5qnH=S}$#vf484|nVHJvzGeCv#|Oxc0DstD zY7l3Tp7x!WE`TroqaPM&|m0%7ibYiwx8e68HEft*X2~YJdkKKZ#oEe46KV z&YPKgiTgG?Haj0h@Ax#YnsDduv8+?^M&*jKET0d49s&o4f*~w-+}FhOS%eQg=vK^=k>HUddr$2+){FjG z`i6^j?f723EjzjeV*FkF%b4$IB1@(+AN#C&oHnm~tGzpY;Br=@A-L9bhZ>j%fnft< zr$zn5b8>QTq1_iaOsfXe;Yc*VIcBZU;-KK51W?vx#(CFy$MHVxPbCcF zF!VDHlY0%bTSjCsaw7E#v*!hPF4T!5(H7E9a{@XJ;6It3vi9Ci-t!*)nz4uGK1x^> z-x%}b*^;+QZbRRwn?5j;dAoAv>NnRqF^(JV=UnfgU%ce+7g$Yu^FsMU=sk{+>=1AV zdWAjfp5*#g0vu*C-=?#&Q~IT?m#h@}F$0Zj9sPgUe9Pp$7|%FfkbV#-y-^(e=1-vk zW*q$)Y&0^)(B4Xic0_W0VQ50A3G}FW-jww<6}%foz1ez_9Uxzf6)1SHDemu2p%u^? zz^P~#`E?IyUwsP<%9#)J>Svy}#M)^wPJNs&r#06LfgGiNyeh*XTHs?){5R0@^2I~* z39^TFRUKq!HvKkX#ZoS(p47Z2zQudC-msqkf0X}~0=L$OmgKwm5L$$qEufd~+jmRj zq*-#w6{VHyeF_(&JqJclcVi;{KL;5omMFGdjV;sx>r?5z9PRWpsC;Vf${_ zevSMK`yayo6&rsvfZHLLz&^Dlp+Mi^FvP0Rui3c~exh&d{CYpXZv!pOp&ZNMLa~$| zRPBt0luzg^l2Mf|3o^M14e4wR4$~`jj@}0?N)6_JZoJ~V34 z+EB_Pb%w_8q^sy;oeN#Av#^XP!m7&4^RKPEw&2>PapR~N{`(8gfbka`b9l)AI0H8^ zXWrn#FR-gh(1mIf&_*If_h4L<%>2@}Av%MTTCY=f1JN>}+cBX6z^o1YF2L1+YAAq9 z031M6T?V#QfL=v)tO>0JJeTr$HtByIngSI69=aa*R}7lQ&H$p2#sgD3LL-4%Y0z{O zsPWL!xQ#r}gSg=*KrPf>6d!)QVn?>`#V0h-DP`am-oUvp>* z^&Wt>MJ+`E0WKc5x5Ui@W(#oK#t{YOq3Mjz0Zm|(-Jp>e;$?gpj6lW{9$)zVh1X~J z`}@DmU$((N#ZZt$ri;jd2;B4~pzu$i<-g>gcA%6W_oVR8(ntxB5A)OwIiuVfiHg04 zvTR&E9N>A!Xm1>B3m@MUQA!05mS7*aaAcDhjL9RpnZT+dN2D&Uk1d?HAGm>&E>9NLEpoemwT4Uleg!*!<@{aG348fXB!Uzau|6JVZE zAhTjRa-hc1N^unc+>wvFVx!BlgZY5FEs}3aCQjmnqKwD2-Lb=iyt>^tqyM!8?5)?y ztck=vQma63svKn6dlY`7dUg8W*BqPo zD&v}`ZieWWuI$L?tin9rR$D^I_uy3Zm~TGUXX?pDJoB){IO-9(|D*ce*iF4P)^p|( zXf?INY{>Njx`+`0eNFwUebCxCTDw3~k!y@oJ!fsSGtO&a!s*0)PF9NGfzj3~+hCNe zUbZvLu3AsqhTtm1Ko_RoY_q7n#p*9sXM5}Zi{=dTL;CeAY7YHVuV7X_K)o2AR*Uje z)=dj}Y+W(?(l=!e1wV+v=)?cP-g`hfl+AAMf2UL^J^}C>)E6Dv2TJjeQB72(qkMT7!&cG_S)w{^eT7@|f!XU)mdfNa|9$K&g>3UFie0P}T$MOt#P1TkiIa;#yt9@ihmiC?~ zrj#Mh?(aGn)bEnMt;X91i#wR&i<54IiTZV9O~z9T9fY^oIX0K;2IW49 zjMlEo&L|=|yg@7sh(=Y*#c%|Z5;qF@PFzr*bC6{~*7P@R{;mC;Jir}9kq+X|p#C7^ET|s^ z^}nEg`=48XKg4(XG%jq%kUsc{`^aXj_R&aL|85~~YJF_Nxh1RWZqgDL;5nr56ReBy z4u9|yPyCPkgf-PR;BC^zv*rL?Sbt)Xjll^ck+I|}crSy{vJ;Ud*z@Z~o@6DRj4a10 z2d+^Sv|Nc4U~O03F$D@Q^oRZs$01Q|xnGl#$ojrdOBDki#{X;~wlVBR*?L9~V*Okt z?PZk}9F)uoChI)-1!qw5W%Uxg(4IHZ4hqZ)A`nN#Evhdvo)#H^e37je?UCYu2;S3* z5uZ}p<0cUS!0%cd{1rG@_$(zjrv|yiBg5G@%zAEpYw5JGW_)gZYN0onn~mD$X7KS( zA-QhK<7(*%cY(*)ao0}jUuxN zFSN&DtF_Sqz2(*LYwiTfdF#;#gWql0y`$?c#^e35nZ53jpR0j$ngd*^x6>~=bEtPS zJ;WHriJqlOjx2M6lbRmY+E^1@mT^D3Sr*r|9nLRw@MUhloRVJ3oBw_8Y2P~D-!61q z$O`g%%DL8w)Mnuh(2@J6y(#&(sb91|K|8*@@gC){-c)CCAUNXR;{Sy7dbLwsBwaXvbbxjx z5|d9Hn2kkjdul@@fTH_8Iod#A_~AV#>d_8sve#vf!_TE}_0US+(LNJHuXq~LzqK%% z^WKoEVY#8a2Y1eN)rtQ2Des<-@&?-ZLjyhN_jC-_&G=oF$o65C@V^N!a5S<6&o5)N zFXH`am}+$LJI3K@jC1Qd^P!AOe|mly|0C*|l2_bE`>9_sw0}msPjg&#{LXXZ(6p&9 zcy_Jeb;k)u6a74Cct)a&i5?z}&MlEq&xwN#UvXIuA)^7=H+m1*U6r3H?dy zjS#&6N1ylY`UUyjAV0r`@es5>1nn!L*Z-xzZ&Y<{oKXX|W+m6NKs(I;*qgq?W+ePd zafyTorl=qi1^@oWEs#&vUW10Gnkv_>3xe*iU*lu&x`jsy5XIoadmJTa&&5WEMbt3rB}r$a|O$mLYd=y(Kal z$T}6d4SJh;kjoC6m_7o%o=Lhc^s;lf@l8E?Vy8T|hKBj=aL|C~RwaA6YQnFlo>3e5=!y$wj2KXZ})yZQ4nu(B=B zxwHh?S&RRDb!I?KFXC7sq3u;~{iZI4%PcG14msXjE3H0cTAvyIVQ3ule)QQL^zgc2f=`(jO5>;d+<+8-Y*x{V$k}w~${od|(Gbh;0cw<6*uwFS2q$H0k3U zy&RwOy9A@M84vN*GjxKRd>#GuD?0POr)TI%xpV9Lu)=Syt^V*TW zhaDf3tLHaeKkmr-R%}vzwD)0PqR>BSRAvL|1Q5!2*x5+(lV6ttgIozj~|5t}7U%M?HM$I>c0iQdJf_ zH5WZ#NqFNZ2nh(I`WE%q+N!qtP10X#r_I~&D{ZaAK8USA19N16esE{1r6FBeUOVY$ zL*C>0N@}MaBJ2WZy*@*FD2Fs4^b}P{uR!}GYeV$KTo|cGDnGI^k$HY5|1Z=coqDQm zdS9&%=`ohWCPD{Wv&7)bD&ALt1FWCTUYu&rdIu4Dj&!2-BjHmu+yLqk22?YrT5+uG zC`MWrMf+mVEupy2y1YXO7ptT-&XRVhVBIo@B8zCrI=F(9Jm2x5aNTH^mbf3{EuhR1 z`BP}uB}G>?W!+a1YLHo&{70Fq{RL=;-}8IP4w;-ca(Bv^n*MVBkNk{pbCUUVfPN3G z4ZLFRW!xm1PqP-yGv8f4CJ`6*rfzO~b|TkL-Yc6a<8TFsR+L9x;jpAW zNFS^JX=FDLz(8aSG&%L*|B3b)Z*uk}?@>QAy=^DiRLLA1`a?{~8e;3kUi zEz)x09p5SYjb7w88qO3?+luR8bw~xce#-jIjs)OUcWlr0P!JT)+wJE@&cemw4Duim zD~=#>NWmpS)2>o7#-CbADB;qiee~DY$kx!k^80vjWI3dK^YlBjUn+V2cYJmi_oT|C zheM}MK!(ykQqQqkqP?@pPu^1hGt7Gfkl#S-cj11F`pu4vagy3JB0Y?qQ15ZyN%;$q z7I)c`$Q_s^o=5)s@n@FpAX||Bm}dX}zU^?(5gaQ0bw@3gonQR-S=*2z=(8wkX`Ok< zApQ*M56qK{ivJw?krNkfna>LG<<3GLW8Ct+GY93{hKQhk6x457$AkL&pZvsG*ojuO z$5mEy@Fr_$uI)hP17%A)orW<1*RcjA-`za! zUjheskQHGi>LFKJbJVNIpe{8NKIJ5+CkI&v#Nt3I@KrRjqUvUcD1VNlkH&Mk zkoJm{o3gI79S)CBKfzzWV6p{27?lD=jK(~dryo&C=nYwp^Nw-Alx@adaR{XjhV=#M z81s=h4p<2oN^2lHSspv2dJfvj1)f_564-{E%&I++{KD0Gy+8RKaqiU1alM^3PVkDJ ztR5=xN8ZVt7P$xLRx{^X9{E=uU&&YIHD0&;=8D@>QA$bG^uK;rUne^v1B(N%8q$yHIHXd)z*v9xz)JL@ z*^#br^a9tju2C-Vn9t?i;-fv)C3Q;lN%gV%SYJ>33^&i4lgV#5h&B=RV!ijEk8%;& zp@ukeI1}AVJVVHLL*Rh{PV|g%H*(R>Au42phw<#_r}uCy9Y}lR zvu=*!_oejTb=rZi@RXX)^H+?G#!K7}1+}iBeQJC5y06nOodUy*gIv#Goy|wyK;Pew z+|HN&9I_d#s*KmB+Hw#i5iK~O`YH1L3d2(&uSwI-c_&K_`f1wW*ySo|KvZ9@)h48U54!f z^aFR3?8sQ>nUNR=%o?@r@+ML5?%e)semSnfZO7#EXeqR-y04Dfjz-t5g_hKBH!(oY zA*Z8tH$F-`&dWQ-u4%?-oZM!Vw|^Spiq6Y^0OY#Ukr5 zV=*pmd~G6w{BDq+KW2}Ip#7m6?eOo{Z~x!0uUux9NT=S9*bXZ5;Trbsk0HHUT~(_| zPl8dHK>Bgq%+TZ2e6>NBdFDy|6PqV_jG2phMCB`&tPiS+x~vu=&vPiJg)FYJRC(lj z?H61b;UDx;>K1Y~xMMj|(@X1us|FA`Wj5o@=;7-x+g0LZH5FXq?CRI8KO1$hVnJ>0(DSq$^W zTjmw}2!IeX!~}qviRMak1@bG?v;eqfbF+b2&3^uZxgM#TP0WVKH0x<&BEN59yk@Q? zJ)Oe?P<(h_h*=pq%JkU|28GNHvO~}O9e|ln#}tMBzmGhH*}X2Z15+jw9O|P`>!OG~ zuob*WRKn7vucAiwM@pdFTx0==*7nHf0dl_~b#jq>KSlL>9@!e2dJWR0^|68A3f_=F zdYZP`R&e9kpU(u&Wd4@DW33+ko^rnis@+Uv22^W3WJh+~A3eWBF;kXd9@@qpV*!er zpaf*zEx;@WA@py~pHDG=GM|Gudx!#$KYV#HOsXOflw-Kwo!M_O=~?=DAzbuhYM`o4 zdIPg)Dg=viE_YNyHsUPC`4CqhI3a=)pg zn*Iia%?qI&JoM zu8^%B0J!sa&rN3x`IpIW=x@mT>v{|OT0(3!Ej&N=a{{|;OIhA%5q8W^qmD*VpNM={ zY{6U%|ABqyHiSdi^~(R!2k#E~8`33w(eXqbncB(nO%14xEm*a%N zI(Z@d=8%g&XH#B^IAUZkD98Lb`B*S0!0C#!#Vf; zeG8D^S!qU5%F$YT$8i|qErHPg8+l(HHAJVsFqF==kHmRN+iF70sZedIibqaVM}?48 zEA)4im*;xo4w6T@pQ@Ww5IoapQJpcRu|patSa?OboLB?kLSl)xBXpseyNqBKY>0r z>QPz_<@8R;OUVs`_=`4D1J!7s%>hjuN5CO&>ZIjg3f#zUgu}&v%e#+#fAX+-iPACdwC}85{40iYx4k3VfXaS>=lXM? z!VZ3Sh#;EuFUI>=J7Q98o3fUjbtNQ396RO*6EgtE3+tBb_*Vs0IoqSV3}2$O<3VtV z;E!Q^sXe&An5Uvhr)yJ`5D)t?gfq^-6yMp`i>jX#-xsCmifLU!*Jtpph=alaNO?`Y zPdtDMr2AN($&PtKhyF|%HPBaW#dwAof86$1e?pD1an3Y#Scq%&wK`$<-vh0eVq9rc za0y699#^s_oexy|Oo4;ca`l+nN!kqx`WWdW!bnAA6&#C(bAKZ9!4=Y_)oHtab)fRz z=K2V2D-d_N1@gq=eal4J`Nku zw6|5NmV{hk4X`6pMXT@Z9Z=6u0{Rtf=f_ka`0QCq1c9oa62efuLM$x$OY5kTr9O<# zwYKBL z6f%fEgZR2M`~{?C&WN)G>i{D!4-M*|V6*a*^Lzo$d8OQ`7#LpJ0Ek%}-$P>j1V81AGM= z4-8R1fG~(Wgjuj8vMmPzke@Y|9mVgG9>UwfX@tU`C+jXO$w=i#)>92_M?H_Vn3Yr< z0PhKZqh^2$!nhxa>Ao|v8B7c~2#hk=uE$p5CY2}s4v0h6#}XLs-a$T4ent7!B5(pY z03%*gTVLs~>WJZAjbv|f0&=KQy5O2~R$VI|S&HozNtE{`VlUAioVa^f41=Jm1-9eD zv+Rd1$8#gp3$l)?@08o-m3DxJ=Ya>$8Ffwh{T|LzKZ~qxov?o4`fuhnqML~W;c?nl zBOrTKkP(J+PO$$PnFPAJO@5n*oUnxF9-_@`_lEzc|}wsHe~Joy||>eNo0VgYoXH;q*HhZ=PjgzlKb~ zA^XYfv$?cyptQeGAcOZLn$6@$Q&%6?uTJz8=T+wnS5xvo;qp6+k#85vEsh?Z37$0` z5B2%XchL75zx&bJX^}rWyA$}5G*^mS6UPsIi*uJwKW2T9)%Fq3TRoj80=G4-*0`#3 zs{DZd*C?iQ9QZYT<^3rSXuo^S?`*l-i{F)VKIvfG=>_y$zBtY3$q}o+QjT$sS-OMw z?JyFpZQOrWU#I8Nf3rOCZg8@1j5pucmiKHhe>Hip+Q`uo+;@y}TofE{4b(cT?v!($ zx+9Jf=t;lI_w8uvJmDxyzH7|AW=HZZ?Hu5caXZu8EPO0G=R-LZi^yr1)cG!1ZGW`! ztwOWANqPtS=NIw2A=)y%Iq$8&TPVL(+8Sh$FK#c@YzOf+;No9MEBMSdh>wZYRh@JU z@y|X*j>i2+%Kg2Xpyu)W@f=0)IoBs^&8^K`AI_m}OG)?Rd-x4`2!nbxWNS=?3z4hQ z4q}m)nTce+D2C%P_?vm|E|{DF%;+A447Zl!EH3j)x{MPJOgShK5zu1xaZOWu(M#8G z-KF|4S83LAVu?@=rI`JCa=i-oKSm0^9*O)XepilY5PlcL|4;nxpYvw`&20tOD>EBa zLQbTVg^@-6oIiv7Zjhhndkoqi{-*YosoEwri2hGP(`UU`t5u5d5b!jG6!V!X56TJW z{!4l@oq3JfYK6_46xNK=!nR6%wJQ85p( zvEm7kkjA!>U+sDHXH^VNMH~nwsxrvWwJ$hE1-!gQq}R+Kr{DnKxzy9RO&V zzvZ8dso>w=xCJIuo6u)Mq-FUB`TP4%0HA*hj10VuNA3*&i+%ulV0hr0zyQ+a{Vn~i zk?s6-{Ux}5B=EcJB=i^dckwg9_zU_=`iF6SUZ6stBWbU{tG_6+l)t`ZJV8(bvx(@1JK5`-I zrvTtQ)ZMAbeh|o?AP=HK?nf?0VP&F5&Ax9>+&h7N2T5PVGqn)1IaIXdSHyNlkWgFO z?T{=SAfQkzD1QU-7^h5O_V3dPR43*2|GA?qiv3zB5W)Hd`!6^?`3{2L-+#>f5`s!D zig+S3lpi@6Q&B18T>^UUMRrzOWuAgSv*%BY_|)kPB9*uJZCgC-ZrLlf3idA5zPBdeINUo zw^g#fKxh5Y-i1Oic>baBiFu#rHt4ak)4}s$NUe}l=>Nl`yGQ(vVR!PwYFW(BMpj_3 z0YTzu?a0uP5kq1oMkbL?aujpa>Y zatw!u7~7(6Q`Vn`GaJ^fdz=mOU6b0>bO{fc-S8pLExo&D%1YMDvNg*!NrX5EDG{>E zQyCs(-r)~V^1f0hi)|^^h7Hsm>i<}49vdBAIDC0KD^ZP`6&_Wq$1<_Az@lP@cyGr? ziCGYLS&woj-a9~fVq6)Xw(Q8+9e$`Jzw_4h)$yLh@c-DkCac`Me|7el>yd2my-~YR zSJt`sK-4#-CTkK(?3f{jTT7Kw4@eI5Kc4jj=@FQ7zoT37R1e2B((hR>>0L>`rZ#zI zkgls&?_dY3)_8g77jCiPIc z>Vn>de8a82Vo+vB)fVeE_usVQWd}cN^lTw+th-tT$0(M~arx(R#u4GX!1YSM{zh?j zhI|@N`<8gI@Yl_#zd6Sk?_=ChpIq6Q<$cq4(HliI7i2y4=oHKR8t?5KDevpnE^{j7 z`zp{^eaUOO1ST3IaMY-CGi4v+WZQ_1uN_8_JF)A~q${<76~%qwKI*UPsTK;{LIBNN zyFG?O@6o=XgEBUuoUYBa*ZB(2+^xA@h5rTkT^FseiYFb88NDI04QBL*$R{D>z)=uf z$03u%!O(y_kp4R>J8n2v^(d;eI9yb+zt5%+WIPXD*AZnBq6)&jI`VN^3S7nx^`TI7 zG>q2AHf-P1+=4@J&uCyfK0W{$<#Im~_n4=+K7seqPFfveY9t~Z80LB*`)Mhd%4J7+ ze<3P4K%fnCfTpThm$?Q(ODY-J2A2&XR(oklHtz92dDr2&>S~1@!ni#(mHfTLg_wp+ zptZ7*}X+6p3Aw4DiQlA>hdy8w0t*?;*TqXqP z>6p(M|ANn?{h8%TS@aY66jF>^X2BE8nDnEX3aKcD_C$N0Tw{GDgrw%CzfyVr&-z&- zIclIU>8C-jGEV+Y{Aq2%4qHE{5Ie%pNzj9==ZTc}puCTDk@|Dq_xs|AjCt-3*cD5-7H_?d zT!mTw4syHons%1wE>a7N^a<^h=q=Flcg1jz@p7izhm+}rtEC)`kZj|d3Lp@v{}u27 zc+=6&ne3pRjvdZ{j#@NVLH`p0aGkL{pc@N$?f}fu4BqpVb<^gfZfHNtS?>Bo_Mk83 z{`bT=kaoYS#jD<2?@f!z`tDSr!dJyiEVL?iB$3As#kMMt&h=&5SXCE&;ao-3;7cok2Slk1N(aeiz-m?rQRl(!ik0`60shJzAX=k<*dx zdnKxDL;;%BnO#2{y`(~q3ST5u)3o-lJvq7+Sh~@WbO>Z5Q<%?FRu2U}UNo$)0j?7(~lakwo z_SvE@)ukQsw5=dGP5&G{@gnJwIOKwx11i5r;3LxFIC_q9cwB=Wq}`9gL&In;g~ zaK7)N-~Am_e=7sHefhB>6+;Oyky!qxif;kmZ`~V_S((5Yk5=4RP5E<&W!Lid1n!lJ zc^Flj-(Am-@DCz?%k#Or8TH(va`h3&*D)Obq-h}!!fJYM^E-FWJ}>w4mSz{p?N7UX zPAq~6=&c7s#<+)be`c1MU4rtD^4}BwRJm14Tx&GP$IcN>lj~jb^ZZ8{7e8x9tT$jR7*C99&;P{QPw}|caoAZ9?%bh{I2M^L4Kaug1?~s;qPT%>BH>zG5wwj!*C7xgz9BG z`t>D7(Ie7}RWw*cQ^)j5DjZo=TV!t(Fk!-Ol3v7C8s=kFRylNd7$OJN5XUCAH)JX1 zU3EcCRu7R^aS4#;64=v!0~xJOE9P}|LED8Zqoy9}uSjGyRYoN$9l2V4r9aL4cjE%a zyt=31o-un$Ne;okzhetH{*ILn*7?7;K=PsFMJX{v0Ejh|YeM-0P|e0>Lvsys zr}3eLt}->#YXZ19W4MM1&@!JjXPMKHUCp{C3W6C%4keM>%{}JlNH=@`Ya(;a46`Y+ zm6;?&K0E(^GXY$LFxzDSjLq-OGG+thaWlr)jm$K+n%@D)R&uDo&y=%1RO}e!Irf)c zM@HjY3Q)sLzr|MMOw>p6M~#iL6}j!GX7b$g5bvT`t^=M4AzKHEcOxxqXkN_G-TrFgi8+KS_SZ7VrT`6}7>y+FtHq)(z!UqlA$7wo^__zZr3|F`Ft z&CEk-%r|)$s_G%rna55eS28O>h-h8aDSICKbN(ct^*`oMn@Fq9PUUZT?ymKdwGdfV zJ+MPmJ&DO{2SBqBdzQ;`y&14H2V!E2GuP1_0;5C7SvS8k=Q6){M!Go~hW8oo=Z^?L zbP)jl726@9Gv+3DIL+vZC1ws}YE>n(oH#_G;AW@P_2)ad&Z5q8ht%Hc049 zy|HtQ{xR=6?;7G{{tkI1Vn*0D2$0Vm*?JH1Z{r-VFX#7DBMyfG#It*3C*+oZ7+Df1 z5un^9UrGF-@G#DH+)yjAS_h{4la<;(RslV>rR%rw`S6FUwiZ}Fk>w^i-dmsvMA(h+ z(qTyibm^A5@+Rxzqv1KRE1RiLK=+cTV5j z-*fuW?0Fd{QX!mNjh*+LlgRgz@czYENAv^MM;_Lz-MMEj{62ctD;VR5GzirFa1y*Iy9E&KO?9r z>z_&@L|J*Rk28l@isy7S#gcVD#vCsORqL+#y9q&M{$kyiBOh=Fpk`dJXLdH9L_ZEs8*z;GKhpE?jgyS0V;v6EzsmEuGqTI%-Xs6%sAJL&TcNzc9cl&p z-sn%t>ahu{Z1&JBAw7lt{!U~9A>JS&p|iqm!fQFP0t6R4!t6{tg92n1o#320kX-Uy z5xz9pxjxb+?!DRpI|STydP$hk0?^wtxc>s*MO)HsAiO?ADpWiKbX=mwH~USI=$R7eTLTyKmVuq9Nz9YzraZBBL*zY|>ubm)WnpTIfoe3;i9i62`!eJ+jy7t%g#3qC zohAK&H-E9e@TWb0fFWehTV;LH)Ka*kKOR!?l}g4(V>zIeYzWkK+J}Ky|+#xX3!pYAJZ^7&!^fs?3`nLmmL1 zu}-sYZWcVm!9~l&F@b%{8>tHpcf=u*_12C4A^2z)ttPm}sa>^?eDr}3Zjm0UeT0*= z;I-JVZIH>ELPF z2cv(S;Ai$nN2+LKyxv;yq1DC8u&9TbZG3E!5Bt|m=MU%y4Pq`t59PV#s-r3jzIAxx zec(9#l3rK`&je299}iHz{CU2L`SQLuoG<9T(Rb1!)`S-$-)Y(=+DN-O?Dn}^zhmIu z6*|g=K2Y_eDvnCCIRRqw)iPJXRrmX5e3=Q3vr1W+DxYS_a|Ya($af`s|0`0iN=6BD z8nExN@IetKXU7%CVIvQF?Zy(#%5YDfzTekL=?chH4erd#}aT zOZY1MO&kDP9y@WI@uucF+IW7X9`87_9picac%Y|2yEsle_d5y!i-(yj%=dV1i}s^> zocm$c203eP}A7X9@;au*IKd^i4`Mw$`F{Yvb$&)|KdBQ}Q3 zqCMWo@9KR6|CgC1KTf1Pm7>d~m!n<}OJ*L)yaqj(lDg;SNc6i!&JU!W^<+I>IqA>p z&M%!j@3^YJr!#Ji^+Y};f7dN%1J{S7FKY{}UC7e@>H)@U{zBh+-@Du&=lR~vIMcm2 zh=K?GOY^4%z`gLXznQ;K-nIHZRfc>EIyJ|uJeRA#<@l6#*lq1%%xTV%uI&zRtT8rl z#mMFQX=|Jqmb6^e#C8NK2cubmba_}x>dC#qHZLHNyQKX#s%C_*ra+kZ83%krCy6a0 zM_I@gCUIbX%+_~qqM1;CjPS)lvVvnL+Rweq4*V75^E`X+k8<6m4%_q7Wi8E~CqgNO z%vVsDWNqHm2${nS=OzF34n#l7xL`K1wH+Lw1H9w^gx{@eM+&RLJg|!2r829WMBam!PC^Fx z-5@`IhH((IKm1+oD}UgZy39cEDD2zcrcNO%z=&iZ2eL8YOUB0wT5r}gO|Pf&B!ZCk zqN*f32M4+|RL7A;*?z!0t9C2h#yL6sm!x~JMS}HSDK=#6Kpr77>Um@V^{%RoJgnNP zEMyqW4(p>TLQWr%{~nGKVBXX_^K>%PR$6MtjUOjW;7;)GZ`cBkzhRw&RsN?INba0m zJb5S%0J((V2bi)WZ<~bLHFIzPcn+XA$Ml#0adQiysCpnNvkibV_e+@FkV9}R005!}Z8K+bf0}uX({(kgit#l**qZ4w-<6$#C^}os zok-2xWIGy6!$44%g3i;nif1M^7i`#&Xyakj6Nb@&}JS3EF zOxVtPJnykau{szc0}BYFN)OqjJvr?NMIbw};zzi#jzuQ^tB zlDl%UM@38yC!eMl8@^aOnd*NXZAGQPcb+eLtim|# zk3%m`JGB(zto6OP+q;E0^Mt#bV#jUp$jEuX^giv*^LP5Qar;7rH!HFJ#OEe#Dc~V= ze690)50X!{=gWRnh4;nxhif=|?Yf!r7miQZnXm(*=|qu2)fQ2{CE=&yD6iV5)w5R7j`7;*z+Cu`BmR#b zLTEWL-?~QA-ak9ehCqnvWjK31kLP<@zc@0HAE?gCOS@!3K*l34vO+fCdc1l;(Ld_8 z)gxy49a30)i)Ut(KH)jef?_n|xnj54Vh~E!s`%B+mu3TS71rX7Pko zpLBcefNMUE9v^zo-1R{?ZoWNYE9-x$8Aa&3e{gmfz8j zPG`4bd<^iV`)F^Yh4Haffcr(vg}6SjQ`~Zip*W*R?v$Ii;HRftj5~D=zU<<*H&0iE z2>mpzbw(8ReP3Ut1P9EA4wZIkYR$40s$v-QrJUi~X>|ozTI++USVHi}+VzQtu58Tn z?X~w%`d}zAq+dohf=Et4*1#a`N5*NN+mVZO)W2Mw->PmA%#9eMD%ilaxOeH6fF1m+c7az{(H-&+)PbJqTS zc_GHMheYmJiF};UbAnHr6K?>*5END0Hjsxw^z;1@`QSC%aUq@SvD`N>O;Jhfg`Bi#@cSmurM+@YPp zVQCoU#emS9xk`?PPiqxT z!k;>$NXl83h)89SLHrrS*NqqlLH!}9|A8<58J`99TiPS2zyB*g;e(%8N4xZAb*q5f zNoo?Z5QlBFLF%|wwnIK*Jx@d4CP@4Sq~c#$|NmF~#1mYJQ$@MNJ8*fKYRT*F(e9($VNMk z7mJhJAMH5lp!`;ZS<(a-S`K5aNxjVv%)Q2FV2TwnE#ghCKdXl6?^B%*cQ;q@002M$ zNklgczzU?{QS7c1#H)Cj@()*I`6{mkMO}|w3`ap2?Xivk?WbC^89?Z}B z9y@GKfi4B|(7O&hw>!W`fm-H+z}J-X6LTU_zqD&zQfsG{2ev#}x>Y>w7}Ki2xnkfU z^9^&V{~8ACqk*E?b?{^TG5qDY-x+>S<$Rd67d>b1fxKh=(ZeDm{RM8Kw{&*ya8gd+ zIqz^^73#N3FU=uWnl;(-TH)w7&pS6c<-IA4x;aR{7A-r#%^HT=q+aTgDkyjtH{ue4 z_l=MINdbNr?g=j%c8L7ao%xR1$XU)X$BR6F-^{YABKtV1$`M@tvHm@Qclmt_<5z;~ zYt9Dl#bUrWx*0>v5b9r4b#&~(ZeBE}e%>4CgJ(h~I?-PpEKC&Ydm*^&>Bl+qb$g_-U+Lv@| zn}1DVL)2Z;?bOTieWS-(>N4q5JV!Y}^)K?xF>rAQ65Q`);}`vfmY87kt})>B5AFL0 zZO0?gFd|8Z;z&Ih*?{(x=U?P2`+)qb(8?`Hmj$iAhwKZUr@U%3^G&X#F_VQN7gJkl z2j)+U@hSYS9VuXmHP6QJ9$e+ubGI5jZ}DG0!A0<8C4?{LwMwYeZfFALy;L*9CuQzc6o( z;5kk2s;VIiYY%khah0Poi5{V`y=tBC3q<&ABK!~h5(yLsfO?8~NO`mmRC8p1^{Y4-s*Ol`GVOJIgY*iYmtfO9TR|V%r`HXJ4i1x z$CxMt?A$lZ$B;3`TPA?f9Ls6CJCQ}q=S?Okv!WSp@ATJ=@8m3C;|0zDehc{q4g?Rm zzt8;2^ddcGJv)-XGiEKbA04v`A9DeQ?Er!SV^bO>7pepr6t$oIRUj&dbn)F59kbv9`tP}uv9s2wl@)@3dgmhDnlE_RP z52Tzf{KylLt)cp9$6)<}{TCdc_V*F|{{H`*U!FpJW`1RMJ7Y&qsmCZ2MRpzE8}pL2 zpPetvUx3ShnLi=-64l3CpJ^?!2`~@-|0uQAkJfC9b%GF{MMa@j+kvVpkn^F^8gu^> z0B}bDZhdyb?nW;^82)qU3*5h<=IN_&09c;4#Roto5d16bs=(7#ck6pe&$ZsMcLoe% zC(==__f|*MMW*3K&ejg*QTOH0cicV6Z%yPG+1YRWfQwmm^zZ@MALLHtdWWc+;e8;= zMufiVk$Jm|RYw$k$DYs>7eG3;S={RAx)3Xs{L>BAx2$20W@f*PeRXrWHYI<@E;uA< zW+M3(A6|S)X^5Mch6O*0>yKUfvAs|19>;Y5S^WCwD!g}@@tFTCzt^)`J%TWcX<49a zB~&nlIyLVC}V6H^X$Ww}}6d|))fj+;`(*?2nj+h8O~NXE3EGRI$|e~Sz%5LKox z#M;J?_ljiDE(-!Tv%9f;+&cO8Mu-)ozj-b{hK)=ElCZ!+z;&!{c^V0JU7+2xvUtWn?^-xvN}StT^>>btj|J4HM1uef{C zSqRmu&z}G7P8|7l(Joq-$Y=<}5#xb^FIG*EFWLLgIQtT0LVH4QG`9DWRL1;G`5=&e zw&6SxlRbndXR!^i3AOX+c=6ny3>qnb46yEV9}+2s|B^G~lT<^}X_$-8Aa(MWbTX;# z+^>yG#1zt_Ah76PzNh=P;kp1%GOnSkZ%8{s?+s9bHV~>mX@F`zJx(PenyC7r#tlyKEwtJJ$YxtxK zu6a(I2$iUD=KUfY-`s|nOX7Maqo*En2J}Alq?8}oJA3j}vJiu6opnUf?pkHc_4Gfu zZ8sF6)LUjQ!AW^GftUbJJ48B`J$!?aVQQS@i(AC2($0(}pN*q(FaR(Pp)>y=?qew@ z(1Q8Mr|7YV$YLC6@HnzC5ek@BAn%?~WyyapUz-qxkAXayFF2EL zx)6T^@U5h3Fivg}0YK{U5;YCyxh){SWnBN~`Lib1ot(_ac(S(QwzHb&m$DxziR+g! zgb4nOw)WXOC1VNlCj_bB&y~m^{tV)4`JRLNLs0({y)mfY2KDz=f36S9{~hZ?BFZN3;GcQQa9s;vi@96Y7wf{lDfX*b^-{pf&D@dB{~9{UhsSPu8)k$a6T@vEH*! z`YjtDKF><_Ch3jfpeo$|i+?GqEw^z{G3_f$=CL;H#BYi$2`U12VRXX;h|FfyT!M7q zNC*xFAH8DpF{faHgy)fi$1;&82yy!ZG6`Hpy;zOkCTKqfJKR@=?>UXx4}3(vNj8r9 zi+?EqIz!*E`4_=$&OiB=HhOP-g?}q&+l$)_btIU z+lV``1zE{zD?7f89RFzJ6=38Tr=f#m^vcd5y2&!q#IwLNhx@MwK9q=?x0~FJNeAy% zE?BW&xu0m3+pZ9Gi~7VGKL@_Wfc-FMPTnx|EN|XCZx8VI)W`-Aa{p0awHV5MhkTFu z`jhXykmez;kzVM_%w$|veyqax6~Nv4SyjwAhVBc^FPAOj<5jD&zaRQfc%kdXKj8h% z?)AKh-jeb51O3rh;Ack23n2=9sCa%OpNxw^pgwSw;Sa`l(!KiI8Ue?rDFuz$xDIxbd60fqZA; zz$(vY{KlK z*C|JM;m&(pj+g`EXUhC$T_aTYO0LiTi{DkF*~;(_e%IQ=SACImH(ZG+r`ksDQqMTt zjOa(^%1bsM?ZREkofP4uTMAlFEjH|Md zB=uqH{nVVG0{}hnxBPSbEo&XD^gmi4`HAEP$+#|TnY+|-RdW=y9fXF;VBmjW5`4}PpUqt%&9e~TcV$3wZMtTYIe;;|> zj5Z4(o0$hpfTuOo>TFO7_WwK0%g9XAYXGE-g~nV{fL0glx_$PqsrhZf_Z;vE01Ft+ zMY)quO+F}E4VZV(2Bal{SppbS@e9pB)&d?1z>EPNK7-uEjxt$b3Zihnk8F+~cPz35 zM7RLl3~2fC$OHURfZ_$9i2%g%Kuoz`g12@=o&#c*MZQ33!tAtR-`|bOdV_Qo_UqL^ z_R*fOh9DnmJ_-E-ILxp;zkVQ)785XDt0A_cw;4lBB7(U(XuHUR zVW16mBUex;_<=+D3;Z*9ZmV_7UO(EJE{plt5yepf%&(4TbWQE#`JeSh`fKR*>)qR& z7q~vj*~ZB{?VK4B>1n~b5feBOcnU)1toM80ee8ktqDqESKfMR1nKwdy;PN`0@D^s+ zcF$*&qhshI59QD7pEXuO!iSCyYZfw$jm~3hpRMvGcE8l38;VRPJ+u1f^}b+Q{@}t3 z=RRh~e!mBI?lb=v-W_kmL+ogSRh~EyQwe@*{KZMvp1$+{tA0ujT?K< z-{U9%v+jpacgL<*C1P-Vb9Tst-F2TnNqJs%E()dmaSJ1oi!`P_XB|22%KW~{oexf~ zAOyWSKVc{3sq#gs!__Ho*{6%OuQHtS&J1Y~*@1d}rmb=CJ7b{f4LnDEN_ma+n-Dv{ z>g_{c;kjw95n*wZvnq7S0G{8fwmX|hd#Xz|!BYZX;d9(CjX9a$v!BDO3lXzV#i>?Y zF920Bi1b}GN2QZqZ7r}3nhT*?R?~0$tez4v$82uSu~^TngW5;-j_VcXVrwARhiWmh zGl_lwyRGru?_}P!iQT0tpzf0I1NKTWPB{1SHLE}OP1fJF$awZB%|hO>(j~+vC!#%c z6{7vs!?`CXvrKo-in;nM4jMNSx;HBWpESa?(#5)Nbu}-WKfvF7>m8aG5Ahbr9-aL* zjvgia4g4rvMrY%g!TWGbxF~1P`)B8OFpgt)-;mkw@+3B*-8k$!xeQ+U*8|h9@?7rt z+_AZZdH=J-I$%9lH90ks{#Q$Bhg!&Vb;J(k9*xO3mwFhqGsGS!!QT0z=F;-lNDn0R zH8R-^N}yGFOPAmT_uGgh#5LX)_?W6+Ms?U-4!~yA@T|BS$juKI*tVN zH;UXKdxxEuop1ZNpG;})Bg^7K$T$Q4JY{>He?q9)w|M@%mc$n+J6W9CI3)1_$U#%n zP#D*fHhKC1(%@~ykt3k&Ug!P<92RJAqGr^zcdjQ={t)i(!ELA}@>}Y66?qTW2);8; zrk%sH)Q`RIUVe85#3c1N4ep_zF!pz_@lqH&ap+IgO?%R=&u>tw54nF+9kv}9%2JLm zNOxg25%7vx0{q4e*6UHrKth_C-k_g_#y3hIBN?*{c-nb-fT>+ieZCvH-YY6SdeT~t@K z=GIVTZ(I#`a{U&2)Q_^B{b0?O^X z|AIMk5o@2U{{cbA^k#>ZqU$rr~7K+2)K^r%f{Ii@lKysN} zFAhs2^`3_|)`N5_90-acD}u^I4zpTHge%OTpV^`66L3Y8`mClVS(Nt|kS}@yzrPI< z5I)MUvXPzGzx+M26p5reyo!AAFgiOkviXlS%a^#n(A`} zPXz`OabN|{?=of^b9pX@Q-KGgU+i#v>NtSx?ONxoOaARR9sLvZkSXpc_f>ZIzy4rd z=4%-G`*`2?{)Dade#xhcfeQ<7Oz54^iE{jGPBqE_Pp;oPp8-D4e<}Y+{t5K4BaSeg z@xc!Gn;zQRe>Tv^M9;X{`S$L#H=y6XEKooEYy3Q}6dPNX_VqsId(TTd_^$=}$QkzG zV?*DKrQZ^E#b1scO81U9v*H{ww?^JIZ!yYSQIFJVZ>yR$z`8-bP6TQhO-a|YMu?-N ztEv0AYZB?R<^tiPlh%? zE(==|N;v{~Mj-=ynYZLoy_^rp&y5~bhxSX#XMFyRfS3yBs{H zJd8It?K#LQXMMqYUUZ!X+i8)z!kdLWi+Z&)scV@I*!TM#d+K<9%GccZLdtJG?Jpll z;rH+9WgYUKY`w6m&wD<|Tjw1{c^2k(_Lt)NyUxXqgXoC`W8(|*J$Wa3qw{A|50Cef zk9>_e{!IbyzIE!xlVyW<@P^>uaibn`6Ych|igB~qU2va7kv z!*hlBO7qF*^}q+B4_RyZuFxmc>->9-`<21(hmoaN2^a@(tPbHnReui4C_p+Lv-ltFD-$#*l2FNpDtk*qn+0^$%f7~?$ikctLr zYZ%}2<@+Pzz#b_d^F$?keo1FWLBGIBcfS4oR|laFA-#qB(a4xT`Q3VUi8NL9PhNN| zTFB$v-_I8vhP=Q`(i&L`Eq5pR&m*Q9^(P*~*P?eRsZ%Kb|8xDb;CE&GsU_BrXfVv5 zYKQQ~M$p07k>qzP${Pjj;kiQPv zADCzVvAzfGD}UIJ&cizFp{2YwPOBK6N%Kg3%QY_0-}(&a0y^O#*x!! zKUWV2FxCsTMV*%QNmXEL&lRMDEn2b2+QjxufUek?nx1+l=m0=V|1JN3;=#Yaa|UOZRLZ1Zv1VTZ$j+js|g+?->gXqXKMO~%9qK)+?SBmWwDxb+zMRCNT9I1_h%dte8{rE(Za|TEQL=X3qsg`irm33jv^hH2MnYiLq`a5 zDGLPc3u&JxXORJ-ciMz-uztb*3yx1QO#YAG-wUXG8sXnUYy~isuTQ?KWvG)DvJ8gF zLdf9v_pkHI^O(o_(JsGQUs;2YeK0}24gh=;b0qT~=Cf3L9xQ9!whfP7HB1b-g!XTv zSPw9kb+G5phnOUpKLMP>RBi5`;moRHq$}#*s}{%%HHl!@08p)~DDL`9{h+=B0_P+5 zekb$3D=H-0Rhs--s6h(FIdD3^slOL8&EM020I;I17IL;~aGyzi|JZ(4(GJrZnYw%UCPcb-MaYC_=~+${q^dgp^~*rv?&KM5g1{3^_Q_5 zHaR-xaE_*xyI<--F^Gb^XT3vmW3fYCz87+z@tE*qT(^QB;CK)bJ~iYcj#hP@*m?va zBYE264H|ETKY6?4%$hG?Gt5iNKlL=k*6;ax);xHZ){#FY-J@K|jyWN$7x~eFW8Nf| zi!CSStjlE`{LS3ryN&)ZutG}1&sk35qV$C2w1d~t)5ZFO*Y9vwSC+d?`KurN%JUza z2aQ`e9AEd$h=+)Slvr-fp}nuOH+w4Sw=~PiIzxZ^#PHA6qZa*!H}YqC3DS$LvHD2T z+te?La(y- zgzC(e(5TjVOA)+LgC4tKKFfQeG3hTwexofEV%1z`eWq>Yep^;#zF#xn>Sb2uer5HI zYXQr%$Lo9$0blb*>b~{+*jY0t=0uGX)bp)~FN@Rv+LP84V?V#UWfn5q^L%aZygb^& z_m1x;AMZCt7|8~TmDUV51;mq&I#j zyuN@iUM^va>#32s~2Jg1c0C^_Acy>l`jLr%8`jZ*wj!7uc~s9yu5k)f>48 zQ~fK*M9@@AWE3SedA>g_QxtiSa!%rBTeLT1-cp~c5pom;2K~l%XyG{e<$|40zWx5T z5b(`N?*j=O=Z%$4#;ogS3yRasq_zZCdPNk0t44Ye3DQMH3Ygt(=> zsvzgn3Z1wP{v>vRjXxm^Wt=_zFZ1U;X402=K8>9KeRys$t{l>?1vqqpd6QngP7jM? zNGfgj$NDGb{I7>UbsJw#V$WD}%I{^JNkl$iWtQ)-C-sX({?F@s5MKxNhoF8G)c=C| zAM51*6@CJIrLEMq+UseO)=zy)`Xnp4%*%H&? zJc4Jeb;NCKE9FvM#gP=OR^MKSBw|q{=?oARIKt*%b|MF9PTTN!0>?)1JI2JK>K%SR zMy(_)zc>uM3SwYKJv9*AlYyCaJ8}>0Cb)!BSbceZBvd~58DpnM9i|+8)MWeUrTW$t z8=qarM0uR&JJ1duo?C?bK|j(&20 zE&3N5a5AjtXy#QI#(AG(f}@F(dg$F8D|B#^tB~gx4|rA|L#u58{}u_XGtd(_QlOS1 zf2%gzPpXpd`}!Q|cYUsNmC8h~n&D{TBEMTXw@0OeLkg5C;ETGC1Arr8K-_P@-?iy4 z-{X4r*__qBHRRVyKdhf&y8hW;&d=|{e-6JMRt7z0Zhl;T0NK#LBELG_zV4p;0q;%h zQoL^A^1#`Nq3(zYy#JoI#MzYRzW0~*mB9b8cSMia{^Y+Xr$iR`^ZJI=YBw1-|Bb!( z4zsG**1fA%>fF%K3?PCcC{a;F5k*uG6$DWML2^cN&eCLP zQbVWJE6lpTQQ$fIgztIocklhGN0o?5~0Llby@LH3`oqdl5Mo%4dDJYBu-dFWq( z>bYM8PIKRP`j(6J))(*X??bQ9bKPzi_XjrT{2F+Z-*Y|PT(o;sAbMI9?UlPKJLEr0 z`ni}DQ9Y2YVu!|3&OoEU=-?vg$Tit5au!m*V{!i290He>PN?D=4V<4-_?Kd-{N5;c zW;X48z0Hjlw`)hQw9_W%Gl%!mXIIOS7OtAEL9Uiue_VZH zXcsdw+$l`^xqtFa^zNh_?|MJ?OrhOA57Z7)e$~x+*HBOPr@m;%!(!Zq1h2(&P{&Wm z1pTPR@k?+^evRjSODwWidH+oPobAU8i15aDMT@+t7m$B7Ujh9LWBLto{9&w?wd46- zaQq4K@y#tS+aG>c@J}3pvP*I8Ra})senvj#3r2CS#mO2y=OF17jGqR`UEKEwQlp{C z^Z%XSt!#OXDL6>i=XwZJ{#CL)qEe+k;P&-$bRlm!N6Z4HCKJ1>CbEkvZXKW|n1j|K zYLOl-e1?W!lKIj6L=3bxq^A+1K*lZnuH@r*iZnYfy{sooepQP3av9gJF%O{c@YbzX zACvXnOZuOOzusbbHi>A!Jb_l4#EdDC3?ycYL_o-n@Yl3M#QqSmubieI{AE8X&Ai9D zgB>c}&SU#50h{j>D~IABMZX3|-~h0Txl$YjO6xeJi%6Y6Ae$01fN{$a0Xe!3GN5n4 zoe00POL*IJ7tW=fTN!ZxpvC`|f3jpo{{F@-&}u-dzO7!y0U!&5JcY{XRJtBfZW;;pWJ_rgazvj)46;o%6^dPK@=yUF2MHP`I3d z&J_p1%J0hV^e@d`e*mphnWF$=kUN~MrZvx>Wopas?BuI%-`kI%XN~zVv)Pe~A_Ab_ z2886yFCZQO9Ao-8lrr(K7Sa`@46Le(yoA{UfM*t>j>^PYL4AVvl>lm=CBWYRc2a(F z%(BE}Y5lkj9k&Lm@(`CZ>)C%2Duu=Qh;g_ZVV41yJH>@MbHN|PJID}_d$`$4zP{ZkPxX~a2^UFzSDw!9rD)19I?OOWXMRqiQh^(&4S;clfXPiIJeXB7ccP9S>oD}Zk_auUKli$@}pJxtK z7=1qxLW}^Hd-XvGhP)m~4boFx=UmfBUvod^{)7A8Rtv>IoST_lBzFesQP2Z?KlkUJ zl5W-!c0zoFQ4vF>sier~{?$FxoehyQILhbK@CUtOZ$l7Vkxo8%iE}`XBNcWccJNEIINl}HAYBIi2g*xRR zSZbEMT51P|{@E4!m${|XzWWXzKg9JbKc%T_5Fdrg7CfGGiDn#gqg#3%h?ud#RoVT? zpI*9ZN+}|AH%o1MighS&$M}+Qo0MucvgWE5wQ1%>;UmFBh_Dvvx6VT>#b-p_%1b-N zwTONo;eN`O-@PZ6dY@`~dF=ir@QFX9w%tPe)m~V(MZ=yv|I3p5YObYzKfAQM1o92_ zrtcch{m>K-6{H-SLPu`B3%`=?O-}3$F?HN^Ai6H!qm%wQO!@V*s)*|<@*Pvo)gD<< zw_&f6tUGgLJn43(yhqx*va7KkLV7$Z3GJ!2V9p(mtg3%iDO^9GwGbcXZRfhI>*f=u zr^EWj5f0_e2;@)*l+T*P14s zps6a@D>yModuD!@8I|!W-@9LUL5T0^Y;rb)X>W7hl#(4#fly$SAENgU=TM$?yJ_zi zcOFfJ_)2}~#1EIA<~^CN4D+$&;(-F+eoL-YmSrt86ukN^!~3g70b@R3)Wy= zS9R8}^ZWDE=Q-r}dZ-ZTJm)paE)IeRtfSzI;Fh;ZH&kmyakI4Y7dHQVoM*&=U?0dp z#_J&N#WP$lwS<%hI#0&sZxB(>An)Z1%KaS}jo>S0!wT{~{Tu(=Ou7nw-ZI|g1p9cd zKZ@C0>i3hHEg|G^Pn>LbxQtTo+j+GODTm*Ckel-J-RD~afgg1B66wY$tfP>xL6P~% z*Ma?GDWnZ1ZQ4;a)z4ePy)V6n`GV3cvN-L6b&V8*ubqFO|C@>;N}1~j{?YAp1D#0v zadyUCM}7;vbPQQmtpn>R)e5BdD{>2H3c^r*fxAIf?ibUmH6~q-^l4-)(IYA1>n}H=W-bsA>|zTmM3c{5Oy*KoQ_iu;MWV-r{eTJu^Io zw`;n*b@-L-G39eZSOVf?(ZZlbqG~<3F zJCeEIr7Iz?u?^%?h!q2Ih>Tv`9a=q#L$u%E4 zX3eapa`+BeE3%H~e8_VaxVKAa<^RD-~90e@=tW3wUvncN{ z??G=W`rEuvq3ol)_f?(kj9>%zoV>mApm$u{c*R^=0X%-^+8gPsd2YPEM^~df&pKC~ zB9teMNTnqyN38oBH{aWn;``Q{3oJVoG@&Krn}tjH7UX$DaDDWmkx_G_Xm7LB`IAE| zRPfzEzTiO0;rGq=y@eh&J$7EyNS@QwUDEw9^-OkN$0bv_#(2VRzK6N2JO=z}el}Hc zSyrBMo;q&Eg_#4sr~eTxqqDPydcTqLj6@J9TCVVwLPe2@MNSqeggtn7u|CDoD-M5u z?3EL~1=!|Owy z@Rs+U#10#iRml&|4XqB&4)T2BRa~J*F+Nq2MAQOf)|GMMD(vd*`WiXGwL-Vxd4G7j zd%>5XL%|P28+qPb-%CFFaj0eR=@5?zk8)lKZ{RuaF+yl>c$LEJa+mSy%k?4X7g@+r zXbJRJM%yf_pVejdpnot{eeykd-^=`$2nG58GoZ|CpRuV-^t)4*hYd2%h@P|omjOTW zDDBGk7Jf>Mu+k#O=#93%xvIa+JIrbS&R=&Y z{m;f6AmMVzdntO@YfOh!wvdD#J=($?MGKs4{^9+4yvF>F&_j~LuL#d zC{;f>0>D*YU)M6<9dr#A{sU*CUU~<=PlrvZOFFNbBDjL>6x(%6WIurB8h69+)S6y^}aRk#IT$E}dx3Og;2U(6oW7;5txEL_6!|oK4_nix! zJ9cho!~uYo|6Bg)zh$i>mHxLD7@a;kV@!2pLRUk<;4XmRsL;sJN67V|EfP96_-^o} z&_rZH@OJPSuCENu3IT|Mp5Q~l-pIFtj|B^JeN3op=qb{9fR?k7&jl_9k0CD*>}x!~ z4+#wjb|GCS*e{6U6)YI65d=_#7KMfe&yY?HtP6Gq7@rUB2m%npYr^%MrU1RRCM2Px z@#ijXQYrRM)x#R(n*!`4y$=&TfS14f@I6(~w3`L!;vpI@0FK|cNoXv*u)O?B&VCD8 z!+ci>X;18-1_kC#I z6JVsv00Z4zk45$UlPdw)% z^^1JJNdIIU{C|(X&+m@E8Ike#zcIh8-9)SUtWb2j$lsGiLCiNRmP52F7kW^Bru$zv03!=odV$a1d?5r+vdFZ{uy+ zwK3_29UJ(YL+}LV1cypU`bF;5wSR#KFjJg{PAt#&#iaU%VV7Ng?dz-8A+l;$e6(l= z`5pgW|8mOxOdxMg6Q%BXtZu(H{aH3{#vLovj{H+H{pWa2)}qXvSp_N2Iy2MvB+pBW z8XLztI7v|9;(zFHO9z-PRx#`#ewSY?KJ zb4YLE2%R+6&Ca0-xf>v|T&@$|Av}MR`$-qyLBC|CdCKuUj+j_C3WjT#Ya4w)LL_#R z_0W94rW(En=H1o$GWkE(*IXznCedUVE~||M7-b#RQLfI`K)Hi;>jS>u3*mmwQ1TVv z)YCD@i>94~nsly&D>z>u2hn?4Al(R`qaZ^bnU0*|!aX*HmDRNe+$u_w{0GWqP zIl?>&!8VLNRZ4wrrc1v{FPU*7btgpRGZ~$Zf*T67Ng7xS!Y!(v_l+dJzezHmNWbM| z&vL=HXy2+?FXX(8^khGj0qzL34-F4Nl!cxTEelgmXRDJgk%?~Q$=s4w1$a<;P1#ii z@XwpLwEiFT$548(Z0LR7ABJ+BL%lnw>8$J!O@w?L$@MA_#xY#4rw7W;aCY6qt9y`# zphTs?vM{&vo%y>WM3lY{SDQabT{2%VqLg|U(=s>)V(M)hNvJm9Z?44$bynWATk?v7ee$O=FdEL=g<4>{0aV)al^mkq}|LO zP757C{yMslb!;d@x#fFH90BP!|GM~7YWa`#8yLaYv`0jLi0DWA=m!z~FQUKu=?DMQ z_=%%;`3c$MKZf<|Gt=EX$@mYcB)xDms%X3?s0Y=vGXF)vRGWHc;wGLw*q~M9}{J$#y{|*!5L&)}+ z!@;@gY4+dCx*xAotOKC~I+)D!mf?8j@cfDDelf_K#b_rb$+wI+az7yrk%Pdy;Hd8Q zsK(~hZ!_stpt^p@BvRbR{_5*+GRmC7A$mH`Yszm z0rE8IO5nre$Rto>H2ED4Mf-qs5BR4bax4yi=aARv0dFHKfIKwveST?)j8_jC@SAzk z^wl*;w<7))<@X%$o^!Y5`T;Z0fP>T_Q&ME&J^3>7ehGZ)m48*9a^U2#(KVycuks8_ z>>RfZqQB>vK^HgRe>eNq=q%y!HUvMA2yD^M<#|4?2d|r)xH!Ht&ns+xkosqTl=D<} zH~gvZE#oO3i+(h{UfEid?&a}5s1x@w|ly@9T5}fMNiJymEJuQJvn{( z)zLTJM6K8#?&9>~c_&=ox$<*g0vcKl&v{yJ6vMQ8vAd;<{_G05C%8wGzeRY8vjl&e zC*uM!g{kks?2S3EP@gWj4+Uv&&~RRf#KGCpRc$|h20j3Nmb2+LTElmLw6@u#m*LJc z68WHdO4mmw>y|chMR5#Xv;dYsX}^b#)km#9GQu=rBnzItZ8vTdSMSt(2g z8yTw`m`{;sC`T3YO(#~zI@0J5;%H=!s7-JxbO`;1L?lrGVg_s@UvFK)q>`q7)}bQ8 zUq|?Py2HP){#Qq;_qVOzJmyPTkR-Ao^R~`ZUukfJn!^UCuFS(NU}Xh=ysckzv7Qkb zv596r;(*M`%v9odl%~s&Z-T3fjePQl`dWApeFzsp)(s*8yy{{e$DIEhn<^CBdJ=?h z())C0yWyZcM*w7y-b&6iq~S;b)>}P@EhTl4>vbM|jpe4-mPT8~v$-eo_qS{T*Wa?% zkxKu23y5NNnq(d*%SFg7?7N%_keO-bSitrTzpvx^3H+?zK;{5%zCzx^PN`E!Iol~8 z@(N~iAJWSSzD_`S$@{5JtgW&t|~0a$>eI7@&Z(%r4% zS3`YicLYAF8v?3?C>~%Httx~ZI!3yw`OeHoHUshj%g8oT+M^LsC%22kY7L?uR}gaP7D^hIHi*w0h*_qY zzl@(5Za3HSs-srny~Y0PA*71|>BSJ*($&UwhV)7HH#6^>Hma*BiF`#hHsx3rDu?oi z2D2RG_)d9&m(iPI%SPuX|5!6shzIS)^OdAi)S2b1<-WR{`96T_9s;W`CEeP@oAv)-N3Vl@;7n1i6POv6}~D&V|a@*Du8rtaCNmI9y8UzFp7jq zHjP99fc(7XTFLztTs`f2mshPd9l5?v9X6~(<`q`umq|BLD-7S0omrn4L;h8)hMkZX zTrcTwxjw_J(GKr>#Az)dV4WjQQg|aW*Xa`egzGPdZ-%Ld)4{pON=}4;@OKXFXqGrZ zvy10+RO_9Sq|;PsAu4(IcS6h(#&oa{Yvz)9ObogzmAzj(Xs_hl)Bc+*heK~xKk*># zblWpFANhT0-rafqyr+=!NU$yMZ5MvsA)Qq>t729ZvRC%ASsVC$cz9ii{^e|Q_JrtH z^sf$~5X9E*aA`m5?1ffWTb|*02bS)PI?4JR?h)D?_EOIIoa(rMa-1{uOa;ny-CVLe z)2_1jYB;}d*4OP0wV}B9(XTPCyDee6n$aZf9nU^`z9WRy%N8eyJ57M}WOjIqB3Oo< zDwB}+Lp)I)J%WAemr3^nUGTjzuzv_9p(3E7CyAlE>{40m=u#EXc>Qzux zFf){Wg*|*O@;b;rIW9Kbb#s|HJ&b8HbT3b z!I8R(x=qX!?lUKG(NJbLUwskje|`L!b{AhKMesG>JEA{C^dr{Yi2fJR-~UJR6Nm2d z6LswhrS!Y3 z1xrZkV|Hh1f7bnnc}@pVmz(QL)qd-M;KD?mk90N$+gxOQ^{s&b=bim6j&nc>g5#Qi zh|-V)L0#Z(e!YWVLf{u!$8TW*75taL^T7r1L)VPp#`{2ftC4cVfZ&(yf8Zb>?%hRRMtA|0(To58b% z!=;?^NLRS01AcYJJDWvM$qe2qn}NJ_Ieoxw@N{-`RydpXO#k}kJsH(`&QkXUaj=dW z6f@qp3>f!XXh--U`NpST&IEU6-n@Mzi+Zswr$;CqygkU*);9wB;>|_CAZc={s$XkLCKcn8#x1U;eGR(YgC5-w#nQ#$|FO+oBq;)ukWh zeJts^54`3)WeTA~D9+D zTNa0TX?{-#oC+PJoI|3^Mo;DX0<}yJquyh0f0jXg%;G>s=vQFdPR>@Y!*koH)uI0-e|3K;%DMV>D3kt^@#gJcvuO`^ymyNCH`?oy=r^K%B7bRDV;8^Ud|FH97u>nq zYWjnb9_!{KNE2qD3%%&1vQYUa0olBf%2XV8Q)3+OCy| zA&idIw%@l0jmtQcBPo7FO8kI_k$2}8j9v3YAD|QgoBya8Ar2;Jt21nbx?Acp$BYPX zeZ)F2=Wv*Y%ujoHl8n=Nclq5(l%D+6!?xc~q_07*naR4=r}FS@765PpOZ|Hp)vX6$d1$YJonNupQF z`Y!y6u4(3ozNr7=uWt|ufbaaD^#{g&guk9cyF~c;i2Wf*KloR*uiVCsPwJP?JY~8v zZ)7k}2_Krl(TSs(C%)F(WqoIk$i;>v&W;kBP?@+yxEV2!5Q}8u7 zO7^hxaw68bH%PYxb;!Kh3Pkh^vJdHENR8_x^Dt#NEJt}Vw|Rs|iTwS&TR^&0gVt@zqVQbJY2@zh7^ipV9H&Oz>{fxW4D za3kP%CfX4eSPx=4e2H`do|z~JYL}^lcc{{}m6qR8d;5!Hff-_+kx*Zd$kCQqUI?7K zOgc>kZ7A19P~)-OcP~4<8gP9XI~a=~=b-LNIrGtKKIC!QEd%N1eGuWCW9GAf@z;N- z9|~Heev$7N>7R^~$oTtzJHOOcF9JQ4`q})#-X57hWB!^yF@iCF8asbt7PIrGIilNJ z$Ejb`b%N!~yt&5$%rnpe6Co%XySlrEF`adB&vGR)|1DRw;a7yPYYSDVh<=h^44rzc zE}}i)%#7GQQ9~h8=7c6W5FzSk=Sh72RqVm|4TX=Sfcw&qhZ zPwcWAOU|kLJnfS1-J8gDr@1MUBkN3a4xfQ=2nWoKfUJjAfb?~_-V1Xg>$1L~zjoh; zEUS8YP%vDT^_TW+^!iYfTxb8ns}eHbylW2OV=T}6U3UJn6QY-k=a{jMmPk7&7;b%r z^l19aBGQdamcu$}R+zj_Rb+b(2jDxH3Yaqw^Lt@)$fS_YRIeGnyQy!ANq>W|tm>jY z)LZ&(A>hm_&Iu`pb6@y~5U*yJc}fiU=3Vxcr*eI5c&)Akff<*z?qp}KKcZgq*F+!u zBkx01!6n9J5};INUEL%*yomqstJ4O!@M8AE*|cwXPq=D`a)p;Tg$(WC9CaEw^uO>? z=kp-txbc0)u?vH-o3}sw_zB8+E$!-r^dUSi+w5m|mx5mGVIv~MVxX7)+ew`UQ((w8 zX?7f}Q)4*9M|QAnwV_*Us#aheA+&@G;{saE}Z|Bb|pgYDfsI$1;0n~^+ zQSyBYF&@{`4iWqr!PlavMD&MET{I^#q1~XrazvOo zY?=pfCoWC;KkE;N@9Gc##=o$NN0aY1E_+|F{=LeII-c}j>-!g+Y)K)gyh9|4IEWfNQ|VIAK0-@lP^o!7umfa&`xQ_FepP z8eAhdqXcw&DWtecEI>>Ksh}@_B@S8xYB~;x6Z!Q~T}=z_xU9di4h9q0dh!eDt{7|u zpUDvjamYNXlo)?|Z~H5d5>BLy6E&oC?lbNAU0$9E>!d=U&1QyC~OJg`?#N z&&&F;mwX#tkL%CS3-WkRdS1bf+bhqf@uRtKad1)?ToSC7 z^QZq=;2S&bL(RxH%^mBaU2?nmXXWl8(7?3_9h+U^@!Ja3`;!F?}!)yv*DV+RuW;_RV9^3+`c^z!h4T8B2fJp2N5iwi?c^we=>9Rm zXx`^p=v(dmo(Ft~+XVf`X%wy|M~Hgs`mTB=@%u{8G!J@HaC2^nK$!OzjT+?L$mB?@?6vNqb>$;K<^Nn z6Y~`It9Q*L&X(wV2hbFHAWL9o zm+?}LURaR)OU&~!-ppJzUmU@8JylxQA^ih~2JS_kFjEcf�JTH-LT2{k#{amm$*r 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ℜ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

Q;V0 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

_WQrvo` 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%0ATqSo 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 z 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%0D~Z-<6#9 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𝔷@`Z$4IVm=&WLxZ1UmUf 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

)} {editorReady ? ( -
+
) : (
Preparing editor… diff --git a/packages/app-core/src/components/FloatingNoteApp.tsx b/packages/app-core/src/components/FloatingNoteApp.tsx index befb8c63..4abc40ba 100644 --- a/packages/app-core/src/components/FloatingNoteApp.tsx +++ b/packages/app-core/src/components/FloatingNoteApp.tsx @@ -62,6 +62,7 @@ export const paperHighlight = HighlightStyle.define([ { tag: t.heading6, class: 'tok-heading6' }, { tag: t.emphasis, class: 'tok-emphasis' }, { tag: t.strong, class: 'tok-strong' }, + { tag: t.strikethrough, class: 'tok-strikethrough' }, { tag: t.link, class: 'tok-link' }, { tag: t.url, class: 'tok-url' }, { tag: t.monospace, class: 'tok-monospace' }, diff --git a/packages/app-core/src/components/PinnedReferencePane.tsx b/packages/app-core/src/components/PinnedReferencePane.tsx index 9b294384..df7a5e81 100644 --- a/packages/app-core/src/components/PinnedReferencePane.tsx +++ b/packages/app-core/src/components/PinnedReferencePane.tsx @@ -60,6 +60,7 @@ const paperHighlight = HighlightStyle.define([ { tag: t.heading6, class: 'tok-heading6' }, { tag: t.emphasis, class: 'tok-emphasis' }, { tag: t.strong, class: 'tok-strong' }, + { tag: t.strikethrough, class: 'tok-strikethrough' }, { tag: t.link, class: 'tok-link' }, { tag: t.url, class: 'tok-url' }, { tag: t.monospace, class: 'tok-monospace' }, diff --git a/packages/app-core/src/components/QuickCaptureApp.tsx b/packages/app-core/src/components/QuickCaptureApp.tsx index cfd4b368..2ac59613 100644 --- a/packages/app-core/src/components/QuickCaptureApp.tsx +++ b/packages/app-core/src/components/QuickCaptureApp.tsx @@ -113,6 +113,7 @@ const captureHighlight = HighlightStyle.define([ { tag: t.heading3, class: 'tok-heading3' }, { tag: t.emphasis, class: 'tok-emphasis' }, { tag: t.strong, class: 'tok-strong' }, + { tag: t.strikethrough, class: 'tok-strikethrough' }, { tag: t.link, class: 'tok-link' }, { tag: t.url, class: 'tok-url' }, { tag: t.monospace, class: 'tok-monospace' }, diff --git a/packages/app-core/src/components/TemplateEditorModal.tsx b/packages/app-core/src/components/TemplateEditorModal.tsx index ec45039d..99f97ce9 100644 --- a/packages/app-core/src/components/TemplateEditorModal.tsx +++ b/packages/app-core/src/components/TemplateEditorModal.tsx @@ -59,6 +59,7 @@ const templateHighlight = HighlightStyle.define([ { tag: t.heading6, class: 'tok-heading6' }, { tag: t.emphasis, class: 'tok-emphasis' }, { tag: t.strong, class: 'tok-strong' }, + { tag: t.strikethrough, class: 'tok-strikethrough' }, { tag: t.link, class: 'tok-link' }, { tag: t.url, class: 'tok-url' }, { tag: t.monospace, class: 'tok-monospace' }, diff --git a/packages/app-core/src/lib/cm-code-block-flair.ts b/packages/app-core/src/lib/cm-code-block-flair.ts new file mode 100644 index 00000000..ed63027e --- /dev/null +++ b/packages/app-core/src/lib/cm-code-block-flair.ts @@ -0,0 +1,149 @@ +/** + * Code-block "flair" for the WYSIWYG (Edit) editor: a small label pinned to + * the top-right of each fenced code block showing its language (or "text" + * when none is given). Clicking the label copies the block's contents — it + * doubles as the copy button, so there's no separate copy/fold chrome. + * + * WYSIWYG-only: this lives in `wysiwygExtensions()` and never loads in the + * Split (source) editor. + */ +import { syntaxTree } from '@codemirror/language' +import { RangeSetBuilder } from '@codemirror/state' +import { + Decoration, + type DecorationSet, + EditorView, + ViewPlugin, + type ViewUpdate, + WidgetType +} from '@codemirror/view' + +const FENCE_RE = /^\s*(?:`{3,}|~{3,})\s*([^\s`]*)/ + +class CodeFlairWidget extends WidgetType { + constructor( + private readonly language: string, + /** Doc offsets of the block content to copy (fences excluded). */ + private readonly contentFrom: number, + private readonly contentTo: number + ) { + super() + } + + eq(other: CodeFlairWidget): boolean { + return ( + other.language === this.language && + other.contentFrom === this.contentFrom && + other.contentTo === this.contentTo + ) + } + + toDOM(view: EditorView): HTMLElement { + const button = document.createElement('button') + button.type = 'button' + button.className = 'cm-code-flair' + button.textContent = this.language + button.title = 'Copy code' + button.setAttribute('aria-label', `Copy ${this.language} code block`) + button.setAttribute('contenteditable', 'false') + + // Don't let the editor move the caret / start a selection when the label + // is pressed — copying shouldn't disturb where the user was typing. + button.addEventListener('mousedown', (event) => { + event.preventDefault() + event.stopPropagation() + }) + button.addEventListener('click', (event) => { + event.preventDefault() + event.stopPropagation() + const text = + this.contentTo > this.contentFrom + ? view.state.doc.sliceString(this.contentFrom, this.contentTo) + : '' + void navigator.clipboard?.writeText(text).then( + () => { + button.classList.add('is-copied') + button.textContent = 'Copied' + window.setTimeout(() => { + button.classList.remove('is-copied') + button.textContent = this.language + }, 1100) + }, + () => { + /* clipboard denied — leave the label as-is */ + } + ) + }) + return button + } + + ignoreEvent(): boolean { + return false + } +} + +function buildDecorations(view: EditorView): DecorationSet { + const { state } = view + const tree = syntaxTree(state) + const seen = new Set() + const pending: Array<{ at: number; deco: Decoration }> = [] + + for (const { from, to } of view.visibleRanges) { + tree.iterate({ + from, + to, + enter: (node) => { + if (node.name !== 'FencedCode') return + const beginLine = state.doc.lineAt(node.from) + if (seen.has(beginLine.from)) return false + seen.add(beginLine.from) + + const langMatch = beginLine.text.match(FENCE_RE) + const language = (langMatch?.[1] || 'text').toLowerCase() + + const lastLine = state.doc.lineAt(Math.max(node.from, node.to - 1)) + // Content sits between the opening and closing fence lines. When the + // block has no body (begin === end), copy nothing. + const contentFrom = + beginLine.number < lastLine.number + ? state.doc.line(beginLine.number + 1).from + : beginLine.to + const contentTo = + lastLine.number > beginLine.number + ? state.doc.line(lastLine.number - 1).to + : beginLine.to + + pending.push({ + at: beginLine.to, + deco: Decoration.widget({ + side: 1, + widget: new CodeFlairWidget(language, contentFrom, contentTo) + }) + }) + return false + } + }) + } + + pending.sort((a, b) => a.at - b.at) + const builder = new RangeSetBuilder() + for (const item of pending) builder.add(item.at, item.at, item.deco) + return builder.finish() +} + +export const codeBlockFlairPlugin = ViewPlugin.fromClass( + class { + decorations: DecorationSet + + constructor(view: EditorView) { + this.decorations = buildDecorations(view) + } + + update(update: ViewUpdate): void { + if (update.docChanged || update.viewportChanged) { + this.decorations = buildDecorations(update.view) + } + } + }, + { decorations: (plugin) => plugin.decorations } +) diff --git a/packages/app-core/src/lib/cm-code-block-font.ts b/packages/app-core/src/lib/cm-code-block-font.ts index 9a7f70fa..a5657114 100644 --- a/packages/app-core/src/lib/cm-code-block-font.ts +++ b/packages/app-core/src/lib/cm-code-block-font.ts @@ -20,33 +20,51 @@ import { } from '@codemirror/view' const codeBlockLine = Decoration.line({ class: 'cm-code-block-line' }) +// First / last line of each block also carry begin/end classes so the WYSIWYG +// stylesheet can round the top and bottom of the code "card". +const codeBlockBegin = Decoration.line({ class: 'cm-code-block-begin' }) +const codeBlockEnd = Decoration.line({ class: 'cm-code-block-end' }) function buildDecorations(view: EditorView): DecorationSet { const tree = syntaxTree(view.state) - // Collect unique line starts first: a block straddling the viewport gap can - // be visited under two visible ranges, so we dedupe and sort before adding to - // satisfy RangeSetBuilder's strictly-ascending requirement. - const lineStarts = new Set() + // Collect each block's line starts and the first/last line so a block that + // straddles the viewport gap (visited twice) is deduped before we add to the + // builder in strictly-ascending order. + const lineClasses = new Map() + const mark = (from: number, key: 'line' | 'begin' | 'end'): void => { + const entry = lineClasses.get(from) ?? { line: false, begin: false, end: false } + entry[key] = true + lineClasses.set(from, entry) + } for (const { from, to } of view.visibleRanges) { tree.iterate({ from, to, enter: (node) => { if (node.name !== 'FencedCode' && node.name !== 'CodeBlock') return + const firstLine = view.state.doc.lineAt(node.from) + const lastLine = view.state.doc.lineAt(Math.max(node.from, node.to - 1)) let pos = node.from while (pos <= node.to) { const line = view.state.doc.lineAt(pos) - lineStarts.add(line.from) + mark(line.from, 'line') if (line.to >= node.to) break pos = line.to + 1 } + mark(firstLine.from, 'begin') + mark(lastLine.from, 'end') return false // whole block handled; skip its children }, }) } const builder = new RangeSetBuilder() - for (const from of [...lineStarts].sort((a, b) => a - b)) { - builder.add(from, from, codeBlockLine) + for (const from of [...lineClasses.keys()].sort((a, b) => a - b)) { + const entry = lineClasses.get(from)! + // Order matters: RangeSetBuilder keeps insertion order for equal points, + // and CodeMirror merges the classes of stacked line decorations. + if (entry.line) builder.add(from, from, codeBlockLine) + if (entry.begin) builder.add(from, from, codeBlockBegin) + if (entry.end) builder.add(from, from, codeBlockEnd) } return builder.finish() } diff --git a/packages/app-core/src/lib/cm-hashtags.test.ts b/packages/app-core/src/lib/cm-hashtags.test.ts new file mode 100644 index 00000000..f572df20 --- /dev/null +++ b/packages/app-core/src/lib/cm-hashtags.test.ts @@ -0,0 +1,41 @@ +// @vitest-environment jsdom + +import { markdown, markdownLanguage } from '@codemirror/lang-markdown' +import { forceParsing } from '@codemirror/language' +import { EditorState } from '@codemirror/state' +import { EditorView } from '@codemirror/view' +import { describe, expect, it } from 'vitest' +import { hashtagExtension } from './cm-hashtags' + +function mount(doc: string): EditorView { + const parent = document.createElement('div') + document.body.append(parent) + const view = new EditorView({ + parent, + state: EditorState.create({ + doc, + extensions: [markdown({ base: markdownLanguage }), hashtagExtension] + }) + }) + forceParsing(view, doc.length, 5000) + view.dispatch({ changes: { from: doc.length, insert: ' ' } }) + view.dispatch({ changes: { from: doc.length, to: doc.length + 1 } }) + return view +} + +describe('hashtagPlugin', () => { + it('marks inline hashtags as pills', () => { + const view = mount('Tags: #tubex #peertube #crawler done') + expect(view.dom.querySelectorAll('.cm-hashtag').length).toBe(3) + view.destroy() + }) + + it('skips `#` inside code and headings', () => { + const view = mount('# Heading #notatag\n\nuse `#include` here\n\n#real') + const tags = Array.from(view.dom.querySelectorAll('.cm-hashtag')).map( + (e) => e.textContent + ) + expect(tags).toEqual(['#real']) + view.destroy() + }) +}) diff --git a/packages/app-core/src/lib/cm-hashtags.ts b/packages/app-core/src/lib/cm-hashtags.ts new file mode 100644 index 00000000..b6dcb226 --- /dev/null +++ b/packages/app-core/src/lib/cm-hashtags.ts @@ -0,0 +1,102 @@ +/** + * Styles Obsidian-style inline hashtags (`#tag`) as pills in the WYSIWYG + * editor, mirroring how the Preview pipeline (`remarkHashtags`) renders them. + * The `#tag` text stays editable — we only add a `cm-hashtag` mark — so this + * needs no active-line handling. Matches in code spans/blocks and headings are + * skipped, same as Preview. + * + * WYSIWYG-only: registered via `wysiwygExtensions()`. + */ +import { syntaxTree } from '@codemirror/language' +import { RangeSetBuilder } from '@codemirror/state' +import { + Decoration, + type DecorationSet, + EditorView, + ViewPlugin, + type ViewUpdate +} from '@codemirror/view' +import { useStore } from '../store' + +// Same shape as the Preview regex: a `#`, preceded by start-of-text or +// whitespace, then a letter and word/`-`/`/` characters. +const HASHTAG_RE = /(^|\s)#([a-zA-Z][\w\-/]*)/g + +/** True when `pos` sits inside a code span/block or a heading — contexts where + * a `#` isn't a tag (`#include`, `# Heading`, …). */ +function skipContext(state: EditorView['state'], pos: number): boolean { + let node = syntaxTree(state).resolveInner(pos, 1) + while (node) { + const n = node.name + if ( + n === 'FencedCode' || + n === 'CodeBlock' || + n === 'InlineCode' || + n.startsWith('ATXHeading') || + n.startsWith('SetextHeading') + ) { + return true + } + if (!node.parent) break + node = node.parent + } + return false +} + +function buildDecorations(view: EditorView): DecorationSet { + const { state } = view + const builder = new RangeSetBuilder() + for (const { from, to } of view.visibleRanges) { + const firstLine = state.doc.lineAt(from).number + const lastLine = state.doc.lineAt(Math.max(from, to - 1)).number + for (let n = firstLine; n <= lastLine; n++) { + const line = state.doc.line(n) + if (!line.text.includes('#')) continue + HASHTAG_RE.lastIndex = 0 + let m: RegExpExecArray | null + while ((m = HASHTAG_RE.exec(line.text)) !== null) { + const tagStart = line.from + m.index + m[1].length + const tagEnd = tagStart + 1 + m[2].length + if (skipContext(state, tagStart)) continue + // Per-match so the tag name rides along for the click handler. + builder.add( + tagStart, + tagEnd, + Decoration.mark({ class: 'cm-hashtag', attributes: { 'data-tag': m[2] } }) + ) + } + } + } + return builder.finish() +} + +const hashtagPlugin = ViewPlugin.fromClass( + class { + decorations: DecorationSet + constructor(view: EditorView) { + this.decorations = buildDecorations(view) + } + update(update: ViewUpdate): void { + if (update.docChanged || update.viewportChanged) { + this.decorations = buildDecorations(update.view) + } + } + }, + { decorations: (p) => p.decorations } +) + +// Clicking a hashtag opens the tag view (Obsidian-style). We intercept on +// mousedown so CodeMirror doesn't first drop the caret into the tag. +const hashtagClick = EditorView.domEventHandlers({ + mousedown: (event) => { + const target = event.target as HTMLElement | null + const el = target?.closest('.cm-hashtag') + const tag = el?.dataset.tag + if (!tag) return false + event.preventDefault() + void useStore.getState().openTagView(tag) + return true + } +}) + +export const hashtagExtension = [hashtagPlugin, hashtagClick] diff --git a/packages/app-core/src/lib/cm-live-preview.test.ts b/packages/app-core/src/lib/cm-live-preview.test.ts index e775f74a..509356bb 100644 --- a/packages/app-core/src/lib/cm-live-preview.test.ts +++ b/packages/app-core/src/lib/cm-live-preview.test.ts @@ -73,9 +73,9 @@ describe('livePreviewPlugin', () => { }) it('replaces an unchecked task marker with a checkbox widget', () => { - const doc = '- [ ] Buy milk' - // Cursor at end of line, off the marker. - const view = mountEditor(doc, doc.length) + // Cursor on the intro line — the task line is inactive, so it renders. + const doc = 'intro\n\n- [ ] Buy milk' + const view = mountEditor(doc, 0) const inputs = view.dom.querySelectorAll('input.cm-task-checkbox-input') expect(inputs).toHaveLength(1) @@ -89,8 +89,8 @@ describe('livePreviewPlugin', () => { }) it('replaces a checked task marker with a checked checkbox', () => { - const doc = '- [x] Done\n- [X] Also done' - const view = mountEditor(doc, doc.length) + const doc = 'intro\n\n- [x] Done\n- [X] Also done' + const view = mountEditor(doc, 0) const inputs = view.dom.querySelectorAll('input.cm-task-checkbox-input') expect(inputs).toHaveLength(2) @@ -114,35 +114,36 @@ describe('livePreviewPlugin', () => { }) it('toggles the underlying marker when the checkbox is clicked', () => { - const doc = '- [ ] Buy milk' - const view = mountEditor(doc, doc.length) + const doc = 'intro\n\n- [ ] Buy milk' + const view = mountEditor(doc, 0) const input = view.dom.querySelector('input.cm-task-checkbox-input') expect(input).toBeTruthy() input!.click() - expect(view.state.doc.toString()).toBe('- [x] Buy milk') + expect(view.state.doc.toString()).toBe('intro\n\n- [x] Buy milk') view.destroy() }) it('toggles back to unchecked from a `[x]` marker', () => { - const doc = '- [x] Already done' - const view = mountEditor(doc, doc.length) + const doc = 'intro\n\n- [x] Already done' + const view = mountEditor(doc, 0) const input = view.dom.querySelector('input.cm-task-checkbox-input') expect(input).toBeTruthy() input!.click() - expect(view.state.doc.toString()).toBe('- [ ] Already done') + expect(view.state.doc.toString()).toBe('intro\n\n- [ ] Already done') view.destroy() }) it('renders checkboxes for ordered, nested, and quoted tasks', () => { - // Task variants the TASK_LINE_RE in shared/tasklists supports. - const doc = ['1. [ ] Ordered', ' - [x] Nested', '> - [ ] Quoted'].join('\n') - const view = mountEditor(doc, doc.length) + // Task variants the TASK_LINE_RE in shared/tasklists supports. Cursor on + // the intro line so every task line is inactive (and thus rendered). + const doc = ['intro', '1. [ ] Ordered', ' - [x] Nested', '> - [ ] Quoted'].join('\n') + const view = mountEditor(doc, 0) const inputs = view.dom.querySelectorAll('input.cm-task-checkbox-input') expect(inputs).toHaveLength(3) diff --git a/packages/app-core/src/lib/cm-live-preview.ts b/packages/app-core/src/lib/cm-live-preview.ts index a54fd4d0..2545ce53 100644 --- a/packages/app-core/src/lib/cm-live-preview.ts +++ b/packages/app-core/src/lib/cm-live-preview.ts @@ -724,11 +724,11 @@ function computeDecorations(view: EditorView): DecorationSet { if (name === TASK_MARKER_NODE) { const line = state.doc.lineAt(node.from).number if (replacedLines.has(line)) return - // Reveal the raw `[ ]` / `[x]` so the user can edit it directly - // when the cursor lands inside the marker. Cursor elsewhere on - // the line still shows the checkbox — same model headings use - // for `#` markers. - if (selectionTouchesRange(state, node.from, node.to)) return + // Reveal the raw `[ ]` / `[x]` on the active line so the whole task + // line reads as source — matching Obsidian, and consistent with the + // list/quote/heading markers, which also reveal on the active line. + // Off the line, render the checkbox. + if (activeLines.has(line)) return const markerText = state.doc.sliceString(node.from, node.to) // `markerText` is `[ ]` / `[x]` / `[X]`; default to unchecked if the // parser ever hands us something unexpected. diff --git a/packages/app-core/src/lib/cm-table-menu.ts b/packages/app-core/src/lib/cm-table-menu.ts new file mode 100644 index 00000000..9125ba3d --- /dev/null +++ b/packages/app-core/src/lib/cm-table-menu.ts @@ -0,0 +1,246 @@ +/** + * Right-click context menu for the WYSIWYG table widget. Self-contained DOM + * menu (the widget lives outside React), mapping Obsidian's table action set + * onto the pure ops in `markdown-table.ts`. Each action produces a new table + * model and hands it to `apply`, which re-serializes and commits. + */ +import { + deleteColumn, + deleteRow, + duplicateColumn, + duplicateRow, + insertColumn, + insertRow, + moveColumn, + moveRow, + setColumnAlign, + sortByColumn, + columnCount, + type ColumnAlign, + type MarkdownTable +} from './markdown-table' + +export interface TableMenuRequest { + x: number + y: number + /** Clicked cell — `row === -1` is the header row. */ + row: number + col: number + model: MarkdownTable + apply: (next: MarkdownTable, focus?: { row: number; col: number }) => void +} + +type MenuItem = + | { kind: 'sep' } + | { kind: 'item'; label: string; disabled?: boolean; run: () => void } + +let openMenu: HTMLElement | null = null +let teardown: (() => void) | null = null + +export function closeTableContextMenu(): void { + if (teardown) teardown() +} + +export function openTableContextMenu(req: TableMenuRequest): void { + closeTableContextMenu() + const { row, col, model, apply } = req + // Restore focus to whatever opened the menu (e.g. a table cell) on close, + // unless an action ran — that focuses its own target cell. + const previouslyFocused = document.activeElement as HTMLElement | null + let actioned = false + const lastRow = model.rows.length - 1 + const lastCol = columnCount(model) - 1 + const onBody = row >= 0 + + const items: MenuItem[] = [ + { + kind: 'item', + label: 'Add row above', + disabled: !onBody, + run: () => apply(insertRow(model, row), { row, col }) + }, + { + kind: 'item', + label: 'Add row below', + run: () => { + const at = onBody ? row + 1 : 0 + apply(insertRow(model, at), { row: at, col }) + } + }, + { + kind: 'item', + label: 'Add column before', + run: () => apply(insertColumn(model, col), { row, col }) + }, + { + kind: 'item', + label: 'Add column after', + run: () => apply(insertColumn(model, col + 1), { row, col: col + 1 }) + }, + { kind: 'sep' }, + { + kind: 'item', + label: 'Move row up', + disabled: !onBody || row === 0, + run: () => apply(moveRow(model, row, row - 1), { row: row - 1, col }) + }, + { + kind: 'item', + label: 'Move row down', + disabled: !onBody || row === lastRow, + run: () => apply(moveRow(model, row, row + 1), { row: row + 1, col }) + }, + { + kind: 'item', + label: 'Move column left', + disabled: col === 0, + run: () => apply(moveColumn(model, col, col - 1), { row, col: col - 1 }) + }, + { + kind: 'item', + label: 'Move column right', + disabled: col === lastCol, + run: () => apply(moveColumn(model, col, col + 1), { row, col: col + 1 }) + }, + { kind: 'sep' }, + { + kind: 'item', + label: 'Duplicate row', + disabled: !onBody, + run: () => apply(duplicateRow(model, row), { row: row + 1, col }) + }, + { + kind: 'item', + label: 'Duplicate column', + run: () => apply(duplicateColumn(model, col), { row, col: col + 1 }) + }, + { kind: 'sep' }, + { + kind: 'item', + label: 'Delete row', + disabled: !onBody, + run: () => apply(deleteRow(model, row)) + }, + { + kind: 'item', + label: 'Delete column', + disabled: columnCount(model) <= 1, + run: () => apply(deleteColumn(model, col)) + }, + { kind: 'sep' }, + alignItem('Align left', 'left', col, model, apply, row), + alignItem('Align center', 'center', col, model, apply, row), + alignItem('Align right', 'right', col, model, apply, row), + { kind: 'sep' }, + { + kind: 'item', + label: 'Sort column (A → Z)', + run: () => apply(sortByColumn(model, col, 'asc')) + }, + { + kind: 'item', + label: 'Sort column (Z → A)', + run: () => apply(sortByColumn(model, col, 'desc')) + } + ] + + const menu = document.createElement('div') + menu.className = 'cm-table-menu' + menu.setAttribute('role', 'menu') + + for (const item of items) { + if (item.kind === 'sep') { + const sep = document.createElement('div') + sep.className = 'cm-table-menu-sep' + menu.append(sep) + continue + } + const button = document.createElement('button') + button.type = 'button' + button.className = 'cm-table-menu-item' + button.textContent = item.label + if (item.disabled) { + button.disabled = true + } else { + button.addEventListener('mousedown', (e) => e.preventDefault()) + button.addEventListener('click', (e) => { + e.preventDefault() + e.stopPropagation() + actioned = true + item.run() + closeTableContextMenu() + }) + } + menu.append(button) + } + + document.body.append(menu) + openMenu = menu + + // Position, flipping to stay on-screen. + const rect = menu.getBoundingClientRect() + const x = Math.min(req.x, window.innerWidth - rect.width - 8) + const y = Math.min(req.y, window.innerHeight - rect.height - 8) + menu.style.left = `${Math.max(8, x)}px` + menu.style.top = `${Math.max(8, y)}px` + + // Keyboard navigation (Vim-friendly): j/k or ↓/↑ move the highlight, Enter + // invokes, Esc closes. Lets the whole action set be driven without a mouse. + const enabledButtons = Array.from( + menu.querySelectorAll('.cm-table-menu-item:not(:disabled)') + ) + let activeIndex = 0 + const focusItem = (i: number): void => { + if (enabledButtons.length === 0) return + activeIndex = (i + enabledButtons.length) % enabledButtons.length + enabledButtons[activeIndex].focus() + } + + const onDown = (e: MouseEvent): void => { + if (!menu.contains(e.target as Node)) closeTableContextMenu() + } + const NAV_KEYS = ['Escape', 'ArrowDown', 'j', 'ArrowUp', 'k', 'Enter', ' '] + const onKey = (e: KeyboardEvent): void => { + if (!NAV_KEYS.includes(e.key)) return + // Consume the key fully so it can't also drive global/editor shortcuts + // while the menu is open. + e.preventDefault() + e.stopPropagation() + if (e.key === 'Escape') closeTableContextMenu() + else if (e.key === 'ArrowDown' || e.key === 'j') focusItem(activeIndex + 1) + else if (e.key === 'ArrowUp' || e.key === 'k') focusItem(activeIndex - 1) + else enabledButtons[activeIndex]?.click() + } + // Defer so the originating contextmenu/right-click doesn't immediately close it. + setTimeout(() => { + window.addEventListener('mousedown', onDown, true) + window.addEventListener('keydown', onKey, true) + focusItem(0) + }, 0) + + teardown = () => { + window.removeEventListener('mousedown', onDown, true) + window.removeEventListener('keydown', onKey, true) + menu.remove() + if (openMenu === menu) openMenu = null + teardown = null + if (!actioned) previouslyFocused?.focus?.() + } +} + +function alignItem( + label: string, + align: ColumnAlign, + col: number, + model: MarkdownTable, + apply: TableMenuRequest['apply'], + row: number +): MenuItem { + const active = model.aligns[col] === align + return { + kind: 'item', + label: active ? `${label} ✓` : label, + run: () => + apply(setColumnAlign(model, col, active ? 'none' : align), { row, col }) + } +} diff --git a/packages/app-core/src/lib/cm-table.test.ts b/packages/app-core/src/lib/cm-table.test.ts new file mode 100644 index 00000000..a580cee4 --- /dev/null +++ b/packages/app-core/src/lib/cm-table.test.ts @@ -0,0 +1,136 @@ +// @vitest-environment jsdom + +import { markdown, markdownLanguage } from '@codemirror/lang-markdown' +import { forceParsing } from '@codemirror/language' +import { EditorState } from '@codemirror/state' +import { EditorView } from '@codemirror/view' +import { describe, expect, it } from 'vitest' +import { tablePlugin, nextWordStart, prevWordStart, nextWordEnd } from './cm-table' +import { closeTableContextMenu } from './cm-table-menu' + +const TABLE_DOC = `Intro text. + +| Name | Age | +| --- | --- | +| Alice | 30 | +| Bob | 25 | + +Outro text.` + +function mount(doc: string): EditorView { + const parent = document.createElement('div') + document.body.append(parent) + const view = new EditorView({ + parent, + state: EditorState.create({ + doc, + extensions: [markdown({ base: markdownLanguage }), tablePlugin] + }) + }) + // Ensure the GFM table node is parsed, then nudge the field to rebuild. + forceParsing(view, doc.length, 5000) + view.dispatch({ changes: { from: 0, insert: ' ' } }) + view.dispatch({ changes: { from: 0, to: 1 } }) + return view +} + +describe('tablePlugin', () => { + it('renders a GFM table as an editable table widget without throwing', () => { + const view = mount(TABLE_DOC) + const widget = view.dom.querySelector('.cm-table-widget') + expect(widget).toBeTruthy() + const cells = widget?.querySelectorAll('.cm-table-cell') ?? [] + // 2 header + 4 body cells. + expect(cells.length).toBe(6) + expect(view.dom.textContent).toContain('Alice') + expect(view.dom.textContent).toContain('Age') + // One row grip per body row (2), one column grip per column (2). + expect(widget?.querySelectorAll('.cm-table-row-handle').length).toBe(2) + expect(widget?.querySelectorAll('.cm-table-col-handle').length).toBe(2) + view.destroy() + }) + + it('renders a plain doc with no table widget', () => { + const view = mount('Just a paragraph, no table here.') + expect(view.dom.querySelector('.cm-table-widget')).toBeNull() + view.destroy() + }) + + // Vim mode defaults on (DEFAULT_PREFS.vimMode), so cells start in NORMAL mode. + it('swallows vim normal-mode motion/printable keys inside a cell', () => { + const view = mount(TABLE_DOC) + const cell = view.dom.querySelector( + '.cm-table-widget [data-row="0"][data-col="0"]' + )! + // h/j/k/l are consumed as motions, not typed. + for (const key of ['h', 'j', 'k', 'l']) { + const ev = new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }) + cell.dispatchEvent(ev) + expect(ev.defaultPrevented).toBe(true) + } + // A stray printable key is swallowed too (won't corrupt the cell text). + const xEv = new KeyboardEvent('keydown', { key: 'x', bubbles: true, cancelable: true }) + cell.dispatchEvent(xEv) + expect(xEv.defaultPrevented).toBe(true) + view.destroy() + }) + + it('enters insert mode on `i`, revealing the raw cell source', () => { + const view = mount(TABLE_DOC) + const cell = view.dom.querySelector( + '.cm-table-widget [data-row="0"][data-col="0"]' + )! + expect(cell.dataset.rendered).toBe('true') + // NORMAL cells are non-editable (no caret); editing turns it on. + expect(cell.getAttribute('contenteditable')).toBe('false') + const iEv = new KeyboardEvent('keydown', { key: 'i', bubbles: true, cancelable: true }) + cell.dispatchEvent(iEv) + expect(iEv.defaultPrevented).toBe(true) + // Now editing: cell is editable, shows raw markdown, accepts typed chars. + expect(cell.getAttribute('contenteditable')).toBe('true') + expect(cell.dataset.rendered).toBe('false') + const xEv = new KeyboardEvent('keydown', { key: 'x', bubbles: true, cancelable: true }) + cell.dispatchEvent(xEv) + expect(xEv.defaultPrevented).toBe(false) + view.destroy() + }) + + it('opens the keyboard-navigable action menu on `m`', () => { + const view = mount(TABLE_DOC) + const cell = view.dom.querySelector( + '.cm-table-widget [data-row="0"][data-col="0"]' + )! + const mEv = new KeyboardEvent('keydown', { key: 'm', bubbles: true, cancelable: true }) + cell.dispatchEvent(mEv) + expect(mEv.defaultPrevented).toBe(true) + const menu = document.querySelector('.cm-table-menu') + expect(menu).toBeTruthy() + // The full Obsidian-style action set (add/move/dup/delete/align/sort). + expect(menu!.querySelectorAll('.cm-table-menu-item').length).toBeGreaterThan(10) + closeTableContextMenu() + view.destroy() + }) +}) + +describe('vim word motions (cell cursor)', () => { + const t = 'foo bar baz' + it('w moves to the next word start', () => { + expect(nextWordStart(t, 0)).toBe(4) + expect(nextWordStart(t, 4)).toBe(8) + expect(nextWordStart(t, 8)).toBe(t.length - 1) // clamps at the last word + }) + it('b moves to the previous word start', () => { + expect(prevWordStart(t, 8)).toBe(4) + expect(prevWordStart(t, 4)).toBe(0) + expect(prevWordStart(t, 0)).toBe(0) + }) + it('e moves to the next word end', () => { + expect(nextWordEnd(t, 0)).toBe(2) + expect(nextWordEnd(t, 2)).toBe(6) + }) + it('treats punctuation as its own word', () => { + // "a, b" → a(0) ,(1) space(2) b(3) + expect(nextWordStart('a, b', 0)).toBe(1) // 'a' → ',' + expect(nextWordStart('a, b', 1)).toBe(3) // ',' → 'b' + }) +}) diff --git a/packages/app-core/src/lib/cm-table.ts b/packages/app-core/src/lib/cm-table.ts new file mode 100644 index 00000000..bf688bd3 --- /dev/null +++ b/packages/app-core/src/lib/cm-table.ts @@ -0,0 +1,1008 @@ +/** + * WYSIWYG table editing for the Edit-mode editor — an Obsidian-style live + * table. Each GFM pipe table is replaced by a block widget that renders a real + * `` with editable cells and GUI affordances (add/move/delete rows & + * columns, alignment, sort, drag-to-reorder). The markdown source stays the + * single source of truth: every edit re-serializes the table model + * (`markdown-table.ts`) and writes it back as one CodeMirror change, so undo, + * autosave, and multi-pane sync keep working. + * + * The replaced range is also marked atomic, so the CM caret never lands in the + * raw `| pipe |` text — all editing happens through the widget. (Raw source + * editing still lives in Split mode, which doesn't load this extension.) + * + * WYSIWYG-only: registered via `wysiwygExtensions()`; never loads in Split. + */ +import { syntaxTree } from '@codemirror/language' +import { Prec, RangeSetBuilder, StateField, type EditorState } from '@codemirror/state' +import { + Decoration, + type DecorationSet, + EditorView, + WidgetType +} from '@codemirror/view' +import { + insertColumn, + insertRow, + moveColumn, + moveRow, + parseTable, + serializeTable, + type MarkdownTable +} from './markdown-table' +import { openTableContextMenu } from './cm-table-menu' +import { renderMarkdown } from './markdown' +import { getCM } from '@replit/codemirror-vim' +import { useStore } from '../store' + +/** True when the editor is in Vim mode — gates the table's modal (normal / + * insert) keyboard navigation. With Vim off, cells stay plain contenteditable + * fields driven by Tab/Enter (unchanged). */ +function vimEnabled(): boolean { + return useStore.getState().vimMode +} + +/** Render a cell's markdown source to inline HTML (sanitized by the markdown + * pipeline). Strips the wrapping `

` so the content sits inline in the cell. + * Empty cells render nothing. */ +function renderInlineCell(text: string): string { + const trimmed = text.trim() + if (!trimmed) return '' + const html = renderMarkdown(trimmed).trim() + const match = html.match(/^]*>([\s\S]*?)<\/p>\s*$/) + return match ? match[1] : html +} + +/** Find the enclosing `Table` node range for a doc position, or null. */ +function tableRangeAt(view: EditorView, pos: number): { from: number; to: number } | null { + let node = syntaxTree(view.state).resolveInner(pos, 1) + while (node) { + if (node.name === 'Table') return { from: node.from, to: node.to } + if (!node.parent) break + node = node.parent + } + return null +} + +/** + * Re-serialize `table` and write it over whichever Table node currently sits + * under the widget's DOM. Resolving the range live (via posAtDOM) keeps the + * write correct even when edits elsewhere have shifted the document. + */ +function commitTable(view: EditorView, dom: HTMLElement, table: MarkdownTable): void { + let pos: number + try { + pos = view.posAtDOM(dom) + } catch { + return + } + const range = tableRangeAt(view, pos) + if (!range) return + const next = serializeTable(table) + if (next === view.state.sliceDoc(range.from, range.to)) return + view.dispatch({ + changes: { from: range.from, to: range.to, insert: next } + }) +} + +type CellAddress = { row: number; col: number } + +class TableWidget extends WidgetType { + /** Working copy edited in place by the cells; committed on focus-out. */ + private model: MarkdownTable + private dom: HTMLElement | null = null + private dirty = false + /** Vim cell mode: 'normal' is a block-cursor that moves over characters + * (h/l) and rows (j/k); 'insert' edits the focused cell. Vim mode only. */ + private cellMode: 'normal' | 'insert' = 'normal' + /** Character index of the Vim block cursor within the focused cell's source. */ + private cursorOffset = 0 + /** Where to land the block cursor on the next cell focus: a char index, or + * 'end' (last char). Lets h/l carry the cursor across cell boundaries. */ + private pendingOffset: number | 'end' | null = null + /** Set in toDOM — CodeMirror hands the live view there. Block widgets are + * provided by a StateField, which has no view at build time. */ + private view!: EditorView + + constructor( + /** Raw markdown of the table block — drives `eq` so unchanged tables keep + * their DOM (and any in-progress cell focus) across rebuilds. */ + private readonly source: string, + parsed: MarkdownTable + ) { + super() + this.model = parsed + } + + eq(other: TableWidget): boolean { + return other.source === this.source + } + + /** Commit a concrete next-model: re-serialize and write it over the source. + * The dispatch rebuilds the decorations (a fresh widget), so we refocus the + * requested cell on the next frame. Used by the context menu, which has + * already computed `next` from the current model. */ + private applyModel(next: MarkdownTable, focus?: CellAddress): void { + // Capture the live range BEFORE the dispatch detaches our DOM. + const dom = this.dom as HTMLElement + this.model = next + this.dirty = false + commitTable(this.view, dom, next) + if (focus) { + requestAnimationFrame(() => this.focusCellAt(focus)) + } + } + + /** Pull pending cell edits into the model, then apply a structural + * transform and commit — all without an intermediate dispatch, so our DOM + * stays attached for the single `commitTable` write. */ + private applyTransform( + fn: (model: MarkdownTable) => MarkdownTable, + focus?: CellAddress + ): void { + this.syncFromDom() + this.applyModel(fn(this.model), focus) + } + + private focusCellAt(addr: CellAddress): void { + const view = this.view + const dom = view.contentDOM.querySelector( + `.cm-table-widget [data-row="${addr.row}"][data-col="${addr.col}"]` + ) + if (dom) { + dom.focus() + placeCaretEnd(dom) + } + } + + /** Pull every cell's text out of the DOM into the model. `data-raw` holds the + * markdown source — `textContent` would lose it when a cell shows rendered + * inline markup (e.g. `code` chips). */ + private syncFromDom(): void { + if (!this.dom) return + const cells = this.dom.querySelectorAll('[data-row]') + cells.forEach((cell) => { + const row = Number(cell.dataset.row) + const col = Number(cell.dataset.col) + const value = cell.dataset.raw ?? cell.textContent ?? '' + if (row === -1) this.model.headers[col] = value + else if (this.model.rows[row]) this.model.rows[row][col] = value + }) + } + + private commitIfDirty(): void { + if (!this.dirty) return + this.syncFromDom() + this.dirty = false + commitTable(this.view, this.dom as HTMLElement, this.model) + } + + toDOM(view: EditorView): HTMLElement { + this.view = view + const root = document.createElement('div') + root.className = 'cm-table-widget' + root.setAttribute('contenteditable', 'false') + this.dom = root + + const wrapper = document.createElement('div') + wrapper.className = 'cm-table-wrapper' + + const table = document.createElement('table') + const thead = document.createElement('thead') + const headRow = document.createElement('tr') + this.model.headers.forEach((text, col) => { + headRow.append(this.buildCell('th', -1, col, text)) + }) + thead.append(headRow) + table.append(thead) + + const tbody = document.createElement('tbody') + this.model.rows.forEach((row, r) => { + const tr = document.createElement('tr') + row.forEach((text, col) => tr.append(this.buildCell('td', r, col, text))) + tbody.append(tr) + }) + table.append(tbody) + wrapper.append(table) + + // Add-row / add-column buttons (appear on hover via CSS). + const addCol = document.createElement('button') + addCol.type = 'button' + addCol.className = 'cm-table-add cm-table-add-col' + addCol.textContent = '+' + addCol.title = 'Add column' + addCol.addEventListener('mousedown', (e) => e.preventDefault()) + addCol.addEventListener('click', (e) => { + e.preventDefault() + const col = this.model.headers.length + this.applyTransform((m) => insertColumn(m, m.headers.length), { row: -1, col }) + }) + + const addRow = document.createElement('button') + addRow.type = 'button' + addRow.className = 'cm-table-add cm-table-add-row' + addRow.textContent = '+' + addRow.title = 'Add row' + addRow.addEventListener('mousedown', (e) => e.preventDefault()) + addRow.addEventListener('click', (e) => { + e.preventDefault() + const row = this.model.rows.length + this.applyTransform((m) => insertRow(m, m.rows.length), { row, col: 0 }) + }) + + wrapper.append(addCol, addRow) + root.append(wrapper) + + // Commit when focus leaves the whole widget. + root.addEventListener('focusout', (event) => { + const next = event.relatedTarget as Node | null + if (next && root.contains(next)) return + this.commitIfDirty() + }) + + return root + } + + private buildCell( + tag: 'th' | 'td', + row: number, + col: number, + text: string + ): HTMLTableCellElement { + const cell = document.createElement(tag) + const align = this.model.aligns[col] ?? 'none' + if (align !== 'none') cell.setAttribute('align', align) + + const editable = document.createElement('div') + editable.className = 'cm-table-cell' + editable.dataset.row = String(row) + editable.dataset.col = String(col) + editable.dataset.raw = text + // Vim: cells are non-editable in NORMAL mode, so there's no native text + // caret (a block cursor is drawn in CSS); i/a turns editing on. tabindex + // keeps the cell focusable for navigation while non-editable. Non-Vim: + // always editable (click-to-edit), unchanged. + editable.setAttribute('tabindex', '-1') + editable.setAttribute('contenteditable', vimEnabled() ? 'false' : 'true') + // Idle: show rendered inline markdown (code/bold/links). Editing: show the + // raw source. `data-raw` stays authoritative for commits either way. + editable.innerHTML = renderInlineCell(text) + editable.dataset.rendered = 'true' + + editable.addEventListener('focus', () => { + if (vimEnabled()) { + // Vim: land in NORMAL mode at the pending offset (carried across cells + // by h/l), or 0. The cell becomes a block cursor over its source. + this.cellMode = 'normal' + const len = (editable.dataset.raw ?? '').length + this.cursorOffset = + this.pendingOffset === 'end' + ? Math.max(0, len - 1) + : Math.max(0, this.pendingOffset ?? 0) + this.pendingOffset = null + this.enterNormalCell(editable) + return + } + if (editable.dataset.rendered === 'true') { + editable.textContent = editable.dataset.raw ?? '' + editable.dataset.rendered = 'false' + placeCaretEnd(editable) + } + }) + editable.addEventListener('input', () => { + this.dirty = true + editable.dataset.raw = editable.textContent ?? '' + }) + editable.addEventListener('blur', () => { + this.clearCellCursor(editable) + editable.classList.remove('is-vim-normal') + // Leaving the cell: pull the (possibly edited) source back out of the DOM + // and re-render it. `data-raw` is authoritative, so read that. + if (editable.dataset.rendered === 'false') { + const raw = editable.dataset.raw ?? editable.textContent ?? '' + editable.dataset.raw = raw + editable.innerHTML = renderInlineCell(raw) + editable.dataset.rendered = 'true' + } + }) + editable.addEventListener('keydown', (event) => this.onCellKeydown(event, row, col, editable)) + cell.addEventListener('contextmenu', (event) => { + event.preventDefault() + event.stopPropagation() + // Pull pending edits into the model (no dispatch) so the menu acts on + // the current contents; the chosen action commits in one write. + this.syncFromDom() + openTableContextMenu({ + x: event.clientX, + y: event.clientY, + row, + col, + model: this.model, + apply: (next, focus) => this.applyModel(next, focus) + }) + }) + cell.append(editable) + + // Drag handles: row grip on the first cell of each body row, column grip + // above each header cell. They appear on hover (CSS) and start a manual + // pointer-drag to reorder. + if (row >= 0 && col === 0) { + cell.append(this.buildDragHandle('row', row)) + } + if (row === -1) { + cell.append(this.buildDragHandle('col', col)) + } + return cell + } + + private buildDragHandle(kind: 'row' | 'col', index: number): HTMLElement { + const handle = document.createElement('div') + handle.className = kind === 'row' ? 'cm-table-row-handle' : 'cm-table-col-handle' + handle.setAttribute('contenteditable', 'false') + handle.title = kind === 'row' ? 'Drag to move row' : 'Drag to move column' + handle.addEventListener('mousedown', (event) => { + if (event.button !== 0) return + event.preventDefault() + event.stopPropagation() + if (kind === 'row') this.startRowDrag(index) + else this.startColDrag(index) + }) + return handle + } + + /** Drag a body row to a new position. Tracks the pointer, draws a drop + * indicator, and commits a `moveRow` on release. */ + private startRowDrag(fromRow: number): void { + const dom = this.dom + if (!dom) return + this.syncFromDom() + const wrapper = dom.querySelector('.cm-table-wrapper') + const rows = Array.from(dom.querySelectorAll('tbody tr')) + if (!wrapper || rows.length === 0) return + dom.classList.add('is-dragging') + const indicator = document.createElement('div') + indicator.className = 'cm-table-drop-indicator cm-table-drop-row' + wrapper.append(indicator) + + // Which row is the pointer over? Drop-onto-row semantics: the dragged row + // moves into that row's slot (so dropping row 0 anywhere on row 1 swaps + // them), which is far more intuitive than a midpoint insertion gap. + const hoveredFor = (y: number): number => { + for (let i = 0; i < rows.length; i++) { + if (y <= rows[i].getBoundingClientRect().bottom) return i + } + return rows.length - 1 + } + const place = (hovered: number): void => { + if (hovered === fromRow) { + indicator.style.display = 'none' + return + } + indicator.style.display = 'block' + const wrect = wrapper.getBoundingClientRect() + const rect = rows[hovered].getBoundingClientRect() + // Line on the far edge in the drag direction. + const top = (hovered > fromRow ? rect.bottom : rect.top) - wrect.top + indicator.style.top = `${top}px` + } + place(fromRow) + + const onMove = (e: MouseEvent): void => place(hoveredFor(e.clientY)) + const onUp = (e: MouseEvent): void => { + window.removeEventListener('mousemove', onMove) + window.removeEventListener('mouseup', onUp) + indicator.remove() + dom.classList.remove('is-dragging') + const hovered = hoveredFor(e.clientY) + if (hovered !== fromRow) { + this.applyModel(moveRow(this.model, fromRow, hovered)) + } + } + window.addEventListener('mousemove', onMove) + window.addEventListener('mouseup', onUp) + } + + /** Drag a column to a new position. */ + private startColDrag(fromCol: number): void { + const dom = this.dom + if (!dom) return + this.syncFromDom() + const wrapper = dom.querySelector('.cm-table-wrapper') + const headers = Array.from(dom.querySelectorAll('thead th')) + if (!wrapper || headers.length === 0) return + dom.classList.add('is-dragging') + const indicator = document.createElement('div') + indicator.className = 'cm-table-drop-indicator cm-table-drop-col' + wrapper.append(indicator) + + // Drop-onto-column semantics, mirroring rows. + const hoveredFor = (x: number): number => { + for (let i = 0; i < headers.length; i++) { + if (x <= headers[i].getBoundingClientRect().right) return i + } + return headers.length - 1 + } + const place = (hovered: number): void => { + if (hovered === fromCol) { + indicator.style.display = 'none' + return + } + indicator.style.display = 'block' + const wrect = wrapper.getBoundingClientRect() + const rect = headers[hovered].getBoundingClientRect() + const left = (hovered > fromCol ? rect.right : rect.left) - wrect.left + indicator.style.left = `${left}px` + } + place(fromCol) + + const onMove = (e: MouseEvent): void => place(hoveredFor(e.clientX)) + const onUp = (e: MouseEvent): void => { + window.removeEventListener('mousemove', onMove) + window.removeEventListener('mouseup', onUp) + indicator.remove() + dom.classList.remove('is-dragging') + const hovered = hoveredFor(e.clientX) + if (hovered !== fromCol) { + this.applyModel(moveColumn(this.model, fromCol, hovered)) + } + } + window.addEventListener('mousemove', onMove) + window.addEventListener('mouseup', onUp) + } + + private onCellKeydown( + event: KeyboardEvent, + row: number, + col: number, + editable: HTMLElement + ): void { + const cols = this.model.headers.length + const rowsCount = this.model.rows.length + + if (vimEnabled()) { + if (this.cellMode === 'insert') { + // INSERT: Escape commits the cell text and drops back to NORMAL, staying + // put (classic Vim). Everything else types normally / falls through to + // the shared Tab / Enter handling below. + if (event.key === 'Escape') { + event.preventDefault() + event.stopPropagation() + const sel = window.getSelection() + const caret = + sel && sel.anchorNode && editable.contains(sel.anchorNode) ? sel.anchorOffset : 0 + editable.dataset.raw = editable.textContent ?? '' + editable.setAttribute('contenteditable', 'false') + this.cellMode = 'normal' + // Vim leaves the cursor on the char left of the caret. + this.cursorOffset = Math.max(0, caret - 1) + this.enterNormalCell(editable) + return + } + } else { + // NORMAL: h/j/k/l move between cells, i/a start editing, Esc leaves the + // table. Stray printable keys are swallowed so they never reach the cell. + // Stop unmodified keys from leaking to global shortcuts (the cell is not + // contenteditable in NORMAL mode, so app-level handlers — e.g. the `m` + // sidebar menu — would otherwise also fire). Modified combos (⌘S etc.) + // still pass through. + if (!event.metaKey && !event.ctrlKey && !event.altKey) { + event.stopPropagation() + } + const cellText = editable.dataset.raw ?? '' + switch (event.key) { + case 'h': + event.preventDefault() + this.moveCharLeft(editable, row, col) + return + case 'l': + event.preventDefault() + this.moveCharRight(editable, row, col) + return + case 'j': + event.preventDefault() + this.moveRowDown(row, col) + return + case 'k': + event.preventDefault() + this.moveRowUp(row, col) + return + case 'i': + event.preventDefault() + this.enterInsertMode(editable, this.cursorOffset) + return + case 'a': + event.preventDefault() + this.enterInsertMode(editable, this.cursorOffset + 1) + return + case 'I': + event.preventDefault() + this.enterInsertMode(editable, 0) + return + case 'A': + event.preventDefault() + this.enterInsertMode(editable, cellText.length) + return + case 'w': + event.preventDefault() + this.cursorOffset = nextWordStart(cellText, this.cursorOffset) + this.renderCellCursor(editable) + return + case 'e': + event.preventDefault() + this.cursorOffset = nextWordEnd(cellText, this.cursorOffset) + this.renderCellCursor(editable) + return + case 'b': + event.preventDefault() + this.cursorOffset = prevWordStart(cellText, this.cursorOffset) + this.renderCellCursor(editable) + return + case '0': + event.preventDefault() + this.cursorOffset = 0 + this.renderCellCursor(editable) + return + case '$': + event.preventDefault() + this.cursorOffset = Math.max(0, cellText.length - 1) + this.renderCellCursor(editable) + return + case 'Escape': + event.preventDefault() + this.exitToEditor('after') + return + case 'm': + // Open the full table action menu (add/move/duplicate/delete/align/ + // sort) for this cell — keyboard-navigable with j/k/Enter/Esc. + event.preventDefault() + this.openCellMenu(editable, row, col) + return + case 'Tab': + case 'Enter': + break // fall through to shared Tab / Enter navigation + default: + if ( + event.key.length === 1 && + !event.metaKey && + !event.ctrlKey && + !event.altKey + ) { + event.preventDefault() + } + return + } + } + } + + // Body rows are 0..rowsCount-1; header is -1. Flatten for navigation. + const order: CellAddress[] = [] + for (let c = 0; c < cols; c++) order.push({ row: -1, col: c }) + for (let r = 0; r < rowsCount; r++) + for (let c = 0; c < cols; c++) order.push({ row: r, col: c }) + const idx = order.findIndex((a) => a.row === row && a.col === col) + + if (event.key === 'Tab') { + event.preventDefault() + const nextIdx = event.shiftKey ? idx - 1 : idx + 1 + if (nextIdx >= 0 && nextIdx < order.length) { + this.moveFocus(order[nextIdx]) + } else if (!event.shiftKey) { + // Tab past the last cell → add a new row and land in it. + const row = this.model.rows.length + this.applyTransform((m) => insertRow(m, m.rows.length), { row, col: 0 }) + } + return + } + + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault() + const target = row === -1 ? { row: 0, col } : { row: row + 1, col } + if (target.row >= rowsCount) { + this.applyTransform((m) => insertRow(m, rowsCount), { row: rowsCount, col }) + } else { + this.moveFocus(target) + } + return + } + + if (event.key === 'Escape') { + event.preventDefault() + this.commitIfDirty() + this.view.focus() + } + } + + private moveFocus(addr: CellAddress): void { + if (!this.dom) return + const el = this.dom.querySelector( + `[data-row="${addr.row}"][data-col="${addr.col}"]` + ) + if (el) { + el.focus() + placeCaretEnd(el) + } + } + + /** Resolve a cell element by address within this widget. */ + private cellEl(row: number, col: number): HTMLElement | null { + return this.dom?.querySelector(`[data-row="${row}"][data-col="${col}"]`) ?? null + } + + /** Focus a cell, landing the block cursor at a source offset (or its last + * char). Used when h/l cross a cell boundary and by j/k. */ + private focusCellAtOffset(row: number, col: number, offset: number | 'end'): void { + const cell = this.cellEl(row, col) + if (!cell) return + this.pendingOffset = offset + cell.focus() + } + + /** Put a focused cell into NORMAL mode: non-editable, raw source shown, with + * the block cursor drawn over the current character. */ + private enterNormalCell(cell: HTMLElement): void { + cell.setAttribute('contenteditable', 'false') + cell.classList.add('is-vim-normal') + // Show the raw source so motions map to real characters (and are + // measurable for the block cursor). + cell.textContent = cell.dataset.raw ?? '' + cell.dataset.rendered = 'false' + this.renderCellCursor(cell) + } + + private clearCellCursor(cell: HTMLElement): void { + cell.querySelector('.cm-table-cell-cursor')?.remove() + } + + /** Draw the block cursor over the character at `cursorOffset`, measuring its + * rect from the cell's text node (so it tracks real glyph positions). */ + private renderCellCursor(cell: HTMLElement): void { + this.clearCellCursor(cell) + const text = cell.dataset.raw ?? '' + const offset = Math.max(0, Math.min(this.cursorOffset, Math.max(0, text.length - 1))) + this.cursorOffset = offset + const cursor = document.createElement('span') + cursor.className = 'cm-table-cell-cursor' + cursor.setAttribute('contenteditable', 'false') + const node = cell.firstChild + const cellRect = cell.getBoundingClientRect() + const range = document.createRange() + if ( + node && + node.nodeType === Node.TEXT_NODE && + text.length > 0 && + typeof range.getBoundingClientRect === 'function' + ) { + range.setStart(node, offset) + range.setEnd(node, Math.min(text.length, offset + 1)) + const rect = range.getBoundingClientRect() + cursor.style.left = `${rect.left - cellRect.left}px` + cursor.style.top = `${rect.top - cellRect.top}px` + cursor.style.width = `${Math.max(rect.width, 3)}px` + cursor.style.height = `${rect.height || 18}px` + } else { + // Empty cell, or no measurement available (e.g. jsdom): a thin block at + // the cell's start. + cursor.style.left = '10px' + cursor.style.top = '5px' + cursor.style.bottom = '5px' + cursor.style.width = '0.5em' + } + cell.appendChild(cursor) + } + + /** Vim NORMAL `l`: next character; at the cell's end, jump to the next + * column. */ + private moveCharRight(cell: HTMLElement, row: number, col: number): void { + const len = (cell.dataset.raw ?? '').length + if (this.cursorOffset < len - 1) { + this.cursorOffset += 1 + this.renderCellCursor(cell) + } else if (col + 1 < this.model.headers.length) { + this.focusCellAtOffset(row, col + 1, 0) + } + } + + /** Vim NORMAL `h`: previous character; at the cell's start, jump to the + * previous column (landing on its last char). */ + private moveCharLeft(cell: HTMLElement, row: number, col: number): void { + if (this.cursorOffset > 0) { + this.cursorOffset -= 1 + this.renderCellCursor(cell) + } else if (col - 1 >= 0) { + this.focusCellAtOffset(row, col - 1, 'end') + } + } + + /** Vim NORMAL `j`: header → first body row → … → past the last row leaves the + * table downward, keeping the column offset (clamped). */ + private moveRowDown(row: number, col: number): void { + const target = row === -1 ? 0 : row + 1 + if (target >= this.model.rows.length) { + this.exitToEditor('after') + return + } + this.focusCellAtOffset(target, col, this.cursorOffset) + } + + /** Vim NORMAL `k`: body row → … → header → past the header leaves upward. */ + private moveRowUp(row: number, col: number): void { + if (row === -1) { + this.exitToEditor('before') + return + } + this.focusCellAtOffset(row === 0 ? -1 : row - 1, col, this.cursorOffset) + } + + /** Switch the focused cell into INSERT mode at a source offset: editable, + * native caret, block cursor removed. Don't re-focus — that re-fires the + * focus handler, snapping back to NORMAL. */ + private enterInsertMode(cell: HTMLElement, caretOffset: number): void { + this.cellMode = 'insert' + this.clearCellCursor(cell) + cell.classList.remove('is-vim-normal') + cell.setAttribute('contenteditable', 'true') + if (cell.dataset.rendered !== 'false') { + cell.textContent = cell.dataset.raw ?? '' + cell.dataset.rendered = 'false' + } + placeCaretAt(cell, caretOffset) + } + + /** Leave the table, returning the caret to the document just before/after the + * table block. The atomic range keeps it from sliding back in. */ + private exitToEditor(side: 'before' | 'after'): void { + this.commitIfDirty() + this.cellMode = 'normal' + const dom = this.dom + if (!dom) { + this.view.focus() + return + } + let pos: number + try { + pos = this.view.posAtDOM(dom) + } catch { + this.view.focus() + return + } + const range = tableRangeAt(this.view, pos) + this.view.focus() + if (!range) return + const doc = this.view.state.doc + const anchor = + side === 'before' ? Math.max(0, range.from - 1) : Math.min(doc.length, range.to + 1) + this.view.dispatch({ selection: { anchor } }) + } + + /** Open the table action menu for the focused cell (Vim NORMAL `m`), + * anchored at the cell. The menu itself is keyboard-navigable. */ + private openCellMenu(editable: HTMLElement, row: number, col: number): void { + this.syncFromDom() + const rect = editable.getBoundingClientRect() + openTableContextMenu({ + x: rect.left, + y: rect.bottom, + row, + col, + model: this.model, + apply: (next, focus) => this.applyModel(next, focus) + }) + } + + ignoreEvent(): boolean { + return true + } + + destroy(): void { + // Flush any uncommitted edits if the widget is torn down while focused. + this.commitIfDirty() + this.dom = null + } +} + +function placeCaretEnd(el: HTMLElement): void { + const range = document.createRange() + range.selectNodeContents(el) + range.collapse(false) + const sel = window.getSelection() + sel?.removeAllRanges() + sel?.addRange(range) +} + +/** Vim character class: 0 = whitespace, 1 = word char, 2 = punctuation. A + * "word" is a maximal run of one non-whitespace class. */ +function charClass(ch: string): 0 | 1 | 2 { + if (/\s/.test(ch)) return 0 + if (/[A-Za-z0-9_]/.test(ch)) return 1 + return 2 +} + +/** Vim `w`: index of the start of the next word (clamped to the last char). */ +export function nextWordStart(text: string, off: number): number { + const n = text.length + if (n === 0) return 0 + let i = off + const cls = charClass(text[i] ?? ' ') + if (cls !== 0) while (i < n && charClass(text[i]) === cls) i++ + while (i < n && charClass(text[i]) === 0) i++ + return Math.min(i, n - 1) +} + +/** Vim `b`: index of the start of the current/previous word. */ +export function prevWordStart(text: string, off: number): number { + let i = off - 1 + while (i > 0 && charClass(text[i]) === 0) i-- + if (i <= 0) return 0 + const cls = charClass(text[i]) + while (i > 0 && charClass(text[i - 1]) === cls) i-- + return Math.max(0, i) +} + +/** Vim `e`: index of the end of the current/next word. */ +export function nextWordEnd(text: string, off: number): number { + const n = text.length + if (n === 0) return 0 + let i = off + 1 + while (i < n && charClass(text[i]) === 0) i++ + if (i >= n) return n - 1 + const cls = charClass(text[i]) + while (i + 1 < n && charClass(text[i + 1]) === cls) i++ + return Math.min(i, n - 1) +} + +/** Collapse the caret at a character offset within the element's text node. */ +function placeCaretAt(el: HTMLElement, offset: number): void { + const node = el.firstChild + const range = document.createRange() + if (node && node.nodeType === Node.TEXT_NODE) { + const max = node.textContent?.length ?? 0 + range.setStart(node, Math.max(0, Math.min(offset, max))) + } else { + range.selectNodeContents(el) + } + range.collapse(true) + const sel = window.getSelection() + sel?.removeAllRanges() + sel?.addRange(range) +} + +function buildDecorations(state: EditorState): DecorationSet { + const tree = syntaxTree(state) + const ranges: Array<{ from: number; to: number; deco: Decoration }> = [] + + // Iterate the whole parsed tree (no viewport): block replace decorations + // must come from a StateField, which has no viewport. Tables are sparse, so + // this stays cheap for typical notes. + tree.iterate({ + enter: (node) => { + if (node.name !== 'Table') return + const source = state.sliceDoc(node.from, node.to) + const parsed = parseTable(source) + if (!parsed) return false + ranges.push({ + from: node.from, + to: node.to, + deco: Decoration.replace({ + block: true, + widget: new TableWidget(source, parsed) + }) + }) + return false + } + }) + + ranges.sort((a, b) => a.from - b.from) + const builder = new RangeSetBuilder() + for (const r of ranges) builder.add(r.from, r.to, r.deco) + return builder.finish() +} + +/** + * Block widgets (and any decoration that replaces line breaks) must be + * supplied through a StateField, not a ViewPlugin — CodeMirror needs to know + * the block structure before the viewport is computed. The field also feeds + * `atomicRanges` so the caret never lands inside the raw pipe source. + */ +export const tablePlugin = StateField.define({ + create: (state) => buildDecorations(state), + update(deco, tr) { + // Rebuild on edits and whenever the parser advances (the syntax tree is a + // fresh object); otherwise positions are unchanged, so reuse as-is. + if (tr.docChanged || syntaxTree(tr.startState) !== syntaxTree(tr.state)) { + return buildDecorations(tr.state) + } + return deco + }, + provide: (field) => [ + EditorView.decorations.from(field), + EditorView.atomicRanges.of((view) => view.state.field(field, false) ?? Decoration.none) + ] +}) + +/** Find a `Table` node directly adjacent to `line`: `down` → a table on the + * next line, `up` → a table on the previous line. Returns its range or null. */ +function adjacentTableRange( + state: EditorState, + line: number, + dir: 'down' | 'up' +): { from: number; to: number } | null { + const targetLine = dir === 'down' ? line + 1 : line - 1 + if (targetLine < 1 || targetLine > state.doc.lines) return null + const probe = + dir === 'down' ? state.doc.line(targetLine).from : state.doc.line(targetLine).to + let node: ReturnType['topNode'] | null = syntaxTree(state).resolveInner( + probe, + dir === 'down' ? 1 : -1 + ) + while (node) { + if (node.name === 'Table') return { from: node.from, to: node.to } + node = node.parent + } + return null +} + +/** Focus the entry cell of the table widget at `tableFrom`: the first header + * cell when entering from above, the first cell of the last row from below. */ +function focusTableEntryCell( + view: EditorView, + tableFrom: number, + edge: 'first' | 'last' +): boolean { + const widgets = view.contentDOM.querySelectorAll('.cm-table-widget') + for (const widget of widgets) { + let pos: number + try { + pos = view.posAtDOM(widget) + } catch { + continue + } + if (pos !== tableFrom) continue + let cell: HTMLElement | null + if (edge === 'first') { + cell = widget.querySelector('[data-row="-1"][data-col="0"]') + } else { + const bodyCol0 = Array.from( + widget.querySelectorAll('[data-col="0"]') + ).filter((c) => Number(c.dataset.row) >= 0) + cell = bodyCol0.length ? bodyCol0[bodyCol0.length - 1] : null + } + if (cell) { + cell.focus() + return true + } + } + return false +} + +/** + * Vim integration: in NORMAL mode, `j` / `k` on a line directly adjacent to a + * rendered table steps the caret into the table (first header cell from above, + * last row from below) instead of jumping over the atomic block. Inside, the + * widget's own key handler takes over. Highest precedence so it runs before Vim + * consumes the key; a no-op (returns false) whenever there's no adjacent table. + */ +export const tableVimEntry = Prec.highest( + EditorView.domEventHandlers({ + keydown(event, view) { + if (!vimEnabled()) return false + if (event.key !== 'j' && event.key !== 'k') return false + if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return false + // Keys from inside a table widget are the widget's own cell navigation — + // this handler only enters a table from the surrounding document. + if ((event.target as HTMLElement | null)?.closest?.('.cm-table-widget')) return false + const vimState = getCM(view)?.state?.vim as { insertMode?: boolean } | undefined + if (vimState?.insertMode) return false + const sel = view.state.selection.main + if (!sel.empty) return false + const line = view.state.doc.lineAt(sel.head).number + const dir = event.key === 'j' ? 'down' : 'up' + const table = adjacentTableRange(view.state, line, dir) + if (!table) return false + if (focusTableEntryCell(view, table.from, dir === 'down' ? 'first' : 'last')) { + event.preventDefault() + return true + } + return false + } + }) +) diff --git a/packages/app-core/src/lib/cm-wikilink-render.test.ts b/packages/app-core/src/lib/cm-wikilink-render.test.ts new file mode 100644 index 00000000..ffafb8a3 --- /dev/null +++ b/packages/app-core/src/lib/cm-wikilink-render.test.ts @@ -0,0 +1,47 @@ +// @vitest-environment jsdom + +import { markdown, markdownLanguage } from '@codemirror/lang-markdown' +import { EditorState } from '@codemirror/state' +import { EditorView } from '@codemirror/view' +import { describe, expect, it } from 'vitest' +import { wikilinkRenderExtension } from './cm-wikilink-render' + +function mount(doc: string, anchor: number): EditorView { + const parent = document.createElement('div') + document.body.append(parent) + return new EditorView({ + parent, + state: EditorState.create({ + doc, + selection: { anchor }, + extensions: [markdown({ base: markdownLanguage }), wikilinkRenderExtension] + }) + }) +} + +describe('wikilinkRenderExtension', () => { + it('hides the [[ ]] brackets and shows the label, with the target on the mark', () => { + const doc = 'see [[Foo]] and [[Bar|baz]] end' + const view = mount(doc, doc.length) + const links = Array.from(view.dom.querySelectorAll('.cm-wikilink')) + expect(links.map((e) => e.textContent)).toEqual(['Foo', 'baz']) + expect(links.map((e) => e.dataset.target)).toEqual(['Foo', 'Bar']) + expect(view.dom.textContent).not.toContain('[[') + expect(view.dom.textContent).not.toContain('Bar|') + view.destroy() + }) + + it('reveals the raw [[...]] on the wikilink the cursor is in', () => { + const doc = 'see [[Foo]] end' + const view = mount(doc, doc.indexOf('Foo')) + expect(view.dom.textContent).toContain('[[Foo]]') + view.destroy() + }) + + it('leaves embeds (![[...]]) alone', () => { + const doc = 'pic ![[image.png]] end' + const view = mount(doc, doc.length) + expect(view.dom.querySelector('.cm-wikilink')).toBeNull() + view.destroy() + }) +}) diff --git a/packages/app-core/src/lib/cm-wikilink-render.ts b/packages/app-core/src/lib/cm-wikilink-render.ts new file mode 100644 index 00000000..8040d3fb --- /dev/null +++ b/packages/app-core/src/lib/cm-wikilink-render.ts @@ -0,0 +1,139 @@ +/** + * WYSIWYG rendering for Obsidian-style `[[wikilinks]]`: hide the `[[ ]]` + * brackets (and the `target|` part of an aliased link), show the label as a + * clickable accent link, and navigate to the note on click. The raw `[[...]]` + * source is revealed on whichever wikilink the cursor is in — matching how the + * rest of live preview reveals the active token. + * + * Image/transclusion embeds (`![[...]]`) are left to the existing embed + * handling and skipped here. + * + * WYSIWYG-only: registered via `wysiwygExtensions()`. + */ +import { RangeSetBuilder } from '@codemirror/state' +import { + Decoration, + type DecorationSet, + EditorView, + ViewPlugin, + type ViewUpdate +} from '@codemirror/view' +import { useStore } from '../store' +import { resolveWikilinkTarget } from './wikilinks' + +// Same shape as the Preview pipeline (remarkWikilinks). +const WIKILINK_RE = /(!?)\[\[([^\]|]+?)(?:\|([^\]]+))?\]\]/g +const hide = Decoration.replace({}) +// Quiet edit markers for the revealed `[[ ]]` / `|` when the cursor is in the +// wikilink (overrides the orange link highlight so brackets read as markers). +const bracketMark = Decoration.mark({ class: 'cm-wikilink-bracket' }) + +function selectionTouches( + state: EditorView['state'], + from: number, + to: number +): boolean { + for (const range of state.selection.ranges) { + if (range.empty) { + if (range.from >= from && range.from <= to) return true + } else if (Math.max(range.from, from) < Math.min(range.to, to)) { + return true + } + } + return false +} + +function buildDecorations(view: EditorView): DecorationSet { + const { state } = view + const pending: Array<{ from: number; to: number; deco: Decoration }> = [] + + for (const { from, to } of view.visibleRanges) { + const firstLine = state.doc.lineAt(from).number + const lastLine = state.doc.lineAt(Math.max(from, to - 1)).number + for (let n = firstLine; n <= lastLine; n++) { + const line = state.doc.line(n) + if (!line.text.includes('[[')) continue + WIKILINK_RE.lastIndex = 0 + let m: RegExpExecArray | null + while ((m = WIKILINK_RE.exec(line.text)) !== null) { + if (m[1] === '!') continue // embed — handled elsewhere + const target = m[2].trim() + if (!target) continue + const matchStart = line.from + m.index + const matchEnd = matchStart + m[0].length + const hasAlias = m[3] != null + const labelStart = hasAlias + ? matchStart + 2 + m[2].length + 1 // after `[[target|` + : matchStart + 2 // after `[[` + const labelEnd = matchEnd - 2 // before `]]` + if (labelEnd <= labelStart) continue + // Cursor inside this wikilink → reveal the raw `[[...]]`, but mute the + // brackets / pipe so they read as quiet edit markers. + if (selectionTouches(state, matchStart, matchEnd)) { + pending.push({ from: matchStart, to: matchStart + 2, deco: bracketMark }) + if (hasAlias) { + pending.push({ from: labelStart - 1, to: labelStart, deco: bracketMark }) + } + pending.push({ from: matchEnd - 2, to: matchEnd, deco: bracketMark }) + continue + } + pending.push({ from: matchStart, to: labelStart, deco: hide }) + pending.push({ + from: labelStart, + to: labelEnd, + deco: Decoration.mark({ + class: 'cm-wikilink', + attributes: { 'data-target': target } + }) + }) + pending.push({ from: labelEnd, to: matchEnd, deco: hide }) + } + } + } + + pending.sort((a, b) => a.from - b.from || a.to - b.to) + const builder = new RangeSetBuilder() + for (const p of pending) builder.add(p.from, p.to, p.deco) + return builder.finish() +} + +const wikilinkRenderPlugin = ViewPlugin.fromClass( + class { + decorations: DecorationSet + constructor(view: EditorView) { + this.decorations = buildDecorations(view) + } + update(update: ViewUpdate): void { + if (update.docChanged || update.selectionSet || update.viewportChanged) { + this.decorations = buildDecorations(update.view) + } + } + }, + { decorations: (p) => p.decorations } +) + +/** Open the note a wikilink points to (resolving the same way Preview does). */ +function openWikilink(target: string): void { + const state = useStore.getState() + const resolved = resolveWikilinkTarget(state.notes, target) + if (!resolved) return // unresolved link — leave note creation to other flows + void state.selectNote(resolved.path).then(() => { + useStore.getState().setFocusedPanel('editor') + requestAnimationFrame(() => useStore.getState().editorViewRef?.focus()) + }) +} + +// Click a rendered wikilink to jump. Intercept on mousedown so CodeMirror +// doesn't first drop the caret into the (hidden) source. +const wikilinkClick = EditorView.domEventHandlers({ + mousedown: (event) => { + const el = (event.target as HTMLElement | null)?.closest('.cm-wikilink') + const target = el?.dataset.target + if (!target) return false + event.preventDefault() + openWikilink(target) + return true + } +}) + +export const wikilinkRenderExtension = [wikilinkRenderPlugin, wikilinkClick] diff --git a/packages/app-core/src/lib/cm-wysiwyg-blocks.test.ts b/packages/app-core/src/lib/cm-wysiwyg-blocks.test.ts new file mode 100644 index 00000000..8133143e --- /dev/null +++ b/packages/app-core/src/lib/cm-wysiwyg-blocks.test.ts @@ -0,0 +1,75 @@ +// @vitest-environment jsdom + +import { markdown, markdownLanguage } from '@codemirror/lang-markdown' +import { forceParsing } from '@codemirror/language' +import { EditorState } from '@codemirror/state' +import { EditorView } from '@codemirror/view' +import { describe, expect, it } from 'vitest' +import { wysiwygBlocksPlugin } from './cm-wysiwyg-blocks' + +const DOC = `# Title + +- first +- second + +> a quote line + +--- + +End.` + +function mount(doc: string): EditorView { + const parent = document.createElement('div') + document.body.append(parent) + const view = new EditorView({ + parent, + // Caret at the very end, away from the list / quote / hr. + state: EditorState.create({ + doc, + selection: { anchor: doc.length }, + extensions: [markdown({ base: markdownLanguage }), wysiwygBlocksPlugin] + }) + }) + forceParsing(view, doc.length, 5000) + view.dispatch({ changes: { from: doc.length, insert: ' ' } }) + view.dispatch({ changes: { from: doc.length, to: doc.length + 1 } }) + return view +} + +describe('wysiwygBlocksPlugin', () => { + it('renders bullets, an hr, and a blockquote bar without throwing', () => { + const view = mount(DOC) + expect(view.dom.querySelectorAll('.cm-wq-bullet').length).toBe(2) + expect(view.dom.querySelector('.cm-wq-hr')).toBeTruthy() + expect(view.dom.querySelector('.cm-wq-quote')).toBeTruthy() + view.destroy() + }) + + it('hides the ``` fences when the cursor is outside the code block', () => { + const view = mount('before\n\n```js\nconst x = 1\n```\n\nafter') + expect(view.dom.textContent).not.toContain('```js') + expect(view.dom.textContent).not.toMatch(/```\s*$/) + expect(view.dom.textContent).toContain('const x = 1') + view.destroy() + }) + + it('reveals the raw list marker on the active line', () => { + const view = mount(DOC) + const firstItem = DOC.indexOf('- first') + view.dispatch({ selection: { anchor: firstItem + 3 } }) + // The active list line shows its `-`; only the second item stays a bullet. + expect(view.dom.querySelectorAll('.cm-wq-bullet').length).toBe(1) + view.destroy() + }) + + it('hides the marker on task-list items (only the plain item gets a bullet)', () => { + // `- [ ]` / `- [x]` are task items — their `-` is hidden (the checkbox from + // the live-preview plugin stands in), matching Obsidian. Only the plain + // `- item` becomes a bullet. + const view = mount('# T\n\n- [ ] open\n- [x] done\n- plain item\n\nEnd.') + expect(view.dom.querySelectorAll('.cm-wq-bullet').length).toBe(1) + // The task markers' dashes are hidden (no "- [" left in the rendered text). + expect(view.dom.textContent).not.toContain('- [') + view.destroy() + }) +}) diff --git a/packages/app-core/src/lib/cm-wysiwyg-blocks.ts b/packages/app-core/src/lib/cm-wysiwyg-blocks.ts new file mode 100644 index 00000000..f0dcde5c --- /dev/null +++ b/packages/app-core/src/lib/cm-wysiwyg-blocks.ts @@ -0,0 +1,191 @@ +/** + * WYSIWYG block rendering that the base live-preview plugin doesn't cover: + * Obsidian-style blockquote bars, unordered-list bullets, and horizontal + * rules. Like the rest of live preview, the raw source is revealed on the + * line the cursor is on; everything else renders. + * + * WYSIWYG-only: registered via `wysiwygExtensions()`; never loads in Split. + */ +import { syntaxTree } from '@codemirror/language' +import { RangeSetBuilder, type EditorState } from '@codemirror/state' +import { + Decoration, + type DecorationSet, + EditorView, + ViewPlugin, + type ViewUpdate, + WidgetType +} from '@codemirror/view' +/** Line number (1-based) of the closing `---` of leading YAML frontmatter, + * or -1 when there is none. Lets us leave the frontmatter fences to the + * frontmatter styling rather than rendering them as horizontal rules. + * (Inlined: the PR's full frontmatter-properties module isn't ported.) */ +function frontmatterEndLine(state: EditorState): number { + const doc = state.doc + if (doc.lines < 2 || doc.line(1).text.trim() !== '---') return -1 + for (let i = 2; i <= doc.lines; i++) { + if (doc.line(i).text.trim() === '---') return i + } + return -1 +} + +const quoteLine = Decoration.line({ class: 'cm-wq-quote' }) + +/** A round bullet that replaces a `-` / `*` / `+` list marker. */ +class BulletWidget extends WidgetType { + eq(): boolean { + return true + } + toDOM(): HTMLElement { + const span = document.createElement('span') + span.className = 'cm-wq-bullet' + span.textContent = '•' + return span + } + ignoreEvent(): boolean { + return true + } +} + +/** A horizontal rule rendered in place of `---` / `***` / `___`. */ +class HrWidget extends WidgetType { + eq(): boolean { + return true + } + toDOM(): HTMLElement { + const span = document.createElement('span') + span.className = 'cm-wq-hr' + return span + } + ignoreEvent(): boolean { + return true + } +} + +const bullet = Decoration.replace({ widget: new BulletWidget() }) +const hrRule = Decoration.replace({ widget: new HrWidget() }) +/** Hide a fence line's ``` ```lang ``` / ``` ``` ``` text (inline, keeps the + * line + its card styling) when the cursor is outside the code block. */ +const hideInline = Decoration.replace({}) + +function activeLineSet(view: EditorView): Set { + const lines = new Set() + for (const r of view.state.selection.ranges) { + const from = view.state.doc.lineAt(r.from).number + const to = view.state.doc.lineAt(r.to).number + for (let l = from; l <= to; l++) lines.add(l) + } + return lines +} + +type Pending = { from: number; to: number; deco: Decoration; line: boolean } + +function buildDecorations(view: EditorView): DecorationSet { + const { state } = view + const active = activeLineSet(view) + const pending: Pending[] = [] + const quotedLines = new Set() + // The properties widget owns the leading frontmatter (its `---` fences parse + // as HorizontalRule); skip that range so we don't emit an overlapping + // replace decoration over the same lines. + const fmEnd = frontmatterEndLine(state) + + for (const { from, to } of view.visibleRanges) { + syntaxTree(state).iterate({ + from, + to, + enter: (node) => { + if (node.name === 'Blockquote') { + // Tag every line the blockquote spans for the left bar. + const first = state.doc.lineAt(node.from).number + const last = state.doc.lineAt(Math.max(node.from, node.to - 1)).number + for (let n = first; n <= last; n++) { + if (quotedLines.has(n)) continue + quotedLines.add(n) + const line = state.doc.line(n) + pending.push({ from: line.from, to: line.from, deco: quoteLine, line: true }) + } + return + } + if (node.name === 'HorizontalRule') { + const lineNo = state.doc.lineAt(node.from).number + if (fmEnd >= 1 && lineNo <= fmEnd) return // leave frontmatter to the properties widget + if (active.has(lineNo)) return // reveal `---` source on the active line + pending.push({ from: node.from, to: node.to, deco: hrRule, line: false }) + return + } + if (node.name === 'FencedCode') { + // Hide the ``` fence lines when the cursor is outside the block, so + // it reads as a clean card (the language flair still shows the lang). + // Clicking into the block reveals the fences for editing. + const firstLine = state.doc.lineAt(node.from) + const lastLine = state.doc.lineAt(Math.max(node.from, node.to - 1)) + let blockActive = false + for (let n = firstLine.number; n <= lastLine.number; n++) { + if (active.has(n)) { + blockActive = true + break + } + } + if (!blockActive) { + if (firstLine.to > firstLine.from) { + pending.push({ from: firstLine.from, to: firstLine.to, deco: hideInline, line: false }) + } + // Only hide the last line if it's actually a closing fence — an + // unclosed block at EOF ends on a content line we must keep. + const closesWithFence = /^\s*(?:`{3,}|~{3,})\s*$/.test(lastLine.text) + if ( + closesWithFence && + lastLine.number !== firstLine.number && + lastLine.to > lastLine.from + ) { + pending.push({ from: lastLine.from, to: lastLine.to, deco: hideInline, line: false }) + } + } + return false // don't descend into the code content + } + if (node.name === 'ListMark') { + const lineNo = state.doc.lineAt(node.from).number + if (active.has(lineNo)) return + const text = state.doc.sliceString(node.from, node.to) + // Only unordered bullets become a •; ordered markers (`1.`) stay. + if (!/^[-*+]$/.test(text)) return + // Task-list items render a checkbox (from the live-preview plugin) in + // place of the marker, so HIDE the `-`/`*`/`+` (and its trailing + // space) rather than bulleting it — the line reads "☐ task" like + // Obsidian, not "- ☐ task" or "• ☐ task". + const afterMark = state.doc.sliceString(node.to, state.doc.lineAt(node.from).to) + if (/^\s*\[[ xX]\]/.test(afterMark)) { + let to = node.to + if (state.doc.sliceString(to, to + 1) === ' ') to += 1 + pending.push({ from: node.from, to, deco: hideInline, line: false }) + return + } + pending.push({ from: node.from, to: node.to, deco: bullet, line: false }) + } + } + }) + } + + // RangeSetBuilder needs ascending order; line decorations sort before + // content decorations at the same position. + pending.sort((a, b) => a.from - b.from || (a.line === b.line ? 0 : a.line ? -1 : 1)) + const builder = new RangeSetBuilder() + for (const p of pending) builder.add(p.from, p.to, p.deco) + return builder.finish() +} + +export const wysiwygBlocksPlugin = ViewPlugin.fromClass( + class { + decorations: DecorationSet + constructor(view: EditorView) { + this.decorations = buildDecorations(view) + } + update(update: ViewUpdate): void { + if (update.docChanged || update.selectionSet || update.viewportChanged) { + this.decorations = buildDecorations(update.view) + } + } + }, + { decorations: (p) => p.decorations } +) diff --git a/packages/app-core/src/lib/cm-wysiwyg-compose.test.ts b/packages/app-core/src/lib/cm-wysiwyg-compose.test.ts new file mode 100644 index 00000000..ab364622 --- /dev/null +++ b/packages/app-core/src/lib/cm-wysiwyg-compose.test.ts @@ -0,0 +1,97 @@ +// @vitest-environment jsdom + +// Integration check: the WYSIWYG plugins (ported from PR #185) are loaded +// together by `wysiwygExtensions()` in EditorPane. Each has its own unit +// test; this verifies they COMPOSE on one document without conflicting +// (e.g. overlapping decorations) and that every block renders at once. + +import { markdown, markdownLanguage } from '@codemirror/lang-markdown' +import { forceParsing } from '@codemirror/language' +import { EditorState } from '@codemirror/state' +import { EditorView } from '@codemirror/view' +import { describe, expect, it, vi } from 'vitest' + +import { livePreviewPlugin } from './cm-live-preview' +import { codeBlockFlairPlugin } from './cm-code-block-flair' +import { tablePlugin } from './cm-table' +import { wysiwygBlocksPlugin } from './cm-wysiwyg-blocks' +import { hashtagExtension } from './cm-hashtags' +import { wikilinkRenderExtension } from './cm-wikilink-render' + +vi.mock('../store', () => { + const state = { + activeNote: null, + assetFiles: [], + notes: [], + noteRefs: {}, + pdfEmbedInEditMode: 'compact', + pinnedRefKind: 'note', + pinnedRefPath: null, + vault: null + } + const useStore = Object.assign(() => null, { + getState: () => state, + subscribe: () => () => {} + }) + return { useStore } +}) + +const RICH_DOC = [ + '# Title', + '', + 'Body with **bold**, ~~struck~~, a #tag and a [[WikiLink]].', + '', + '> a blockquote line', + '', + '- bullet one', + '- bullet two', + '', + '---', + '', + '| A | B |', + '| - | - |', + '| 1 | 2 |', + '', + '```js', + 'const x = 1', + '```', + '' +].join('\n') + +describe('wysiwyg plugin composition', () => { + it('renders every block construct together without conflicting', () => { + const parent = document.createElement('div') + document.body.append(parent) + const view = new EditorView({ + parent, + state: EditorState.create({ + // Cursor on the title line so every block below is inactive (rendered). + doc: RICH_DOC, + selection: { anchor: 0 }, + extensions: [ + markdown({ base: markdownLanguage }), + livePreviewPlugin, + codeBlockFlairPlugin, + tablePlugin, + wysiwygBlocksPlugin, + hashtagExtension, + wikilinkRenderExtension + ] + }) + }) + // Force a full parse + a no-op edit so every viewport-driven plugin emits. + forceParsing(view, RICH_DOC.length, 5000) + view.dispatch({ changes: { from: RICH_DOC.length, insert: ' ' } }) + view.dispatch({ changes: { from: RICH_DOC.length, to: RICH_DOC.length + 1 } }) + + expect(view.dom.querySelectorAll('.cm-table-widget').length).toBeGreaterThanOrEqual(1) + expect(view.dom.querySelectorAll('.cm-wq-hr').length).toBeGreaterThanOrEqual(1) + expect(view.dom.querySelectorAll('.cm-wq-bullet').length).toBeGreaterThanOrEqual(2) + expect(view.dom.querySelectorAll('.cm-wq-quote').length).toBeGreaterThanOrEqual(1) + expect(view.dom.querySelectorAll('.cm-hashtag').length).toBeGreaterThanOrEqual(1) + expect(view.dom.querySelectorAll('.cm-wikilink').length).toBeGreaterThanOrEqual(1) + expect(view.dom.querySelectorAll('.cm-code-flair').length).toBeGreaterThanOrEqual(1) + + view.destroy() + }) +}) diff --git a/packages/app-core/src/lib/markdown-table.test.ts b/packages/app-core/src/lib/markdown-table.test.ts new file mode 100644 index 00000000..4c4092ed --- /dev/null +++ b/packages/app-core/src/lib/markdown-table.test.ts @@ -0,0 +1,239 @@ +import { describe, it, expect } from 'vitest' +import { + parseTable, + serializeTable, + insertRow, + deleteRow, + duplicateRow, + moveRow, + insertColumn, + deleteColumn, + duplicateColumn, + moveColumn, + setColumnAlign, + sortByColumn, + setCell, + clearCells, + cellWidth, + type MarkdownTable +} from './markdown-table' + +const SIMPLE = `| A | B | +| --- | --- | +| 1 | 2 | +| 3 | 4 |` + +function parse(src: string): MarkdownTable { + const t = parseTable(src) + expect(t).not.toBeNull() + return t as MarkdownTable +} + +describe('parseTable', () => { + it('parses headers, aligns, and rows', () => { + const t = parse(SIMPLE) + expect(t.headers).toEqual(['A', 'B']) + expect(t.aligns).toEqual(['none', 'none']) + expect(t.rows).toEqual([ + ['1', '2'], + ['3', '4'] + ]) + }) + + it('reads column alignments', () => { + const t = parse(`| a | b | c | +| :-- | :-: | --: | +| x | y | z |`) + expect(t.aligns).toEqual(['left', 'center', 'right']) + }) + + it('pads short rows and truncates long rows to header width', () => { + const t = parse(`| a | b | c | +| - | - | - | +| 1 | +| 1 | 2 | 3 | 4 |`) + expect(t.rows).toEqual([ + ['1', '', ''], + ['1', '2', '3'] + ]) + }) + + it('handles escaped pipes inside cells', () => { + const t = parse(`| a | b | +| - | - | +| x \\| y | z |`) + expect(t.rows[0]).toEqual(['x | y', 'z']) + }) + + it('tolerates missing leading/trailing pipes', () => { + const t = parse(`a | b +- | - +1 | 2`) + expect(t.headers).toEqual(['a', 'b']) + expect(t.rows).toEqual([['1', '2']]) + }) + + it('returns null when the second line is not a delimiter', () => { + expect(parseTable(`| a | b |\n| 1 | 2 |`)).toBeNull() + expect(parseTable(`just text`)).toBeNull() + }) +}) + +describe('serializeTable round-trip', () => { + it('produces aligned, padded markdown', () => { + const t = parse(SIMPLE) + expect(serializeTable(t)).toBe(`| A | B | +| --- | --- | +| 1 | 2 | +| 3 | 4 |`) + }) + + it('re-parses to an equal model (stable round-trip)', () => { + const t = parse(SIMPLE) + const again = parse(serializeTable(t)) + expect(again).toEqual(t) + }) + + it('keeps alignment markers in the delimiter', () => { + const t = parse(`| a | b | c | +| :-- | :-: | --: | +| x | y | z |`) + const out = serializeTable(t) + expect(out.split('\n')[1]).toBe('| :-- | :-: | --: |') + expect(parse(out).aligns).toEqual(['left', 'center', 'right']) + }) + + it('escapes pipes on the way out and round-trips them', () => { + const t = setCell(parse(SIMPLE), { row: 0, col: 0 }, 'a | b') + const out = serializeTable(t) + expect(out).toContain('a \\| b') + expect(parse(out).rows[0][0]).toBe('a | b') + }) +}) + +describe('row operations', () => { + it('inserts an empty row', () => { + const t = insertRow(parse(SIMPLE), 1) + expect(t.rows).toEqual([ + ['1', '2'], + ['', ''], + ['3', '4'] + ]) + }) + + it('deletes a row', () => { + expect(deleteRow(parse(SIMPLE), 0).rows).toEqual([['3', '4']]) + }) + + it('duplicates a row right below', () => { + expect(duplicateRow(parse(SIMPLE), 0).rows).toEqual([ + ['1', '2'], + ['1', '2'], + ['3', '4'] + ]) + }) + + it('moves a row', () => { + expect(moveRow(parse(SIMPLE), 0, 1).rows).toEqual([ + ['3', '4'], + ['1', '2'] + ]) + }) +}) + +describe('column operations', () => { + it('inserts a column with alignment', () => { + const t = insertColumn(parse(SIMPLE), 1, 'center') + expect(t.headers).toEqual(['A', '', 'B']) + expect(t.aligns).toEqual(['none', 'center', 'none']) + expect(t.rows[0]).toEqual(['1', '', '2']) + }) + + it('deletes a column but never the last one', () => { + const t = deleteColumn(parse(SIMPLE), 0) + expect(t.headers).toEqual(['B']) + expect(t.rows).toEqual([['2'], ['4']]) + const single = parse(`| only |\n| - |\n| v |`) + expect(deleteColumn(single, 0)).toBe(single) // unchanged + }) + + it('duplicates a column', () => { + const t = duplicateColumn(parse(SIMPLE), 0) + expect(t.headers).toEqual(['A', 'A', 'B']) + expect(t.rows[0]).toEqual(['1', '1', '2']) + }) + + it('moves a column', () => { + const t = moveColumn(parse(SIMPLE), 0, 1) + expect(t.headers).toEqual(['B', 'A']) + expect(t.rows[0]).toEqual(['2', '1']) + }) + + it('sets column alignment', () => { + expect(setColumnAlign(parse(SIMPLE), 1, 'right').aligns).toEqual([ + 'none', + 'right' + ]) + }) +}) + +describe('sortByColumn', () => { + it('sorts numerically when all values are numbers', () => { + const t = parse(`| n | +| - | +| 10 | +| 2 | +| 1 |`) + expect(sortByColumn(t, 0, 'asc').rows.map((r) => r[0])).toEqual([ + '1', + '2', + '10' + ]) + expect(sortByColumn(t, 0, 'desc').rows.map((r) => r[0])).toEqual([ + '10', + '2', + '1' + ]) + }) + + it('sorts as strings otherwise', () => { + const t = parse(`| s | +| - | +| banana | +| apple | +| cherry |`) + expect(sortByColumn(t, 0, 'asc').rows.map((r) => r[0])).toEqual([ + 'apple', + 'banana', + 'cherry' + ]) + }) +}) + +describe('cell editing', () => { + it('sets a header cell with row === -1', () => { + expect(setCell(parse(SIMPLE), { row: -1, col: 1 }, 'Z').headers).toEqual([ + 'A', + 'Z' + ]) + }) + + it('clears a list of cells', () => { + const t = clearCells(parse(SIMPLE), [ + { row: 0, col: 0 }, + { row: 1, col: 1 } + ]) + expect(t.rows).toEqual([ + ['', '2'], + ['3', ''] + ]) + }) +}) + +describe('cellWidth', () => { + it('counts CJK as double width', () => { + expect(cellWidth('ab')).toBe(2) + expect(cellWidth('中文')).toBe(4) + expect(cellWidth('a中')).toBe(3) + }) +}) diff --git a/packages/app-core/src/lib/markdown-table.ts b/packages/app-core/src/lib/markdown-table.ts new file mode 100644 index 00000000..7294da1b --- /dev/null +++ b/packages/app-core/src/lib/markdown-table.ts @@ -0,0 +1,376 @@ +/** + * Pure model for GFM pipe tables: parse a table block into a structured + * form, run structural edits (insert/move/duplicate/delete rows & columns, + * alignment, sort, clear), and serialize back to nicely-padded markdown. + * + * The on-disk markdown stays the single source of truth — the live-preview + * table widget (`cm-table.ts`) parses the block under the cursor, applies an + * op here, and writes the serialized result back as one CodeMirror change. + * Keeping all of this side-effect-free makes it trivially unit-testable and + * keeps undo/redo, autosave, and multi-pane sync working for free. + */ + +export type ColumnAlign = 'none' | 'left' | 'center' | 'right' + +export interface MarkdownTable { + /** Header cell text, one per column. */ + headers: string[] + /** Body rows; each row has exactly `headers.length` cells (padded). */ + rows: string[][] + /** Per-column alignment, length === headers.length. */ + aligns: ColumnAlign[] +} + +/** A cell address. `row === -1` denotes the header row; body rows are 0-based. */ +export interface CellRef { + row: number + col: number +} + +// --------------------------------------------------------------------------- +// Width helpers — pad source columns so the raw markdown lines up in a +// monospace view (and reads tidily when Live Preview is off). CJK and other +// fullwidth glyphs count as two columns. +// --------------------------------------------------------------------------- + +function isWide(codePoint: number): boolean { + return ( + codePoint >= 0x1100 && + (codePoint <= 0x115f || // Hangul Jamo + codePoint === 0x2329 || + codePoint === 0x232a || + (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) || // CJK..Yi + (codePoint >= 0xac00 && codePoint <= 0xd7a3) || // Hangul Syllables + (codePoint >= 0xf900 && codePoint <= 0xfaff) || // CJK Compatibility Ideographs + (codePoint >= 0xfe30 && codePoint <= 0xfe4f) || // CJK Compatibility Forms + (codePoint >= 0xff00 && codePoint <= 0xff60) || // Fullwidth Forms + (codePoint >= 0xffe0 && codePoint <= 0xffe6) || + (codePoint >= 0x1f300 && codePoint <= 0x1faff) || // emoji & symbols + (codePoint >= 0x20000 && codePoint <= 0x3fffd)) // CJK Ext B+ + ) +} + +export function cellWidth(text: string): number { + let width = 0 + for (const ch of text) { + const cp = ch.codePointAt(0) ?? 0 + width += isWide(cp) ? 2 : 1 + } + return width +} + +// --------------------------------------------------------------------------- +// Parsing +// --------------------------------------------------------------------------- + +/** Split one `| a | b |` row into trimmed, unescaped cell strings. Honors + * `\|` (escaped pipe) and `\\` so cells can contain literal pipes. */ +function splitRow(line: string): string[] { + let trimmed = line.trim() + // Drop a single leading / trailing unescaped pipe (the common form). + if (trimmed.startsWith('|')) trimmed = trimmed.slice(1) + if (endsWithUnescapedPipe(trimmed)) trimmed = trimmed.slice(0, -1) + + const cells: string[] = [] + let current = '' + for (let i = 0; i < trimmed.length; i++) { + const ch = trimmed[i] + if (ch === '\\' && i + 1 < trimmed.length) { + // Preserve the escape sequence verbatim for an exact round-trip; + // unescaping happens in `unescapeCell` for display. + current += ch + trimmed[i + 1] + i++ + continue + } + if (ch === '|') { + cells.push(current) + current = '' + continue + } + current += ch + } + cells.push(current) + return cells.map((c) => unescapeCell(c.trim())) +} + +function endsWithUnescapedPipe(text: string): boolean { + if (!text.endsWith('|')) return false + // Count trailing backslashes before the final pipe; even count → unescaped. + let backslashes = 0 + for (let i = text.length - 2; i >= 0 && text[i] === '\\'; i--) backslashes++ + return backslashes % 2 === 0 +} + +function unescapeCell(text: string): string { + return text.replace(/\\([|\\])/g, '$1') +} + +function escapeCell(text: string): string { + return text.replace(/([|\\])/g, '\\$1') +} + +function parseAlign(spec: string): ColumnAlign { + const s = spec.trim() + const left = s.startsWith(':') + const right = s.endsWith(':') + if (left && right) return 'center' + if (right) return 'right' + if (left) return 'left' + return 'none' +} + +/** Returns true when `line` looks like a GFM delimiter row (`|---|:--:|`). */ +function isDelimiterRow(line: string): boolean { + const cells = splitRow(line) + if (cells.length === 0) return false + return cells.every((c) => /^:?-+:?$/.test(c.trim())) +} + +/** + * Parse a markdown table block. Returns null when the text is not a + * well-formed table (header + delimiter + zero or more body rows). + */ +export function parseTable(block: string): MarkdownTable | null { + const lines = block.replace(/\n+$/, '').split('\n') + if (lines.length < 2) return null + if (!isDelimiterRow(lines[1])) return null + + const headers = splitRow(lines[0]) + const alignCells = splitRow(lines[1]) + const colCount = headers.length + if (colCount === 0) return null + + const aligns: ColumnAlign[] = [] + for (let c = 0; c < colCount; c++) aligns.push(parseAlign(alignCells[c] ?? '')) + + const rows: string[][] = [] + for (let i = 2; i < lines.length; i++) { + if (lines[i].trim() === '') continue + rows.push(normalizeRow(splitRow(lines[i]), colCount)) + } + + return { headers, rows, aligns } +} + +function normalizeRow(cells: string[], colCount: number): string[] { + const out = cells.slice(0, colCount) + while (out.length < colCount) out.push('') + return out +} + +// --------------------------------------------------------------------------- +// Serialization (padded, Obsidian/prettier-ish) +// --------------------------------------------------------------------------- + +function padCell(text: string, width: number, align: ColumnAlign): string { + const pad = Math.max(0, width - cellWidth(text)) + if (align === 'right') return ' '.repeat(pad) + text + if (align === 'center') { + const leftPad = Math.floor(pad / 2) + return ' '.repeat(leftPad) + text + ' '.repeat(pad - leftPad) + } + return text + ' '.repeat(pad) +} + +function delimiterCell(width: number, align: ColumnAlign): string { + // Width here is the inner content width; dashes fill it (min 3 visually + // with the colons). Keep at least one dash. + const dashes = Math.max(1, width - (align === 'center' ? 2 : align === 'none' ? 0 : 1)) + const bar = '-'.repeat(dashes) + if (align === 'center') return `:${bar}:` + if (align === 'left') return `:${bar}` + if (align === 'right') return `${bar}:` + return bar +} + +export function serializeTable(table: MarkdownTable): string { + const colCount = table.headers.length + const widths: number[] = [] + for (let c = 0; c < colCount; c++) { + let w = cellWidth(escapeCell(table.headers[c] ?? '')) + for (const row of table.rows) w = Math.max(w, cellWidth(escapeCell(row[c] ?? ''))) + // Delimiter needs room for `:` markers and at least one dash. + const align = table.aligns[c] ?? 'none' + const minDelim = align === 'center' ? 3 : align === 'none' ? 1 : 2 + widths.push(Math.max(3, w, minDelim)) + } + + const renderRow = (cells: string[]): string => { + const padded = cells.map((cell, c) => + padCell(escapeCell(cell ?? ''), widths[c], table.aligns[c] ?? 'none') + ) + return `| ${padded.join(' | ')} |` + } + + const header = renderRow(table.headers) + const delim = `| ${table.aligns + .map((a, c) => delimiterCell(widths[c], a)) + .join(' | ')} |` + const body = table.rows.map(renderRow) + return [header, delim, ...body].join('\n') +} + +// --------------------------------------------------------------------------- +// Structural operations — all return a new table, never mutate the input. +// --------------------------------------------------------------------------- + +function clone(table: MarkdownTable): MarkdownTable { + return { + headers: [...table.headers], + rows: table.rows.map((r) => [...r]), + aligns: [...table.aligns] + } +} + +export function columnCount(table: MarkdownTable): number { + return table.headers.length +} + +export function setCell( + table: MarkdownTable, + ref: CellRef, + value: string +): MarkdownTable { + const next = clone(table) + if (ref.row < 0) { + if (ref.col >= 0 && ref.col < next.headers.length) next.headers[ref.col] = value + } else if (ref.row < next.rows.length) { + const row = next.rows[ref.row] + if (ref.col >= 0 && ref.col < row.length) row[ref.col] = value + } + return next +} + +function emptyRow(colCount: number): string[] { + return Array.from({ length: colCount }, () => '') +} + +/** Insert an empty body row at `index` (clamped). */ +export function insertRow(table: MarkdownTable, index: number): MarkdownTable { + const next = clone(table) + const at = Math.max(0, Math.min(index, next.rows.length)) + next.rows.splice(at, 0, emptyRow(columnCount(next))) + return next +} + +export function deleteRow(table: MarkdownTable, index: number): MarkdownTable { + if (index < 0 || index >= table.rows.length) return table + const next = clone(table) + next.rows.splice(index, 1) + return next +} + +export function duplicateRow(table: MarkdownTable, index: number): MarkdownTable { + if (index < 0 || index >= table.rows.length) return table + const next = clone(table) + next.rows.splice(index + 1, 0, [...next.rows[index]]) + return next +} + +export function moveRow( + table: MarkdownTable, + from: number, + to: number +): MarkdownTable { + if (from < 0 || from >= table.rows.length) return table + const next = clone(table) + const [row] = next.rows.splice(from, 1) + const at = Math.max(0, Math.min(to, next.rows.length)) + next.rows.splice(at, 0, row) + return next +} + +/** Insert an empty column at `index` (clamped) with the given alignment. */ +export function insertColumn( + table: MarkdownTable, + index: number, + align: ColumnAlign = 'none' +): MarkdownTable { + const next = clone(table) + const at = Math.max(0, Math.min(index, columnCount(next))) + next.headers.splice(at, 0, '') + next.aligns.splice(at, 0, align) + for (const row of next.rows) row.splice(at, 0, '') + return next +} + +export function deleteColumn(table: MarkdownTable, index: number): MarkdownTable { + if (index < 0 || index >= columnCount(table)) return table + if (columnCount(table) <= 1) return table // never leave a 0-column table + const next = clone(table) + next.headers.splice(index, 1) + next.aligns.splice(index, 1) + for (const row of next.rows) row.splice(index, 1) + return next +} + +export function duplicateColumn( + table: MarkdownTable, + index: number +): MarkdownTable { + if (index < 0 || index >= columnCount(table)) return table + const next = clone(table) + next.headers.splice(index + 1, 0, next.headers[index]) + next.aligns.splice(index + 1, 0, next.aligns[index]) + for (const row of next.rows) row.splice(index + 1, 0, row[index]) + return next +} + +export function moveColumn( + table: MarkdownTable, + from: number, + to: number +): MarkdownTable { + if (from < 0 || from >= columnCount(table)) return table + const next = clone(table) + const moveItem = (arr: T[]): void => { + const [item] = arr.splice(from, 1) + const at = Math.max(0, Math.min(to, arr.length)) + arr.splice(at, 0, item) + } + moveItem(next.headers) + moveItem(next.aligns) + for (const row of next.rows) moveItem(row) + return next +} + +export function setColumnAlign( + table: MarkdownTable, + index: number, + align: ColumnAlign +): MarkdownTable { + if (index < 0 || index >= columnCount(table)) return table + const next = clone(table) + next.aligns[index] = align + return next +} + +/** Sort body rows by a column. Numeric when every value parses as a number; + * otherwise a locale-aware string compare. */ +export function sortByColumn( + table: MarkdownTable, + index: number, + direction: 'asc' | 'desc' +): MarkdownTable { + if (index < 0 || index >= columnCount(table)) return table + const next = clone(table) + const dir = direction === 'asc' ? 1 : -1 + const values = next.rows.map((r) => (r[index] ?? '').trim()) + const allNumeric = + values.length > 0 && + values.every((v) => v !== '' && !Number.isNaN(Number(v))) + next.rows.sort((a, b) => { + const av = (a[index] ?? '').trim() + const bv = (b[index] ?? '').trim() + if (allNumeric) return (Number(av) - Number(bv)) * dir + return av.localeCompare(bv) * dir + }) + return next +} + +/** Set a list of cells to empty strings (header cells use row === -1). */ +export function clearCells(table: MarkdownTable, cells: CellRef[]): MarkdownTable { + let next = table + for (const ref of cells) next = setCell(next, ref, '') + return next +} diff --git a/packages/app-core/src/styles/index.css b/packages/app-core/src/styles/index.css index d466bddc..632a29fd 100644 --- a/packages/app-core/src/styles/index.css +++ b/packages/app-core/src/styles/index.css @@ -1797,6 +1797,9 @@ textarea, .cm-editor .tok-strong { font-weight: 700; } +.cm-editor .tok-strikethrough { + text-decoration: line-through; +} .cm-editor .tok-quote { font-style: italic; } @@ -3383,3 +3386,419 @@ textarea, .prose-zen .prose-heading-fold-arrow.is-folded { color: rgb(var(--z-accent)); } + +/* ===================================================================== + WYSIWYG live-preview rendering — ported from PR #185 (author: songgnqing). + Rendered tables, blockquote bars, list bullets, horizontal rules, + fenced-code cards + language flair, hashtag chips, and wikilinks. + Block-card styling is gated by the .cm-wysiwyg wrapper, added to the + editor container when livePreview is on (see EditorPane). + ===================================================================== */ +/* The inline-code chip (background/padding/radius from `.tok-monospace`) is for + true inline code only. Inside fenced/indented blocks the Lezer markdown + grammar tags every token with the same `monospace` highlight tag, so each + token would otherwise get its own chip — peppering the block with ragged, + per-line gray boxes. Neutralize it here; the block card (WYSIWYG) or raw view + provides the background. Mirrors the preview's `.prose-zen pre code`. */ +.cm-editor .cm-code-block-line .tok-monospace { + background: transparent; + padding: 0; + border-radius: 0; +} + +/* --- WYSIWYG code-block card + language flair (Edit mode only) -------- */ +/* Give fenced code blocks a continuous "card": a shared background with side + borders, the first/last line rounding the top/bottom. Mirrors Obsidian's + HyperMD-codeblock-bg, scoped to Edit (WYSIWYG) — Split stays raw. */ +.cm-wysiwyg .cm-code-block-line { + position: relative; + background: rgb(var(--z-bg-2) / 0.55); + border-left: 1px solid rgb(var(--z-bg-3) / 0.7); + border-right: 1px solid rgb(var(--z-bg-3) / 0.7); + padding-left: 16px; + padding-right: 16px; +} +.cm-wysiwyg .cm-code-block-begin { + border-top: 1px solid rgb(var(--z-bg-3) / 0.7); + border-top-left-radius: 8px; + border-top-right-radius: 8px; +} +.cm-wysiwyg .cm-code-block-end { + border-bottom: 1px solid rgb(var(--z-bg-3) / 0.7); + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; +} +/* Language label pinned top-right of the block; clicking it copies the code, + so there is no separate copy or fold control. */ +.cm-wysiwyg .cm-code-flair { + position: absolute; + top: 3px; + right: 8px; + z-index: 2; + padding: 1px 7px; + border: none; + border-radius: 6px; + background: transparent; + font-family: var(--z-interface-font, system-ui, sans-serif); + font-size: 11px; + line-height: 1.5; + color: rgb(var(--z-grey-1)); + cursor: pointer; + user-select: none; + transition: background 0.12s ease, color 0.12s ease; +} +.cm-wysiwyg .cm-code-flair:hover { + background: rgb(var(--z-bg-3) / 0.85); + color: rgb(var(--z-fg)); +} +.cm-wysiwyg .cm-code-flair.is-copied { + color: rgb(var(--z-accent)); +} + +/* --- WYSIWYG live table widget (Edit mode) --------------------------- */ +/* A GFM pipe table rendered as a real, editable

. Mirrors Obsidian's + cm-table-widget: rounded bordered grid, header tint, hover add-buttons, and + in-cell editing. Source stays authoritative; see cm-table.ts. */ +.cm-table-widget { + /* Use PADDING, not vertical margin, for the block gap: CodeMirror measures a + block widget's height via offsetHeight, which excludes margins — a vertical + margin here desyncs the height map and makes clicks below the table land a + line high. The padding also leaves room for the hover grips (top/left) and + add-buttons (right/bottom). + The negative LEFT margin cancels the left padding so the table content + still lines up with the body text (and matches Preview) instead of sitting + ~2 chars indented. Horizontal margin is safe — only vertical margin breaks + the height measurement. */ + margin: 0 0 0 -16px; + padding: 16px; + overflow-x: auto; + white-space: normal; + font-family: var( + --z-text-font, + -apple-system, + BlinkMacSystemFont, + "SF Pro Text", + Inter, + "PingFang SC", + "Microsoft YaHei", + system-ui, + sans-serif + ); + font-size: 0.94em; +} +.cm-table-wrapper { + position: relative; + width: fit-content; + max-width: 100%; +} +.cm-table-widget table { + border-collapse: collapse; + border-spacing: 0; +} +.cm-table-widget th, +.cm-table-widget td { + border: 1px solid rgb(var(--z-bg-3) / 0.9); + padding: 0; + vertical-align: top; + min-width: 64px; + position: relative; +} +.cm-table-widget th { + background: rgb(var(--z-bg-2) / 0.7); + font-weight: 600; +} +.cm-table-widget .cm-table-cell { + padding: 5px 10px; + outline: none; + min-height: 1.5em; + line-height: 1.5; + white-space: pre-wrap; + word-break: break-word; + cursor: text; +} +.cm-table-widget .cm-table-cell:focus { + background: rgb(var(--z-accent) / 0.1); + box-shadow: inset 0 0 0 2px rgb(var(--z-accent) / 0.45); +} +/* Vim NORMAL mode: the cell is selected for navigation, not being typed into. + `caret-shape: block` isn't supported in our Chromium yet, so hide the native + (insert-style) caret and draw a Vim-style block cursor ourselves via ::before. + Editing (i/a) removes this class, restoring the normal thin caret. */ +.cm-table-widget .cm-table-cell.is-vim-normal { + position: relative; + background: rgb(var(--z-accent) / 0.07); +} +/* Vim block cursor — a real cursor positioned over the current character by + cm-table.ts (renderCellCursor), so h/l move it glyph-by-glyph like Vim. */ +.cm-table-widget .cm-table-cell-cursor { + position: absolute; + background: rgb(var(--z-accent) / 0.5); + border-radius: 1px; + pointer-events: none; +} +/* While a table cell is focused, the "real" cursor lives in the cell, so hide + the editor's own cursor layer — otherwise its Vim block cursor lingers at the + last document position (e.g. above the table) as a second block cursor. */ +.cm-editor:has(.cm-table-cell:focus) .cm-cursorLayer { + display: none; +} +/* Rendered inline markdown inside idle cells (the source pipeline emits real + /// tags, so style them to match the editor chrome). */ +.cm-table-widget .cm-table-cell code { + font-family: var(--z-mono-font, "SF Mono", ui-monospace, monospace); + font-size: 0.92em; + background: rgb(var(--z-bg-2)); + padding: 1px 4px; + border-radius: 4px; +} +.cm-table-widget .cm-table-cell strong { + font-weight: 700; +} +.cm-table-widget .cm-table-cell em { + font-style: italic; +} +.cm-table-widget .cm-table-cell a { + color: rgb(var(--z-accent)); + text-decoration: underline; + text-decoration-color: rgb(var(--z-accent) / 0.4); +} +.cm-table-widget td[align="center"] .cm-table-cell, +.cm-table-widget th[align="center"] .cm-table-cell { + text-align: center; +} +.cm-table-widget td[align="right"] .cm-table-cell, +.cm-table-widget th[align="right"] .cm-table-cell { + text-align: right; +} +/* Hover add-buttons: a thin "+" rail along the right edge (column) and the + bottom edge (row) of the grid. */ +.cm-table-widget .cm-table-add { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid rgb(var(--z-bg-3) / 0.9); + background: rgb(var(--z-bg-1) / 0.9); + color: rgb(var(--z-grey-1)); + font-size: 13px; + line-height: 1; + cursor: pointer; + opacity: 0; + transition: opacity 0.1s ease, background 0.1s ease, color 0.1s ease; + border-radius: 5px; +} +.cm-table-widget:hover .cm-table-add { + opacity: 1; +} +.cm-table-widget .cm-table-add:hover { + background: rgb(var(--z-accent) / 0.85); + color: #fff; + border-color: transparent; +} +.cm-table-widget .cm-table-add-col { + top: 0; + right: 0; + width: 13px; + height: 100%; +} +.cm-table-widget .cm-table-add-row { + left: 0; + bottom: 0; + height: 13px; + /* Span the grid, not the button rail on the right. */ + width: calc(100% - 14px); +} + +/* Drag-to-reorder grips: row grip left of each body row, column grip above + each header cell. Hidden until you hover the row/column. */ +.cm-table-widget .cm-table-row-handle, +.cm-table-widget .cm-table-col-handle { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + background: rgb(var(--z-bg-2)); + border: 1px solid rgb(var(--z-bg-3) / 0.9); + color: rgb(var(--z-grey-1)); + cursor: grab; + opacity: 0; + z-index: 3; + border-radius: 4px; + font-size: 9px; + line-height: 1; + user-select: none; +} +.cm-table-widget .cm-table-row-handle::before { + content: "⠿"; +} +.cm-table-widget .cm-table-col-handle::before { + content: "⠿"; + transform: rotate(90deg); +} +.cm-table-widget .cm-table-row-handle { + right: 100%; + top: 0; + height: 100%; + width: 12px; + border-start-start-radius: 4px; + border-end-start-radius: 4px; +} +.cm-table-widget .cm-table-col-handle { + bottom: 100%; + left: 0; + width: 100%; + height: 12px; +} +.cm-table-widget tbody tr:hover .cm-table-row-handle, +.cm-table-widget thead th:hover .cm-table-col-handle, +.cm-table-widget .cm-table-row-handle:hover, +.cm-table-widget .cm-table-col-handle:hover { + opacity: 1; +} +.cm-table-widget.is-dragging .cm-table-row-handle, +.cm-table-widget.is-dragging .cm-table-col-handle { + opacity: 0; +} +.cm-table-widget .cm-table-row-handle:active, +.cm-table-widget .cm-table-col-handle:active { + cursor: grabbing; + background: rgb(var(--z-accent) / 0.85); + color: #fff; + border-color: transparent; +} +/* Accent drop indicator shown while dragging. */ +.cm-table-widget .cm-table-drop-indicator { + position: absolute; + background: rgb(var(--z-accent)); + z-index: 4; + pointer-events: none; + border-radius: 2px; +} +.cm-table-widget .cm-table-drop-row { + left: 0; + right: 0; + height: 2px; + transform: translateY(-1px); +} +.cm-table-widget .cm-table-drop-col { + top: 0; + bottom: 0; + width: 2px; + transform: translateX(-1px); +} + +/* Table context menu (right-click) — self-contained, theme-aware. */ +.cm-table-menu { + position: fixed; + z-index: 80; + min-width: 190px; + padding: 4px; + border-radius: 10px; + background: rgb(var(--z-bg-softer)); + border: 1px solid rgb(var(--z-bg-3) / 0.7); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.32); + font-family: var(--z-interface-font, system-ui, sans-serif); + font-size: 13px; + color: rgb(var(--z-fg)); +} +.cm-table-menu-item { + display: block; + width: 100%; + text-align: left; + padding: 5px 10px; + border: none; + border-radius: 6px; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.cm-table-menu-item:hover:not(:disabled) { + background: rgb(var(--z-accent) / 0.85); + color: #fff; +} +.cm-table-menu-item:disabled { + opacity: 0.4; + cursor: default; +} +.cm-table-menu-sep { + height: 1px; + margin: 4px 6px; + background: rgb(var(--z-bg-3) / 0.7); +} + +.cm-wysiwyg .cm-editor .cm-wq-quote { + border-left: 3px solid rgb(var(--z-accent) / 0.45); + padding-left: 16px; + color: rgb(var(--z-fg-2)); +} +.cm-wysiwyg .cm-editor .cm-wq-quote .tok-quote { + font-style: normal; + color: inherit; +} + +/* Unordered-list bullet rendered in place of `-` / `*` / `+`. */ +.cm-wysiwyg .cm-editor .cm-wq-bullet { + color: rgb(var(--z-grey-1)); + font-weight: 700; +} + +/* Horizontal rule rendered in place of `---` / `***` / `___`. */ +.cm-wysiwyg .cm-editor .cm-wq-hr { + display: inline-block; + width: 100%; + height: 0; + vertical-align: middle; + border-top: 2px solid rgb(var(--z-bg-3)); +} + +/* Links — subtle underline like Obsidian (accent color is set above). */ +.cm-wysiwyg .cm-editor .tok-link, +.cm-wysiwyg .cm-editor .tok-url { + text-decoration: underline; + text-decoration-color: rgb(var(--z-accent) / 0.4); + text-underline-offset: 2px; +} + +/* Wikilinks — Obsidian internal-link style: accent underlined text, clickable + (brackets hidden via decorations in cm-wikilink-render.ts). */ +.cm-wysiwyg .cm-editor .cm-wikilink { + color: rgb(var(--z-accent)); + text-decoration: underline; + text-decoration-color: rgb(var(--z-accent) / 0.4); + text-underline-offset: 2px; + cursor: pointer; +} +.cm-wysiwyg .cm-editor .cm-wikilink:hover { + text-decoration-color: rgb(var(--z-accent)); +} +/* Revealed `[[ ]]` / `|` markers when editing a wikilink — quiet grey, upright, + no underline (the inner brackets otherwise inherit the italic/underline link + highlight, which left one bracket slanted). */ +/* Target the bracket span AND any nested highlight span inside it — the inner + `[`/`]` is a LinkMark whose own highlight span sets italic + link color, and + an !important on the parent can't override a child's own declaration. The + `*` reaches that inner span so all four brackets render the same. */ +.cm-wysiwyg .cm-editor .cm-wikilink-bracket, +.cm-wysiwyg .cm-editor .cm-wikilink-bracket * { + color: rgb(var(--z-grey-1)) !important; + font-style: normal !important; + text-decoration: none !important; + opacity: 0.7; +} + +/* Inline hashtags rendered as clickable pills — Obsidian's `.tag` style: + accent text on a faint accent tint, pill radius. */ +.cm-wysiwyg .cm-editor .cm-hashtag { + color: rgb(var(--z-accent)); + background-color: rgb(var(--z-accent) / 0.1); + padding: 0.25em 0.65em; + border-radius: 2em; + font-size: 0.8em; + font-weight: inherit; + line-height: 1; + cursor: pointer; +} +.cm-wysiwyg .cm-editor .cm-hashtag:hover { + background-color: rgb(var(--z-accent) / 0.2); +} From 111fb308d3100ed1c6b904951bfb7244a40ba40d Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Mon, 15 Jun 2026 17:14:05 -0500 Subject: [PATCH 22/66] Render Obsidian-style callouts in Edit mode Detect `> [!type] title` blockquotes and render them as colored cards matching the Preview palette (note/info=blue, tip/hint/success=green, warning/warn=yellow, danger/error=red, quote=grey). The typed title shows bold (custom title, or the capitalized type name when none); the `[!type]` token is hidden off the line and revealed when the cursor is on the header for editing. Also fixes nested lists inside callouts/blockquotes breaking out of the box (re-add the box padding on top of the list hanging-indent) and de-italicizes callout body text to match plain blockquotes. --- .../src/lib/cm-wysiwyg-blocks.test.ts | 19 ++++ .../app-core/src/lib/cm-wysiwyg-blocks.ts | 89 ++++++++++++++++++- packages/app-core/src/styles/index.css | 60 +++++++++++++ 3 files changed, 167 insertions(+), 1 deletion(-) diff --git a/packages/app-core/src/lib/cm-wysiwyg-blocks.test.ts b/packages/app-core/src/lib/cm-wysiwyg-blocks.test.ts index 8133143e..a73f9e4b 100644 --- a/packages/app-core/src/lib/cm-wysiwyg-blocks.test.ts +++ b/packages/app-core/src/lib/cm-wysiwyg-blocks.test.ts @@ -62,6 +62,25 @@ describe('wysiwygBlocksPlugin', () => { view.destroy() }) + it('renders a callout as a colored card with its custom title', () => { + const view = mount('# T\n\n> [!warning] Heads up\n> body line\n\nEnd.') + expect(view.dom.querySelectorAll('.cm-callout').length).toBeGreaterThanOrEqual(2) + expect(view.dom.querySelector('.cm-callout-warning')).toBeTruthy() + expect(view.dom.querySelector('.cm-callout-head')).toBeTruthy() + expect(view.dom.querySelector('.cm-callout-foot')).toBeTruthy() + // The `[!warning]` token is hidden; the custom title stays. + expect(view.dom.textContent).not.toContain('[!warning]') + expect(view.dom.textContent).toContain('Heads up') + view.destroy() + }) + + it('shows the type name as the title when a callout has no custom title', () => { + const view = mount('# T\n\n> [!note]\n> body\n\nEnd.') + expect(view.dom.querySelector('.cm-callout-title')?.textContent).toBe('Note') + expect(view.dom.textContent).not.toContain('[!note]') + view.destroy() + }) + it('hides the marker on task-list items (only the plain item gets a bullet)', () => { // `- [ ]` / `- [x]` are task items — their `-` is hidden (the checkbox from // the live-preview plugin stands in), matching Obsidian. Only the plain diff --git a/packages/app-core/src/lib/cm-wysiwyg-blocks.ts b/packages/app-core/src/lib/cm-wysiwyg-blocks.ts index f0dcde5c..0a73317c 100644 --- a/packages/app-core/src/lib/cm-wysiwyg-blocks.ts +++ b/packages/app-core/src/lib/cm-wysiwyg-blocks.ts @@ -68,6 +68,51 @@ const hrRule = Decoration.replace({ widget: new HrWidget() }) * line + its card styling) when the cursor is outside the code block. */ const hideInline = Decoration.replace({}) +/** Header of an Obsidian-style callout: `> [!type] optional title`. */ +const CALLOUT_RE = /^(\s*>\s?)\[!(\w+)\]\s?(.*)$/ + +/** Collapse callout type aliases onto one color group, mirroring the Preview + * renderer (markdown.ts). Unknown types fall back to the neutral group. */ +function calloutGroup(type: string): string { + const t = type.toLowerCase() + if (t === 'note' || t === 'info' || t === 'abstract' || t === 'summary') return 'note' + if (t === 'tip' || t === 'hint' || t === 'success' || t === 'check' || t === 'done') + return 'tip' + if (t === 'warning' || t === 'warn' || t === 'caution' || t === 'attention') return 'warning' + if (t === 'danger' || t === 'error' || t === 'bug' || t === 'fail' || t === 'failure') + return 'danger' + if (t === 'quote' || t === 'cite') return 'quote' + return 'note' +} + +/** Default title for a callout with no custom title: the capitalized type. */ +function calloutTitle(type: string): string { + return type.charAt(0).toUpperCase() + type.slice(1).toLowerCase() +} + +/** Styled label shown in place of a `[!type]` token when the callout has no + * custom title (e.g. renders "Note"). */ +class CalloutTitleWidget extends WidgetType { + constructor( + private readonly label: string, + private readonly group: string + ) { + super() + } + eq(other: CalloutTitleWidget): boolean { + return other.label === this.label && other.group === this.group + } + toDOM(): HTMLElement { + const span = document.createElement('span') + span.className = `cm-callout-title cm-callout-${this.group}` + span.textContent = this.label + return span + } + ignoreEvent(): boolean { + return true + } +} + function activeLineSet(view: EditorView): Set { const lines = new Set() for (const r of view.state.selection.ranges) { @@ -96,9 +141,51 @@ function buildDecorations(view: EditorView): DecorationSet { to, enter: (node) => { if (node.name === 'Blockquote') { - // Tag every line the blockquote spans for the left bar. const first = state.doc.lineAt(node.from).number const last = state.doc.lineAt(Math.max(node.from, node.to - 1)).number + const firstLine = state.doc.line(first) + const callout = firstLine.text.match(CALLOUT_RE) + if (callout) { + // Obsidian-style callout: a colored card with a typed title. Tag + // each line for the box (head/foot round the top/bottom). + const group = calloutGroup(callout[2]) + for (let n = first; n <= last; n++) { + if (quotedLines.has(n)) continue + quotedLines.add(n) + const ln = state.doc.line(n) + let cls = `cm-callout cm-callout-${group}` + if (n === first) cls += ' cm-callout-head' + if (n === last) cls += ' cm-callout-foot' + pending.push({ + from: ln.from, + to: ln.from, + deco: Decoration.line({ class: cls }), + line: true + }) + } + // Header: render the title. Off the line, hide the `[!type]` token — + // the custom title stays (styled), or we show the type name. + if (!active.has(first)) { + const bStart = firstLine.from + callout[1].length + const bEnd = bStart + `[!${callout[2]}]`.length + if (callout[3].trim()) { + let to = bEnd + if (state.doc.sliceString(to, to + 1) === ' ') to += 1 + pending.push({ from: bStart, to, deco: hideInline, line: false }) + } else { + pending.push({ + from: bStart, + to: bEnd, + deco: Decoration.replace({ + widget: new CalloutTitleWidget(calloutTitle(callout[2]), group) + }), + line: false + }) + } + } + return + } + // Plain blockquote: a left bar on every line it spans. for (let n = first; n <= last; n++) { if (quotedLines.has(n)) continue quotedLines.add(n) diff --git a/packages/app-core/src/styles/index.css b/packages/app-core/src/styles/index.css index 632a29fd..d836aa4f 100644 --- a/packages/app-core/src/styles/index.css +++ b/packages/app-core/src/styles/index.css @@ -3737,6 +3737,66 @@ textarea, color: inherit; } +/* Callout cards in Edit mode — mirrors the Preview `.callout` colors. Every + line of the callout carries `cm-callout` (+ `-head`/`-foot` to round the + ends); the per-group class sets `--callout-color`, used for the box border, + tint, and the bold typed title. */ +.cm-wysiwyg .cm-editor .cm-callout { + --callout-color: var(--z-grey-0); + background: rgb(var(--callout-color) / 0.08); + border-left: 1px solid rgb(var(--callout-color) / 0.35); + border-right: 1px solid rgb(var(--callout-color) / 0.35); + padding-left: 14px; + padding-right: 14px; +} +.cm-wysiwyg .cm-editor .cm-callout-note { + --callout-color: var(--z-blue); +} +.cm-wysiwyg .cm-editor .cm-callout-tip { + --callout-color: var(--z-green); +} +.cm-wysiwyg .cm-editor .cm-callout-warning { + --callout-color: var(--z-yellow); +} +.cm-wysiwyg .cm-editor .cm-callout-danger { + --callout-color: var(--z-red); +} +.cm-wysiwyg .cm-editor .cm-callout-quote { + --callout-color: var(--z-grey-0); +} +.cm-wysiwyg .cm-editor .cm-callout-head { + border-top: 1px solid rgb(var(--callout-color) / 0.35); + border-top-left-radius: 8px; + border-top-right-radius: 8px; + padding-top: 4px; + font-weight: 600; + color: rgb(var(--callout-color)); +} +.cm-wysiwyg .cm-editor .cm-callout-foot { + border-bottom: 1px solid rgb(var(--callout-color) / 0.35); + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; + padding-bottom: 4px; +} +.cm-wysiwyg .cm-editor .cm-callout-title { + font-weight: 600; + color: rgb(var(--callout-color)); +} +/* A list line inside a callout/blockquote keeps its hanging indent, but it has + to sit on TOP of the box's own left padding. Otherwise the list's negative + text-indent (see .cm-markdown-list-line) drags the bullet out past the box's + left edge. Re-add the box padding to the hanging indent. */ +.cm-wysiwyg .cm-editor .cm-callout .tok-quote { + font-style: normal; + color: inherit; +} +.cm-wysiwyg .cm-editor .cm-callout.cm-markdown-list-line { + padding-left: calc(14px + var(--z-list-hanging-indent, 0)); +} +.cm-wysiwyg .cm-editor .cm-wq-quote.cm-markdown-list-line { + padding-left: calc(16px + var(--z-list-hanging-indent, 0)); +} + /* Unordered-list bullet rendered in place of `-` / `*` / `+`. */ .cm-wysiwyg .cm-editor .cm-wq-bullet { color: rgb(var(--z-grey-1)); From 53f507cbfa45a8b3b13d267cbce822233972926f Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Mon, 15 Jun 2026 18:05:09 -0500 Subject: [PATCH 23/66] Add vim editing to WYSIWYG table cells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In a table cell (Vim mode), beyond h/l/w/b/e/0/$ navigation: - Editing operators: x, D, C, and operator-pending dd/cc, dw/cw, d$/c$, d0, db. - Char-wise visual mode (v) with motions + text objects, then d/c/y; rendered as a themed overlay (not a native selection, which CodeMirror would mirror across cells and show in the OS color). - Text objects: viw/vaw, vi"/va", vi(/va) (and operators diw/ciw/ca", …), via a pure, unit-tested textObjectRange (word/quote/bracket). - Undo/redo: u and Ctrl-r route to the editor history (committing the pending cell edit first). - Esc in normal mode is a no-op (was jumping out below the table); leave via j/k at the edges. - Cells disable native selection in view mode (re-enabled while editing), so the only highlight is the themed visual overlay. --- packages/app-core/src/lib/cm-table.test.ts | 113 +++++- packages/app-core/src/lib/cm-table.ts | 385 ++++++++++++++++++++- packages/app-core/src/styles/index.css | 27 +- 3 files changed, 521 insertions(+), 4 deletions(-) diff --git a/packages/app-core/src/lib/cm-table.test.ts b/packages/app-core/src/lib/cm-table.test.ts index a580cee4..89a02810 100644 --- a/packages/app-core/src/lib/cm-table.test.ts +++ b/packages/app-core/src/lib/cm-table.test.ts @@ -2,10 +2,17 @@ import { markdown, markdownLanguage } from '@codemirror/lang-markdown' import { forceParsing } from '@codemirror/language' +import { history } from '@codemirror/commands' import { EditorState } from '@codemirror/state' import { EditorView } from '@codemirror/view' import { describe, expect, it } from 'vitest' -import { tablePlugin, nextWordStart, prevWordStart, nextWordEnd } from './cm-table' +import { + tablePlugin, + nextWordStart, + prevWordStart, + nextWordEnd, + textObjectRange +} from './cm-table' import { closeTableContextMenu } from './cm-table-menu' const TABLE_DOC = `Intro text. @@ -24,7 +31,7 @@ function mount(doc: string): EditorView { parent, state: EditorState.create({ doc, - extensions: [markdown({ base: markdownLanguage }), tablePlugin] + extensions: [markdown({ base: markdownLanguage }), history(), tablePlugin] }) }) // Ensure the GFM table node is parsed, then nudge the field to rebuild. @@ -110,6 +117,87 @@ describe('tablePlugin', () => { closeTableContextMenu() view.destroy() }) + + it('supports x / dd / D editing operators in a cell', () => { + const view = mount(TABLE_DOC) + const cell = view.dom.querySelector( + '.cm-table-widget [data-row="0"][data-col="0"]' + )! + expect(cell.dataset.raw).toBe('Alice') + // x deletes the char under the cursor (offset 0). + cell.dispatchEvent(new KeyboardEvent('keydown', { key: 'x', bubbles: true, cancelable: true })) + expect(cell.dataset.raw).toBe('lice') + // D deletes to end of cell. + cell.dispatchEvent(new KeyboardEvent('keydown', { key: 'D', bubbles: true, cancelable: true })) + expect(cell.dataset.raw).toBe('') + view.destroy() + }) + + it('supports char-wise visual mode: v + motion + d deletes the selection', () => { + const view = mount(TABLE_DOC) + const cell = view.dom.querySelector( + '.cm-table-widget [data-row="0"][data-col="0"]' + )! + expect(cell.dataset.raw).toBe('Alice') + // v (anchor at 0) → l (extend to 1) → d (delete [0,2) = "Al"). + cell.dispatchEvent(new KeyboardEvent('keydown', { key: 'v', bubbles: true, cancelable: true })) + cell.dispatchEvent(new KeyboardEvent('keydown', { key: 'l', bubbles: true, cancelable: true })) + cell.dispatchEvent(new KeyboardEvent('keydown', { key: 'd', bubbles: true, cancelable: true })) + expect(cell.dataset.raw).toBe('ice') + view.destroy() + }) + + it('u commits the pending cell edit and undoes it', () => { + const view = mount(TABLE_DOC) + const cell = view.dom.querySelector( + '.cm-table-widget [data-row="0"][data-col="0"]' + )! + cell.dispatchEvent(new KeyboardEvent('keydown', { key: 'x', bubbles: true, cancelable: true })) + expect(cell.dataset.raw).toBe('lice') + cell.dispatchEvent(new KeyboardEvent('keydown', { key: 'u', bubbles: true, cancelable: true })) + // The committed edit is undone — the source table is back to "Alice". + expect(view.state.doc.toString()).toContain('| Alice |') + view.destroy() + }) + + it('diw deletes the inner word (operator + text object)', () => { + const view = mount(TABLE_DOC) + const cell = view.dom.querySelector( + '.cm-table-widget [data-row="0"][data-col="0"]' + )! + expect(cell.dataset.raw).toBe('Alice') + for (const key of ['d', 'i', 'w']) { + cell.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true })) + } + expect(cell.dataset.raw).toBe('') + view.destroy() + }) + + it('Esc in a normal-mode cell is a no-op (stays put, no jump below)', () => { + const view = mount(TABLE_DOC) + const cell = view.dom.querySelector( + '.cm-table-widget [data-row="0"][data-col="0"]' + )! + const ev = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }) + cell.dispatchEvent(ev) + expect(ev.defaultPrevented).toBe(true) + // Cell content untouched; widget still present (didn't tear down / jump out). + expect(cell.dataset.raw).toBe('Alice') + expect(view.dom.querySelector('.cm-table-widget')).toBeTruthy() + view.destroy() + }) + + it('supports the dd operator (clear cell) via operator-pending', () => { + const view = mount(TABLE_DOC) + const cell = view.dom.querySelector( + '.cm-table-widget [data-row="1"][data-col="0"]' + )! + expect(cell.dataset.raw).toBe('Bob') + cell.dispatchEvent(new KeyboardEvent('keydown', { key: 'd', bubbles: true, cancelable: true })) + cell.dispatchEvent(new KeyboardEvent('keydown', { key: 'd', bubbles: true, cancelable: true })) + expect(cell.dataset.raw).toBe('') + view.destroy() + }) }) describe('vim word motions (cell cursor)', () => { @@ -134,3 +222,24 @@ describe('vim word motions (cell cursor)', () => { expect(nextWordStart('a, b', 1)).toBe(3) // ',' → 'b' }) }) + +describe('text objects (vi / va, di / ca)', () => { + it('iw / aw select the word (a includes trailing space)', () => { + const t = 'foo bar baz' + expect(textObjectRange(t, 5, 'i', 'w')).toEqual({ from: 4, to: 7 }) // "bar" + expect(textObjectRange(t, 5, 'a', 'w')).toEqual({ from: 4, to: 8 }) // "bar " + }) + it('i" / a" select inside / around quotes', () => { + const t = 'say "hi" now' + expect(textObjectRange(t, 5, 'i', '"')).toEqual({ from: 5, to: 7 }) // hi + expect(textObjectRange(t, 5, 'a', '"')).toEqual({ from: 4, to: 8 }) // "hi" + }) + it('i( / a) select inside / around brackets', () => { + const t = 'f(x, y)' + expect(textObjectRange(t, 3, 'i', '(')).toEqual({ from: 2, to: 6 }) // x, y + expect(textObjectRange(t, 3, 'a', ')')).toEqual({ from: 1, to: 7 }) // (x, y) + }) + it('returns null when the object is absent', () => { + expect(textObjectRange('plain', 0, 'i', '"')).toBeNull() + }) +}) diff --git a/packages/app-core/src/lib/cm-table.ts b/packages/app-core/src/lib/cm-table.ts index bf688bd3..11348904 100644 --- a/packages/app-core/src/lib/cm-table.ts +++ b/packages/app-core/src/lib/cm-table.ts @@ -33,6 +33,7 @@ import { import { openTableContextMenu } from './cm-table-menu' import { renderMarkdown } from './markdown' import { getCM } from '@replit/codemirror-vim' +import { undo, redo } from '@codemirror/commands' import { useStore } from '../store' /** True when the editor is in Vim mode — gates the table's modal (normal / @@ -100,6 +101,16 @@ class TableWidget extends WidgetType { /** Where to land the block cursor on the next cell focus: a char index, or * 'end' (last char). Lets h/l carry the cursor across cell boundaries. */ private pendingOffset: number | 'end' | null = null + /** A pending operator (`d`/`c`) waiting for its motion key (dw, cc, d$, …). */ + private pendingOp: 'd' | 'c' | null = null + /** Char-wise visual mode: true after `v`; `visualAnchor` is the fixed end of + * the selection, `cursorOffset` is the moving end. */ + private visualMode = false + private visualAnchor = 0 + /** Pending text-object scope after `i`/`a` — in visual mode (`viw`) and in + * operator-pending (`diw`, `ca"`). */ + private visualScope: 'i' | 'a' | null = null + private pendingScope: 'i' | 'a' | null = null /** Set in toDOM — CodeMirror hands the live view there. Block widgets are * provided by a StateField, which has no view at build time. */ private view!: EditorView @@ -274,6 +285,10 @@ class TableWidget extends WidgetType { // Vim: land in NORMAL mode at the pending offset (carried across cells // by h/l), or 0. The cell becomes a block cursor over its source. this.cellMode = 'normal' + this.pendingOp = null + this.pendingScope = null + this.visualMode = false + this.visualScope = null const len = (editable.dataset.raw ?? '').length this.cursorOffset = this.pendingOffset === 'end' @@ -489,6 +504,36 @@ class TableWidget extends WidgetType { event.stopPropagation() } const cellText = editable.dataset.raw ?? '' + if (this.visualMode) { + event.preventDefault() + this.handleVisualKey(editable, event.key, cellText) + return + } + if (this.pendingOp) { + event.preventDefault() + const op = this.pendingOp + if (this.pendingScope) { + // Third key: the text-object char (diw, ca", di(, …). + const scope = this.pendingScope + this.pendingOp = null + this.pendingScope = null + const r = textObjectRange(cellText, this.cursorOffset, scope, event.key) + if (r) { + this.deleteRange(editable, r.from, r.to) + if (op === 'c') this.enterInsertMode(editable, r.from) + } + return + } + if (event.key === 'i' || event.key === 'a') { + // Operator + text-object scope (di…, ca…): wait for the object key. + this.pendingScope = event.key + return + } + // Plain motion: dd / cc, dw / cw, d$ / c$, etc. + this.pendingOp = null + this.applyOperator(editable, op, event.key, cellText) + return + } switch (event.key) { case 'h': event.preventDefault() @@ -547,9 +592,57 @@ class TableWidget extends WidgetType { this.cursorOffset = Math.max(0, cellText.length - 1) this.renderCellCursor(editable) return + case 'x': + event.preventDefault() + this.deleteRange(editable, this.cursorOffset, this.cursorOffset + 1) + return + case 'D': + event.preventDefault() + this.deleteRange(editable, this.cursorOffset, cellText.length) + return + case 'C': { + event.preventDefault() + const at = this.cursorOffset + this.deleteRange(editable, at, cellText.length) + this.enterInsertMode(editable, at) + return + } + case 'd': + event.preventDefault() + this.pendingOp = 'd' + return + case 'c': + event.preventDefault() + this.pendingOp = 'c' + return + case 'v': + event.preventDefault() + this.visualMode = true + this.visualAnchor = this.cursorOffset + this.renderCellSelection(editable) + return + case 'u': + // Undo: flush pending cell edits as one history step, then undo it + // (or the previous change). Hands focus back to the editor. + event.preventDefault() + this.commitIfDirty() + undo(this.view) + this.view.focus() + return + case 'r': + // Ctrl-r → redo. Plain `r` is swallowed (no replace-char yet). + event.preventDefault() + if (event.ctrlKey) { + event.stopPropagation() + this.commitIfDirty() + redo(this.view) + this.view.focus() + } + return case 'Escape': + // Vim: Esc in normal mode is a no-op — don't jump out of the table. + // Leave a cell with j/k at the top/bottom edges (or click away). event.preventDefault() - this.exitToEditor('after') return case 'm': // Open the full table action menu (add/move/duplicate/delete/align/ @@ -651,6 +744,7 @@ class TableWidget extends WidgetType { private clearCellCursor(cell: HTMLElement): void { cell.querySelector('.cm-table-cell-cursor')?.remove() + cell.querySelector('.cm-table-cell-sel')?.remove() } /** Draw the block cursor over the character at `cursorOffset`, measuring its @@ -738,6 +832,10 @@ class TableWidget extends WidgetType { * focus handler, snapping back to NORMAL. */ private enterInsertMode(cell: HTMLElement, caretOffset: number): void { this.cellMode = 'insert' + this.pendingOp = null + this.pendingScope = null + this.visualMode = false + this.visualScope = null this.clearCellCursor(cell) cell.classList.remove('is-vim-normal') cell.setAttribute('contenteditable', 'true') @@ -748,11 +846,199 @@ class TableWidget extends WidgetType { placeCaretAt(cell, caretOffset) } + /** Delete `[from, to)` from the focused cell's source in place (no dispatch — + * committed on blur, like typing). Re-renders the NORMAL block cursor. */ + private deleteRange(cell: HTMLElement, from: number, to: number): void { + const text = cell.dataset.raw ?? '' + const a = Math.max(0, Math.min(from, text.length)) + const b = Math.max(a, Math.min(to, text.length)) + if (b === a) return + const next = text.slice(0, a) + text.slice(b) + cell.dataset.raw = next + this.dirty = true + this.cursorOffset = Math.max(0, Math.min(a, next.length - 1)) + cell.textContent = next + cell.dataset.rendered = 'false' + this.renderCellCursor(cell) + } + + /** Apply a pending operator (`d`/`c`) over the motion in `key`: dd/cc clear + * the cell; dw/cw, d$/c$, d0/c0, dl, db act over that range. `c` then edits. */ + private applyOperator( + cell: HTMLElement, + op: 'd' | 'c', + key: string, + text: string + ): void { + const off = this.cursorOffset + let from = off + let to = off + if (key === op) { + // dd / cc → the whole cell + from = 0 + to = text.length + } else { + switch (key) { + case 'w': + to = op === 'c' ? Math.min(text.length, nextWordEnd(text, off) + 1) : nextWordStart(text, off) + break + case 'e': + to = Math.min(text.length, nextWordEnd(text, off) + 1) + break + case '$': + to = text.length + break + case '0': + from = 0 + to = off + break + case 'l': + to = Math.min(text.length, off + 1) + break + case 'h': + from = Math.max(0, off - 1) + break + case 'b': + from = prevWordStart(text, off) + break + default: + return // unknown motion — cancel the operator + } + } + const wholeCell = from === 0 && to === text.length + if (to > from || wholeCell) { + this.deleteRange(cell, from, to) + if (op === 'c') this.enterInsertMode(cell, from) + } + } + + /** Render the char-wise visual selection [anchor..head] as a themed overlay + * measured over the range — NOT a native DOM selection, which CodeMirror + * would mirror into a multi-cell editor selection (and which falls back to + * the washed-out OS color). */ + private renderCellSelection(cell: HTMLElement): void { + this.clearCellCursor(cell) + const text = cell.dataset.raw ?? '' + const node = cell.firstChild + if (!node || node.nodeType !== Node.TEXT_NODE || text.length === 0) return + const from = Math.max(0, Math.min(this.visualAnchor, this.cursorOffset)) + const to = Math.min(text.length, Math.max(this.visualAnchor, this.cursorOffset) + 1) + const range = document.createRange() + range.setStart(node, from) + range.setEnd(node, to) + if (typeof range.getBoundingClientRect !== 'function') return + const rect = range.getBoundingClientRect() + const cellRect = cell.getBoundingClientRect() + const overlay = document.createElement('span') + overlay.className = 'cm-table-cell-sel' + overlay.style.left = `${rect.left - cellRect.left}px` + overlay.style.top = `${rect.top - cellRect.top}px` + overlay.style.width = `${Math.max(rect.width, 2)}px` + overlay.style.height = `${rect.height || 18}px` + cell.appendChild(overlay) + } + + private exitVisual(): void { + this.visualMode = false + this.visualScope = null + window.getSelection()?.removeAllRanges() + } + + /** Keys while in char-wise visual mode: motions extend the selection; d/x + * delete it, c/s change it, y yanks it, Esc cancels. */ + private handleVisualKey(cell: HTMLElement, key: string, text: string): void { + const selFrom = (): number => Math.max(0, Math.min(this.visualAnchor, this.cursorOffset)) + const selTo = (): number => + Math.min(text.length, Math.max(this.visualAnchor, this.cursorOffset) + 1) + if (this.visualScope) { + // `vi{obj}` / `va{obj}`: select the text object. + const scope = this.visualScope + this.visualScope = null + const r = textObjectRange(text, this.cursorOffset, scope, key) + if (r) { + this.visualAnchor = r.from + this.cursorOffset = Math.max(r.from, r.to - 1) + this.renderCellSelection(cell) + } + return + } + if (key === 'i' || key === 'a') { + this.visualScope = key + return + } + switch (key) { + case 'h': + this.cursorOffset = Math.max(0, this.cursorOffset - 1) + this.renderCellSelection(cell) + return + case 'l': + this.cursorOffset = Math.min(Math.max(0, text.length - 1), this.cursorOffset + 1) + this.renderCellSelection(cell) + return + case 'w': + this.cursorOffset = nextWordStart(text, this.cursorOffset) + this.renderCellSelection(cell) + return + case 'e': + this.cursorOffset = nextWordEnd(text, this.cursorOffset) + this.renderCellSelection(cell) + return + case 'b': + this.cursorOffset = prevWordStart(text, this.cursorOffset) + this.renderCellSelection(cell) + return + case '0': + this.cursorOffset = 0 + this.renderCellSelection(cell) + return + case '$': + this.cursorOffset = Math.max(0, text.length - 1) + this.renderCellSelection(cell) + return + case 'd': + case 'x': { + const from = selFrom() + const to = selTo() + this.exitVisual() + this.deleteRange(cell, from, to) + return + } + case 'c': + case 's': { + const from = selFrom() + const to = selTo() + this.exitVisual() + this.deleteRange(cell, from, to) + this.enterInsertMode(cell, from) + return + } + case 'y': { + const from = selFrom() + const to = selTo() + void navigator.clipboard?.writeText(text.slice(from, to)) + this.cursorOffset = from + this.exitVisual() + this.renderCellCursor(cell) + return + } + case 'Escape': + this.exitVisual() + this.renderCellCursor(cell) + return + default: + return + } + } + /** Leave the table, returning the caret to the document just before/after the * table block. The atomic range keeps it from sliding back in. */ private exitToEditor(side: 'before' | 'after'): void { this.commitIfDirty() this.cellMode = 'normal' + this.pendingOp = null + this.pendingScope = null + this.visualMode = false + this.visualScope = null const dom = this.dom if (!dom) { this.view.focus() @@ -850,6 +1136,103 @@ export function nextWordEnd(text: string, off: number): number { return Math.min(i, n - 1) } +/** Vim text object: the range covering `iw`/`aw` (word), `i"`/`a"` (and `'`/`` ` ``), + * and `i(`/`a(` etc. (brackets) around `offset`. `to` is exclusive. Returns + * null if the object isn't found (e.g. unbalanced brackets). */ +export function textObjectRange( + text: string, + offset: number, + scope: 'i' | 'a', + obj: string +): { from: number; to: number } | null { + const n = text.length + if (n === 0) return null + const off = Math.max(0, Math.min(offset, n - 1)) + + if (obj === 'w') { + const cls = charClass(text[off]) + let from = off + let to = off + 1 + while (from > 0 && charClass(text[from - 1]) === cls) from-- + while (to < n && charClass(text[to]) === cls) to++ + if (scope === 'a') { + const afterEnd = to + while (to < n && charClass(text[to]) === 0) to++ + if (to === afterEnd) while (from > 0 && charClass(text[from - 1]) === 0) from-- + } + return { from, to } + } + + if (obj === '"' || obj === "'" || obj === '`') { + let open = -1 + for (let i = off; i >= 0; i--) { + if (text[i] === obj) { + open = i + break + } + } + if (open === -1) { + for (let i = off; i < n; i++) { + if (text[i] === obj) { + open = i + break + } + } + } + if (open === -1) return null + let close = -1 + for (let i = open + 1; i < n; i++) { + if (text[i] === obj) { + close = i + break + } + } + if (close === -1) return null + return scope === 'i' ? { from: open + 1, to: close } : { from: open, to: close + 1 } + } + + const OPENERS: Record = { '(': ')', '[': ']', '{': '}' } + const CLOSERS: Record = { ')': '(', ']': '[', '}': '{' } + let openCh: string + let closeCh: string + if (OPENERS[obj]) { + openCh = obj + closeCh = OPENERS[obj] + } else if (CLOSERS[obj]) { + openCh = CLOSERS[obj] + closeCh = obj + } else { + return null + } + let depth = 0 + let openPos = -1 + for (let i = off; i >= 0; i--) { + if (text[i] === closeCh && i !== off) depth++ + else if (text[i] === openCh) { + if (depth === 0) { + openPos = i + break + } + depth-- + } + } + if (openPos === -1) return null + depth = 0 + let closePos = -1 + for (let i = openPos + 1; i < n; i++) { + if (text[i] === openCh) depth++ + else if (text[i] === closeCh) { + if (depth === 0) { + closePos = i + break + } + depth-- + } + } + if (closePos === -1) return null + return scope === 'i' ? { from: openPos + 1, to: closePos } : { from: openPos, to: closePos + 1 } +} + /** Collapse the caret at a character offset within the element's text node. */ function placeCaretAt(el: HTMLElement, offset: number): void { const node = el.firstChild diff --git a/packages/app-core/src/styles/index.css b/packages/app-core/src/styles/index.css index d836aa4f..d322b6c1 100644 --- a/packages/app-core/src/styles/index.css +++ b/packages/app-core/src/styles/index.css @@ -3515,6 +3515,15 @@ textarea, white-space: pre-wrap; word-break: break-word; cursor: text; + /* No native text selection in view/normal mode — it renders as the OS color + and CodeMirror mirrors it across cells. Vim visual mode draws its own + themed overlay instead. Editing (contenteditable) re-enables selection. */ + user-select: none; + -webkit-user-select: none; +} +.cm-table-widget .cm-table-cell[contenteditable='true'] { + user-select: text; + -webkit-user-select: text; } .cm-table-widget .cm-table-cell:focus { background: rgb(var(--z-accent) / 0.1); @@ -3536,10 +3545,26 @@ textarea, border-radius: 1px; pointer-events: none; } +/* Vim visual-mode selection inside a cell uses the native selection; match the + editor's own selection tint (theme accent at the same strength) so it reads + consistently instead of the washed-out OS default. */ +.cm-table-widget .cm-table-cell::selection, +.cm-table-widget .cm-table-cell ::selection { + background: rgb(var(--z-accent) / 0.22); +} +/* Vim visual-mode selection overlay (cm-table.ts renderCellSelection) — themed + so it matches the editor selection across every theme. */ +.cm-table-widget .cm-table-cell-sel { + position: absolute; + background: rgb(var(--z-accent) / 0.28); + border-radius: 2px; + pointer-events: none; +} /* While a table cell is focused, the "real" cursor lives in the cell, so hide the editor's own cursor layer — otherwise its Vim block cursor lingers at the last document position (e.g. above the table) as a second block cursor. */ -.cm-editor:has(.cm-table-cell:focus) .cm-cursorLayer { +.cm-editor:has(.cm-table-cell:focus) .cm-cursorLayer, +.cm-editor:has(.cm-table-cell:focus) .cm-selectionLayer { display: none; } /* Rendered inline markdown inside idle cells (the source pipeline emits real From 92aaddfa253659cad0fc941cc45353172d61e73c Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Tue, 16 Jun 2026 08:03:29 -0500 Subject: [PATCH 24/66] Close the window on Cmd+W when no tab is open (#192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `global.closeActiveTab` (Cmd+W / Ctrl+W) called closeActiveNote(), which returns early when there's no active note — so on an empty window (all tabs closed) nothing happened and the window never closed. In vim mode Ctrl+W was also always reserved for the `` pane prefix, swallowing the empty-window case on Linux. Now: when no tab is open, close the window via window.zen.windowClose(); and only reserve Ctrl+W for vim when a tab is actually open (an empty window has no pane to navigate), so Cmd+W / Ctrl+W closes the window on macOS and Linux, vim on or off. --- packages/app-core/src/App.tsx | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/app-core/src/App.tsx b/packages/app-core/src/App.tsx index 65d2f424..c21dd2eb 100644 --- a/packages/app-core/src/App.tsx +++ b/packages/app-core/src/App.tsx @@ -538,16 +538,28 @@ function App(): JSX.Element { return } if (matchesShortcut(e, overrides, 'global.closeActiveTab')) { - // On Linux/Windows `Mod+W` (close tab) resolves to Ctrl+W, which also - // is the vim pane-focus prefix (`hjkl`) and insert-mode word - // delete. When vim mode is on, reserve that key for vim — VimNav's - // capture-phase handler arms the prefix; close tabs via :q / :bd / the - // command palette. On macOS close-tab is Cmd+W, so this never triggers. - if (state.vimMode && matchesSequenceToken(e, overrides, 'vim.panePrefix')) { + // On Linux/Windows `Mod+W` (close tab) resolves to Ctrl+W, which is also + // the vim pane-focus prefix (`hjkl`) and insert-mode word delete. + // When vim mode is on AND a tab is open, reserve Ctrl+W for vim (close + // tabs via :q / :bd / the palette). With no tab open the prefix has + // nothing to act on, so fall through and close the window. On macOS + // close-tab is Cmd+W, so the vim guard never matches there. + const hasActiveTab = !!state.selectedPath + if ( + state.vimMode && + hasActiveTab && + matchesSequenceToken(e, overrides, 'vim.panePrefix') + ) { return } e.preventDefault() - void state.closeActiveNote() + if (hasActiveTab) { + void state.closeActiveNote() + } else { + // No tab left to close — close the window, matching native Cmd+W + // (macOS) / Ctrl+W behavior even with vim mode on (#192). + window.zen.windowClose() + } return } if (e.key === 'Escape' && state.searchOpen) { From bd1c6c4805858bcd66e3f33356906eca5b80864f Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Tue, 16 Jun 2026 08:11:27 -0500 Subject: [PATCH 25/66] Keep the reference-link definition colon visible in Edit mode (#188) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `:` in a reference-style definition (`[label]: url`) parses as a LinkMark, which the live-preview plugin hides along with inline-link brackets — so the colon disappeared in Edit mode, leaving a broken-looking `[label] url`. Skip hiding LinkMarks whose parent is LinkReference; inline-link brackets (parent Link/Image) still hide as before. Preview was already correct (the definition is consumed and the reference link resolves). --- packages/app-core/src/lib/cm-live-preview.test.ts | 11 +++++++++++ packages/app-core/src/lib/cm-live-preview.ts | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/packages/app-core/src/lib/cm-live-preview.test.ts b/packages/app-core/src/lib/cm-live-preview.test.ts index 509356bb..416523f1 100644 --- a/packages/app-core/src/lib/cm-live-preview.test.ts +++ b/packages/app-core/src/lib/cm-live-preview.test.ts @@ -53,6 +53,17 @@ describe('livePreviewPlugin', () => { view.destroy() }) + it('keeps the colon visible in a reference-link definition (#188)', () => { + // The `:` parses as a LinkMark; live preview must not hide it, or the + // definition reads as a broken `[label] url`. + const doc = 'intro\n\n[Markdown Lang]: https://www.markdownlang.com' + const view = mountEditor(doc, 0) // cursor on "intro" → definition line inactive + + expect(view.dom.textContent).toContain('[Markdown Lang]: https://www.markdownlang.com') + + view.destroy() + }) + it('keeps heading markers hidden when editing the heading text', () => { const doc = '# Code blocks\n\nBody' const view = mountEditor(doc, doc.indexOf('Code')) diff --git a/packages/app-core/src/lib/cm-live-preview.ts b/packages/app-core/src/lib/cm-live-preview.ts index 2545ce53..d256bfbc 100644 --- a/packages/app-core/src/lib/cm-live-preview.ts +++ b/packages/app-core/src/lib/cm-live-preview.ts @@ -757,6 +757,11 @@ function computeDecorations(view: EditorView): DecorationSet { if ((name === 'CodeMark' || name === 'CodeInfo') && node.node.parent?.name === 'FencedCode') return + // The `:` in a reference-link definition (`[label]: url`) parses as a + // LinkMark whose parent is LinkReference. Keep it visible — hiding it + // makes the definition read as a broken `[label] url`. (#188) + if (name === 'LinkMark' && node.node.parent?.name === 'LinkReference') return + const line = state.doc.lineAt(node.from).number if (replacedLines.has(line)) return if (isLinkSyntax) { From 9ac1b187e467329fc9f04168b1da994520ff7d57 Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Tue, 16 Jun 2026 08:58:29 -0500 Subject: [PATCH 26/66] Show images/PDFs in a capital-cased Inbox folder (#186) On a case-insensitive filesystem (macOS/Windows), an inbox folder stored as `Inbox/` (or any system folder with non-canonical casing) made the file browser hide its images and PDFs while still showing its notes. The vault walk's `realpath` (symlink-loop detection) resolves to the on-disk case, so both note and asset paths come back as `Inbox/...`. Notes still appeared because the walk assigns `folder: 'inbox'` authoritatively, but assets carry no folder field and the renderer's `folderForVaultRelativePath` matched system folders case-sensitively, dropping the capital path to `null` -> hidden. Make the renderer classify and strip system-folder prefixes case-insensitively (`folderForVaultRelativePath`, `assetPathWithinFolder`, `notePathWithinFolder`), so a capital `Inbox/` behaves exactly like `inbox/` and notes + their sibling assets group into the same subpath. The real on-disk path is preserved for file ops, keeping this correct on case-sensitive Linux. Adds renderer unit coverage plus an end-to-end test wiring the real main-process listAssets/listNotes to the real renderer classifier. --- .../desktop/src/main/vault-asset-case.test.ts | 96 +++++++++++++++++++ apps/desktop/tsconfig.node.json | 1 + .../app-core/src/lib/vault-layout.test.ts | 45 +++++++++ packages/app-core/src/lib/vault-layout.ts | 28 +++++- 4 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/src/main/vault-asset-case.test.ts diff --git a/apps/desktop/src/main/vault-asset-case.test.ts b/apps/desktop/src/main/vault-asset-case.test.ts new file mode 100644 index 00000000..57a6b476 --- /dev/null +++ b/apps/desktop/src/main/vault-asset-case.test.ts @@ -0,0 +1,96 @@ +import { mkdtemp, mkdir, rm, stat, writeFile } from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import type { VaultSettings } from '@shared/ipc' +import { + assetBelongsToFolderView, + assetFolderSubpath, + folderForVaultRelativePath, + noteBelongsToFolderView, + noteFolderSubpath +} from '@renderer/lib/vault-layout' +import { listAssets, listNotes } from './vault' + +// End-to-end reproduction of #186 wiring the REAL main-process vault walk to the +// REAL renderer classifier. The bug: on a case-insensitive filesystem an inbox +// folder stored as `Inbox/` (capital) makes `listNotes` emit lowercase +// `inbox/…` paths (built from `folderRoot()`), while `listAssets` walks real +// directory entries and emits `Inbox/…`. The renderer then classified the +// capital asset paths to `null` and hid every image/PDF. + +const tempDirs: string[] = [] + +async function makeTempVault(): Promise { + const dir = await mkdtemp(path.join(os.tmpdir(), 'zennotes-186-')) + tempDirs.push(dir) + return dir +} + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))) +}) + +const inboxSettings = { primaryNotesLocation: 'inbox' } as VaultSettings + +describe('#186 — capitalized Inbox folder keeps its images/PDFs visible', () => { + it('classifies real listAssets output for a capital Inbox/ into the inbox view', async () => { + const root = await makeTempVault() + // A folder physically named "Inbox" (capital) — exactly the reporter's vault. + await mkdir(path.join(root, 'Inbox'), { recursive: true }) + await writeFile(path.join(root, 'Inbox', 'note.md'), '# hi', 'utf8') + await writeFile(path.join(root, 'Inbox', 'photo.png'), 'x', 'utf8') + await writeFile(path.join(root, 'Inbox', 'doc.pdf'), 'x', 'utf8') + + const assets = await listAssets(root) + const photo = assets.find((a) => a.name === 'photo.png') + const pdf = assets.find((a) => a.name === 'doc.pdf') + + // listAssets preserves the on-disk casing ("Inbox/…"), which is the crux. + expect(photo?.path).toBe('Inbox/photo.png') + expect(pdf?.path).toBe('Inbox/doc.pdf') + + // The fix: the renderer now folds that capital path into the inbox view at + // the top level (subpath ''), instead of returning null and hiding it. + expect(folderForVaultRelativePath(photo!.path, inboxSettings)).toBe('inbox') + expect(folderForVaultRelativePath(pdf!.path, inboxSettings)).toBe('inbox') + expect(assetFolderSubpath(photo!, inboxSettings)).toBe('') + }) + + it('puts a nested capital asset in the same subpath tree as its sibling note', async () => { + const root = await makeTempVault() + await mkdir(path.join(root, 'Inbox', 'Projects'), { recursive: true }) + await writeFile(path.join(root, 'Inbox', 'Projects', 'plan.md'), '# plan', 'utf8') + await writeFile(path.join(root, 'Inbox', 'Projects', 'diagram.png'), 'x', 'utf8') + + const assets = await listAssets(root) + const diagram = assets.find((a) => a.name === 'diagram.png') + + // Both note and asset paths come back with the on-disk capital casing, + // because `realpath` (used for symlink-loop detection in the walk) resolves + // `inbox` → `Inbox` on a case-insensitive filesystem. + expect(diagram?.path).toBe('Inbox/Projects/diagram.png') + expect(folderForVaultRelativePath(diagram!.path, inboxSettings)).toBe('inbox') + expect(assetFolderSubpath(diagram!, inboxSettings)).toBe('Projects') + expect(assetBelongsToFolderView(diagram!, 'inbox', 'Projects', inboxSettings)).toBe(true) + + // On a case-insensitive FS, prove the note half: it shows (folder assigned + // by the walk) AND resolves to the SAME "Projects" subpath as its image, so + // they group together in the file browser instead of splitting apart. + const caseInsensitive = await stat(path.join(root, 'inbox')) + .then(() => true) + .catch(() => false) + if (caseInsensitive) { + const notes = await listNotes(root) + const plan = notes.find((n) => n.title === 'plan') + expect(plan?.folder).toBe('inbox') + expect(plan?.path).toBe('Inbox/Projects/plan.md') + expect(noteFolderSubpath(plan!, inboxSettings)).toBe('Projects') + expect(noteBelongsToFolderView(plan!, 'inbox', 'Projects', inboxSettings)).toBe(true) + // The asset and its sibling note resolve to one and the same node. + expect(noteFolderSubpath(plan!, inboxSettings)).toBe( + assetFolderSubpath(diagram!, inboxSettings) + ) + } + }) +}) diff --git a/apps/desktop/tsconfig.node.json b/apps/desktop/tsconfig.node.json index 0524e0ff..5b07f59c 100644 --- a/apps/desktop/tsconfig.node.json +++ b/apps/desktop/tsconfig.node.json @@ -5,6 +5,7 @@ "types": ["node", "electron-vite/node"], "baseUrl": ".", "paths": { + "@renderer/*": ["../../packages/app-core/src/*"], "@shared/*": ["../../packages/shared-domain/src/*"], "@bridge-contract/*": ["../../packages/bridge-contract/src/*"] } diff --git a/packages/app-core/src/lib/vault-layout.test.ts b/packages/app-core/src/lib/vault-layout.test.ts index c35227cd..40c212d6 100644 --- a/packages/app-core/src/lib/vault-layout.test.ts +++ b/packages/app-core/src/lib/vault-layout.test.ts @@ -1,10 +1,14 @@ import { describe, expect, it } from 'vitest' import type { NoteMeta, VaultSettings } from '@shared/ipc' import { + assetBelongsToFolderView, + assetFolderSubpath, classifyDateNote, dailyNoteLocationForDate, dateNoteFolderMayBelongToDatePattern, dateNoteDirectoryDisplayLabel, + folderForVaultRelativePath, + noteFolderSubpath, weeklyNoteLocationForDate } from './vault-layout' @@ -407,3 +411,44 @@ describe('dateNoteFolderMayBelongToDatePattern', () => { expect(dateNoteFolderMayBelongToDatePattern('Projects', vaultSettings)).toBe(false) }) }) + +describe('folderForVaultRelativePath — case-insensitive system folders (#186)', () => { + // On case-insensitive filesystems, `listAssets` preserves the on-disk casing + // (`Inbox/photo.png`) while notes always arrive canonical-lowercased + // (`inbox/note.md`). Both must classify to the same folder, or the browser + // shows the markdown but hides the images/PDFs. + const inboxMode = { primaryNotesLocation: 'inbox' } as VaultSettings + + it('classifies a capitalized inbox folder the same as lowercase', () => { + expect(folderForVaultRelativePath('inbox/photo.png', inboxMode)).toBe('inbox') + expect(folderForVaultRelativePath('Inbox/photo.png', inboxMode)).toBe('inbox') + expect(folderForVaultRelativePath('INBOX/photo.png', inboxMode)).toBe('inbox') + }) + + it('matches the other system folders case-insensitively too', () => { + expect(folderForVaultRelativePath('Archive/scan.pdf', inboxMode)).toBe('archive') + expect(folderForVaultRelativePath('Quick/clip.png', inboxMode)).toBe('quick') + expect(folderForVaultRelativePath('Trash/old.png', inboxMode)).toBe('trash') + }) + + it('still returns null for a non-system root folder in inbox mode', () => { + expect(folderForVaultRelativePath('Random/photo.png', inboxMode)).toBeNull() + }) + + it('keeps a capitalized asset in the same subpath tree as its notes', () => { + // A note under a capital `Inbox/Projects/` resolves to subpath `Projects` + // (folder is assigned authoritatively by the main-process walk); the asset + // under the same folder must resolve to the identical `Projects`. + expect(noteFolderSubpath({ folder: 'inbox', path: 'Inbox/Projects/n.md' }, inboxMode)).toBe( + 'Projects' + ) + expect(assetFolderSubpath({ path: 'Inbox/Projects/photo.png' }, inboxMode)).toBe('Projects') + expect( + assetBelongsToFolderView({ path: 'Inbox/Projects/photo.png' }, 'inbox', 'Projects', inboxMode) + ).toBe(true) + expect(assetBelongsToFolderView({ path: 'Inbox/photo.png' }, 'inbox', '', inboxMode)).toBe(true) + // A top-level note in a capital `Inbox/` is at the root of the view, not + // nested under a phantom `Inbox` subfolder. + expect(noteFolderSubpath({ folder: 'inbox', path: 'Inbox/n.md' }, inboxMode)).toBe('') + }) +}) diff --git a/packages/app-core/src/lib/vault-layout.ts b/packages/app-core/src/lib/vault-layout.ts index 65d377b2..b0b0d502 100644 --- a/packages/app-core/src/lib/vault-layout.ts +++ b/packages/app-core/src/lib/vault-layout.ts @@ -567,7 +567,10 @@ export function notePathWithinFolder( ): string { if (folder === 'inbox' && isPrimaryNotesAtRoot(settings)) return path const prefix = `${folder}/` - return path.startsWith(prefix) ? path.slice(prefix.length) : path + // Case-insensitive so a note under a capitalized on-disk system folder + // (e.g. `Inbox/`) lands in the same subpath as its sibling assets, which use + // the same lenient stripping. Mirrors `assetPathWithinFolder`. (#186) + return path.toLowerCase().startsWith(prefix) ? path.slice(prefix.length) : path } export function noteFolderSubpath( @@ -955,6 +958,18 @@ export function findDateNoteByTitle( return null } +// Match a path's top segment to a system folder case-insensitively. On +// case-insensitive filesystems (macOS/Windows) the inbox folder can be stored +// with different casing than the canonical lowercase the rest of the app emits: +// `listNotes` builds note paths from `folderRoot()` (always `inbox/…`), but +// `listAssets`/the watcher walk real directory entries and preserve the on-disk +// case (e.g. `Inbox/…`). Comparing case-sensitively dropped those assets to +// `null`, so a capitalized `Inbox/` showed its notes but hid its images/PDFs. (#186) +function systemFolderForTopSegment(top: string): NoteFolder | null { + const lower = top.toLowerCase() + return SYSTEM_FOLDERS.has(lower as NoteFolder) ? (lower as NoteFolder) : null +} + export function folderForVaultRelativePath( relPath: string, settings: VaultSettings | null | undefined @@ -962,8 +977,9 @@ export function folderForVaultRelativePath( const normalized = relPath.replace(/\\/g, '/').replace(/^\/+/, '') const top = normalized.split('/')[0] ?? '' if (!top || top.startsWith('.')) return null - if (SYSTEM_FOLDERS.has(top as NoteFolder)) return top as NoteFolder - if (isPrimaryNotesAtRoot(settings) && !RESERVED_ROOT_NAMES.has(top)) return 'inbox' + const system = systemFolderForTopSegment(top) + if (system) return system + if (isPrimaryNotesAtRoot(settings) && !RESERVED_ROOT_NAMES.has(top.toLowerCase())) return 'inbox' return null } @@ -975,7 +991,11 @@ export function assetPathWithinFolder( const normalized = assetPath.replace(/\\/g, '/').replace(/^\/+/, '') if (folder === 'inbox' && isPrimaryNotesAtRoot(settings)) return normalized const prefix = `${folder}/` - return normalized.startsWith(prefix) ? normalized.slice(prefix.length) : normalized + // Strip the system-folder prefix case-insensitively so a capitalized on-disk + // folder (e.g. `Inbox/`) lands in the same subpath tree as its notes. (#186) + return normalized.toLowerCase().startsWith(prefix) + ? normalized.slice(prefix.length) + : normalized } export function assetFolderSubpath( From a480be0df6bfb3da9c03c0de97bce9809ff81847 Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Tue, 16 Jun 2026 11:00:46 -0500 Subject: [PATCH 27/66] Stop logging an error when an open database is deleted Deleting a database (or any client removing the .csv) left the renderer trying to re-read the gone file: the watcher's delete event called syncDatabaseFromDisk, and nothing ever removed the database from state or closed its tab, so DatabaseView kept re-requesting it. Each attempt threw "Database not found" from the open-database IPC handler, which Electron logs as "Error occurred in handler for 'vault:open-database'". - The open-database handler now returns null for a missing database (a stale tab outliving its file is not exceptional); real parse/ permission errors still throw. - loadDatabase / syncDatabaseFromDisk treat null as "gone" and forget the database, closing its tab (both the database-tab and .csv asset-tab forms) instead of looping on a missing file. - The watcher's delete event proactively forgets the database, and openDatabase won't open a tab for one that no longer exists. Covers live deletes, a stale tab restored on startup, and external deletes. Adds store tests for all three. --- apps/desktop/src/main/index.ts | 12 +++- apps/desktop/src/preload/index.ts | 2 +- packages/app-core/src/store.test.ts | 81 ++++++++++++++++++++++++++ packages/app-core/src/store.ts | 53 ++++++++++++++++- packages/bridge-contract/src/bridge.ts | 3 +- 5 files changed, 145 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 08997d59..2941cba8 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -2181,7 +2181,17 @@ function registerIpc(): void { handle(IPC.VAULT_OPEN_DATABASE, async (_e, relPath: string) => { ensureLocalForDatabases() - return await readDatabase(requireVault().root, relPath) + try { + return await readDatabase(requireVault().root, relPath) + } catch (err) { + // A missing database isn't exceptional — its tab can simply outlive the + // file (deleted by us or another client). Return null so the renderer + // forgets it, instead of rejecting and logging a noisy + // "Error occurred in handler for 'vault:open-database'". Real errors + // (parse/permission) still throw. + if (err instanceof Error && err.message.startsWith('Database not found')) return null + throw err + } }) handle(IPC.VAULT_WRITE_DATABASE_ROWS, async (_e, relPath: string, rows: DbRow[]) => { diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index b27a0975..bbca17e2 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -299,7 +299,7 @@ const api: ZenBridge = { scanTasks: (): Promise => ipcRenderer.invoke(IPC.VAULT_SCAN_TASKS), scanTasksForPath: (relPath: string): Promise => ipcRenderer.invoke(IPC.VAULT_SCAN_TASKS_FOR, relPath), - openDatabase: (relPath: string): Promise => + openDatabase: (relPath: string): Promise => ipcRenderer.invoke(IPC.VAULT_OPEN_DATABASE, relPath), writeDatabaseRows: (relPath: string, rows: DbRow[]): Promise => ipcRenderer.invoke(IPC.VAULT_WRITE_DATABASE_ROWS, relPath, rows), diff --git a/packages/app-core/src/store.test.ts b/packages/app-core/src/store.test.ts index fc975fea..e802806d 100644 --- a/packages/app-core/src/store.test.ts +++ b/packages/app-core/src/store.test.ts @@ -739,3 +739,84 @@ describe('note jump history with database tabs', () => { expect(useStore.getState().selectedPath).toBe(dbTab) }) }) + +describe('database deletion', () => { + it('forgets a deleted database instead of re-reading the gone file', async () => { + const csvPath = 'quick/Untitled Database.csv' + const doc = { path: csvPath, title: 'Untitled Database', rows: [], columns: [], views: [] } + const openDatabase = vi.fn().mockResolvedValue(doc) + installZen({ openDatabase }) + + const { useStore } = await loadStore() + const paneId = useStore.getState().activePaneId + const tabPath = databaseTabPath(csvPath) + + // Open the database: caches the doc and opens its tab. + await useStore.getState().openDatabase(csvPath) + expect(useStore.getState().databases[csvPath]).toBeTruthy() + expect(findLeaf(useStore.getState().paneLayout, paneId)?.tabs).toContain(tabPath) + + openDatabase.mockClear() + + // The watcher reports the .csv was deleted (the user removed the database). + await useStore.getState().applyChange({ + kind: 'unlink', + path: csvPath, + folder: 'quick', + scope: 'database' + }) + + // It must NOT re-read the gone file (that throws "Database not found" and + // logs an Electron handler error in the terminal). + expect(openDatabase).not.toHaveBeenCalled() + // The cached doc is dropped and its tab is closed. + expect(useStore.getState().databases[csvPath]).toBeUndefined() + expect(findLeaf(useStore.getState().paneLayout, paneId)?.tabs ?? []).not.toContain(tabPath) + }) + + it('still re-reads on a non-delete database change', async () => { + const csvPath = 'quick/Live.csv' + const v1 = { path: csvPath, title: 'Live', rows: [], columns: [], views: [] } + const v2 = { ...v1, rows: [{ id: 'r1' }] } + const openDatabase = vi.fn().mockResolvedValueOnce(v1).mockResolvedValue(v2) + installZen({ openDatabase }) + + const { useStore } = await loadStore() + await useStore.getState().openDatabase(csvPath) + openDatabase.mockClear() + + // A change (not a delete) more than the write-echo window ago re-syncs. + await useStore.getState().applyChange({ + kind: 'change', + path: csvPath, + folder: 'quick', + scope: 'database' + }) + + expect(openDatabase).toHaveBeenCalledWith(csvPath) + expect(useStore.getState().databases[csvPath]).toBeTruthy() + }) + + it('closes a stale tab when the database no longer exists (open returns null)', async () => { + // Simulates a database tab restored on startup after its .csv was deleted: + // DatabaseView calls loadDatabase, the main process returns null (no throw), + // and the renderer forgets the tab instead of looping on a missing file. + const csvPath = 'inbox/Gone.csv' + const openDatabase = vi.fn().mockResolvedValue(null) + installZen({ openDatabase }) + + const { useStore } = await loadStore() + const paneId = useStore.getState().activePaneId + const tabPath = databaseTabPath(csvPath) + + // Restore the tab directly (as the persisted layout would on launch). + await useStore.getState().openNoteInPane(paneId, tabPath) + expect(findLeaf(useStore.getState().paneLayout, paneId)?.tabs).toContain(tabPath) + + // DatabaseView's effect fires this when it has no cached doc. + await useStore.getState().loadDatabase(csvPath) + + expect(useStore.getState().databases[csvPath]).toBeUndefined() + expect(findLeaf(useStore.getState().paneLayout, paneId)?.tabs ?? []).not.toContain(tabPath) + }) +}) diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index 90e2c126..96fbf527 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -38,7 +38,7 @@ import { HELP_TAB_PATH, isHelpTabPath } from '@shared/help' import { ARCHIVE_TAB_PATH, isArchiveTabPath } from '@shared/archive' import { TRASH_TAB_PATH, isTrashTabPath } from '@shared/trash' import { QUICK_NOTES_TAB_PATH, isQuickNotesTabPath } from '@shared/quick-notes' -import { isAssetTabPath, assetPathFromTab } from './lib/asset-tabs' +import { isAssetTabPath, assetPathFromTab, assetTabPath } from './lib/asset-tabs' import { FENCE_RE, TASK_LINE_RE, @@ -1657,6 +1657,8 @@ interface Store { updateDatabaseSchema: (csvPath: string, next: DatabaseDoc) => void /** Re-read a database from disk after an external change (skips our own write echoes). */ syncDatabaseFromDisk: (csvPath: string) => Promise + /** Drop a deleted database's cached doc and close its tab (no disk read). */ + forgetDatabase: (csvPath: string) => Promise /** Open a record as a markdown "page" note (creating + linking it on first open). */ openRecordPage: (csvPath: string, rowId: string) => Promise /** Rename a record's linked page note to match its title (no-op if unlinked). */ @@ -2987,15 +2989,28 @@ export const useStore = create((set, get) => { set((s) => ({ databasesLoading: { ...s.databasesLoading, [csvPath]: true } })) try { const doc = await window.zen.openDatabase(csvPath) + if (!doc) { + // The .csv is gone — drop it and close any stale tab rather than leave + // a grid pointed at a deleted file (and re-requesting it on every render). + await get().forgetDatabase(csvPath) + return + } set((s) => ({ databases: { ...s.databases, [csvPath]: doc } })) } catch (err) { console.error('loadDatabase failed', err) } finally { - set((s) => ({ databasesLoading: { ...s.databasesLoading, [csvPath]: false } })) + set((s) => + csvPath in s.databasesLoading + ? { databasesLoading: { ...s.databasesLoading, [csvPath]: false } } + : {} + ) } }, openDatabase: async (csvPath) => { await get().loadDatabase(csvPath) + // The load may have failed/forgotten a now-missing database — don't open an + // empty tab for it. + if (!get().databases[csvPath]) return await get().openNoteInPane(get().activePaneId, databaseTabPath(csvPath)) ;(document.activeElement as HTMLElement | null)?.blur?.() set({ focusedPanel: 'editor' }) @@ -3027,11 +3042,37 @@ export const useStore = create((set, get) => { if (databaseSaveTimers.has(csvPath)) return try { const doc = await window.zen.openDatabase(csvPath) + if (!doc) { + await get().forgetDatabase(csvPath) + return + } set((s) => (s.databases[csvPath] ? { databases: { ...s.databases, [csvPath]: doc } } : {})) } catch (err) { console.error('syncDatabaseFromDisk failed', err) } }, + forgetDatabase: async (csvPath) => { + // Close the database's tab in every pane that holds it. A .csv can be open + // either as a `zen://database/…` tab or as a `.csv` asset tab that renders + // the same grid, so close both forms. + const tabPaths = [databaseTabPath(csvPath), assetTabPath(csvPath)] + for (const leaf of allLeaves(get().paneLayout)) { + for (const tabPath of tabPaths) { + if (leaf.tabs.includes(tabPath)) { + await get().closeTabInPane(leaf.id, tabPath) + } + } + } + // Drop the cached doc + loading flag so nothing re-reads a file that's gone. + set((s) => { + if (!(csvPath in s.databases) && !(csvPath in s.databasesLoading)) return {} + const databases = { ...s.databases } + const databasesLoading = { ...s.databasesLoading } + delete databases[csvPath] + delete databasesLoading[csvPath] + return { databases, databasesLoading } + }) + }, openRecordPage: async (csvPath, rowId) => { const doc = get().databases[csvPath] if (!doc) return @@ -3554,7 +3595,13 @@ export const useStore = create((set, get) => { return } if (ev.scope === 'database') { - await get().syncDatabaseFromDisk(ev.path) + // On delete, forget the database instead of re-reading a file that's gone + // (which throws "Database not found"); otherwise sync from disk. + if (ev.kind === 'unlink') { + await get().forgetDatabase(ev.path) + } else { + await get().syncDatabaseFromDisk(ev.path) + } // Surface a newly-created (or removed) .csv in the note list. if (ev.kind !== 'change') await get().refreshAssets() return diff --git a/packages/bridge-contract/src/bridge.ts b/packages/bridge-contract/src/bridge.ts index 169d7476..44c7eb4d 100644 --- a/packages/bridge-contract/src/bridge.ts +++ b/packages/bridge-contract/src/bridge.ts @@ -135,7 +135,8 @@ export interface ZenBridge { writeNoteComments(relPath: string, comments: NoteCommentInput[]): Promise scanTasks(): Promise scanTasksForPath(relPath: string): Promise - openDatabase(relPath: string): Promise + /** Resolves to null when the `.csv` no longer exists (e.g. a stale tab). */ + openDatabase(relPath: string): Promise writeDatabaseRows(relPath: string, rows: DbRow[]): Promise writeDatabaseSchema(relPath: string, sidecar: DatabaseSidecar, rows: DbRow[]): Promise createDatabase(folder: NoteFolder, subpath: string, title?: string): Promise From 53144c7f97f837a5fb8310792446e9efde7c62ff Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Tue, 16 Jun 2026 11:01:15 -0500 Subject: [PATCH 28/66] Don't trigger actions on the Enter that confirms an IME conversion (#183) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When typing with an IME (e.g. Japanese), pressing Enter to confirm a conversion also fired our own keydown handlers — committing a rename, selecting a search result, blurring a field, etc. — because the handlers never checked whether a composition was in progress. Add a shared `isImeComposing(e)` helper (standard `isComposing` flag plus the legacy `keyCode === 229` fallback, for both React synthetic and raw DOM events) and guard every text-entry "Enter confirms" handler: the rename prompt and breadcrumb rename, all search/command palettes, database inline renames (view/field/cell/group/tag), settings fields and the combobox picker, the remote-profile and directory-picker modals, quick-capture result pickers, and the tasks filter and Kanban column rename. While composing, these now let the IME own Enter/Arrows/Tab; a fresh Enter still confirms once composition ends. Pure-navigation handlers and modifier combos (e.g. Cmd+Enter) are left as-is, and the onboarding handler already bailed on editable targets. --- .../app-core/src/components/BufferPalette.tsx | 3 ++ .../src/components/CommandPalette.tsx | 3 ++ .../src/components/DatabaseBoardView.tsx | 3 +- .../src/components/DatabaseTableView.tsx | 7 ++-- .../app-core/src/components/DatabaseView.tsx | 3 +- .../app-core/src/components/EditorPane.tsx | 4 +++ .../src/components/OutlinePalette.tsx | 3 ++ .../app-core/src/components/PromptModal.tsx | 4 +++ .../src/components/QuickCaptureApp.tsx | 5 +++ .../RemoteWorkspaceProfileModal.tsx | 3 ++ .../app-core/src/components/SearchPalette.tsx | 3 ++ .../components/ServerDirectoryPickerModal.tsx | 3 ++ .../app-core/src/components/SettingsModal.tsx | 5 ++- packages/app-core/src/components/TagView.tsx | 3 +- .../app-core/src/components/TasksKanban.tsx | 3 ++ .../app-core/src/components/TasksView.tsx | 3 ++ .../src/components/TemplatePalette.tsx | 3 ++ .../src/components/VaultTextSearchPalette.tsx | 3 ++ packages/app-core/src/lib/ime.test.ts | 32 +++++++++++++++++++ packages/app-core/src/lib/ime.ts | 17 ++++++++++ 20 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 packages/app-core/src/lib/ime.test.ts create mode 100644 packages/app-core/src/lib/ime.ts diff --git a/packages/app-core/src/components/BufferPalette.tsx b/packages/app-core/src/components/BufferPalette.tsx index 14a58a0c..dd277b1f 100644 --- a/packages/app-core/src/components/BufferPalette.tsx +++ b/packages/app-core/src/components/BufferPalette.tsx @@ -13,6 +13,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { useStore } from '../store' import { rankItems } from '../lib/fuzzy-score' import { isPaletteNextKey, isPalettePreviousKey } from '../lib/palette-nav' +import { isImeComposing } from '../lib/ime' import { allLeaves, findLeafWithActiveTab, @@ -283,6 +284,8 @@ export function BufferPalette(): JSX.Element { placeholder="Switch buffer…" onChange={(e) => setQuery(e.target.value)} onKeyDown={(e) => { + // While composing (IME), let the input own Enter/Arrows. (#183) + if (isImeComposing(e)) return if (isPaletteNextKey(e)) { e.preventDefault() e.stopPropagation() diff --git a/packages/app-core/src/components/CommandPalette.tsx b/packages/app-core/src/components/CommandPalette.tsx index 8c94b2df..1df5281f 100644 --- a/packages/app-core/src/components/CommandPalette.tsx +++ b/packages/app-core/src/components/CommandPalette.tsx @@ -13,6 +13,7 @@ import { } from '../lib/command-history' import { rankItems } from '../lib/fuzzy-score' import { isPaletteNextKey, isPalettePreviousKey } from '../lib/palette-nav' +import { isImeComposing } from '../lib/ime' import { canReturnToCommandList } from '../lib/command-palette-mode' import { THEMES, type ThemeFamily, type ThemeMode, type ThemeOption } from '../lib/themes' import { @@ -291,6 +292,8 @@ export function CommandPalette(): JSX.Element { placeholder={inputPlaceholder} onChange={(e) => setQuery(e.target.value)} onKeyDown={(e) => { + // While composing (IME), let the input own Enter/Arrows. (#183) + if (isImeComposing(e)) return if (isPaletteNextKey(e)) { e.preventDefault() e.stopPropagation() diff --git a/packages/app-core/src/components/DatabaseBoardView.tsx b/packages/app-core/src/components/DatabaseBoardView.tsx index f3c5e3d8..32c63309 100644 --- a/packages/app-core/src/components/DatabaseBoardView.tsx +++ b/packages/app-core/src/components/DatabaseBoardView.tsx @@ -13,6 +13,7 @@ import { splitMultiSelect, isCheckboxTrue } from '../lib/database-cells' +import { isImeComposing } from '../lib/ime' import { PlusIcon, ArrowUpRightIcon } from './icons' import { IconButton } from './ui/Button' @@ -193,7 +194,7 @@ export function DatabaseBoardView({ csvPath, doc, view }: Props): JSX.Element { setAddingOption(false) }} onKeyDown={(e) => { - if (e.key === 'Enter') e.currentTarget.blur() + if (e.key === 'Enter' && !isImeComposing(e)) e.currentTarget.blur() else if (e.key === 'Escape') { setOptionDraft('') setAddingOption(false) diff --git a/packages/app-core/src/components/DatabaseTableView.tsx b/packages/app-core/src/components/DatabaseTableView.tsx index e658d118..4b090bc7 100644 --- a/packages/app-core/src/components/DatabaseTableView.tsx +++ b/packages/app-core/src/components/DatabaseTableView.tsx @@ -30,6 +30,7 @@ import { ContextMenu, type ContextMenuItem } from './ContextMenu' import { IconButton } from './ui/Button' import { MoreIcon, TrashIcon, PlusIcon, DocumentIcon, DocumentTextIcon, ArrowUpRightIcon } from './icons' import { focusEditorNormalMode } from '../lib/editor-focus' +import { isImeComposing } from '../lib/ime' const FIELD_TYPE_LABELS: Record = { text: 'Text', @@ -436,7 +437,7 @@ export function DatabaseTableView({ csvPath, doc, view, isActive }: Props): JSX. gridRef.current?.focus({ preventScroll: true }) }} onKeyDown={(e) => { - if (e.key === 'Enter') e.currentTarget.blur() + if (e.key === 'Enter' && !isImeComposing(e)) e.currentTarget.blur() else if (e.key === 'Escape') { setRenamingField(null) gridRef.current?.focus({ preventScroll: true }) @@ -714,7 +715,7 @@ function Cell({ field, value, editing, onStartEdit, onEndEdit, onCommit }: CellP }} onKeyDown={(e) => { e.stopPropagation() - if (e.key === 'Enter') e.currentTarget.blur() + if (e.key === 'Enter' && !isImeComposing(e)) e.currentTarget.blur() else if (e.key === 'Escape') onEndEdit() }} className="w-full bg-paper-50 px-2 py-1.5 text-sm text-ink-900 outline-none ring-1 ring-inset ring-accent" @@ -840,7 +841,7 @@ function SelectCell({ field, value, editing, onStartEdit, onEndEdit, onCommit }: onChange={(e) => setDraft(e.target.value)} onKeyDown={(e) => { e.stopPropagation() - if (e.key === 'Enter' && draft.trim()) { + if (e.key === 'Enter' && !isImeComposing(e) && draft.trim()) { toggle(draft.trim().replace(/,/g, ' ')) setDraft('') } else if (e.key === 'Escape') { diff --git a/packages/app-core/src/components/DatabaseView.tsx b/packages/app-core/src/components/DatabaseView.tsx index 81a92e1a..0dd28b45 100644 --- a/packages/app-core/src/components/DatabaseView.tsx +++ b/packages/app-core/src/components/DatabaseView.tsx @@ -10,6 +10,7 @@ import { removeView, renameView } from '../lib/database-cells' +import { isImeComposing } from '../lib/ime' import { DatabaseTableView } from './DatabaseTableView' import { DatabaseBoardView } from './DatabaseBoardView' import { ContextMenu, type ContextMenuItem } from './ContextMenu' @@ -98,7 +99,7 @@ export function DatabaseView({ setRenamingView(null) }} onKeyDown={(e) => { - if (e.key === 'Enter') e.currentTarget.blur() + if (e.key === 'Enter' && !isImeComposing(e)) e.currentTarget.blur() else if (e.key === 'Escape') setRenamingView(null) }} className="w-24 rounded border border-accent bg-paper-50 px-1.5 py-1 text-xs text-ink-900 outline-none" diff --git a/packages/app-core/src/components/EditorPane.tsx b/packages/app-core/src/components/EditorPane.tsx index 9c060b9f..ace10f7e 100644 --- a/packages/app-core/src/components/EditorPane.tsx +++ b/packages/app-core/src/components/EditorPane.tsx @@ -48,6 +48,7 @@ import { undo } from '@codemirror/commands' import { markdown, markdownLanguage } from '@codemirror/lang-markdown' +import { isImeComposing } from '../lib/ime' import { resolveCodeLanguage } from '../lib/cm-code-languages' import { markdownListIndentPlugin } from '../lib/cm-markdown-list-indent' import { completionNavKeymap } from '../lib/cm-completion-nav' @@ -3614,6 +3615,9 @@ function Breadcrumb({ if (commitRename()) setEditing(false) }} onKeyDown={(e) => { + // While an IME composition is active, Enter confirms the conversion + // (and Escape cancels it) — don't commit/cancel the rename. (#183) + if (isImeComposing(e)) return if (e.key === 'Enter') { e.preventDefault() e.stopPropagation() diff --git a/packages/app-core/src/components/OutlinePalette.tsx b/packages/app-core/src/components/OutlinePalette.tsx index f7346f75..d2499953 100644 --- a/packages/app-core/src/components/OutlinePalette.tsx +++ b/packages/app-core/src/components/OutlinePalette.tsx @@ -10,6 +10,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { useStore } from '../store' import { rankItems } from '../lib/fuzzy-score' import { isPaletteNextKey, isPalettePreviousKey } from '../lib/palette-nav' +import { isImeComposing } from '../lib/ime' import { parseOutline, type OutlineItem } from '../lib/outline' import { isHelpTabPath } from '@shared/help' import { isArchiveTabPath } from '@shared/archive' @@ -87,6 +88,8 @@ export function OutlinePalette(): JSX.Element { placeholder="Jump to heading…" onChange={(e) => setQuery(e.target.value)} onKeyDown={(e) => { + // While composing (IME), let the input own Enter/Arrows. (#183) + if (isImeComposing(e)) return if (isPaletteNextKey(e)) { e.preventDefault() e.stopPropagation() diff --git a/packages/app-core/src/components/PromptModal.tsx b/packages/app-core/src/components/PromptModal.tsx index 2ce6312a..96d3494e 100644 --- a/packages/app-core/src/components/PromptModal.tsx +++ b/packages/app-core/src/components/PromptModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useRef, useState } from 'react' +import { isImeComposing } from '../lib/ime' import { Modal } from './ui/Modal' import { Button } from './ui/Button' @@ -151,6 +152,9 @@ export function PromptModal({ setDismissed(false) }} onKeyDown={(e) => { + // Let the IME own Enter/Tab/Arrows while composing (e.g. confirming + // a Japanese conversion) instead of submitting the prompt. (#183) + if (isImeComposing(e)) return if (e.key === 'Tab' && filteredSuggestions.length > 0) { e.preventDefault() moveSuggestion(e.shiftKey ? -1 : 1) diff --git a/packages/app-core/src/components/QuickCaptureApp.tsx b/packages/app-core/src/components/QuickCaptureApp.tsx index 2ac59613..1e305f7a 100644 --- a/packages/app-core/src/components/QuickCaptureApp.tsx +++ b/packages/app-core/src/components/QuickCaptureApp.tsx @@ -62,6 +62,7 @@ import { import { deriveTitleFromBody, planQuickCaptureSave } from '../lib/quick-capture-save' import { applyVimInsertEscape } from '../lib/vim-insert-escape' import { isPaletteNextKey, isPalettePreviousKey } from '../lib/palette-nav' +import { isImeComposing } from '../lib/ime' import { PinIcon } from './icons' const PREFS_KEY = 'zen:prefs:v2' @@ -696,6 +697,8 @@ function NotePickerOverlay({ notes, onPick, onCancel }: NotePickerOverlayProps): useEffect(() => setActive(0), [query]) const onKeyDown = (e: React.KeyboardEvent): void => { + // While composing (IME), let the input own Enter/Arrows. (#183) + if (isImeComposing(e)) return if (isPaletteNextKey(e)) { e.preventDefault() setActive((i) => Math.min(results.length - 1, i + 1)) @@ -821,6 +824,8 @@ function CommandOverlay({ modKey, mode, onAction, onCancel }: CommandOverlayProp useEffect(() => setActive(0), [query]) const onKeyDown = (e: React.KeyboardEvent): void => { + // While composing (IME), let the input own Enter/Arrows. (#183) + if (isImeComposing(e)) return if (isPaletteNextKey(e)) { e.preventDefault() setActive((i) => Math.min(results.length - 1, i + 1)) diff --git a/packages/app-core/src/components/RemoteWorkspaceProfileModal.tsx b/packages/app-core/src/components/RemoteWorkspaceProfileModal.tsx index 3a7bc9cf..cdeb1dad 100644 --- a/packages/app-core/src/components/RemoteWorkspaceProfileModal.tsx +++ b/packages/app-core/src/components/RemoteWorkspaceProfileModal.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from 'react' +import { isImeComposing } from '../lib/ime' import type { RemoteWorkspaceProfileInput } from '@shared/ipc' import { Modal } from './ui/Modal' import { Button } from './ui/Button' @@ -78,6 +79,8 @@ export function RemoteWorkspaceProfileModal({ // Esc handled by Modal; we keep Enter (→ submit) here. closeOnEsc stays true. useEffect(() => { const handler = (e: KeyboardEvent): void => { + // While composing (IME), let the input own Enter/Arrows. (#183) + if (isImeComposing(e)) return if (e.key === 'Enter') { e.preventDefault() e.stopPropagation() diff --git a/packages/app-core/src/components/SearchPalette.tsx b/packages/app-core/src/components/SearchPalette.tsx index 4004fe68..30fff881 100644 --- a/packages/app-core/src/components/SearchPalette.tsx +++ b/packages/app-core/src/components/SearchPalette.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { useStore } from '../store' import type { NoteMeta } from '@shared/ipc' import { isPaletteNextKey, isPalettePreviousKey } from '../lib/palette-nav' +import { isImeComposing } from '../lib/ime' import { buildNoteSearchIndex, parseNoteSearchQuery, @@ -62,6 +63,8 @@ export function SearchPalette(): JSX.Element { placeholder="Search notes… · use #tag to filter" onChange={(e) => setQuery(e.target.value)} onKeyDown={(e) => { + // While composing (IME), let the input own Enter/Arrows. (#183) + if (isImeComposing(e)) return if (isPaletteNextKey(e)) { e.preventDefault() e.stopPropagation() diff --git a/packages/app-core/src/components/ServerDirectoryPickerModal.tsx b/packages/app-core/src/components/ServerDirectoryPickerModal.tsx index a5192b44..071c453d 100644 --- a/packages/app-core/src/components/ServerDirectoryPickerModal.tsx +++ b/packages/app-core/src/components/ServerDirectoryPickerModal.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { isImeComposing } from '../lib/ime' import type { DirectoryBrowseEntry, DirectoryBrowseShortcut } from '@shared/ipc' import { Modal } from './ui/Modal' import { Button } from './ui/Button' @@ -196,6 +197,8 @@ export function ServerDirectoryPickerModal({ setSubmitError(null) }} onKeyDown={(e) => { + // While composing (IME), let the input own Enter/Arrows. (#183) + if (isImeComposing(e)) return if (e.key === 'Enter') { e.preventDefault() void loadDirectory(draftPath.trim()) diff --git a/packages/app-core/src/components/SettingsModal.tsx b/packages/app-core/src/components/SettingsModal.tsx index c4d86601..4fc310ff 100644 --- a/packages/app-core/src/components/SettingsModal.tsx +++ b/packages/app-core/src/components/SettingsModal.tsx @@ -63,6 +63,7 @@ import { useAppUpdateState } from '../lib/app-update-state' import { getZenBridge } from '@zennotes/bridge-contract/bridge' import companyLogo from '../assets/lumary-labs-logo.svg' import { confirmApp } from '../lib/confirm-requests' +import { isImeComposing } from '../lib/ime' import { RemoteWorkspaceProfileModal } from './RemoteWorkspaceProfileModal' import { Button } from './ui/Button' @@ -3267,7 +3268,7 @@ function TextInputRow({ }} onKeyDown={(e) => { if (!commitOnBlur) return - if (e.key === 'Enter') e.currentTarget.blur() + if (e.key === 'Enter' && !isImeComposing(e)) e.currentTarget.blur() if (e.key === 'Escape') { setDraft(value) e.currentTarget.blur() @@ -3434,6 +3435,8 @@ function FontRow({ } const handleKey = (e: React.KeyboardEvent): void => { + // While composing (IME), let the input own Enter/Arrows. (#183) + if (isImeComposing(e)) return if (e.key === 'ArrowDown') { e.preventDefault() setActiveIdx((i) => (i + 1 >= items.length ? items.length - 1 : i + 1)) diff --git a/packages/app-core/src/components/TagView.tsx b/packages/app-core/src/components/TagView.tsx index 4f9e610c..cd4c350e 100644 --- a/packages/app-core/src/components/TagView.tsx +++ b/packages/app-core/src/components/TagView.tsx @@ -5,6 +5,7 @@ import { extractTags } from '../lib/tags' import { TagIcon, CloseIcon, DocumentIcon } from './icons' import { advanceSequence, getKeymapBinding, matchesSequenceToken } from '../lib/keymaps' import { isPrimaryNotesAtRoot, noteFolderSubpath } from '../lib/vault-layout' +import { isImeComposing } from '../lib/ime' function formatDate(ms: number): string { const d = new Date(ms) @@ -298,7 +299,7 @@ export function TagView(): JSX.Element { if (filter) setFilter('') else e.currentTarget.blur() } - if (e.key === 'Enter') e.currentTarget.blur() + if (e.key === 'Enter' && !isImeComposing(e)) e.currentTarget.blur() }} className="w-56 rounded-md border border-current/15 bg-current/5 px-2 py-1 text-xs outline-none focus:border-current/30" /> diff --git a/packages/app-core/src/components/TasksKanban.tsx b/packages/app-core/src/components/TasksKanban.tsx index 243e0509..c577eca6 100644 --- a/packages/app-core/src/components/TasksKanban.tsx +++ b/packages/app-core/src/components/TasksKanban.tsx @@ -32,6 +32,7 @@ import { groupTasks, isOverdue as isTaskOverdue, toIsoDateLocal } from '@shared/ import { useStore, type KanbanGroupBy, type TaskMutation } from '../store' import { ArrowUpRightIcon, PencilIcon } from './icons' import { InlineMarkdown } from '../lib/inline-markdown' +import { isImeComposing } from '../lib/ime' interface Props { tasks: VaultTask[] @@ -887,6 +888,8 @@ export function TasksKanban({ tasks, today, onOpenTask, onToggleTask }: Props): onClick={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()} onKeyDown={(e) => { + // While composing (IME), let the input own Enter/Arrows. (#183) + if (isImeComposing(e)) return if (e.key === 'Enter') { e.preventDefault() commitColumnRename(column.id) diff --git a/packages/app-core/src/components/TasksView.tsx b/packages/app-core/src/components/TasksView.tsx index 1b223f5d..a161ca31 100644 --- a/packages/app-core/src/components/TasksView.tsx +++ b/packages/app-core/src/components/TasksView.tsx @@ -7,6 +7,7 @@ import { TasksCalendar } from './TasksCalendar' import { TasksKanban } from './TasksKanban' import { CalendarIcon, CheckSquareIcon, KanbanIcon, ListIcon } from './icons' import { advanceSequence, getKeymapBinding, matchesSequenceToken } from '../lib/keymaps' +import { isImeComposing } from '../lib/ime' type GroupKey = 'today' | 'upcoming' | 'waiting' | 'done' @@ -367,6 +368,8 @@ export function TasksView(): JSX.Element { value={filter} onChange={(e) => setFilter(e.target.value)} onKeyDown={(e) => { + // While composing (IME), let the input own Enter/Arrows. (#183) + if (isImeComposing(e)) return if (e.key === 'Escape') { e.stopPropagation() if (filter) setFilter('') diff --git a/packages/app-core/src/components/TemplatePalette.tsx b/packages/app-core/src/components/TemplatePalette.tsx index 4f237513..2cd12f13 100644 --- a/packages/app-core/src/components/TemplatePalette.tsx +++ b/packages/app-core/src/components/TemplatePalette.tsx @@ -10,6 +10,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { useStore } from '../store' import { rankItems } from '../lib/fuzzy-score' import { isPaletteNextKey, isPalettePreviousKey } from '../lib/palette-nav' +import { isImeComposing } from '../lib/ime' import { focusEditorNormalMode } from '../lib/editor-focus' import { BUILTIN_TEMPLATES } from '@shared/builtin-templates' import { mergeTemplates } from '@shared/template-files' @@ -81,6 +82,8 @@ export function TemplatePalette(): JSX.Element { placeholder={mode === 'insert' ? 'Insert template into note…' : 'Create note from template…'} onChange={(e) => setQuery(e.target.value)} onKeyDown={(e) => { + // While composing (IME), let the input own Enter/Arrows. (#183) + if (isImeComposing(e)) return if (isPaletteNextKey(e)) { e.preventDefault() e.stopPropagation() diff --git a/packages/app-core/src/components/VaultTextSearchPalette.tsx b/packages/app-core/src/components/VaultTextSearchPalette.tsx index aebf414b..0f634b46 100644 --- a/packages/app-core/src/components/VaultTextSearchPalette.tsx +++ b/packages/app-core/src/components/VaultTextSearchPalette.tsx @@ -7,6 +7,7 @@ import type { import { useStore } from '../store' import { resolveSystemFolderLabels } from '../lib/system-folder-labels' import { isPaletteNextKey, isPalettePreviousKey } from '../lib/palette-nav' +import { isImeComposing } from '../lib/ime' import { recordRendererPerf } from '../lib/perf' import { focusEditorNormalMode } from '../lib/editor-focus' import { Modal } from './ui/Modal' @@ -381,6 +382,8 @@ export function VaultTextSearchPalette(): JSX.Element { placeholder="Search text across the vault…" onChange={(e) => setQuery(e.target.value)} onKeyDown={(e) => { + // While composing (IME), let the input own Enter/Arrows. (#183) + if (isImeComposing(e)) return if (isPaletteNextKey(e)) { e.preventDefault() e.stopPropagation() diff --git a/packages/app-core/src/lib/ime.test.ts b/packages/app-core/src/lib/ime.test.ts new file mode 100644 index 00000000..88d0ef1f --- /dev/null +++ b/packages/app-core/src/lib/ime.test.ts @@ -0,0 +1,32 @@ +import type { KeyboardEvent as ReactKeyboardEvent } from 'react' +import { describe, expect, it } from 'vitest' +import { isImeComposing } from './ime' + +// React synthetic events expose composition state on `.nativeEvent`; raw DOM +// listeners (window/document) get a native KeyboardEvent with `.isComposing`. +function reactEvent(isComposing: boolean, keyCode = 13): ReactKeyboardEvent { + return { nativeEvent: { isComposing } as KeyboardEvent, keyCode } as ReactKeyboardEvent +} +function nativeEvent(isComposing: boolean, keyCode = 13): KeyboardEvent { + return { isComposing, keyCode } as KeyboardEvent +} + +describe('isImeComposing (#183)', () => { + it('is true while a React synthetic event is composing', () => { + expect(isImeComposing(reactEvent(true))).toBe(true) + }) + + it('is false for a React Enter that is not composing', () => { + expect(isImeComposing(reactEvent(false))).toBe(false) + }) + + it('reads composition state from a native KeyboardEvent (no nativeEvent field)', () => { + expect(isImeComposing(nativeEvent(true))).toBe(true) + expect(isImeComposing(nativeEvent(false))).toBe(false) + }) + + it('honors the legacy keyCode === 229 fallback for both event shapes', () => { + expect(isImeComposing(reactEvent(false, 229))).toBe(true) + expect(isImeComposing(nativeEvent(false, 229))).toBe(true) + }) +}) diff --git a/packages/app-core/src/lib/ime.ts b/packages/app-core/src/lib/ime.ts new file mode 100644 index 00000000..1e9b7ecf --- /dev/null +++ b/packages/app-core/src/lib/ime.ts @@ -0,0 +1,17 @@ +import type { KeyboardEvent as ReactKeyboardEvent } from 'react' + +/** + * True while an IME (Japanese / Chinese / Korean, etc.) composition is in + * progress. The keystroke that confirms an IME conversion is usually Enter (and + * Space/Arrows/Tab navigate candidates), so our own keydown handlers must ignore + * those keys while composing — otherwise pressing Enter to accept a conversion + * also submits the rename, selects the search result, blurs the field, and so + * on. + * + * `isComposing` is the standard signal; `keyCode === 229` is the legacy fallback + * some browsers/IMEs still report during composition. (#183) + */ +export function isImeComposing(e: ReactKeyboardEvent | KeyboardEvent): boolean { + const composing = 'nativeEvent' in e ? e.nativeEvent.isComposing : e.isComposing + return composing || e.keyCode === 229 +} From cbbbe7677bc3be9eb5c16246a8d428052ae8a42f Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Tue, 16 Jun 2026 12:44:14 -0500 Subject: [PATCH 29/66] Don't let the file watcher hang the server where inotify is restricted (#179) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In an unprivileged LXC container, inotify on a bind-mounted vault can wedge the process (unkillable, host volume locked) rather than returning an error, and errors from fsw.Add() were silently discarded so a restricted environment failed invisibly. - Add ZENNOTES_DISABLE_WATCHER=1 → a no-op watcher (no fsnotify handle, no tree walk, no inotify syscalls). The vault is still fully served; only live auto-refresh stops. This is the reliable escape hatch on restricted hosts because it never makes the syscalls that hang. - New watcher.StartOrDisabled is used at startup and on vault switch: if the watcher can't be created it logs a warning and falls back to the no-op watcher instead of log.Fatal / failing the switch. - Surface fsw.Add failures: the initial walk counts and logs them once (with a pointer to ZENNOTES_DISABLE_WATCHER), and watching a newly created directory logs on error. Document the variable and a "container hangs and won't stop" troubleshooting entry in the Docker how-to. Adds tests for the no-op watcher. --- apps/server/cmd/zennotes-server/main.go | 7 +-- apps/server/internal/config/config.go | 5 ++ apps/server/internal/httpserver/server.go | 7 +-- apps/server/internal/watcher/watcher.go | 62 +++++++++++++++++++- apps/server/internal/watcher/watcher_test.go | 40 +++++++++++++ docs/how-to/self-host-with-docker.md | 22 +++++++ 6 files changed, 132 insertions(+), 11 deletions(-) diff --git a/apps/server/cmd/zennotes-server/main.go b/apps/server/cmd/zennotes-server/main.go index 3d659158..4c86d599 100644 --- a/apps/server/cmd/zennotes-server/main.go +++ b/apps/server/cmd/zennotes-server/main.go @@ -41,10 +41,9 @@ func main() { log.Printf("warning: ignoring legacy vault config at %s; server secrets now stay in host config only", config.LegacyVaultConfigPath(v.Root())) } - w, err := watcher.Start(v.Root()) - if err != nil { - log.Fatalf("watcher start: %v", err) - } + // Never fatal: where inotify is restricted (e.g. unprivileged LXC) the + // watcher falls back to a no-op so the server still serves the vault. (#179) + w := watcher.StartOrDisabled(v.Root(), cfg.DisableWatcher) defer w.Close() dist, err := web.Dist() diff --git a/apps/server/internal/config/config.go b/apps/server/internal/config/config.go index 60518f1a..b41259f3 100644 --- a/apps/server/internal/config/config.go +++ b/apps/server/internal/config/config.go @@ -36,6 +36,10 @@ type Config struct { AllowUnscopedBrowse bool `json:"-"` AllowInsecureNoAuth bool `json:"-"` DevMode bool `json:"-"` + // DisableWatcher turns off the inotify file watcher (ZENNOTES_DISABLE_WATCHER). + // Live updates stop; the vault is still fully served. Useful where inotify is + // restricted and can hang the process (e.g. unprivileged LXC). (#179) + DisableWatcher bool `json:"-"` // Limits and security knobs. MaxAssetBytes int64 `json:"-"` @@ -111,6 +115,7 @@ func Load() Config { cfg.AllowUnscopedBrowse = envEnabled("ZENNOTES_ALLOW_UNSCOPED_BROWSE") cfg.AllowInsecureNoAuth = envEnabled("ZENNOTES_ALLOW_INSECURE_NOAUTH") cfg.DevMode = envEnabled("ZENNOTES_DEV") + cfg.DisableWatcher = envEnabled("ZENNOTES_DISABLE_WATCHER") cfg.BehindTLS = envEnabled("ZENNOTES_BEHIND_TLS") cfg.TrustedProxies = parseCIDRListEnv("ZENNOTES_TRUSTED_PROXIES") if v := parseInt64Env("ZENNOTES_MAX_ASSET_BYTES"); v > 0 { diff --git a/apps/server/internal/httpserver/server.go b/apps/server/internal/httpserver/server.go index 765751a6..4beb8c4d 100644 --- a/apps/server/internal/httpserver/server.go +++ b/apps/server/internal/httpserver/server.go @@ -86,10 +86,9 @@ func (s *Server) switchVaultRoot(nextPath string) (*vault.Vault, error) { if err != nil { return nil, err } - nextWatcher, err := watcher.Start(nextVault.Root()) - if err != nil { - return nil, err - } + // Non-fatal: a vault switch must not fail just because inotify is + // unavailable; fall back to a no-op watcher in that case. (#179) + nextWatcher := watcher.StartOrDisabled(nextVault.Root(), cfg.DisableWatcher) s.mu.Lock() prevWatcher := s.Watcher diff --git a/apps/server/internal/watcher/watcher.go b/apps/server/internal/watcher/watcher.go index a0c7ed73..f9b129e9 100644 --- a/apps/server/internal/watcher/watcher.go +++ b/apps/server/internal/watcher/watcher.go @@ -48,6 +48,8 @@ func Start(root string) (*Watcher, error) { dirs: map[string]struct{}{}, } // Recursively add all existing directories under the vault. + var addErrs int + var firstAddErr error _ = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { if err != nil { return nil @@ -57,15 +59,60 @@ func Start(root string) (*Watcher, error) { if path != root && strings.HasPrefix(name, ".") && name != internalVaultDir { return filepath.SkipDir } - _ = fsw.Add(path) + // Don't discard the error: inotify can be exhausted or restricted + // (notably in unprivileged LXC containers), and a silent failure + // leaves clients without live updates for no apparent reason. (#179) + if addErr := fsw.Add(path); addErr != nil { + addErrs++ + if firstAddErr == nil { + firstAddErr = addErr + } + } w.dirs[path] = struct{}{} } return nil }) + if addErrs > 0 { + log.Printf("watcher: could not watch %d director(ies) (first error: %v); live updates may be incomplete — set ZENNOTES_DISABLE_WATCHER=1 if this environment restricts inotify (e.g. unprivileged LXC)", addErrs, firstAddErr) + } go w.loop() return w, nil } +// Disabled returns a watcher that does no filesystem watching. It still +// supports Subscribe/Close so the rest of the server can treat it like a real +// watcher; it simply never emits change events. Used where inotify is +// unavailable or explicitly turned off — notably unprivileged LXC containers, +// where inotify operations can wedge the process (unkillable, bind-mount +// locked) instead of returning an error. (#179) +func Disabled(root string) *Watcher { + w := &Watcher{ + root: root, + fs: nil, + subs: map[chan vault.ChangeEvent]struct{}{}, + stopCh: make(chan struct{}), + dirs: map[string]struct{}{}, + } + go w.loop() + return w +} + +// StartOrDisabled starts a real watcher, or falls back to a no-op watcher when +// watching is turned off (disable) or unavailable. It never returns an error, +// so the server can always serve the vault even where inotify is restricted. (#179) +func StartOrDisabled(root string, disable bool) *Watcher { + if disable { + log.Printf("watcher: disabled via ZENNOTES_DISABLE_WATCHER; live updates are off") + return Disabled(root) + } + w, err := Start(root) + if err != nil { + log.Printf("watcher: unavailable (%v); continuing without live updates — set ZENNOTES_DISABLE_WATCHER=1 to disable watching explicitly", err) + return Disabled(root) + } + return w +} + func (w *Watcher) Subscribe() (<-chan vault.ChangeEvent, func()) { ch := make(chan vault.ChangeEvent, 64) w.mu.Lock() @@ -94,10 +141,17 @@ func (w *Watcher) Close() { close(ch) } w.mu.Unlock() - _ = w.fs.Close() + if w.fs != nil { + _ = w.fs.Close() + } } func (w *Watcher) loop() { + // A disabled (no-op) watcher has no fsnotify handle — just block until close. + if w.fs == nil { + <-w.stopCh + return + } for { select { case <-w.stopCh: @@ -144,7 +198,9 @@ func (w *Watcher) handle(ev fsnotify.Event) { info, statErr := os.Stat(ev.Name) if statErr == nil && info.IsDir() { if ev.Op&fsnotify.Create != 0 { - _ = w.fs.Add(ev.Name) + if err := w.fs.Add(ev.Name); err != nil { + log.Printf("watcher: cannot watch new directory %s: %v", ev.Name, err) + } w.dirs[ev.Name] = struct{}{} // An empty folder produces no note event, so clients would never // learn about it until a manual refresh. Surface it explicitly. diff --git a/apps/server/internal/watcher/watcher_test.go b/apps/server/internal/watcher/watcher_test.go index 2f456ed6..68f36fd8 100644 --- a/apps/server/internal/watcher/watcher_test.go +++ b/apps/server/internal/watcher/watcher_test.go @@ -77,6 +77,46 @@ func TestWatcherBroadcastsFolderCreateAndRemove(t *testing.T) { } } +func TestDisabledWatcherIsNoop(t *testing.T) { + root := t.TempDir() + w := Disabled(root) + if w.fs != nil { + t.Fatal("disabled watcher should have no fsnotify handle") + } + ch, unsub := w.Subscribe() + defer unsub() + + // Creating a directory must NOT produce an event — nothing is watched. + if err := os.MkdirAll(filepath.Join(root, "inbox", "Projects"), 0o700); err != nil { + t.Fatal(err) + } + select { + case ev := <-ch: + t.Fatalf("disabled watcher emitted an event: %+v", ev) + case <-time.After(100 * time.Millisecond): + // Expected: a no-op watcher never emits. + } + + // Close must be safe even though there is no fsnotify handle to close. + w.Close() +} + +func TestStartOrDisabledFallsBackWhenDisabled(t *testing.T) { + root := t.TempDir() + + disabled := StartOrDisabled(root, true) + if disabled.fs != nil { + t.Error("StartOrDisabled(_, true) should return a no-op watcher") + } + disabled.Close() + + enabled := StartOrDisabled(root, false) + if enabled.fs == nil { + t.Error("StartOrDisabled(_, false) should start a real watcher") + } + enabled.Close() +} + func TestWatcherDoesNotSurfaceInternalDirAsFolder(t *testing.T) { root := t.TempDir() w := newTestWatcher(t, root) diff --git a/docs/how-to/self-host-with-docker.md b/docs/how-to/self-host-with-docker.md index a496773f..901399ac 100644 --- a/docs/how-to/self-host-with-docker.md +++ b/docs/how-to/self-host-with-docker.md @@ -190,6 +190,11 @@ or via the orchestrator of your choice. subpath instead of the domain root. Use this when deploying behind a reverse proxy that routes by path (e.g. `example.com/zennotes/`). See [Reverse-proxy with a path prefix](#reverse-proxy-with-a-path-prefix). +- `ZENNOTES_DISABLE_WATCHER=1` — turn off the inotify file watcher. The + vault is still fully served; only live updates (auto-refresh when files + change on disk) stop. Set this where inotify is restricted or unstable — + notably **unprivileged LXC containers**, where inotify on a bind-mount can + wedge the process and lock the volume (see Common problems below). ## Reverse-proxy with a path prefix @@ -271,6 +276,23 @@ If you want a flatter layout, change: - `Settings -> Vault -> Primary notes location -> Vault root` +### The container hangs and won't stop (unprivileged LXC) + +If the web page loads but the container ignores `docker stop`/`docker kill` +(and even `kill -9`), and the bind-mounted volume on the host is locked, the +culprit is almost always the inotify file watcher on a restricted host — +typically an **unprivileged LXC container**, where inotify on a bind-mounted +directory can put the process into an unkillable state. + +Run with the watcher off: + +- `ZENNOTES_DISABLE_WATCHER=1` + +The vault is still fully served; you only lose live auto-refresh when files +change on disk (reload the page to pick up external edits). On startup the +server now also logs a warning instead of failing silently if it can only +watch part of the vault. + ## Related docs - [Connect Desktop to a Remote ZenNotes Server](./connect-desktop-to-remote-server.md) From 84eb7ba193ce7e50bf192e32e4d6b8c326b77e09 Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Tue, 16 Jun 2026 15:06:49 -0500 Subject: [PATCH 30/66] Make a database a self-contained .base folder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A database is now one folder, `.base/`, holding `data.csv`, `schema.json` (the sidecar), and its record-page notes as `.md` files. The folder is the database: rename/move/trash are plain folder operations and record-page paths in the schema are stored relative to it, so nothing needs path rewriting. The sidebar renders a `.base` folder as a database — database icon, title without the suffix, click opens the grid, expand reveals its record pages — and the folder menu renames/deletes it as a database. The database header title is editable (double-click) and renames the folder, rehoming the open grid tab. Walks treat a `.base` folder as one unit: the desktop lists it as a database and its record pages as notes, but hides data.csv/schema.json from notes, folders, and assets; the MCP server hides the folder; the self-hosted Go server hides it entirely (databases are desktop-only). A legacy loose `.csv` plus co-located `.csv.base.json` (with record notes under `/`) is migrated to the new layout on vault open — idempotent, and only for managed databases (a plain CSV is left alone). Also fixes a couple of grid rough edges: a new/blank row no longer collapses shorter than filled rows, and the rename input is sized to the title. Adds shared-path, main-process (folder ops + migration + walk), and Go-server tests. --- .../src/main/databases-migrate.test.ts | 103 ++++++++++++ apps/desktop/src/main/databases-walk.test.ts | 47 ++++++ apps/desktop/src/main/databases.test.ts | 41 ++++- apps/desktop/src/main/databases.ts | 136 ++++++++++++--- apps/desktop/src/main/index.ts | 6 + apps/desktop/src/main/vault.ts | 159 +++++++++++++++++- apps/desktop/src/main/watcher.ts | 6 +- apps/desktop/src/mcp/vault-ops.ts | 6 + apps/desktop/src/preload/index.ts | 2 + apps/server/internal/vault/vault.go | 22 +++ apps/server/internal/vault/vault_test.go | 78 +++++++++ apps/web/src/bridge/http-bridge.ts | 4 + .../src/components/DatabaseTableView.tsx | 6 +- .../app-core/src/components/DatabaseView.tsx | 40 ++++- packages/app-core/src/components/Sidebar.tsx | 57 +++++-- packages/app-core/src/store.ts | 44 +++++ packages/bridge-contract/src/bridge.ts | 2 + packages/bridge-contract/src/ipc.ts | 1 + packages/shared-domain/package.json | 4 +- packages/shared-domain/src/databases.test.ts | 73 ++++++++ packages/shared-domain/src/databases.ts | 144 ++++++++++++++-- 21 files changed, 915 insertions(+), 66 deletions(-) create mode 100644 apps/desktop/src/main/databases-migrate.test.ts create mode 100644 apps/desktop/src/main/databases-walk.test.ts create mode 100644 packages/shared-domain/src/databases.test.ts diff --git a/apps/desktop/src/main/databases-migrate.test.ts b/apps/desktop/src/main/databases-migrate.test.ts new file mode 100644 index 00000000..ebd0640b --- /dev/null +++ b/apps/desktop/src/main/databases-migrate.test.ts @@ -0,0 +1,103 @@ +import { mkdtemp, mkdir, rm, writeFile, readFile, access } from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { migrateLegacyDatabases } from './vault' +import { readDatabase } from './databases' + +const tempDirs: string[] = [] + +async function makeVault(): Promise { + const dir = await mkdtemp(path.join(os.tmpdir(), 'zen-dbmig-')) + tempDirs.push(dir) + await mkdir(path.join(dir, 'inbox'), { recursive: true }) + return dir +} + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((d) => rm(d, { recursive: true, force: true }))) +}) + +const exists = (p: string): Promise => + access(p) + .then(() => true) + .catch(() => false) + +// A minimal legacy database: loose `.csv` + co-located sidecar, with one +// record-page note under the per-database `/` folder. +async function seedLegacyDatabase(root: string): Promise { + await writeFile(path.join(root, 'inbox', 'Books.csv'), 'id,Title\nr1,Dune\n', 'utf8') + await mkdir(path.join(root, 'inbox', 'Books'), { recursive: true }) + await writeFile(path.join(root, 'inbox', 'Books', 'Dune.md'), '# Dune\n\nGreat book.', 'utf8') + const sidecar = { + version: 1, + idFieldId: 'f_id', + fields: [ + { id: 'f_id', name: 'id', type: 'text', hidden: true }, + { id: 'f_title', name: 'Title', type: 'text' } + ], + views: [{ id: 'v1', name: 'Table', type: 'table', filters: [], sorts: [] }], + activeViewId: 'v1', + pages: { r1: 'inbox/Books/Dune.md' } + } + await writeFile( + path.join(root, 'inbox', 'Books.csv.base.json'), + JSON.stringify(sidecar, null, 2), + 'utf8' + ) +} + +describe('migrateLegacyDatabases (#185 .base reorg)', () => { + it('moves a legacy database into a self-contained .base folder', async () => { + const root = await makeVault() + await seedLegacyDatabase(root) + + const count = await migrateLegacyDatabases(root) + expect(count).toBe(1) + + const baseDir = path.join(root, 'inbox', 'Books.base') + expect(await exists(path.join(baseDir, 'data.csv'))).toBe(true) + expect(await exists(path.join(baseDir, 'schema.json'))).toBe(true) + expect(await exists(path.join(baseDir, 'Dune.md'))).toBe(true) + + // Old files + per-database folder are gone. + expect(await exists(path.join(root, 'inbox', 'Books.csv'))).toBe(false) + expect(await exists(path.join(root, 'inbox', 'Books.csv.base.json'))).toBe(false) + expect(await exists(path.join(root, 'inbox', 'Books'))).toBe(false) + + // schema.json stores the page path RELATIVE to the folder. + const schema = JSON.parse(await readFile(path.join(baseDir, 'schema.json'), 'utf8')) + expect(schema.pages).toEqual({ r1: 'Dune.md' }) + + // data.csv content is preserved. + expect(await readFile(path.join(baseDir, 'data.csv'), 'utf8')).toContain('Dune') + }) + + it('readDatabase resolves the migrated db and its pages to full paths', async () => { + const root = await makeVault() + await seedLegacyDatabase(root) + await migrateLegacyDatabases(root) + + const doc = await readDatabase(root, 'inbox/Books.base/data.csv') + expect(doc.title).toBe('Books') + expect(doc.rows).toHaveLength(1) + // pages resolved back to full vault-relative paths on read. + expect(doc.pages?.r1).toBe('inbox/Books.base/Dune.md') + }) + + it('is idempotent — a second run migrates nothing and leaves data intact', async () => { + const root = await makeVault() + await seedLegacyDatabase(root) + expect(await migrateLegacyDatabases(root)).toBe(1) + expect(await migrateLegacyDatabases(root)).toBe(0) + expect(await exists(path.join(root, 'inbox', 'Books.base', 'data.csv'))).toBe(true) + }) + + it('leaves a plain CSV (no sidecar) untouched', async () => { + const root = await makeVault() + await writeFile(path.join(root, 'inbox', 'data-dump.csv'), 'a,b\n1,2\n', 'utf8') + expect(await migrateLegacyDatabases(root)).toBe(0) + expect(await exists(path.join(root, 'inbox', 'data-dump.csv'))).toBe(true) + expect(await exists(path.join(root, 'inbox', 'data-dump.base'))).toBe(false) + }) +}) diff --git a/apps/desktop/src/main/databases-walk.test.ts b/apps/desktop/src/main/databases-walk.test.ts new file mode 100644 index 00000000..8ecfa9bf --- /dev/null +++ b/apps/desktop/src/main/databases-walk.test.ts @@ -0,0 +1,47 @@ +import { mkdtemp, mkdir, rm } from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { createDatabase, createRecordPage } from './databases' +import { listFolders, listNotes, listAssets } from './vault' + +const tempDirs: string[] = [] + +async function makeVault(): Promise { + const dir = await mkdtemp(path.join(os.tmpdir(), 'zen-dbwalk-')) + tempDirs.push(dir) + await mkdir(path.join(dir, 'inbox'), { recursive: true }) + return dir +} + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((d) => rm(d, { recursive: true, force: true }))) +}) + +describe('vault walk treats a .base database as one unit (#185)', () => { + it('surfaces the database folder + record pages, hides data.csv/schema.json', async () => { + const root = await makeVault() + const doc = await createDatabase(root, 'inbox', '', 'Reading List') + expect(doc.path).toBe('inbox/Reading List.base/data.csv') + await createRecordPage(root, doc.path, 'Dune', '# Dune') + + // The database appears as a folder node (the sidebar renders it as a database). + const folderEntries = await listFolders(root) + expect(folderEntries.some((f) => f.subpath === 'Reading List.base')).toBe(true) + + // The record page is a note nested under the database folder. + const notes = await listNotes(root) + const page = notes.find((n) => n.path === 'inbox/Reading List.base/Dune.md') + expect(page).toBeTruthy() + expect(page?.folder).toBe('inbox') + + // data.csv and schema.json never appear as assets or notes. + const assets = await listAssets(root) + expect(assets.some((a) => a.path.includes('.base'))).toBe(false) + expect(notes.some((n) => n.path.endsWith('data.csv') || n.path.endsWith('schema.json'))).toBe( + false + ) + // The database folder is NOT descended into as ordinary subfolders. + expect(folderEntries.some((f) => f.subpath.startsWith('Reading List.base/'))).toBe(false) + }) +}) diff --git a/apps/desktop/src/main/databases.test.ts b/apps/desktop/src/main/databases.test.ts index 8894201b..205db6cf 100644 --- a/apps/desktop/src/main/databases.test.ts +++ b/apps/desktop/src/main/databases.test.ts @@ -2,7 +2,13 @@ import { mkdtemp, readFile, rm, writeFile, mkdir } from 'node:fs/promises' import os from 'node:os' import path from 'node:path' import { afterEach, describe, expect, it } from 'vitest' -import { createDatabase, createRecordPage, readDatabase, writeDatabaseRows } from './databases' +import { + createDatabase, + createRecordPage, + readDatabase, + renameDatabase, + writeDatabaseRows +} from './databases' const tmpDirs: string[] = [] async function makeVault(): Promise { @@ -17,19 +23,19 @@ afterEach(async () => { }) describe('createDatabase + readDatabase', () => { - it('creates a .csv + sidecar and reads it back', async () => { + it('creates a .base folder (data.csv + schema.json) and reads it back', async () => { const root = await makeVault() const doc = await createDatabase(root, 'inbox', '', 'Projects') - expect(doc.path.endsWith('Projects.csv')).toBe(true) + expect(doc.path).toBe('inbox/Projects.base/data.csv') expect(doc.title).toBe('Projects') expect(doc.fields.map((f) => f.name)).toEqual(['id', 'Name']) expect(doc.rows).toEqual([]) expect(doc.views).toHaveLength(1) - // both files exist on disk + // both files exist inside the .base folder on disk await expect(readFile(path.join(root, doc.path), 'utf8')).resolves.toContain('id,Name') const sidecar = JSON.parse( - await readFile(path.join(root, `${doc.path}.base.json`), 'utf8') + await readFile(path.join(root, 'inbox/Projects.base/schema.json'), 'utf8') ) expect(sidecar.version).toBe(1) expect(sidecar.fields).toHaveLength(2) @@ -67,7 +73,7 @@ describe('writeDatabaseRows round-trip', () => { }) describe('createRecordPage', () => { - it('creates a page note in a per-database folder', async () => { + it('creates a record-page note inside the database .base folder', async () => { const root = await makeVault() const doc = await createDatabase(root, 'inbox', '', 'Projects') const noteRel = await createRecordPage( @@ -76,12 +82,31 @@ describe('createRecordPage', () => { 'My Task', '---\nName: My Task\n---\n# My Task\n' ) - expect(noteRel.endsWith('.md')).toBe(true) - expect(noteRel).toContain('Projects/') // pages folder named after the db + expect(noteRel).toBe('inbox/Projects.base/My Task.md') // record pages live in the .base folder await expect(readFile(path.join(root, noteRel), 'utf8')).resolves.toContain('# My Task') }) }) +describe('renameDatabase', () => { + it('renames the .base folder, preserving data, schema, and record pages', async () => { + const root = await makeVault() + const doc = await createDatabase(root, 'inbox', '', 'Old') + await createRecordPage(root, doc.path, 'Rec', '# Rec') + + const newPath = await renameDatabase(root, doc.path, 'New Name') + expect(newPath).toBe('inbox/New Name.base/data.csv') + + const reopened = await readDatabase(root, newPath) + expect(reopened.title).toBe('New Name') + // The record page moved with the folder. + await expect( + readFile(path.join(root, 'inbox/New Name.base/Rec.md'), 'utf8') + ).resolves.toContain('# Rec') + // The old folder is gone. + await expect(readFile(path.join(root, 'inbox/Old.base/data.csv'), 'utf8')).rejects.toThrow() + }) +}) + describe('adopting a plain CSV (no sidecar)', () => { it('infers schema, materializes the sidecar + stable ids, and is stable on re-read', async () => { const root = await makeVault() diff --git a/apps/desktop/src/main/databases.ts b/apps/desktop/src/main/databases.ts index e2f82249..06fea523 100644 --- a/apps/desktop/src/main/databases.ts +++ b/apps/desktop/src/main/databases.ts @@ -1,8 +1,9 @@ /** * Main-process IO for CSV-backed databases. Bridges the pure shared-domain - * logic (@shared/database-csv) to disk, mirroring the comments/tasks helpers: - * read/write the `.csv` + co-located `.csv.base.json` sidecar with the atomic - * writer, and adopt a plain CSV (no sidecar) by inferring its schema. + * logic (@shared/database-csv) to disk. A database is a self-contained + * `.base/` folder holding `data.csv`, `schema.json` (the sidecar), and a + * `pages/` folder of record-page notes; a legacy loose `.csv` + co-located + * `.csv.base.json` sidecar is still read (and migrated elsewhere on open). */ import { promises as fs } from 'node:fs' import path from 'node:path' @@ -15,7 +16,12 @@ import { serializeRows } from '@shared/database-csv' import { + csvPathForFormDir, DATABASE_SIDECAR_SUFFIX, + FORM_DIR_SUFFIX, + formDirFromCsvPath, + formTitleFromCsvPath, + isFormDirName, type DatabaseDoc, type DatabaseSidecar, type DatabaseSummary, @@ -39,11 +45,36 @@ function toPosix(p: string): string { return p.replace(/\\/g, '/') } +/** Database title: its `.base` folder name (legacy: the `.csv` basename). */ function titleFromPath(rel: string): string { - const base = toPosix(rel).split('/').filter(Boolean).pop() ?? rel + const posix = toPosix(rel) + if (formDirFromCsvPath(posix)) return formTitleFromCsvPath(posix) + const base = posix.split('/').filter(Boolean).pop() ?? rel return base.replace(/\.csv$/i, '') } +// Record-page paths are stored RELATIVE to the database folder (e.g. `pages/X.md`) +// so a folder rename/move never rewrites them; on read they're resolved to full +// vault-relative paths (e.g. `Books.base/pages/X.md`). Legacy loose databases +// have no folder, so paths pass through unchanged. +function pagesToFull(csvRel: string, pages: Record): Record { + const formDir = formDirFromCsvPath(toPosix(csvRel)) + if (!formDir) return pages + const prefix = `${formDir}/` + return Object.fromEntries( + Object.entries(pages).map(([id, p]) => [id, p.startsWith(prefix) ? p : `${prefix}${p}`]) + ) +} + +function pagesToRelative(csvRel: string, pages: Record): Record { + const formDir = formDirFromCsvPath(toPosix(csvRel)) + if (!formDir) return pages + const prefix = `${formDir}/` + return Object.fromEntries( + Object.entries(pages).map(([id, p]) => [id, p.startsWith(prefix) ? p.slice(prefix.length) : p]) + ) +} + function isMissing(err: unknown): boolean { return (err as NodeJS.ErrnoException)?.code === 'ENOENT' } @@ -84,7 +115,9 @@ function normalizeSidecar(raw: unknown): DatabaseSidecar | null { async function readSidecar(root: string, rel: string): Promise { try { const raw = await fs.readFile(databaseSidecarPath(root, rel), 'utf8') - return normalizeSidecar(JSON.parse(raw)) + const sidecar = normalizeSidecar(JSON.parse(raw)) + if (sidecar?.pages) sidecar.pages = pagesToFull(toPosix(rel), sidecar.pages) + return sidecar } catch (err) { if (isMissing(err)) return null if (err instanceof SyntaxError) return null @@ -135,7 +168,12 @@ async function readPageContentFlags( } async function persistSidecar(root: string, rel: string, sidecar: DatabaseSidecar): Promise { - await writeFileAtomic(databaseSidecarPath(root, rel), `${JSON.stringify(sidecar, null, 2)}\n`) + // Store record-page paths relative to the database folder (so a folder rename + // never rewrites them); a legacy loose database has no folder → unchanged. + const onDisk: DatabaseSidecar = sidecar.pages + ? { ...sidecar, pages: pagesToRelative(toPosix(rel), sidecar.pages) } + : sidecar + await writeFileAtomic(databaseSidecarPath(root, rel), `${JSON.stringify(onDisk, null, 2)}\n`) } // --------------------------------------------------------------------------- @@ -224,18 +262,20 @@ export async function createDatabase( const cleanSub = toPosix(subpath).replace(/^\/+|\/+$/g, '') const dirAbs = cleanSub ? path.join(topAbs, cleanSub) : topAbs const dirRel = toPosix(path.relative(root, dirAbs)) - const makeRel = (name: string): string => (dirRel ? `${dirRel}/${name}.csv` : `${name}.csv`) - // Resolve a non-colliding path under the directory. - let rel = makeRel(baseName) + const makeFormDir = (name: string): string => + dirRel ? `${dirRel}/${name}${FORM_DIR_SUFFIX}` : `${name}${FORM_DIR_SUFFIX}` + // Resolve a non-colliding `.base` folder under the directory. + let formDirRel = makeFormDir(baseName) let n = 2 for (;;) { try { - await fs.access(databaseDataPath(root, rel)) - rel = makeRel(`${baseName} ${n++}`) + await fs.access(databaseDataPath(root, formDirRel)) + formDirRel = makeFormDir(`${baseName} ${n++}`) } catch { break } } + const rel = csvPathForFormDir(formDirRel) const idField: DbField = { id: randomUUID(), name: 'id', type: 'text', hidden: true } const nameField: DbField = { id: randomUUID(), name: 'Name', type: 'text' } @@ -248,15 +288,18 @@ export async function createDatabase( views, activeViewId } + // Create the folder, then the two data files. + await fs.mkdir(databaseDataPath(root, formDirRel), { recursive: true }) await persistSidecar(root, rel, sidecar) await writeFileAtomic(databaseDataPath(root, rel), serializeRows([], fields)) return hydrate(rel, sidecar, []) } /** - * Create a "page" note for a database record under a per-database folder - * (`//.md`) with the given pre-composed body, and return - * its vault-relative path. The pages folder sits next to the `.csv`. + * Create a "page" note for a database record inside the database folder + * (`<Name>.base/<title>.md`) with the given pre-composed body, and return its + * vault-relative path. Record pages live directly in the `.base` folder so they + * nest under the database in the sidebar. */ export async function createRecordPage( root: string, @@ -264,20 +307,62 @@ export async function createRecordPage( title: string, body: string ): Promise<string> { - const posix = toPosix(csvPath) - const slash = posix.lastIndexOf('/') - const dbDir = slash >= 0 ? posix.slice(0, slash) : '' - const dbName = (slash >= 0 ? posix.slice(slash + 1) : posix).replace(/\.csv$/i, '') - const pagesDirRel = dbDir ? `${dbDir}/${dbName}` : dbName - const dirAbs = databaseDataPath(root, pagesDirRel) // resolveSafe + posix + const formDir = formDirFromCsvPath(toPosix(csvPath)) + if (!formDir) throw new Error(`Not a database folder: ${csvPath}`) + const dirAbs = databaseDataPath(root, formDir) // resolveSafe + posix await fs.mkdir(dirAbs, { recursive: true }) const finalTitle = await uniqueTitle(dirAbs, sanitizeNoteTitle(title)) - const noteRel = `${pagesDirRel}/${finalTitle}.md` + const noteRel = `${formDir}/${finalTitle}.md` await fs.writeFile(databaseDataPath(root, noteRel), body, 'utf8') return noteRel } -/** List `.csv` databases in the vault (skips sidecars, trash, and `.zennotes`). */ +/** + * Permanently delete a database: remove its entire `<Name>.base` folder + * (data.csv, schema.json, and the `pages/` record notes) in one shot. + */ +export async function deleteDatabase(root: string, csvRel: string): Promise<void> { + const formDir = formDirFromCsvPath(toPosix(csvRel)) + if (!formDir) throw new Error(`Not a database folder: ${csvRel}`) + await fs.rm(databaseDataPath(root, formDir), { recursive: true, force: true }) +} + +/** + * Rename a database in place: rename its `<oldName>.base` folder to + * `<newName>.base` (non-colliding). Returns the new `data.csv` path. Because the + * data, schema, and pages all live inside, nothing else needs rewriting. + */ +export async function renameDatabase( + root: string, + csvRel: string, + newTitle: string +): Promise<string> { + const formDir = formDirFromCsvPath(toPosix(csvRel)) + if (!formDir) throw new Error(`Not a database folder: ${csvRel}`) + const parentRel = formDir.includes('/') ? formDir.slice(0, formDir.lastIndexOf('/')) : '' + const safeName = (newTitle.trim() || 'Untitled Database').replace(/[\\/:*?"<>|]/g, '-') + const makeFormDir = (name: string): string => + parentRel ? `${parentRel}/${name}${FORM_DIR_SUFFIX}` : `${name}${FORM_DIR_SUFFIX}` + let targetRel = makeFormDir(safeName) + if (toPosix(targetRel) === toPosix(formDir)) return csvRel + let n = 2 + for (;;) { + try { + await fs.access(databaseDataPath(root, targetRel)) + targetRel = makeFormDir(`${safeName} ${n++}`) + } catch { + break + } + } + await fs.rename(databaseDataPath(root, formDir), databaseDataPath(root, targetRel)) + return csvPathForFormDir(targetRel) +} + +/** + * List databases in the vault: each `<Name>.base` folder (surfaced as its + * `data.csv` path), plus any legacy loose `.csv` not yet migrated. Skips + * `.base` internals, trash, and `.zennotes`. + */ export async function listDatabases(root: string): Promise<DatabaseSummary[]> { const out: DatabaseSummary[] = [] const walk = async (dirRel: string): Promise<void> => { @@ -293,11 +378,18 @@ export async function listDatabases(root: string): Promise<DatabaseSummary[]> { if (name.startsWith('.') || name === 'trash' || name === 'node_modules') continue const childRel = dirRel ? `${dirRel}/${name}` : name if (entry.isDirectory()) { + if (isFormDirName(name)) { + // A database folder — surface it, don't descend into its internals. + const rel = csvPathForFormDir(childRel) + out.push({ path: toPosix(rel), title: titleFromPath(rel) }) + continue + } await walk(childRel) } else if ( name.toLowerCase().endsWith('.csv') && !name.toLowerCase().endsWith(`.csv${DATABASE_SIDECAR_SUFFIX}`) ) { + // Legacy loose `.csv` (pre-migration). out.push({ path: toPosix(childRel), title: titleFromPath(childRel) }) } } diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 2941cba8..6fe9aaf1 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -110,6 +110,7 @@ import { writeDatabaseRows, writeDatabaseSchema, createDatabase, + renameDatabase, createRecordPage, listDatabases } from './databases' @@ -2215,6 +2216,11 @@ function registerIpc(): void { } ) + handle(IPC.VAULT_RENAME_DATABASE, async (_e, csvPath: string, newTitle: string) => { + ensureLocalForDatabases() + return await renameDatabase(requireVault().root, csvPath, newTitle) + }) + handle( IPC.VAULT_CREATE_RECORD_PAGE, async (_e, csvPath: string, title: string, body: string) => { diff --git a/apps/desktop/src/main/vault.ts b/apps/desktop/src/main/vault.ts index 8640984b..c4e83da1 100644 --- a/apps/desktop/src/main/vault.ts +++ b/apps/desktop/src/main/vault.ts @@ -42,7 +42,15 @@ import { VaultInfo } from '@shared/ipc' import { DEMO_TOUR_DIR } from '@shared/demo-tour' -import { DATABASE_SIDECAR_SUFFIX } from '@shared/databases' +import { + DATABASE_SIDECAR_SUFFIX, + databaseSchemaPathFor, + FORM_DATA_FILE, + FORM_DIR_SUFFIX, + FORM_SCHEMA_FILE, + isDatabaseInternalPath, + isFormDirName +} from '@shared/databases' import { DEMO_TOUR_ASSETS, DEMO_TOUR_NOTES } from './demo-tour-data' const CONFIG_FILE = 'zennotes.config.json' @@ -674,9 +682,14 @@ export function databaseDataPath(root: string, rel: string): string { return resolveSafe(root, toPosix(rel)) } -/** Absolute path of a database's co-located `.csv.base.json` sidecar. */ +/** + * Absolute path of a database's schema/sidecar. New layout: `<Name>.base/ + * schema.json`. A legacy loose `.csv` (not in a `.base` folder) falls back to + * the co-located `<rel>.csv.base.json` so old vaults still read. + */ export function databaseSidecarPath(root: string, rel: string): string { - return resolveSafe(root, `${toPosix(rel)}${DATABASE_SIDECAR_SUFFIX}`) + const schemaRel = databaseSchemaPathFor(toPosix(rel)) + return resolveSafe(root, schemaRel ?? `${toPosix(rel)}${DATABASE_SIDECAR_SUFFIX}`) } function cloneVaultSettings(settings: VaultSettings): VaultSettings { @@ -1031,6 +1044,137 @@ export async function ensureVaultLayout(root: string): Promise<void> { await fs.writeFile(welcomePath, WELCOME_NOTE, 'utf8') } } + if (!wasEmpty) { + try { + await migrateLegacyDatabases(root) + } catch (err) { + console.error('legacy database migration failed', err) + } + } +} + +/** + * One-time, idempotent migration: convert legacy loose databases — a + * `<dir>/<Name>.csv` + co-located `<dir>/<Name>.csv.base.json` sidecar, with + * record-page notes under `<dir>/<Name>/` — into the self-contained + * `<dir>/<Name>.base/` folder (data.csv + schema.json + record `.md` pages). + * Safe to run on every open: anything already in `.base` form, or whose target + * folder exists, is skipped. Per-database failures are logged, not fatal. + * Returns the number of databases migrated. + */ +export async function migrateLegacyDatabases(root: string): Promise<number> { + let migrated = 0 + const walk = async (dirRel: string): Promise<void> => { + const dirAbs = dirRel ? path.join(root, dirRel) : root + let entries: Dirent[] + try { + entries = await fs.readdir(dirAbs, { withFileTypes: true }) + } catch { + return + } + for (const entry of entries) { + const name = entry.name + if (name.startsWith('.')) continue + const childRel = dirRel ? `${dirRel}/${name}` : name + if (entry.isDirectory()) { + if (isFormDirName(name)) continue // already a database folder + await walk(childRel) + continue + } + const lower = name.toLowerCase() + if (!lower.endsWith('.csv') || lower.endsWith(`.csv${DATABASE_SIDECAR_SUFFIX}`)) continue + // Only migrate a managed database (a `.csv` with a sibling sidecar); a + // plain CSV without one is left as a file. + const sidecarRel = `${childRel}${DATABASE_SIDECAR_SUFFIX}` + try { + await fs.access(resolveSafe(root, sidecarRel)) + } catch { + continue + } + try { + if (await migrateOneLegacyDatabase(root, childRel, sidecarRel)) migrated++ + } catch (err) { + console.error(`database migration failed for ${childRel}`, err) + } + } + } + await walk('') + if (migrated > 0) { + console.log(`[zen] migrated ${migrated} database(s) to the .base folder layout`) + } + return migrated +} + +async function migrateOneLegacyDatabase( + root: string, + csvRel: string, + sidecarRel: string +): Promise<boolean> { + const name = path.basename(csvRel).replace(/\.csv$/i, '') + const parentRel = csvRel.includes('/') ? csvRel.slice(0, csvRel.lastIndexOf('/')) : '' + const formDirRel = parentRel ? `${parentRel}/${name}${FORM_DIR_SUFFIX}` : `${name}${FORM_DIR_SUFFIX}` + const formDirAbs = resolveSafe(root, formDirRel) + // Idempotent / collision safety: never clobber an existing folder. + try { + await fs.access(formDirAbs) + return false + } catch { + /* target free — proceed */ + } + + let sidecar: Record<string, unknown> = {} + try { + sidecar = JSON.parse(await fs.readFile(resolveSafe(root, sidecarRel), 'utf8')) as Record< + string, + unknown + > + } catch { + /* unreadable sidecar — still migrate the data, infer schema on next open */ + } + + await fs.mkdir(formDirAbs, { recursive: true }) + + // Move record-page notes into the folder; rewrite pages → relative basenames. + const pages = + sidecar.pages && typeof sidecar.pages === 'object' + ? (sidecar.pages as Record<string, unknown>) + : {} + const newPages: Record<string, string> = {} + const oldPageDirs = new Set<string>() + for (const [rowId, p] of Object.entries(pages)) { + if (typeof p !== 'string') continue + const srcAbs = resolveSafe(root, p) + try { + await fs.access(srcAbs) + } catch { + continue // page note already gone — drop the mapping + } + const finalTitle = await uniqueTitle(formDirAbs, sanitizeNoteTitle(path.basename(p, '.md'))) + await fs.rename(srcAbs, resolveSafe(root, `${formDirRel}/${finalTitle}.md`)) + newPages[rowId] = `${finalTitle}.md` + if (p.includes('/')) oldPageDirs.add(p.slice(0, p.lastIndexOf('/'))) + } + + // Move the data file, write schema.json (relative pages), drop the old sidecar. + await fs.rename(resolveSafe(root, csvRel), resolveSafe(root, `${formDirRel}/${FORM_DATA_FILE}`)) + const outSidecar: Record<string, unknown> = { ...sidecar } + if (Object.keys(newPages).length > 0) outSidecar.pages = newPages + else delete outSidecar.pages + await writeFileAtomic( + resolveSafe(root, `${formDirRel}/${FORM_SCHEMA_FILE}`), + `${JSON.stringify(outSidecar, null, 2)}\n` + ) + await fs.rm(resolveSafe(root, sidecarRel), { force: true }) + + // Remove now-empty legacy per-database page folders (e.g. `<dir>/<Name>/`). + for (const d of oldPageDirs) { + try { + await fs.rmdir(resolveSafe(root, d)) + } catch { + /* not empty / not ours — leave it */ + } + } + return true } export function vaultInfo(root: string): VaultInfo { @@ -2005,6 +2149,10 @@ export async function listFolders(root: string): Promise<FolderEntry[]> { if (isPrimaryRoot && dirAbs === topAbs && shouldHidePrimaryRootEntry(e.name)) continue const nextSub = subpath ? `${subpath}/${e.name}` : e.name out.push({ folder, subpath: nextSub, siblingOrder: index, isSymlink: e.isSymbolicLink() }) + // A `<Name>.base` database folder is listed (the renderer shows it as a + // database), but we don't descend — its data.csv/schema.json internals + // aren't folders, and its record-page notes are surfaced via listNotes. + if (isFormDirName(e.name)) continue ancestors.add(childReal) await walk(full, childReal, nextSub) ancestors.delete(childReal) @@ -3017,6 +3165,9 @@ export async function listAssets(root: string): Promise<AssetMeta[]> { const childReal = await resolveDirDescent(full, entry, dirReal, ancestors) if (childReal !== null) { if (dirAbs === root && entry.name === INTERNAL_VAULT_DIR) continue + // A `<Name>.base` database folder isn't an asset container — its + // data.csv/schema.json/record notes never appear in the assets list. + if (isFormDirName(entry.name)) continue ancestors.add(childReal) await walk(full, childReal, ancestors) ancestors.delete(childReal) @@ -3024,6 +3175,8 @@ export async function listAssets(root: string): Promise<AssetMeta[]> { } if (!entry.isFile()) continue if (entry.name.toLowerCase().endsWith('.md')) continue + // Legacy co-located sidecar/.bak (pre-migration) — not a user asset. + if (isDatabaseInternalPath(entry.name)) continue let stat try { stat = await fs.stat(full) diff --git a/apps/desktop/src/main/watcher.ts b/apps/desktop/src/main/watcher.ts index 538678cd..b1fbc37a 100644 --- a/apps/desktop/src/main/watcher.ts +++ b/apps/desktop/src/main/watcher.ts @@ -80,8 +80,10 @@ export class VaultWatcher { }) return } - // A `.csv` data file or its `.csv.base.json` sidecar — normalize both to - // the canonical `.csv` path so the renderer re-hydrates the right database. + // Any database file — `<Name>.base/data.csv` or `schema.json` (or a legacy + // loose `.csv`/sidecar) — normalizes to the canonical `data.csv` path so + // the renderer re-hydrates the right database. (Record-page `.md` notes in + // a `.base` folder return null here and ride the normal note path below.) const dbCsvPath = databaseCsvPathFor(toPosix(path.relative(this.root, absPath))) if (dbCsvPath) { onEvent({ diff --git a/apps/desktop/src/mcp/vault-ops.ts b/apps/desktop/src/mcp/vault-ops.ts index a3bf99d1..e438dad6 100644 --- a/apps/desktop/src/mcp/vault-ops.ts +++ b/apps/desktop/src/mcp/vault-ops.ts @@ -15,6 +15,10 @@ import os from 'node:os' export type NoteFolder = 'inbox' | 'quick' | 'archive' | 'trash' const FOLDERS: NoteFolder[] = ['inbox', 'quick', 'archive', 'trash'] const LIVE_FOLDERS: NoteFolder[] = ['inbox', 'quick', 'archive'] +/** A database is a self-contained folder whose name ends with `.base`; its + * internals (data.csv, schema.json, record-page notes) aren't part of the MCP + * note/folder surface, so the walks skip these folders. */ +const isFormDirName = (name: string): boolean => name.toLowerCase().endsWith('.base') const PRIMARY_ATTACHMENTS_DIR = 'attachements' const LEGACY_ATTACHMENTS_DIRS = ['_assets'] const ATTACHMENTS_DIRS = [PRIMARY_ATTACHMENTS_DIR, ...LEGACY_ATTACHMENTS_DIRS] @@ -421,6 +425,7 @@ export async function listNotes(root: string): Promise<NoteMeta[]> { const full = path.join(dirAbs, entry.name) if (entry.isDirectory()) { if (entry.name.startsWith('.')) continue + if (isFormDirName(entry.name)) continue // database folder — not loose notes // When walking the vault root in primary='root' mode, system // subdirectories (quick/, archive/, trash/, attachments) are // not part of inbox — they're walked separately as their own @@ -458,6 +463,7 @@ export async function listFolders(root: string): Promise<{ folder: NoteFolder; s } for (const e of entries) { if (!e.isDirectory() || e.name.startsWith('.')) continue + if (isFormDirName(e.name)) continue // database folder — not a user folder if (isPrimaryRoot && dirAbs === topAbs && HIDDEN_PRIMARY_ROOT_NAMES.has(e.name)) { continue } diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index bbca17e2..739609fe 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -311,6 +311,8 @@ const api: ZenBridge = { ipcRenderer.invoke(IPC.VAULT_WRITE_DATABASE_SCHEMA, relPath, sidecar, rows), createDatabase: (folder: NoteFolder, subpath: string, title?: string): Promise<DatabaseDoc> => ipcRenderer.invoke(IPC.VAULT_CREATE_DATABASE, folder, subpath, title), + renameDatabase: (csvPath: string, newTitle: string): Promise<string> => + ipcRenderer.invoke(IPC.VAULT_RENAME_DATABASE, csvPath, newTitle), createRecordPage: (csvPath: string, title: string, body: string): Promise<string> => ipcRenderer.invoke(IPC.VAULT_CREATE_RECORD_PAGE, csvPath, title, body), listDatabases: (): Promise<DatabaseSummary[]> => ipcRenderer.invoke(IPC.VAULT_LIST_DATABASES), diff --git a/apps/server/internal/vault/vault.go b/apps/server/internal/vault/vault.go index 65044fd5..a2f858a5 100644 --- a/apps/server/internal/vault/vault.go +++ b/apps/server/internal/vault/vault.go @@ -27,8 +27,18 @@ const ( noteCommentsDir = "comments" noteCommentsSuffix = ".comments.json" noteMetaReadLimit = 64 + // formDirSuffix marks a database folder (`<Name>.base/`), a self-contained + // folder holding data.csv, schema.json, and record-page notes. Databases are + // a desktop-only feature; the server hides these folders (it neither serves + // the grid nor exposes the internals as loose notes/assets). + formDirSuffix = ".base" ) +// isFormDirName reports whether a folder name marks a database folder. +func isFormDirName(name string) bool { + return strings.HasSuffix(strings.ToLower(name), formDirSuffix) +} + // ErrAssetTooLarge is returned when an asset upload exceeds the // vault's MaxAssetBytes limit. var ErrAssetTooLarge = errors.New("asset exceeds maximum size") @@ -751,6 +761,9 @@ func (v *Vault) ListNotes() ([]NoteMeta, error) { if strings.HasPrefix(d.Name(), ".") && path != folderRoot { return filepath.SkipDir } + if isFormDirName(d.Name()) { + return filepath.SkipDir // database folder — not loose notes + } if isPrimaryRoot && path != folderRoot { parent := filepath.Dir(path) if filepath.Clean(parent) == filepath.Clean(folderRoot) { @@ -855,6 +868,9 @@ func (v *Vault) ListFolders() ([]FolderEntry, error) { if strings.HasPrefix(d.Name(), ".") { return filepath.SkipDir } + if isFormDirName(d.Name()) { + return filepath.SkipDir // database folder — not a user folder + } if isPrimaryRoot { parent := filepath.Dir(path) if filepath.Clean(parent) == filepath.Clean(folderRoot) { @@ -914,6 +930,9 @@ func (v *Vault) ListAssets() ([]AssetMeta, error) { if filepath.Clean(dir) == filepath.Clean(v.root) && name == internalVaultDir { continue } + if isFormDirName(name) { + continue // database folder — its data.csv/schema.json aren't assets + } if err := walk(full); err != nil { if isSkippableWalkErr(err) { continue @@ -1733,6 +1752,9 @@ func (v *Vault) ScanTasks() ([]Task, error) { if strings.HasPrefix(d.Name(), ".") && path != folderRoot { return filepath.SkipDir } + if isFormDirName(d.Name()) { + return filepath.SkipDir // database folder — not loose notes + } if isPrimaryRoot && path != folderRoot { parent := filepath.Dir(path) if filepath.Clean(parent) == filepath.Clean(folderRoot) { diff --git a/apps/server/internal/vault/vault_test.go b/apps/server/internal/vault/vault_test.go index 908e7413..4014d0a1 100644 --- a/apps/server/internal/vault/vault_test.go +++ b/apps/server/internal/vault/vault_test.go @@ -684,3 +684,81 @@ func TestListSkipsUnreadableEntries(t *testing.T) { t.Fatalf("ListFolders aborted instead of skipping the unreadable dir: %v", err) } } + +func TestDatabaseBaseFolderIsHidden(t *testing.T) { + root := t.TempDir() + v, err := New(root, Options{}) + if err != nil { + t.Fatal(err) + } + // A database folder with its internals + a record-page note. + baseDir := filepath.Join(root, "inbox", "Books.base") + if err := os.MkdirAll(baseDir, 0o700); err != nil { + t.Fatal(err) + } + for name, body := range map[string]string{ + "data.csv": "id,Title\nr1,Dune\n", + "schema.json": `{"version":1}`, + "Dune.md": "# Dune", + } { + if err := os.WriteFile(filepath.Join(baseDir, name), []byte(body), 0o600); err != nil { + t.Fatal(err) + } + } + // A regular note + folder that MUST still surface. + if err := os.WriteFile(filepath.Join(root, "inbox", "Regular.md"), []byte("# Hi"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(root, "inbox", "RealFolder"), 0o700); err != nil { + t.Fatal(err) + } + + notes, err := v.ListNotes() + if err != nil { + t.Fatal(err) + } + for _, n := range notes { + if strings.Contains(n.Path, ".base") { + t.Errorf("ListNotes leaked a database-internal note: %s", n.Path) + } + } + if !hasNotePath(notes, "inbox/Regular.md") { + t.Error("ListNotes dropped a regular note") + } + + folders, err := v.ListFolders() + if err != nil { + t.Fatal(err) + } + sawReal := false + for _, f := range folders { + if strings.Contains(f.Subpath, ".base") { + t.Errorf("ListFolders leaked a database folder: %s", f.Subpath) + } + if f.Subpath == "RealFolder" { + sawReal = true + } + } + if !sawReal { + t.Error("ListFolders dropped a regular folder") + } + + assets, err := v.ListAssets() + if err != nil { + t.Fatal(err) + } + for _, a := range assets { + if strings.Contains(a.Path, ".base") { + t.Errorf("ListAssets leaked a database-internal file: %s", a.Path) + } + } +} + +func hasNotePath(notes []NoteMeta, path string) bool { + for _, n := range notes { + if n.Path == path { + return true + } + } + return false +} diff --git a/apps/web/src/bridge/http-bridge.ts b/apps/web/src/bridge/http-bridge.ts index 4c376062..31f19ea9 100644 --- a/apps/web/src/bridge/http-bridge.ts +++ b/apps/web/src/bridge/http-bridge.ts @@ -576,6 +576,9 @@ function writeDatabaseSchema(): Promise<DatabaseDoc> { function createDatabase(): Promise<DatabaseDoc> { return Promise.reject(new Error(DATABASES_WEB_MSG)) } +function renameDatabase(): Promise<string> { + return Promise.reject(new Error(DATABASES_WEB_MSG)) +} function createRecordPage(): Promise<string> { return Promise.reject(new Error(DATABASES_WEB_MSG)) } @@ -1135,6 +1138,7 @@ export const httpBridge: ZenBridge = { writeDatabaseRows, writeDatabaseSchema, createDatabase, + renameDatabase, createRecordPage, listDatabases, writeNote, diff --git a/packages/app-core/src/components/DatabaseTableView.tsx b/packages/app-core/src/components/DatabaseTableView.tsx index 4b090bc7..6f94f391 100644 --- a/packages/app-core/src/components/DatabaseTableView.tsx +++ b/packages/app-core/src/components/DatabaseTableView.tsx @@ -725,7 +725,11 @@ function Cell({ field, value, editing, onStartEdit, onEndEdit, onCommit }: CellP return ( <button type="button" onClick={onStartEdit} className="block h-full w-full px-2 py-1.5 text-left"> - <span className="block truncate text-ink-900">{field.type === 'date' ? formatDate(value) : value}</span> + {/* min-h reserves one line so an empty cell keeps the same row height as a + filled one (otherwise a new/blank row collapses shorter). (#185) */} + <span className="block min-h-5 truncate text-ink-900"> + {field.type === 'date' ? formatDate(value) : value} + </span> </button> ) } diff --git a/packages/app-core/src/components/DatabaseView.tsx b/packages/app-core/src/components/DatabaseView.tsx index 0dd28b45..bd4079ad 100644 --- a/packages/app-core/src/components/DatabaseView.tsx +++ b/packages/app-core/src/components/DatabaseView.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { csvPathFromDatabaseTab } from '@shared/databases' +import { csvPathFromDatabaseTab, formDirFromCsvPath } from '@shared/databases' import { serializeRows } from '@shared/database-csv' import { useStore } from '../store' import { @@ -36,9 +36,13 @@ export function DatabaseView({ const updateDatabaseSchema = useStore((s) => s.updateDatabaseSchema) const sidebarOpen = useStore((s) => s.sidebarOpen) const toggleSidebar = useStore((s) => s.toggleSidebar) + const renameDatabase = useStore((s) => s.renameDatabase) const [viewMenu, setViewMenu] = useState<{ viewId: string; x: number; y: number } | null>(null) const [renamingView, setRenamingView] = useState<string | null>(null) const [rawMode, setRawMode] = useState(false) + const [editingTitle, setEditingTitle] = useState(false) + // Only `.base` databases rename by title (a legacy loose `.csv` doesn't). + const canRenameTitle = !!csvPath && !!formDirFromCsvPath(csvPath) useEffect(() => { if (csvPath && !doc && !loading) void loadDatabase(csvPath) @@ -80,7 +84,39 @@ export function DatabaseView({ </IconButton> )} <DatabaseIcon className="h-4 w-4 shrink-0 text-ink-500" /> - <h2 className="truncate text-sm font-semibold text-ink-900">{doc.title}</h2> + {editingTitle && canRenameTitle ? ( + <input + autoFocus + defaultValue={doc.title} + size={Math.max(doc.title.length + 1, 6)} + onFocus={(e) => e.currentTarget.select()} + onBlur={(e) => { + const next = e.currentTarget.value.trim() + setEditingTitle(false) + if (next && next !== doc.title && csvPath) void renameDatabase(csvPath, next) + }} + onKeyDown={(e) => { + e.stopPropagation() + if (e.key === 'Enter' && !isImeComposing(e)) e.currentTarget.blur() + else if (e.key === 'Escape') { + e.currentTarget.value = doc.title + e.currentTarget.blur() + } + }} + className="max-w-full rounded border border-accent bg-paper-50 px-1.5 py-0.5 text-sm font-semibold text-ink-900 outline-none" + /> + ) : ( + <h2 + className={[ + 'truncate text-sm font-semibold text-ink-900', + canRenameTitle ? 'cursor-text rounded px-1 -mx-1 hover:bg-paper-200/60' : '' + ].join(' ')} + title={canRenameTitle ? 'Double-click to rename' : undefined} + onDoubleClick={() => canRenameTitle && setEditingTitle(true)} + > + {doc.title} + </h2> + )} <span className="shrink-0 text-xs text-ink-500">{doc.rows.length}</span> <div className="ml-2 flex items-center gap-0.5 rounded-md bg-paper-200/60 p-0.5"> diff --git a/packages/app-core/src/components/Sidebar.tsx b/packages/app-core/src/components/Sidebar.tsx index f32835c0..ff6a1050 100644 --- a/packages/app-core/src/components/Sidebar.tsx +++ b/packages/app-core/src/components/Sidebar.tsx @@ -23,6 +23,7 @@ import { ChevronRightIcon, CheckSquareIcon, CloseIcon, + DatabaseIcon, DocumentIcon, ExpandAllIcon, FolderPlusIcon, @@ -63,6 +64,12 @@ import { } from "../lib/dnd"; import { resolveSystemFolderLabels } from "../lib/system-folder-labels"; import { assetTabPath } from "../lib/asset-tabs"; +import { + csvPathForFormDir, + FORM_DIR_SUFFIX, + formTitleFromDir, + isFormDirName, +} from "@shared/databases"; import { FolderGlyphIcon, resolveFolderIconId, @@ -1737,13 +1744,17 @@ export function Sidebar(): JSX.Element { if (!isTop) { items.push({ kind: "separator" }); + const leafName = subpath.split("/").slice(-1)[0]; + const isDb = isFormDirName(leafName); items.push({ label: "Rename…", onSelect: async () => { - const leaf = subpath.split("/").slice(-1)[0]; + const leaf = leafName; + // A database folder renames by its title; the `.base` suffix is part of + // the folder name and must be preserved. (#185) const next = await promptApp({ - title: "Rename folder", - initialValue: leaf, + title: isDb ? "Rename database" : "Rename folder", + initialValue: isDb ? formTitleFromDir(leaf) : leaf, okLabel: "Rename", validate: (v) => { if (v.includes("/")) return "Use only a leaf name"; @@ -1752,9 +1763,11 @@ export function Sidebar(): JSX.Element { }); if (!next) return; const clean = next.trim().replace(/^\/+|\/+$/g, ""); - if (!clean || clean === leaf) return; + if (!clean) return; + const nextLeaf = isDb ? `${clean}${FORM_DIR_SUFFIX}` : clean; + if (nextLeaf === leaf) return; const parent = subpath.split("/").slice(0, -1).join("/"); - const nextSubpath = parent ? `${parent}/${clean}` : clean; + const nextSubpath = parent ? `${parent}/${nextLeaf}` : nextLeaf; try { await renameFolderAction(folder, subpath, nextSubpath); } catch (err) { @@ -1763,13 +1776,16 @@ export function Sidebar(): JSX.Element { }, }); items.push({ - label: "Delete folder…", + label: isDb ? "Delete database…" : "Delete folder…", danger: true, onSelect: async () => { + const label = isDb ? formTitleFromDir(leafName) : subpath; const ok = await confirmApp({ - title: `Delete "${subpath}" and everything inside it?`, + title: isDb + ? `Delete the "${label}" database and all its records?` + : `Delete "${label}" and everything inside it?`, description: "This cannot be undone.", - confirmLabel: "Delete folder", + confirmLabel: isDb ? "Delete database" : "Delete folder", danger: true, }); if (!ok) return; @@ -3780,6 +3796,10 @@ function SubTree({ node.subpath, vaultSettings.folderIcons, ); + // A `<Name>.base` folder is a database: render it with a database icon and a + // title without the suffix; clicking the row opens the grid, while the + // chevron still expands to reveal its record-page notes. (#185) + const isDatabase = isFormDirName(node.name); const entries = useMemo( () => getTreeRenderEntries(node, showNotes, sortComparator, groupByKind), [node, showNotes, sortComparator, groupByKind], @@ -3815,6 +3835,15 @@ function SubTree({ const handleSelect = ( e: React.MouseEvent | React.KeyboardEvent, ): void => { + if (isDatabase) { + const csvPath = csvPathForFormDir( + vaultRelativeFolderPath(folder, node.subpath, vaultSettings), + ); + onSelectItem(e, { kind: "folder", folder, subpath: node.subpath }, () => { + void useStore.getState().openDatabase(csvPath); + }); + return; + } onSelectItem(e, { kind: "folder", folder, subpath: node.subpath }, () => { setView({ kind: "folder", folder, subpath: node.subpath }); if (hasChildren) { @@ -3827,8 +3856,16 @@ function SubTree({ return ( <div className="flex flex-col"> <TreeRow - icon={iconOption.id === "folder" ? <FolderGlyphIcon open={!isCollapsed && hasChildren} /> : iconOption.icon} - label={node.name} + icon={ + isDatabase ? ( + <DatabaseIcon /> + ) : iconOption.id === "folder" ? ( + <FolderGlyphIcon open={!isCollapsed && hasChildren} /> + ) : ( + iconOption.icon + ) + } + label={isDatabase ? formTitleFromDir(node.name) : node.name} isSymlink={node.isSymlink} count={countNotesInTree(node)} active={isFolderActive(folder, node.subpath)} diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index 96fbf527..6e074607 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -27,6 +27,7 @@ import { TASKS_TAB_PATH, isTasksTabPath } from '@shared/tasks' import type { DatabaseDoc, DatabaseSidecar } from '@shared/databases' import { databaseTabPath, + formTitleFromCsvPath, isDatabaseInternalPath, isDatabaseTabPath, isDatabaseCsvPath @@ -1651,6 +1652,8 @@ interface Store { openDatabase: (csvPath: string) => Promise<void> /** Create a new empty database under `folder`/`subpath` and open it. */ createDatabase: (folder: NoteFolder, subpath?: string, title?: string) => Promise<void> + /** Rename a database (its `.base` folder); rehomes the open grid tab. */ + renameDatabase: (csvPath: string, newTitle: string) => Promise<void> /** Optimistically replace a database's rows and debounce-persist the CSV. */ updateDatabaseRows: (csvPath: string, next: DatabaseDoc) => void /** Optimistically replace a database's schema/views and debounce-persist sidecar + CSV. */ @@ -3026,6 +3029,47 @@ export const useStore = create<Store>((set, get) => { console.error('createDatabase failed', err) } }, + renameDatabase: async (csvPath, newTitle) => { + if (typeof window.zen.renameDatabase !== 'function') return + try { + const newCsvPath = await window.zen.renameDatabase(csvPath, newTitle) + if (!newCsvPath || newCsvPath === csvPath) { + await get().refreshNotes() + return + } + // The `.base` folder moved, so the open grid tab's path changed. Rehome it + // in place (and the cached doc) instead of leaving a stale tab. + const oldTab = databaseTabPath(csvPath) + const newTab = databaseTabPath(newCsvPath) + set((s) => { + const rewrite = (p: string): string => (p === oldTab ? newTab : p) + const ensured = ensureActivePane(rewritePathsInTree(s.paneLayout, rewrite), s.activePaneId) + const databases = { ...s.databases } + const loading = { ...s.databasesLoading } + const prev = databases[csvPath] + if (prev) { + databases[newCsvPath] = { + ...prev, + path: newCsvPath, + title: formTitleFromCsvPath(newCsvPath) + } + delete databases[csvPath] + } + delete loading[csvPath] + return { + paneLayout: ensured.layout, + activePaneId: ensured.activePaneId, + databases, + databasesLoading: loading, + ...activeFieldsFrom(ensured.layout, ensured.activePaneId, s.noteContents, s.noteDirty) + } + }) + await get().refreshNotes() + } catch (err) { + console.error('renameDatabase failed', err) + window.alert(err instanceof Error ? err.message : String(err)) + } + }, updateDatabaseRows: (csvPath, next) => { set((s) => ({ databases: { ...s.databases, [csvPath]: next } })) scheduleDatabaseWrite(csvPath, 'rows', () => get().databases[csvPath]) diff --git a/packages/bridge-contract/src/bridge.ts b/packages/bridge-contract/src/bridge.ts index 44c7eb4d..3111681a 100644 --- a/packages/bridge-contract/src/bridge.ts +++ b/packages/bridge-contract/src/bridge.ts @@ -140,6 +140,8 @@ export interface ZenBridge { writeDatabaseRows(relPath: string, rows: DbRow[]): Promise<DatabaseDoc> writeDatabaseSchema(relPath: string, sidecar: DatabaseSidecar, rows: DbRow[]): Promise<DatabaseDoc> createDatabase(folder: NoteFolder, subpath: string, title?: string): Promise<DatabaseDoc> + /** Rename a database's `.base` folder; resolves to the new `data.csv` path. */ + renameDatabase(csvPath: string, newTitle: string): Promise<string> /** Create a record's "page" note (returns its vault-relative path). */ createRecordPage(csvPath: string, title: string, body: string): Promise<string> listDatabases(): Promise<DatabaseSummary[]> diff --git a/packages/bridge-contract/src/ipc.ts b/packages/bridge-contract/src/ipc.ts index d1a53b97..d08b9e26 100644 --- a/packages/bridge-contract/src/ipc.ts +++ b/packages/bridge-contract/src/ipc.ts @@ -69,6 +69,7 @@ export const IPC = { VAULT_WRITE_DATABASE_ROWS: 'vault:write-database-rows', VAULT_WRITE_DATABASE_SCHEMA: 'vault:write-database-schema', VAULT_CREATE_DATABASE: 'vault:create-database', + VAULT_RENAME_DATABASE: 'vault:rename-database', VAULT_CREATE_RECORD_PAGE: 'vault:create-record-page', VAULT_LIST_DATABASES: 'vault:list-databases', APP_LIST_FONTS: 'app:list-fonts', diff --git a/packages/shared-domain/package.json b/packages/shared-domain/package.json index 101b92b6..dfbfd8a8 100644 --- a/packages/shared-domain/package.json +++ b/packages/shared-domain/package.json @@ -9,7 +9,7 @@ "scripts": { "typecheck": "tsc --noEmit -p tsconfig.json", "build": "tsc --noEmit -p tsconfig.json", - "test": "echo 'No shared-domain tests yet'", - "test:run": "echo 'No shared-domain tests yet'" + "test": "vitest", + "test:run": "vitest run" } } diff --git a/packages/shared-domain/src/databases.test.ts b/packages/shared-domain/src/databases.test.ts new file mode 100644 index 00000000..e1ea513f --- /dev/null +++ b/packages/shared-domain/src/databases.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from 'vitest' +import { + csvPathForFormDir, + databaseCsvPathFor, + databaseSchemaPathFor, + databaseTitleFromTab, + databaseTabPath, + formDirContaining, + formDirFromCsvPath, + formTitleFromCsvPath, + isDatabaseCsvPath, + isDatabaseInternalPath, + isFormDirName, + pagesDirFromCsvPath +} from './databases' + +describe('.base database paths', () => { + it('recognizes a .base folder name', () => { + expect(isFormDirName('Books.base')).toBe(true) + expect(isFormDirName('a/b/Books.base')).toBe(true) + expect(isFormDirName('Books')).toBe(false) + }) + + it('round-trips folder ⇄ data.csv path', () => { + expect(csvPathForFormDir('a/Books.base')).toBe('a/Books.base/data.csv') + expect(formDirFromCsvPath('a/Books.base/data.csv')).toBe('a/Books.base') + expect(formDirFromCsvPath('a/Books.base/other.csv')).toBeNull() + expect(formDirFromCsvPath('a/loose.csv')).toBeNull() + }) + + it('derives schema + pages paths', () => { + expect(databaseSchemaPathFor('a/Books.base/data.csv')).toBe('a/Books.base/schema.json') + expect(pagesDirFromCsvPath('a/Books.base/data.csv')).toBe('a/Books.base/pages') + expect(databaseSchemaPathFor('a/loose.csv')).toBeNull() + }) + + it('derives the title from the folder name', () => { + expect(formTitleFromCsvPath('inbox/Reading List.base/data.csv')).toBe('Reading List') + expect(databaseTitleFromTab(databaseTabPath('inbox/Reading List.base/data.csv'))).toBe( + 'Reading List' + ) + // Legacy loose csv keeps its basename title. + expect(databaseTitleFromTab(databaseTabPath('inbox/Old.csv'))).toBe('Old') + }) + + it('finds the containing .base folder for nested files', () => { + expect(formDirContaining('a/Books.base/pages/r.md')).toBe('a/Books.base') + expect(formDirContaining('a/Books.base/schema.json')).toBe('a/Books.base') + expect(formDirContaining('a/notes/r.md')).toBeNull() + }) + + it('treats .base internals as internal but pages as user-facing', () => { + expect(isDatabaseInternalPath('a/Books.base/data.csv')).toBe(true) + expect(isDatabaseInternalPath('a/Books.base/schema.json')).toBe(true) + expect(isDatabaseInternalPath('a/Books.base/pages/record.md')).toBe(false) + // Legacy. + expect(isDatabaseInternalPath('a/Old.csv.base.json')).toBe(true) + }) + + it('classifies database data files (new + legacy), excluding loose CSV inside a .base', () => { + expect(isDatabaseCsvPath('a/Books.base/data.csv')).toBe(true) + expect(isDatabaseCsvPath('a/Old.csv')).toBe(true) + expect(isDatabaseCsvPath('a/Books.base/extra.csv')).toBe(false) + }) + + it('normalizes any database file back to its data.csv identity', () => { + expect(databaseCsvPathFor('a/Books.base/data.csv')).toBe('a/Books.base/data.csv') + expect(databaseCsvPathFor('a/Books.base/schema.json')).toBe('a/Books.base/data.csv') + expect(databaseCsvPathFor('a/Old.csv')).toBe('a/Old.csv') + expect(databaseCsvPathFor('a/Old.csv.base.json')).toBe('a/Old.csv') + expect(databaseCsvPathFor('a/notes/plain.md')).toBeNull() + }) +}) diff --git a/packages/shared-domain/src/databases.ts b/packages/shared-domain/src/databases.ts index c18ba284..4af30097 100644 --- a/packages/shared-domain/src/databases.ts +++ b/packages/shared-domain/src/databases.ts @@ -4,11 +4,20 @@ * are typed fields. The same data is shown through multiple VIEWS (an editable * Table and a Board grouped by a field). * - * On disk a database is a pair: - * <folder>/<Name>.csv — the data (an `id` UUID column gives rows a - * stable identity across external edits) - * <folder>/<Name>.csv.base.json — the sidecar: field types, select options, - * and view definitions (a CSV can't hold these) + * On disk a database is ONE self-contained folder whose name ends with the + * reserved `.base` suffix (so it never collides with an ordinary folder): + * <Name>.base/data.csv — the data (an `id` UUID column gives rows a + * stable identity across external edits) + * <Name>.base/schema.json — field types, select options, view definitions, + * and the row→page map (a CSV can't hold these) + * <Name>.base/pages/ — the record-page notes (one `.md` per record) + * The database's identity / cache key is its `data.csv` path; its title is the + * folder name minus `.base`. Because everything lives in one folder, rename = + * folder rename and trash/move = folder move (no path rewriting). Record-page + * paths in `schema.json` are stored RELATIVE to the folder for the same reason. + * + * Legacy layout (pre-2.4.0), still recognized so old vaults keep working until + * migrated: a loose `<Name>.csv` + co-located `<Name>.csv.base.json` sidecar. * * This module is PURE (no node/DOM imports) so it is shared by the main process * and the renderer. Cell values are stored as raw CSV strings; the field type @@ -16,11 +25,89 @@ * on load). */ +/** Legacy sidecar suffix (`<Name>.csv.base.json`) — kept only for path guards. */ export const DATABASE_SIDECAR_SUFFIX = '.base.json' export const DEFAULT_ID_FIELD_NAME = 'id' /** Board column key for rows whose group-by cell is empty/unmatched. */ export const EMPTY_GROUP = '__empty__' +/** A database lives in a folder whose name ends with this suffix. */ +export const FORM_DIR_SUFFIX = '.base' +/** Fixed names of a database folder's contents. */ +export const FORM_DATA_FILE = 'data.csv' +export const FORM_SCHEMA_FILE = 'schema.json' +export const FORM_PAGES_DIR = 'pages' + +const toPosixPath = (p: string): string => p.replace(/\\/g, '/') +const lastSegment = (p: string): string => { + const s = toPosixPath(p) + return s.slice(s.lastIndexOf('/') + 1) +} + +/** True when a folder name (or a path's last segment) marks a database folder. */ +export function isFormDirName(nameOrPath: string): boolean { + return lastSegment(nameOrPath).toLowerCase().endsWith(FORM_DIR_SUFFIX) +} + +/** + * The database folder path for a `data.csv` path, or null when `csvPath` isn't a + * database data file. e.g. `a/X.base/data.csv` → `a/X.base`. + */ +export function formDirFromCsvPath(csvPath: string): string | null { + const p = toPosixPath(csvPath) + const slash = p.lastIndexOf('/') + if (slash < 0) return null + const dir = p.slice(0, slash) + const file = p.slice(slash + 1) + if (file.toLowerCase() !== FORM_DATA_FILE) return null + return isFormDirName(dir) ? dir : null +} + +/** The `data.csv` path for a database folder path. e.g. `a/X.base` → `a/X.base/data.csv`. */ +export function csvPathForFormDir(formDir: string): string { + return `${toPosixPath(formDir)}/${FORM_DATA_FILE}` +} + +/** The `schema.json` path for a database's `data.csv` path, or null. */ +export function databaseSchemaPathFor(csvPath: string): string | null { + const dir = formDirFromCsvPath(csvPath) + return dir ? `${dir}/${FORM_SCHEMA_FILE}` : null +} + +/** The pages-dir path for a database's `data.csv` path, or null. */ +export function pagesDirFromCsvPath(csvPath: string): string | null { + const dir = formDirFromCsvPath(csvPath) + return dir ? `${dir}/${FORM_PAGES_DIR}` : null +} + +/** + * If `relPath` lives inside a `.base` database folder, return that folder path; + * otherwise null. e.g. `a/X.base/pages/r.md` → `a/X.base`. + */ +export function formDirContaining(relPath: string): string | null { + const parts = toPosixPath(relPath).split('/') + for (let i = 0; i < parts.length; i++) { + if (parts[i].toLowerCase().endsWith(FORM_DIR_SUFFIX)) { + return parts.slice(0, i + 1).join('/') + } + } + return null +} + +/** Display title from a database folder name/path (strips the `.base` suffix). */ +export function formTitleFromDir(nameOrPath: string): string { + const name = lastSegment(nameOrPath) + return name.toLowerCase().endsWith(FORM_DIR_SUFFIX) + ? name.slice(0, -FORM_DIR_SUFFIX.length) + : name +} + +/** Display title from a database's `data.csv` path. */ +export function formTitleFromCsvPath(csvPath: string): string { + const dir = formDirFromCsvPath(csvPath) + return dir ? formTitleFromDir(dir) : csvPath +} + export type FieldType = 'text' | 'number' | 'checkbox' | 'date' | 'select' | 'multiSelect' export interface SelectOption { @@ -94,7 +181,7 @@ export interface DbView { cardFieldIds?: string[] } -/** The sidecar JSON written to `<name>.csv.base.json`. */ +/** The sidecar JSON written to `<Name>.base/schema.json`. */ export interface DatabaseSidecar { version: 1 /** Field whose cells hold the row UUID (its `name` is the CSV header). */ @@ -116,9 +203,9 @@ export interface DbRow { /** Fully-hydrated database handed to the renderer (sidecar + rows + identity). */ export interface DatabaseDoc extends DatabaseSidecar { - /** Vault-relative POSIX path of the `.csv` — identity / cache key. */ + /** Vault-relative POSIX path of the `data.csv` — identity / cache key. */ path: string - /** Basename without `.csv`. */ + /** Database name: the `.base` folder name (legacy: the `.csv` basename). */ title: string rows: DbRow[] /** @@ -164,6 +251,8 @@ export function csvPathFromDatabaseTab(path: string | null | undefined): string export function databaseTitleFromTab(path: string | null | undefined): string { const csv = csvPathFromDatabaseTab(path) if (!csv) return 'Database' + // A database's title is its `<Name>.base` folder name, not the `data.csv` basename. + if (formDirFromCsvPath(csv)) return formTitleFromCsvPath(csv) const base = csv.split('/').filter(Boolean).pop() ?? csv return base.replace(/\.csv$/i, '') } @@ -174,11 +263,19 @@ export function isDatabaseSidecarPath(relPath: string): boolean { } /** - * True for files that belong to a database but aren't the user-facing `.csv` — - * the sidecar and any `.bak` backups. These are hidden from the note list. + * True for files that belong to a database but aren't the user-facing entity. + * New layout: inside a `.base` folder, the record-page notes (`*.md`) are + * user-facing (they nest under the database); `data.csv`, `schema.json`, and any + * backups are internal. Legacy: the co-located sidecar and `.bak` backups. + * Hidden from the note/asset list. */ export function isDatabaseInternalPath(relPath: string): boolean { - const l = relPath.toLowerCase() + const p = toPosixPath(relPath) + if (formDirContaining(p)) { + // Record pages are markdown; everything else in the folder is internal. + return !p.toLowerCase().endsWith('.md') + } + const l = p.toLowerCase() return ( l.endsWith(`.csv${DATABASE_SIDECAR_SUFFIX}`) || l.endsWith('.csv.bak') || @@ -186,15 +283,30 @@ export function isDatabaseInternalPath(relPath: string): boolean { ) } -/** True for a database data file (`*.csv`, but not the sidecar). */ +/** True for a database data file: new `<Name>.base/data.csv`, or a legacy loose `.csv`. */ export function isDatabaseCsvPath(relPath: string): boolean { - const lower = relPath.toLowerCase() + if (formDirFromCsvPath(relPath)) return true + const lower = toPosixPath(relPath).toLowerCase() + // Legacy loose CSV — but not one that lives inside a `.base` folder. + if (formDirContaining(relPath)) return false return lower.endsWith('.csv') && !lower.endsWith(`.csv${DATABASE_SIDECAR_SUFFIX}`) } -/** Given a `.csv` or its `.base.json` sidecar, return the canonical `.csv` path. */ +/** + * Given any file that belongs to a database (`data.csv`, `schema.json`, or a + * legacy `.csv`/sidecar), return the canonical `data.csv` path; null otherwise. + * Used to normalize a watcher event on any database file back to its identity. + */ export function databaseCsvPathFor(relPath: string): string | null { - if (isDatabaseSidecarPath(relPath)) return relPath.slice(0, -DATABASE_SIDECAR_SUFFIX.length) - if (isDatabaseCsvPath(relPath)) return relPath + const p = toPosixPath(relPath) + if (p.toLowerCase().endsWith(`/${FORM_SCHEMA_FILE}`)) { + const dir = p.slice(0, p.lastIndexOf('/')) + if (isFormDirName(dir)) return `${dir}/${FORM_DATA_FILE}` + } + if (formDirFromCsvPath(p)) return p + // Legacy: a `<Name>.csv.base.json` sidecar maps to its `.csv`. + if (isDatabaseSidecarPath(p)) return p.slice(0, -DATABASE_SIDECAR_SUFFIX.length) + // Legacy loose `.csv` (not inside a `.base` folder). + if (!formDirContaining(p) && isDatabaseCsvPath(p)) return p return null } From d20c782c065ab4027403258fc33ae275be086145 Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Tue, 16 Jun 2026 15:07:10 -0500 Subject: [PATCH 31/66] Add back/forward nav and restyle the tab bar (from #185) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Back/forward history navigation: ◀ ▶ buttons in the tab-bar header (wired to the existing jump history, dimmed when there's nothing to go to) plus `global.historyBack`/`historyForward` shortcuts bound to Alt+←/Alt+→ on every platform (VS Code-style); vim's Ctrl+O/Ctrl+I history nav is unchanged. Restyle the workspace tabs from rounded "browser pills" to flat, full-height VS Code-style segments: right-border separators, no rounded tops, the active tab filled with an accent ring, inactive tabs muted with hover. The close button stays on the active tab and reveals on hover for the rest, and the active tab scrolls into view when switching among many tabs. --- packages/app-core/src/App.tsx | 11 +++ .../app-core/src/components/EditorPane.tsx | 93 +++++++++++++++---- packages/app-core/src/components/icons.tsx | 14 +++ packages/app-core/src/lib/keymaps.ts | 20 ++++ .../app-core/src/lib/tab-strip-style.test.ts | 5 +- 5 files changed, 121 insertions(+), 22 deletions(-) diff --git a/packages/app-core/src/App.tsx b/packages/app-core/src/App.tsx index c21dd2eb..4df35052 100644 --- a/packages/app-core/src/App.tsx +++ b/packages/app-core/src/App.tsx @@ -529,6 +529,17 @@ function App(): JSX.Element { void window.zen.resetAppZoom() return } + if (matchesShortcut(e, overrides, 'global.historyBack')) { + // Back in note navigation history (works in any mode). + e.preventDefault() + void state.jumpToPreviousNote() + return + } + if (matchesShortcut(e, overrides, 'global.historyForward')) { + e.preventDefault() + void state.jumpToNextNote() + return + } if (matchesShortcut(e, overrides, 'global.searchNotes')) { // ⌘P — note search e.preventDefault() diff --git a/packages/app-core/src/components/EditorPane.tsx b/packages/app-core/src/components/EditorPane.tsx index ace10f7e..eec7d24d 100644 --- a/packages/app-core/src/components/EditorPane.tsx +++ b/packages/app-core/src/components/EditorPane.tsx @@ -134,6 +134,8 @@ import { } from '../lib/preview-outline-jump' import { ArchiveIcon, + ArrowLeftIcon, + ArrowRightIcon, ArrowUpRightIcon, CalendarIcon, CheckSquareIcon, @@ -628,6 +630,11 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { const textFont = useStore((s) => s.textFont) const tabsEnabled = useStore((s) => s.tabsEnabled) const wrapTabs = useStore((s) => s.wrapTabs) + const jumpToPreviousNote = useStore((s) => s.jumpToPreviousNote) + const jumpToNextNote = useStore((s) => s.jumpToNextNote) + const canGoBack = useStore((s) => s.noteBackstack.length > 0) + const canGoForward = useStore((s) => s.noteForwardstack.length > 0) + const tabNavOverrides = useStore((s) => s.keymapOverrides) const workspaceMode = useStore((s) => s.workspaceMode) const wordWrap = useStore((s) => s.wordWrap) const systemFolderLabels = useStore((s) => s.systemFolderLabels) @@ -2510,15 +2517,17 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { )} <div className={[ - 'group flex h-8 min-w-0 items-center gap-1 rounded-t-lg border border-b-0 px-1.5 text-sm transition-colors', + // Flat, full-height segmented tabs (VS Code-style): right-border + // separators, no rounded tops; the active tab is filled. (#185) + 'group relative flex h-full min-h-8 min-w-0 items-center gap-1.5 border-r border-paper-300/60 px-2 text-sm transition-colors', tab.pinned ? 'max-w-[140px]' : 'max-w-[220px]', active && isActive ? focusedPanel === 'tabs' - ? 'border-accent/70 bg-paper-100 text-ink-900 ring-1 ring-inset ring-accent/60' - : 'border-paper-300/80 bg-paper-100 text-ink-900' + ? 'bg-paper-200 font-medium text-ink-900 ring-1 ring-inset ring-accent/60' + : 'bg-paper-200 font-medium text-ink-900' : active - ? 'border-paper-300/60 bg-paper-100/70 text-ink-800' - : 'border-transparent bg-paper-200/45 text-ink-500 hover:bg-paper-200/70 hover:text-ink-900' + ? 'bg-paper-200/70 text-ink-700' + : 'text-ink-500 hover:bg-paper-200/40 hover:text-ink-900' ].join(' ')} > {tab.pinned && ( @@ -2573,10 +2582,12 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { aria-label={`Close ${tab.title}`} onClick={() => void closeTabInPane(paneId, tab.path)} className={[ - 'flex h-4 w-4 shrink-0 items-center justify-center rounded-sm transition-colors', + // The active tab keeps its close affordance; inactive tabs + // reveal it on hover/focus (VS Code convention). (#185) + 'flex h-4 w-4 shrink-0 items-center justify-center rounded-sm transition', active ? 'text-ink-500 hover:bg-paper-200 hover:text-ink-900' - : 'hover:bg-paper-300/70' + : 'opacity-0 hover:bg-paper-300/70 group-hover:opacity-100 focus-visible:opacity-100' ].join(' ')} > <CloseIcon width={12} height={12} /> @@ -2735,11 +2746,24 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { } }, [hasTabs, wrapTabs, tabStripMeasureKey]) + // Keep the active tab scrolled into view in the horizontally-scrolling strip, + // so switching tabs (e.g. via the keyboard) never leaves it off-screen. (#185) + useEffect(() => { + if (!hasTabs || wrapTabs || !activeTab) return + const el = tabStripRef.current?.querySelector<HTMLElement>('[data-tab-active="true"]') + el?.scrollIntoView({ inline: 'nearest', block: 'nearest' }) + }, [activeTab, hasTabs, wrapTabs, tabStripMeasureKey]) + + // Outer header holds the back/forward nav buttons + the (flex-1) tab strip. + const tabStripHeaderClass = [ + 'glass-header flex shrink-0 items-stretch border-b border-paper-300/70 pl-1', + wrapTabs ? 'min-h-10' : 'h-10' + ].join(' ') const tabStripClass = [ - 'workspace-tab-strip glass-header flex shrink-0 items-start gap-1 border-b border-paper-300/70 px-3 pt-2', + 'workspace-tab-strip flex min-w-0 flex-1 items-stretch gap-0', wrapTabs ? 'min-h-10 flex-wrap content-start overflow-x-hidden overflow-y-visible' - : `${tabStripOverflowing ? 'h-14 overflow-x-auto' : 'h-10 overflow-x-hidden'} overflow-y-hidden` + : `h-10 ${tabStripOverflowing ? 'overflow-x-auto' : 'overflow-x-hidden'} overflow-y-hidden` ].join(' ') const deferEditorHydration = shouldDeferEditorHydration( showEditor, @@ -2962,13 +2986,36 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { }} > {hasTabs && ( - <div - ref={tabStripRef} - className={tabStripClass} - onDragOver={handleTabStripDragOver} - onDrop={handleTabStripDrop} - > - {tabItems.map((tab, i) => { + <div className={tabStripHeaderClass}> + <div className="flex shrink-0 items-center gap-0.5 self-center"> + <IconBtn + title={`Go back (${getKeymapDisplay( + tabNavOverrides, + vimMode ? 'vim.historyBack' : 'global.historyBack' + )})`} + onClick={() => void jumpToPreviousNote()} + disabled={!canGoBack} + > + <ArrowLeftIcon width={16} height={16} /> + </IconBtn> + <IconBtn + title={`Go forward (${getKeymapDisplay( + tabNavOverrides, + vimMode ? 'vim.historyForward' : 'global.historyForward' + )})`} + onClick={() => void jumpToNextNote()} + disabled={!canGoForward} + > + <ArrowRightIcon width={16} height={16} /> + </IconBtn> + </div> + <div + ref={tabStripRef} + className={tabStripClass} + onDragOver={handleTabStripDragOver} + onDrop={handleTabStripDrop} + > + {tabItems.map((tab, i) => { // Draw a subtle vertical separator between the last pinned // tab and the first unpinned one (VSCode convention). The // separator is a flex sibling, not a wrapper, so drag hit- @@ -2987,6 +3034,7 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { </Fragment> ) })} + </div> </div> )} {content && !zenMode && ( @@ -3448,24 +3496,29 @@ function IconBtn({ children, onClick, title, - active = false + active = false, + disabled = false }: { children: JSX.Element onClick: () => void title: string active?: boolean + disabled?: boolean }): JSX.Element { return ( <button type="button" onClick={onClick} + disabled={disabled} aria-label={title} aria-pressed={active} className={[ 'group relative flex h-7 w-7 items-center justify-center rounded-md transition-colors', - active - ? 'bg-paper-200 text-ink-900' - : 'text-ink-500 hover:bg-paper-200 hover:text-ink-900' + disabled + ? 'cursor-default text-ink-500/40' + : active + ? 'bg-paper-200 text-ink-900' + : 'text-ink-500 hover:bg-paper-200 hover:text-ink-900' ].join(' ')} > <span className="pointer-events-none">{children}</span> diff --git a/packages/app-core/src/components/icons.tsx b/packages/app-core/src/components/icons.tsx index 638dc6be..f2723bd5 100644 --- a/packages/app-core/src/components/icons.tsx +++ b/packages/app-core/src/components/icons.tsx @@ -141,6 +141,20 @@ export const ChevronLeftIcon = (p: IconProps): JSX.Element => ( </I> ) +export const ArrowLeftIcon = (p: IconProps): JSX.Element => ( + <I {...p}> + <path d="M19 12H5" /> + <path d="m12 19-7-7 7-7" /> + </I> +) + +export const ArrowRightIcon = (p: IconProps): JSX.Element => ( + <I {...p}> + <path d="M5 12h14" /> + <path d="m12 5 7 7-7 7" /> + </I> +) + export const CalendarIcon = (p: IconProps): JSX.Element => ( <I {...p}> <rect x="3" y="5" width="18" height="16" rx="2" /> diff --git a/packages/app-core/src/lib/keymaps.ts b/packages/app-core/src/lib/keymaps.ts index 4a29a7ba..92a39115 100644 --- a/packages/app-core/src/lib/keymaps.ts +++ b/packages/app-core/src/lib/keymaps.ts @@ -33,6 +33,8 @@ export type KeymapId = | "global.zoomIn" | "global.zoomOut" | "global.zoomReset" + | "global.historyBack" + | "global.historyForward" | "vim.leaderPrefix" | "vim.leaderOpenBuffers" | "vim.leaderSearchNotes" @@ -321,6 +323,24 @@ const KEYMAP_DEFINITIONS: KeymapDefinition[] = [ description: "Restore the app zoom factor to its default size.", defaultBinding: "Mod+0", }, + { + id: "global.historyBack", + kind: "shortcut", + scope: "app", + group: "global", + title: "Go back in note history", + description: "Jump to the previous note location in history. Works in any mode.", + defaultBinding: "Alt+ArrowLeft", + }, + { + id: "global.historyForward", + kind: "shortcut", + scope: "app", + group: "global", + title: "Go forward in note history", + description: "Jump forward in note history. Works in any mode.", + defaultBinding: "Alt+ArrowRight", + }, { id: "vim.leaderPrefix", kind: "sequence", diff --git a/packages/app-core/src/lib/tab-strip-style.test.ts b/packages/app-core/src/lib/tab-strip-style.test.ts index 51d6011a..fcc769fc 100644 --- a/packages/app-core/src/lib/tab-strip-style.test.ts +++ b/packages/app-core/src/lib/tab-strip-style.test.ts @@ -16,10 +16,11 @@ const stylesSource = readFileSync(new URL('../styles/index.css', import.meta.url describe('workspace tab strip overflow styles', () => { it('keeps horizontal tab overflow visible without lifting tabs', () => { expect(editorPaneSource).toContain('workspace-tab-strip') + // Flat tabs stay at h-10 and scroll horizontally when overflowing (no lift). expect(editorPaneSource).toContain( - "tabStripOverflowing ? 'h-14 overflow-x-auto' : 'h-10 overflow-x-hidden'" + "tabStripOverflowing ? 'overflow-x-auto' : 'overflow-x-hidden'" ) - expect(editorPaneSource).toContain('items-start') + expect(editorPaneSource).toContain('items-stretch') expect(stylesSource).toMatch( /\.workspace-tab-strip::-webkit-scrollbar\s*\{[^}]*height:\s*6px/s ) From 6cf89df31dacd1e769cae3ced247e55f4743d497 Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Tue, 16 Jun 2026 15:41:41 -0500 Subject: [PATCH 32/66] Unify assets under assets/ + add an Assets view (from #185) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assets are no longer mixed with notes: they live in a top-level `assets/` folder. A `migrateLooseAssets` step runs on vault open and moves loose root-level attachments plus the legacy `attachements/`/`_assets/` dirs into `assets/`, skipping any basename collision so existing `![[…]]`/`![](…)` embeds keep resolving (the resolver falls back to a unique basename). Notes and `.base` databases are never touched. New imports — including pasted images — now land in `assets/`. A dedicated Assets view (the `zen://assets` tab + a sidebar entry) browses every asset in one place, with filter, open, and delete. The desktop main, MCP server, and self-hosted Go server all recognize `assets/` (reserved root); the Go server lists from it. Asset migration runs on the desktop. Adds migration tests and updates the paste-image tests for the new target. --- apps/desktop/src/main/assets-migrate.test.ts | 67 +++++++++ apps/desktop/src/main/vault.test.ts | 19 +-- apps/desktop/src/main/vault.ts | 97 +++++++++++- apps/desktop/src/main/watcher.ts | 2 +- apps/desktop/src/mcp/vault-ops.ts | 5 +- apps/server/internal/vault/vault.go | 9 +- apps/server/internal/watcher/watcher.go | 4 +- .../app-core/src/components/AssetsView.tsx | 140 ++++++++++++++++++ .../app-core/src/components/EditorPane.tsx | 18 +++ packages/app-core/src/components/Sidebar.tsx | 18 +++ packages/app-core/src/components/icons.tsx | 14 ++ packages/app-core/src/lib/vault-layout.ts | 1 + packages/app-core/src/store.ts | 19 +++ packages/shared-domain/src/assets-view.ts | 17 +++ 14 files changed, 407 insertions(+), 23 deletions(-) create mode 100644 apps/desktop/src/main/assets-migrate.test.ts create mode 100644 packages/app-core/src/components/AssetsView.tsx create mode 100644 packages/shared-domain/src/assets-view.ts diff --git a/apps/desktop/src/main/assets-migrate.test.ts b/apps/desktop/src/main/assets-migrate.test.ts new file mode 100644 index 00000000..81882418 --- /dev/null +++ b/apps/desktop/src/main/assets-migrate.test.ts @@ -0,0 +1,67 @@ +import { mkdtemp, mkdir, rm, writeFile, access } from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { migrateLooseAssets, listAssets } from './vault' + +const tempDirs: string[] = [] + +async function makeVault(): Promise<string> { + const dir = await mkdtemp(path.join(os.tmpdir(), 'zen-amig-')) + tempDirs.push(dir) + return dir +} + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((d) => rm(d, { recursive: true, force: true }))) +}) + +const exists = (root: string, rel: string): Promise<boolean> => + access(path.join(root, rel)) + .then(() => true) + .catch(() => false) + +describe('migrateLooseAssets (#185 assets/ unification)', () => { + it('moves root-level attachments + legacy dirs into assets/, never notes or databases', async () => { + const root = await makeVault() + await writeFile(path.join(root, 'photo.png'), 'x', 'utf8') + await writeFile(path.join(root, 'note.md'), '# hi', 'utf8') // a note — must stay + await writeFile(path.join(root, 'books.csv'), 'id\n', 'utf8') // legacy db — must stay + await mkdir(path.join(root, 'attachements'), { recursive: true }) + await writeFile(path.join(root, 'attachements', 'doc.pdf'), 'x', 'utf8') + + const { moved, skipped } = await migrateLooseAssets(root) + expect(moved.sort()).toEqual(['assets/doc.pdf', 'assets/photo.png']) + expect(skipped).toEqual([]) + + expect(await exists(root, 'assets/photo.png')).toBe(true) + expect(await exists(root, 'assets/doc.pdf')).toBe(true) + expect(await exists(root, 'photo.png')).toBe(false) + expect(await exists(root, 'attachements')).toBe(false) // emptied + removed + // Notes and database files are untouched. + expect(await exists(root, 'note.md')).toBe(true) + expect(await exists(root, 'books.csv')).toBe(true) + }) + + it('skips a file whose basename already exists in assets/ (keeps refs unambiguous)', async () => { + const root = await makeVault() + await mkdir(path.join(root, 'assets'), { recursive: true }) + await writeFile(path.join(root, 'assets', 'logo.png'), 'existing', 'utf8') + await writeFile(path.join(root, 'logo.png'), 'loose', 'utf8') + + const { moved, skipped } = await migrateLooseAssets(root) + expect(moved).toEqual([]) + expect(skipped).toEqual(['logo.png']) + expect(await exists(root, 'logo.png')).toBe(true) // left in place + }) + + it('is idempotent and surfaces migrated assets via listAssets', async () => { + const root = await makeVault() + await writeFile(path.join(root, 'pic.png'), 'x', 'utf8') + expect((await migrateLooseAssets(root)).moved).toEqual(['assets/pic.png']) + expect((await migrateLooseAssets(root)).moved).toEqual([]) + + const assets = await listAssets(root) + expect(assets.map((a) => a.path)).toContain('assets/pic.png') + }) +}) diff --git a/apps/desktop/src/main/vault.test.ts b/apps/desktop/src/main/vault.test.ts index 392bc67b..7656f85b 100644 --- a/apps/desktop/src/main/vault.test.ts +++ b/apps/desktop/src/main/vault.test.ts @@ -137,7 +137,7 @@ describe('appendToNote', () => { }) describe('importPastedImage', () => { - it('writes clipboard image bytes to the vault root and returns a wiki embed', async () => { + it('writes clipboard image bytes into assets/ and returns a wiki embed', async () => { const root = await makeTempDir('zennotes-paste-image-') await ensureVaultLayout(root) @@ -153,19 +153,20 @@ describe('importPastedImage', () => { expect(imported).toEqual({ name: 'Screenshot 2026-05-13.png', - path: 'Screenshot 2026-05-13.png', - markdown: '![[Screenshot 2026-05-13.png]]', + path: 'assets/Screenshot 2026-05-13.png', + markdown: '![[assets/Screenshot 2026-05-13.png]]', kind: 'image' }) - await expect(readFile(path.join(root, 'Screenshot 2026-05-13.png'))).resolves.toEqual( + await expect(readFile(path.join(root, 'assets/Screenshot 2026-05-13.png'))).resolves.toEqual( Buffer.from([137, 80, 78, 71]) ) }) - it('generates a unique root filename when the clipboard has no useful name', async () => { + it('generates a unique filename in assets/ when the clipboard has no useful name', async () => { const root = await makeTempDir('zennotes-paste-image-name-') await ensureVaultLayout(root) - await writeFile(path.join(root, 'Pasted Image 2026-05-13 150405.webp'), 'existing', 'utf8') + await mkdir(path.join(root, 'assets'), { recursive: true }) + await writeFile(path.join(root, 'assets/Pasted Image 2026-05-13 150405.webp'), 'existing', 'utf8') const imported = await importPastedImage( root, @@ -177,9 +178,9 @@ describe('importPastedImage', () => { ) expect(imported.name).toBe('Pasted Image 2026-05-13 150405 2.webp') - expect(imported.path).toBe('Pasted Image 2026-05-13 150405 2.webp') - expect(imported.markdown).toBe('![[Pasted Image 2026-05-13 150405 2.webp]]') - await expect(readFile(path.join(root, imported.name))).resolves.toEqual(Buffer.from([1, 2, 3])) + expect(imported.path).toBe('assets/Pasted Image 2026-05-13 150405 2.webp') + expect(imported.markdown).toBe('![[assets/Pasted Image 2026-05-13 150405 2.webp]]') + await expect(readFile(path.join(root, imported.path))).resolves.toEqual(Buffer.from([1, 2, 3])) }) }) diff --git a/apps/desktop/src/main/vault.ts b/apps/desktop/src/main/vault.ts index c4e83da1..29b5541e 100644 --- a/apps/desktop/src/main/vault.ts +++ b/apps/desktop/src/main/vault.ts @@ -44,6 +44,7 @@ import { import { DEMO_TOUR_DIR } from '@shared/demo-tour' import { DATABASE_SIDECAR_SUFFIX, + databaseCsvPathFor, databaseSchemaPathFor, FORM_DATA_FILE, FORM_DIR_SUFFIX, @@ -56,9 +57,12 @@ import { DEMO_TOUR_ASSETS, DEMO_TOUR_NOTES } from './demo-tour-data' const CONFIG_FILE = 'zennotes.config.json' const FOLDERS: NoteFolder[] = ['inbox', 'quick', 'archive', 'trash'] const SYSTEM_FOLDERS = new Set<NoteFolder>(FOLDERS) +// Assets are unified under a top-level `assets/` folder. `attachements`/`_assets` +// are recognized legacy dirs (read + migrated, never the import target). (#185) +const ASSETS_DIR = 'assets' const PRIMARY_ATTACHMENTS_DIR = 'attachements' -const LEGACY_ATTACHMENTS_DIRS = ['_assets'] -const ATTACHMENTS_DIRS = [PRIMARY_ATTACHMENTS_DIR, ...LEGACY_ATTACHMENTS_DIRS] +const LEGACY_ATTACHMENTS_DIRS = [PRIMARY_ATTACHMENTS_DIR, '_assets'] +const ATTACHMENTS_DIRS = [ASSETS_DIR, ...LEGACY_ATTACHMENTS_DIRS] const INTERNAL_VAULT_DIR = '.zennotes' const DELETED_ASSETS_DIR = 'deleted-assets' const VAULT_SETTINGS_FILE = 'vault.json' @@ -1050,7 +1054,82 @@ export async function ensureVaultLayout(root: string): Promise<void> { } catch (err) { console.error('legacy database migration failed', err) } + try { + await migrateLooseAssets(root) + } catch (err) { + console.error('loose asset migration failed', err) + } + } +} + +/** + * One-time, idempotent migration: unify assets under `assets/`. Moves loose + * root-level attachments and any files in the legacy `attachements/` / `_assets/` + * dirs into `assets/`, skipping a file whose basename already exists there (so + * the basename-fallback resolution of `![[…]]`/`![](…)` embeds stays + * unambiguous). Never touches notes (`.md`) or database files. Safe on every + * open. Returns the moved (assets-relative) and skipped (source) paths. + */ +export async function migrateLooseAssets( + root: string +): Promise<{ moved: string[]; skipped: string[] }> { + const moved: string[] = [] + const skipped: string[] = [] + const assetsAbs = path.join(root, ASSETS_DIR) + + const moveIntoAssets = async (srcRel: string): Promise<void> => { + const destRel = `${ASSETS_DIR}/${path.basename(srcRel)}` + if (toPosix(srcRel) === destRel) return + const destAbs = resolveSafe(root, destRel) + try { + await fs.access(destAbs) // a same-named asset already exists — don't clobber/rename + skipped.push(toPosix(srcRel)) + return + } catch { + /* destination free */ + } + await fs.mkdir(assetsAbs, { recursive: true }) + await fs.rename(resolveSafe(root, srcRel), destAbs) + moved.push(destRel) + } + + // 1. Loose asset files sitting directly at the vault root. + let rootEntries: Dirent[] + try { + rootEntries = await fs.readdir(root, { withFileTypes: true }) + } catch { + return { moved, skipped } } + for (const entry of rootEntries) { + if (entry.name.startsWith('.') || !entry.isFile()) continue + if (entry.name.toLowerCase().endsWith('.md')) continue // a note + if (databaseCsvPathFor(entry.name) || isDatabaseInternalPath(entry.name)) continue // a database + await moveIntoAssets(entry.name) + } + + // 2. Files in the legacy attachment dirs, then drop the dir if it empties. + for (const dir of LEGACY_ATTACHMENTS_DIRS) { + let entries: Dirent[] + try { + entries = await fs.readdir(path.join(root, dir), { withFileTypes: true }) + } catch { + continue + } + for (const entry of entries) { + if (entry.name.startsWith('.') || !entry.isFile()) continue + await moveIntoAssets(`${dir}/${entry.name}`) + } + try { + await fs.rmdir(path.join(root, dir)) + } catch { + /* not empty — leave it */ + } + } + + if (moved.length > 0) { + console.log(`[zen] migrated ${moved.length} asset(s) into ${ASSETS_DIR}/`) + } + return { moved, skipped } } /** @@ -2530,7 +2609,8 @@ function cleanAssetFilename(name: string): string { function cleanAssetTargetDir(root: string, targetDir: string): string { const normalized = normalizeVaultRelativePath(targetDir).replace(/^\/+|\/+$/g, '') - if (!normalized) return root + // No explicit target → the unified `assets/` folder (not loose at the root). + if (!normalized) return resolveSafe(root, ASSETS_DIR) if (normalized.split('/').includes(INTERNAL_VAULT_DIR)) { throw new Error('Cannot move assets into internal ZenNotes files.') } @@ -3076,7 +3156,7 @@ export function folderAbsolutePath( } export function assetsAbsolutePath(root: string): string { - return path.join(root, PRIMARY_ATTACHMENTS_DIR) + return path.join(root, ASSETS_DIR) } async function removeFileIfExists(abs: string): Promise<void> { @@ -3300,13 +3380,14 @@ export async function importPastedImage( input: PastedImageInput, now = new Date() ): Promise<ImportedAsset> { - await fs.mkdir(root, { recursive: true }) - const bytes = pastedImageBuffer(input.data) if (bytes.byteLength === 0) throw new Error('Clipboard image is empty.') - const finalName = await uniqueFilename(root, pastedImageFilename(input, now)) - const destAbs = path.join(root, finalName) + // Pasted images land in the unified `assets/` folder. + const assetsDir = path.join(root, ASSETS_DIR) + await fs.mkdir(assetsDir, { recursive: true }) + const finalName = await uniqueFilename(assetsDir, pastedImageFilename(input, now)) + const destAbs = path.join(assetsDir, finalName) await fs.writeFile(destAbs, bytes) const vaultRelPath = toPosix(path.relative(root, destAbs)) diff --git a/apps/desktop/src/main/watcher.ts b/apps/desktop/src/main/watcher.ts index b1fbc37a..7d439ac1 100644 --- a/apps/desktop/src/main/watcher.ts +++ b/apps/desktop/src/main/watcher.ts @@ -4,7 +4,7 @@ import type { NoteFolder, VaultChangeEvent, VaultChangeKind } from '@shared/ipc' import { databaseCsvPathFor } from '@shared/databases' import { folderForRelativePath } from './vault' -const ATTACHMENTS_DIRS = new Set(['attachements', '_assets']) +const ATTACHMENTS_DIRS = new Set(['assets', 'attachements', '_assets']) const INTERNAL_VAULT_DIR = '.zennotes' const VAULT_SETTINGS_RELATIVE_PATH = `${INTERNAL_VAULT_DIR}/vault.json` const NOTE_COMMENTS_PREFIX = `${INTERNAL_VAULT_DIR}/comments/` diff --git a/apps/desktop/src/mcp/vault-ops.ts b/apps/desktop/src/mcp/vault-ops.ts index e438dad6..627514ca 100644 --- a/apps/desktop/src/mcp/vault-ops.ts +++ b/apps/desktop/src/mcp/vault-ops.ts @@ -19,9 +19,10 @@ const LIVE_FOLDERS: NoteFolder[] = ['inbox', 'quick', 'archive'] * internals (data.csv, schema.json, record-page notes) aren't part of the MCP * note/folder surface, so the walks skip these folders. */ const isFormDirName = (name: string): boolean => name.toLowerCase().endsWith('.base') +const ASSETS_DIR = 'assets' const PRIMARY_ATTACHMENTS_DIR = 'attachements' -const LEGACY_ATTACHMENTS_DIRS = ['_assets'] -const ATTACHMENTS_DIRS = [PRIMARY_ATTACHMENTS_DIR, ...LEGACY_ATTACHMENTS_DIRS] +const LEGACY_ATTACHMENTS_DIRS = [PRIMARY_ATTACHMENTS_DIR, '_assets'] +const ATTACHMENTS_DIRS = [ASSETS_DIR, ...LEGACY_ATTACHMENTS_DIRS] const INTERNAL_VAULT_DIR = '.zennotes' const VAULT_SETTINGS_FILE = 'vault.json' diff --git a/apps/server/internal/vault/vault.go b/apps/server/internal/vault/vault.go index a2f858a5..0a2d85ba 100644 --- a/apps/server/internal/vault/vault.go +++ b/apps/server/internal/vault/vault.go @@ -19,6 +19,9 @@ import ( ) const ( + // AssetsDir is the canonical top-level folder for assets; attachements/_assets + // are recognized legacy dirs. Asset migration runs on the desktop (#185). + AssetsDir = "assets" PrimaryAttachmentsDir = "attachements" internalVaultDir = ".zennotes" vaultSettingsFile = "vault.json" @@ -43,12 +46,13 @@ func isFormDirName(name string) bool { // vault's MaxAssetBytes limit. var ErrAssetTooLarge = errors.New("asset exceeds maximum size") -var legacyAttachmentsDirs = []string{"_assets"} +var legacyAttachmentsDirs = []string{PrimaryAttachmentsDir, "_assets"} var reservedRootNames = map[string]struct{}{ string(FolderInbox): {}, string(FolderQuick): {}, string(FolderArchive): {}, string(FolderTrash): {}, + AssetsDir: {}, PrimaryAttachmentsDir: {}, internalVaultDir: {}, } @@ -57,6 +61,7 @@ var hiddenPrimaryRootNames = map[string]struct{}{ string(FolderQuick): {}, string(FolderArchive): {}, string(FolderTrash): {}, + AssetsDir: {}, PrimaryAttachmentsDir: {}, internalVaultDir: {}, } @@ -975,7 +980,7 @@ func (v *Vault) ListAssets() ([]AssetMeta, error) { func (v *Vault) HasAssetsDir() bool { v.mu.RLock() defer v.mu.RUnlock() - for _, dir := range append([]string{PrimaryAttachmentsDir}, legacyAttachmentsDirs...) { + for _, dir := range append([]string{AssetsDir}, legacyAttachmentsDirs...) { info, err := os.Stat(filepath.Join(v.root, dir)) if err == nil && info.IsDir() { return true diff --git a/apps/server/internal/watcher/watcher.go b/apps/server/internal/watcher/watcher.go index f9b129e9..50abd8c4 100644 --- a/apps/server/internal/watcher/watcher.go +++ b/apps/server/internal/watcher/watcher.go @@ -256,7 +256,9 @@ func (w *Watcher) handle(ev fsnotify.Event) { } folder, ok := vault.FolderForRelativePath(relPosix) if !ok { - if relPosix == vault.PrimaryAttachmentsDir || + if relPosix == vault.AssetsDir || + strings.HasPrefix(relPosix, vault.AssetsDir+"/") || + relPosix == vault.PrimaryAttachmentsDir || strings.HasPrefix(relPosix, vault.PrimaryAttachmentsDir+"/") || relPosix == "_assets" || strings.HasPrefix(relPosix, "_assets/") { diff --git a/packages/app-core/src/components/AssetsView.tsx b/packages/app-core/src/components/AssetsView.tsx new file mode 100644 index 00000000..b94affe8 --- /dev/null +++ b/packages/app-core/src/components/AssetsView.tsx @@ -0,0 +1,140 @@ +import { useMemo, useState } from 'react' +import type { AssetMeta } from '@shared/ipc' +import { useStore } from '../store' +import { assetTabPath } from '../lib/asset-tabs' +import { confirmMoveToTrash } from '../lib/confirm-trash' +import { naturalCompare } from '../lib/natural-sort' +import { + DocumentIcon, + ImageIcon, + PanelLeftIcon, + PaperclipIcon, + SearchIcon, + TrashIcon +} from './icons' + +function formatBytes(bytes: number): string { + if (bytes < 1024) return `${bytes} B` + const units = ['KB', 'MB', 'GB'] + let value = bytes / 1024 + let unit = 0 + while (value >= 1024 && unit < units.length - 1) { + value /= 1024 + unit++ + } + return `${value.toFixed(value >= 10 || unit === 0 ? 0 : 1)} ${units[unit]}` +} + +function formatDate(ms: number): string { + const d = new Date(ms) + const sameYear = d.getFullYear() === new Date().getFullYear() + return d.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + year: sameYear ? undefined : 'numeric' + }) +} + +function AssetGlyph({ kind }: { kind: AssetMeta['kind'] }): JSX.Element { + if (kind === 'image') return <ImageIcon width={15} height={15} /> + if (kind === 'pdf') return <DocumentIcon width={15} height={15} /> + return <PaperclipIcon width={15} height={15} /> +} + +/** + * The built-in Assets view: browse every asset in the vault in one place + * (images, PDFs, attachments), independent of the notes tree. + */ +export function AssetsView(): JSX.Element { + const assetFiles = useStore((s) => s.assetFiles) + const openNoteInTab = useStore((s) => s.openNoteInTab) + const deleteAsset = useStore((s) => s.deleteAsset) + const sidebarOpen = useStore((s) => s.sidebarOpen) + const toggleSidebar = useStore((s) => s.toggleSidebar) + const [filter, setFilter] = useState('') + + const assets = useMemo(() => { + const q = filter.trim().toLowerCase() + const matched = q + ? assetFiles.filter((a) => a.name.toLowerCase().includes(q) || a.path.toLowerCase().includes(q)) + : assetFiles + return [...matched].sort((a, b) => naturalCompare(a.name, b.name)) + }, [assetFiles, filter]) + + return ( + <div className="flex min-h-0 flex-1 flex-col bg-paper-100 text-ink-900"> + <header className="glass-header flex h-12 shrink-0 items-center gap-2 px-4"> + {!sidebarOpen && ( + <button + type="button" + title="Show sidebar (⌘1)" + onClick={() => toggleSidebar()} + className="flex h-7 w-7 items-center justify-center rounded-md text-ink-500 hover:bg-paper-200 hover:text-ink-900" + > + <PanelLeftIcon className="h-4 w-4" /> + </button> + )} + <PaperclipIcon className="h-4 w-4 shrink-0 text-ink-500" /> + <h2 className="text-sm font-semibold text-ink-900">Assets</h2> + <span className="shrink-0 text-xs text-ink-500">{assetFiles.length}</span> + <div className="ml-auto flex items-center gap-1.5 rounded-md bg-paper-200/60 px-2 py-1"> + <SearchIcon className="h-3.5 w-3.5 text-ink-400" /> + <input + value={filter} + onChange={(e) => setFilter(e.target.value)} + placeholder="Filter assets" + className="w-40 bg-transparent text-xs text-ink-900 outline-none placeholder:text-ink-400" + /> + </div> + </header> + + <div className="min-h-0 flex-1 overflow-auto px-3 py-2"> + {assets.length === 0 ? ( + <div className="flex h-full items-center justify-center text-sm text-ink-400"> + {assetFiles.length === 0 ? 'No assets yet.' : 'No assets match your filter.'} + </div> + ) : ( + <ul className="flex flex-col"> + {assets.map((asset) => ( + <li key={asset.path}> + <div className="group flex items-center gap-3 rounded-md px-2 py-1.5 hover:bg-paper-200/40"> + <button + type="button" + onClick={() => void openNoteInTab(assetTabPath(asset.path))} + className="flex min-w-0 flex-1 items-center gap-2.5 text-left" + title={asset.path} + > + <span className="shrink-0 text-ink-500"> + <AssetGlyph kind={asset.kind} /> + </span> + <span className="min-w-0 flex-1 truncate text-sm text-ink-900">{asset.name}</span> + <span className="shrink-0 text-2xs uppercase tracking-wide text-ink-400"> + {asset.kind} + </span> + <span className="hidden shrink-0 text-xs tabular-nums text-ink-500 sm:inline"> + {formatBytes(asset.size)} + </span> + <span className="hidden w-16 shrink-0 text-right text-xs text-ink-500 md:inline"> + {formatDate(asset.updatedAt)} + </span> + </button> + <button + type="button" + aria-label={`Delete ${asset.name}`} + title="Move to Trash" + onClick={async () => { + if (await confirmMoveToTrash(asset.name)) await deleteAsset(asset.path) + }} + className="flex h-6 w-6 shrink-0 items-center justify-center rounded text-ink-400 opacity-0 transition hover:bg-paper-300/60 hover:text-danger group-hover:opacity-100" + > + <TrashIcon width={14} height={14} /> + </button> + </div> + </li> + ))} + </ul> + )} + </div> + </div> + ) +} diff --git a/packages/app-core/src/components/EditorPane.tsx b/packages/app-core/src/components/EditorPane.tsx index eec7d24d..0dd1d8a0 100644 --- a/packages/app-core/src/components/EditorPane.tsx +++ b/packages/app-core/src/components/EditorPane.tsx @@ -90,6 +90,7 @@ import { TagView } from './TagView' import { HelpView } from './HelpView' import { ArchiveView } from './ArchiveView' import { TrashView } from './TrashView' +import { AssetsView } from './AssetsView' import { QuickNotesView } from './QuickNotesView' import { isTasksTabPath } from '@shared/tasks' import { isDatabaseTabPath, databaseTitleFromTab, databaseTabPath, isDatabaseCsvPath } from '@shared/databases' @@ -97,6 +98,7 @@ import { isTagsTabPath } from '@shared/tags' import { isHelpTabPath } from '@shared/help' import { isArchiveTabPath } from '@shared/archive' import { isTrashTabPath } from '@shared/trash' +import { isAssetsViewTabPath } from '@shared/assets-view' import { isQuickNotesTabPath } from '@shared/quick-notes' import { hasZenAssetItem, @@ -140,6 +142,7 @@ import { CalendarIcon, CheckSquareIcon, CloseIcon, + PaperclipIcon, DocumentIcon, FileDownIcon, FeedbackIcon, @@ -2181,6 +2184,7 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { isHelp: false, isArchive: false, isTrash: false, + isAssetsView: false, isAsset: false, isDiagram: false, isDatabase: false @@ -2227,6 +2231,13 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { isTrash: true } } + if (isAssetsViewTabPath(path)) { + return { + ...base, + title: 'Assets', + isAssetsView: true + } + } if (isAssetTabPath(path)) { const assetPath = assetPathFromTab(path) return { @@ -2429,6 +2440,7 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { isHelp: boolean isArchive: boolean isTrash: boolean + isAssetsView: boolean isAsset: boolean isDiagram: boolean }) => { @@ -2440,6 +2452,7 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { tab.isHelp || tab.isArchive || tab.isTrash || + tab.isAssetsView || tab.isAsset || tab.isDiagram return ( @@ -2567,6 +2580,9 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { {tab.isTrash && ( <TrashIcon width={13} height={13} className="shrink-0 text-accent" /> )} + {tab.isAssetsView && ( + <PaperclipIcon width={13} height={13} className="shrink-0 text-accent" /> + )} {tab.isAsset && ( <DocumentIcon width={13} height={13} className="shrink-0 text-accent" /> )} @@ -3091,6 +3107,8 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { <ArchiveView /> ) : isTrashTabPath(activeTab) ? ( <TrashView /> + ) : isAssetsViewTabPath(activeTab) ? ( + <AssetsView /> ) : activeTab && isAssetTabPath(activeTab) ? ( isDatabaseCsvPath(assetPathFromTab(activeTab) ?? '') ? ( <DatabaseView diff --git a/packages/app-core/src/components/Sidebar.tsx b/packages/app-core/src/components/Sidebar.tsx index ff6a1050..09d0476a 100644 --- a/packages/app-core/src/components/Sidebar.tsx +++ b/packages/app-core/src/components/Sidebar.tsx @@ -1,6 +1,7 @@ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { isArchiveViewActive, + isAssetsViewActive, isHelpViewActive, isQuickNotesViewActive, isTagsViewActive, @@ -26,6 +27,7 @@ import { DatabaseIcon, DocumentIcon, ExpandAllIcon, + PaperclipIcon, FolderPlusIcon, NotePlusIcon, PanelLeftIcon, @@ -375,6 +377,9 @@ export function Sidebar(): JSX.Element { const archiveViewActive = useStore(isArchiveViewActive); const openTrashView = useStore((s) => s.openTrashView); const trashViewActive = useStore(isTrashViewActive); + const openAssetsView = useStore((s) => s.openAssetsView); + const assetsViewActive = useStore(isAssetsViewActive); + const assetCount = useStore((s) => s.assetFiles.length); const openTagView = useStore((s) => s.openTagView); const selectedTags = useStore((s) => s.selectedTags); const tagsViewActive = useStore(isTagsViewActive); @@ -2836,6 +2841,19 @@ export function Sidebar(): JSX.Element { vimHighlight={vimCursor === idxCounter.current.value - 1} sidebarFocused={isSidebarFocused} /> + + <TrashSidebarRow + label="Assets" + icon={<PaperclipIcon width={16} height={16} />} + count={assetCount} + active={assetsViewActive} + onClick={() => { + void openAssetsView(); + }} + sidebarIdx={idxCounter.current.value++} + vimHighlight={vimCursor === idxCounter.current.value - 1} + sidebarFocused={isSidebarFocused} + /> </div> <SidebarSectionHeading diff --git a/packages/app-core/src/components/icons.tsx b/packages/app-core/src/components/icons.tsx index f2723bd5..c5b9b956 100644 --- a/packages/app-core/src/components/icons.tsx +++ b/packages/app-core/src/components/icons.tsx @@ -141,6 +141,20 @@ export const ChevronLeftIcon = (p: IconProps): JSX.Element => ( </I> ) +export const ImageIcon = (p: IconProps): JSX.Element => ( + <I {...p}> + <rect x="3" y="3" width="18" height="18" rx="2" /> + <circle cx="8.5" cy="8.5" r="1.5" /> + <path d="m21 15-4.5-4.5L5 21" /> + </I> +) + +export const PaperclipIcon = (p: IconProps): JSX.Element => ( + <I {...p}> + <path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" /> + </I> +) + export const ArrowLeftIcon = (p: IconProps): JSX.Element => ( <I {...p}> <path d="M19 12H5" /> diff --git a/packages/app-core/src/lib/vault-layout.ts b/packages/app-core/src/lib/vault-layout.ts index b0b0d502..5ecbe1e4 100644 --- a/packages/app-core/src/lib/vault-layout.ts +++ b/packages/app-core/src/lib/vault-layout.ts @@ -21,6 +21,7 @@ const RESERVED_ROOT_NAMES = new Set<string>([ 'quick', 'archive', 'trash', + 'assets', 'attachements', '_assets', '.zennotes' diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index 6e074607..34f9e848 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -38,6 +38,7 @@ import { TAGS_TAB_PATH, isTagsTabPath } from '@shared/tags' import { HELP_TAB_PATH, isHelpTabPath } from '@shared/help' import { ARCHIVE_TAB_PATH, isArchiveTabPath } from '@shared/archive' import { TRASH_TAB_PATH, isTrashTabPath } from '@shared/trash' +import { ASSETS_VIEW_TAB_PATH, isAssetsViewTabPath } from '@shared/assets-view' import { QUICK_NOTES_TAB_PATH, isQuickNotesTabPath } from '@shared/quick-notes' import { isAssetTabPath, assetPathFromTab, assetTabPath } from './lib/asset-tabs' import { @@ -1420,6 +1421,15 @@ export function isArchiveViewActive(state: { return leaf?.activeTab === ARCHIVE_TAB_PATH } +/** True when the active pane's active tab is the built-in Assets view. */ +export function isAssetsViewActive(state: { + paneLayout: PaneLayout + activePaneId: string +}): boolean { + const leaf = findLeaf(state.paneLayout, state.activePaneId) + return leaf?.activeTab === ASSETS_VIEW_TAB_PATH +} + /** True when the active pane's active tab is the built-in Quick Notes view. */ export function isQuickNotesViewActive(state: { paneLayout: PaneLayout @@ -1644,6 +1654,7 @@ interface Store { openQuickNotesView: () => Promise<void> /** Open the built-in Archive tab in the active pane. */ openArchiveView: () => Promise<void> + openAssetsView: () => Promise<void> /** Open the built-in Trash tab in the active pane. */ openTrashView: () => Promise<void> /** Read a CSV database (CSV + sidecar) into `databases` if not already loaded. */ @@ -2987,6 +2998,14 @@ export const useStore = create<Store>((set, get) => { ;(document.activeElement as HTMLElement | null)?.blur?.() set({ focusedPanel: 'editor' }) }, + + openAssetsView: async () => { + const state = get() + await get().refreshAssets() + await get().openNoteInPane(state.activePaneId, ASSETS_VIEW_TAB_PATH) + ;(document.activeElement as HTMLElement | null)?.blur?.() + set({ focusedPanel: 'editor' }) + }, loadDatabase: async (csvPath) => { if (get().databasesLoading[csvPath]) return set((s) => ({ databasesLoading: { ...s.databasesLoading, [csvPath]: true } })) diff --git a/packages/shared-domain/src/assets-view.ts b/packages/shared-domain/src/assets-view.ts new file mode 100644 index 00000000..f891cd73 --- /dev/null +++ b/packages/shared-domain/src/assets-view.ts @@ -0,0 +1,17 @@ +/** + * Virtual path used to identify the built-in Assets view as a tab in the pane + * layout. Uses the `zen://` scheme so it never collides with a real vault path. + * Distinct from `zen://asset/<path>` (a single asset opened as a tab). + */ +export const ASSETS_VIEW_TAB_PATH = 'zen://assets' + +/** True when `path` points at the built-in Assets view tab. */ +export function isAssetsViewTabPath(path: string | null | undefined): boolean { + return path === ASSETS_VIEW_TAB_PATH +} + +/** + * The canonical top-level folder where the vault keeps its assets (images, + * PDFs, and other attachments), unified so they no longer mix with notes. + */ +export const ASSETS_DIR = 'assets' From 1bf0fa52bdff4163fed330943cf21f960a6ded3b Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Tue, 16 Jun 2026 16:35:06 -0500 Subject: [PATCH 33/66] Assets view: usage + actions + table layout, and nav polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assets view: - Show "used in N" / "unused" per asset, computed from a new per-note assetEmbeds field (captures ![[asset]] and ![](path) embeds, which note metadata previously dropped) resolved via the same relative-path + unique-basename logic embeds render with. The note-meta cache version is bumped so it rebuilds with the new field on first open. - Right-click context menu: Open, Copy embed, Reveal in file manager, Rename…, Move to Trash. - Reorganized the list into an aligned table (header + shared grid columns, right-aligned numeric fields, whole-row click, hover-reveal delete). - Fix a restore crash: the zen://assets tab is now treated as a virtual tab, so it's never loaded from disk as a note (ENOENT on relaunch). Nav polish: - Move the sidebar toggle up next to back/forward (shown when the sidebar is hidden); drop the redundant toggles from the breadcrumb, database, and assets headers. - Anchor the leftmost toolbar tooltips to the button's left edge so "Show sidebar"/"Go back" no longer clip at the window edge. --- apps/desktop/src/main/vault.test.ts | 20 +- apps/desktop/src/main/vault.ts | 37 ++- .../app-core/src/components/AssetsView.tsx | 218 +++++++++++++----- .../app-core/src/components/DatabaseView.tsx | 9 +- .../app-core/src/components/EditorPane.tsx | 29 ++- packages/app-core/src/lib/workspace-tabs.ts | 2 + packages/app-core/src/store.ts | 3 +- packages/bridge-contract/src/ipc.ts | 3 + 8 files changed, 243 insertions(+), 78 deletions(-) diff --git a/apps/desktop/src/main/vault.test.ts b/apps/desktop/src/main/vault.test.ts index 7656f85b..ab41f116 100644 --- a/apps/desktop/src/main/vault.test.ts +++ b/apps/desktop/src/main/vault.test.ts @@ -536,7 +536,7 @@ describe('listNotes metadata cache', () => { await writeFile( path.join(root, '.zennotes', 'note-meta-cache-v1.json'), `${JSON.stringify({ - version: 1, + version: 2, entries: [ { path: rel, @@ -552,6 +552,7 @@ describe('listNotes metadata cache', () => { size: info.size, tags: ['cached'], wikilinks: ['Cached Target'], + assetEmbeds: [], hasAttachments: false, excerpt: 'cached excerpt' } @@ -617,6 +618,23 @@ describe('listNotes metadata cache', () => { }) }) +describe('listNotes asset embeds (#185 usage)', () => { + it('captures ![[asset]] and ![](asset) targets, not note wikilinks or URLs', async () => { + const root = await makeTempDir('zennotes-asset-embeds-') + await ensureVaultLayout(root) + await writeFile( + path.join(root, 'inbox', 'n.md'), + // Includes the angle-bracket + alt-text form the editor writes: ![alt](<path>). + '![[photo.png]]\n![](assets/doc.pdf)\n![GreenGrass](<GreenGrass.jpg>)\n[[Some Note]]\n![](https://x.com/a.png)\n', + 'utf8' + ) + const notes = await listNotes(root) + const note = notes.find((n) => n.path === 'inbox/n.md') + expect(note?.assetEmbeds.sort()).toEqual(['GreenGrass.jpg', 'assets/doc.pdf', 'photo.png']) + expect(note?.wikilinks).toEqual(['Some Note']) // note links stay separate + }) +}) + describe('archive / trash round-trips', () => { async function makeVaultWithNestedNote(): Promise<{ root: string }> { const root = await makeTempDir('zennotes-folder-moves-') diff --git a/apps/desktop/src/main/vault.ts b/apps/desktop/src/main/vault.ts index 29b5541e..afb9759b 100644 --- a/apps/desktop/src/main/vault.ts +++ b/apps/desktop/src/main/vault.ts @@ -67,7 +67,7 @@ const INTERNAL_VAULT_DIR = '.zennotes' const DELETED_ASSETS_DIR = 'deleted-assets' const VAULT_SETTINGS_FILE = 'vault.json' const NOTE_META_CACHE_FILE = 'note-meta-cache-v1.json' -const NOTE_META_CACHE_VERSION = 1 +const NOTE_META_CACHE_VERSION = 2 const NOTE_COMMENTS_DIR = 'comments' const NOTE_COMMENTS_SUFFIX = '.comments.json' const RESERVED_ROOT_NAMES = new Set<string>([...FOLDERS, ...ATTACHMENTS_DIRS, INTERNAL_VAULT_DIR]) @@ -1351,6 +1351,37 @@ function extractWikilinks(body: string): string[] { return [...seen] } +/** Pull unique asset-embed targets out of markdown — both `![[asset]]` (which + * extractWikilinks deliberately skips) and `![](path)` image/file embeds. The + * raw targets are resolved to assets per-note by the renderer (relative path + + * basename fallback), to show which notes use each asset. */ +function extractAssetEmbeds(body: string): string[] { + const stripped = stripCodeContent(body) + const seen = new Set<string>() + if (stripped.includes('![[')) { + const re = /!\[\[([^\]|]+?)(?:\|[^\]]+)?\]\]/g + let m: RegExpExecArray | null + while ((m = re.exec(stripped)) !== null) { + const t = (m[1] ?? '').trim() + if (t && localAssetTargetKind(t)) seen.add(t) + } + } + if (stripped.includes('](')) { + const re = /!\[[^\]]*\]\(\s*<?([^)>\s]+)>?[^)]*\)/g + let m: RegExpExecArray | null + while ((m = re.exec(stripped)) !== null) { + const raw = (m[1] ?? '').trim() + if (!raw || raw.startsWith('#') || /^[a-zA-Z][\w+.-]*:/.test(raw)) continue // skip URLs/anchors + try { + seen.add(decodeURIComponent(raw)) + } catch { + seen.add(raw) + } + } + } + return [...seen] +} + /** Build a short plaintext preview from markdown. */ function buildExcerpt(body: string): string { const withoutFront = body.startsWith('---\n') ? body.replace(/^---\n[\s\S]*?\n---\n/, '') : body @@ -1485,6 +1516,8 @@ function normalizeCachedNoteMeta(value: unknown): NoteMeta | null { !candidate.tags.every((tag) => typeof tag === 'string') || !Array.isArray(candidate.wikilinks) || !candidate.wikilinks.every((wikilink) => typeof wikilink === 'string') || + !Array.isArray(candidate.assetEmbeds) || + !candidate.assetEmbeds.every((embed) => typeof embed === 'string') || typeof candidate.hasAttachments !== 'boolean' || typeof candidate.excerpt !== 'string' ) { @@ -1500,6 +1533,7 @@ function normalizeCachedNoteMeta(value: unknown): NoteMeta | null { size: candidate.size, tags: candidate.tags, wikilinks: candidate.wikilinks, + assetEmbeds: candidate.assetEmbeds, hasAttachments: candidate.hasAttachments, excerpt: candidate.excerpt } @@ -2153,6 +2187,7 @@ async function readMeta( size: stat.size, tags: extractTags(body), wikilinks: extractWikilinks(body), + assetEmbeds: extractAssetEmbeds(body), hasAttachments: bodyHasLocalAsset(body), excerpt: buildExcerpt(body), isSymlink: linked diff --git a/packages/app-core/src/components/AssetsView.tsx b/packages/app-core/src/components/AssetsView.tsx index b94affe8..817b46f4 100644 --- a/packages/app-core/src/components/AssetsView.tsx +++ b/packages/app-core/src/components/AssetsView.tsx @@ -3,15 +3,11 @@ import type { AssetMeta } from '@shared/ipc' import { useStore } from '../store' import { assetTabPath } from '../lib/asset-tabs' import { confirmMoveToTrash } from '../lib/confirm-trash' +import { promptApp } from '../lib/prompt-requests' import { naturalCompare } from '../lib/natural-sort' -import { - DocumentIcon, - ImageIcon, - PanelLeftIcon, - PaperclipIcon, - SearchIcon, - TrashIcon -} from './icons' +import { resolveAssetVaultRelativePath } from '../lib/local-assets' +import { ContextMenu, type ContextMenuItem } from './ContextMenu' +import { DocumentIcon, ImageIcon, PaperclipIcon, SearchIcon, TrashIcon } from './icons' function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B` @@ -35,6 +31,11 @@ function formatDate(ms: number): string { }) } +// Shared column template for the header + every row, so columns line up: +// name (flex) · used · type · size · modified · action. +const ASSET_ROW_GRID = + 'grid grid-cols-[minmax(0,1fr)_6rem_4rem_5rem_5rem_1.75rem] items-center gap-4' + function AssetGlyph({ kind }: { kind: AssetMeta['kind'] }): JSX.Element { if (kind === 'image') return <ImageIcon width={15} height={15} /> if (kind === 'pdf') return <DocumentIcon width={15} height={15} /> @@ -49,9 +50,11 @@ export function AssetsView(): JSX.Element { const assetFiles = useStore((s) => s.assetFiles) const openNoteInTab = useStore((s) => s.openNoteInTab) const deleteAsset = useStore((s) => s.deleteAsset) - const sidebarOpen = useStore((s) => s.sidebarOpen) - const toggleSidebar = useStore((s) => s.toggleSidebar) + const refreshAssets = useStore((s) => s.refreshAssets) + const notes = useStore((s) => s.notes) + const vaultRoot = useStore((s) => s.vault?.root ?? null) const [filter, setFilter] = useState('') + const [menu, setMenu] = useState<{ x: number; y: number; asset: AssetMeta } | null>(null) const assets = useMemo(() => { const q = filter.trim().toLowerCase() @@ -61,19 +64,67 @@ export function AssetsView(): JSX.Element { return [...matched].sort((a, b) => naturalCompare(a.name, b.name)) }, [assetFiles, filter]) + // assetPath → note paths that embed it (resolved via relative-path + the + // unique-basename fallback, matching how embeds render). (#185) + const usage = useMemo(() => { + const map = new Map<string, string[]>() + for (const note of notes) { + for (const href of note.assetEmbeds ?? []) { + const resolved = resolveAssetVaultRelativePath(vaultRoot, note.path, href) + if (!resolved) continue + const arr = map.get(resolved) + if (arr) { + if (!arr.includes(note.path)) arr.push(note.path) + } else { + map.set(resolved, [note.path]) + } + } + } + return map + }, [notes, vaultRoot, assetFiles]) + + const copyEmbed = (asset: AssetMeta): void => { + void navigator.clipboard?.writeText(`![[${asset.name}]]`) + } + + const renameAsset = async (asset: AssetMeta): Promise<void> => { + if (typeof window.zen.renameAsset !== 'function') return + const ext = asset.name.includes('.') ? asset.name.slice(asset.name.lastIndexOf('.')) : '' + const base = ext ? asset.name.slice(0, -ext.length) : asset.name + const next = await promptApp({ + title: 'Rename asset', + initialValue: base, + okLabel: 'Rename', + validate: (v) => (v.includes('/') ? 'Use only a name' : null) + }) + if (!next) return + const clean = next.trim() + if (!clean || `${clean}${ext}` === asset.name) return + try { + await window.zen.renameAsset(asset.path, `${clean}${ext}`) + await refreshAssets() + } catch (err) { + window.alert(err instanceof Error ? err.message : String(err)) + } + } + + const menuItems = (asset: AssetMeta): ContextMenuItem[] => [ + { label: 'Open', onSelect: () => void openNoteInTab(assetTabPath(asset.path)) }, + { label: 'Copy embed', onSelect: () => copyEmbed(asset) }, + { label: 'Reveal in file manager', onSelect: () => void window.zen.revealNote(asset.path) }, + { label: 'Rename…', onSelect: () => void renameAsset(asset) }, + { + label: 'Move to Trash', + danger: true, + onSelect: async () => { + if (await confirmMoveToTrash(asset.name)) await deleteAsset(asset.path) + } + } + ] + return ( <div className="flex min-h-0 flex-1 flex-col bg-paper-100 text-ink-900"> <header className="glass-header flex h-12 shrink-0 items-center gap-2 px-4"> - {!sidebarOpen && ( - <button - type="button" - title="Show sidebar (⌘1)" - onClick={() => toggleSidebar()} - className="flex h-7 w-7 items-center justify-center rounded-md text-ink-500 hover:bg-paper-200 hover:text-ink-900" - > - <PanelLeftIcon className="h-4 w-4" /> - </button> - )} <PaperclipIcon className="h-4 w-4 shrink-0 text-ink-500" /> <h2 className="text-sm font-semibold text-ink-900">Assets</h2> <span className="shrink-0 text-xs text-ink-500">{assetFiles.length}</span> @@ -88,53 +139,100 @@ export function AssetsView(): JSX.Element { </div> </header> - <div className="min-h-0 flex-1 overflow-auto px-3 py-2"> + <div className="min-h-0 flex-1 overflow-auto"> {assets.length === 0 ? ( <div className="flex h-full items-center justify-center text-sm text-ink-400"> {assetFiles.length === 0 ? 'No assets yet.' : 'No assets match your filter.'} </div> ) : ( - <ul className="flex flex-col"> - {assets.map((asset) => ( - <li key={asset.path}> - <div className="group flex items-center gap-3 rounded-md px-2 py-1.5 hover:bg-paper-200/40"> - <button - type="button" - onClick={() => void openNoteInTab(assetTabPath(asset.path))} - className="flex min-w-0 flex-1 items-center gap-2.5 text-left" - title={asset.path} - > - <span className="shrink-0 text-ink-500"> - <AssetGlyph kind={asset.kind} /> - </span> - <span className="min-w-0 flex-1 truncate text-sm text-ink-900">{asset.name}</span> - <span className="shrink-0 text-2xs uppercase tracking-wide text-ink-400"> - {asset.kind} - </span> - <span className="hidden shrink-0 text-xs tabular-nums text-ink-500 sm:inline"> - {formatBytes(asset.size)} - </span> - <span className="hidden w-16 shrink-0 text-right text-xs text-ink-500 md:inline"> - {formatDate(asset.updatedAt)} - </span> - </button> - <button - type="button" - aria-label={`Delete ${asset.name}`} - title="Move to Trash" - onClick={async () => { - if (await confirmMoveToTrash(asset.name)) await deleteAsset(asset.path) - }} - className="flex h-6 w-6 shrink-0 items-center justify-center rounded text-ink-400 opacity-0 transition hover:bg-paper-300/60 hover:text-danger group-hover:opacity-100" - > - <TrashIcon width={14} height={14} /> - </button> - </div> - </li> - ))} - </ul> + <> + <div className={`${ASSET_ROW_GRID} sticky top-0 z-10 border-b border-paper-300/60 bg-paper-100 px-3 py-1.5 text-2xs font-medium uppercase tracking-wide text-ink-400`}> + <span>Name</span> + <span className="text-right">Used</span> + <span className="text-right">Type</span> + <span className="text-right">Size</span> + <span className="text-right">Modified</span> + <span /> + </div> + <ul className="flex flex-col py-1"> + {assets.map((asset) => { + const usedIn = usage.get(asset.path)?.length ?? 0 + const open = (): void => void openNoteInTab(assetTabPath(asset.path)) + return ( + <li key={asset.path}> + <div + role="button" + tabIndex={0} + onClick={open} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + open() + } + }} + onContextMenu={(e) => { + e.preventDefault() + setMenu({ x: e.clientX, y: e.clientY, asset }) + }} + className={`${ASSET_ROW_GRID} group cursor-pointer px-3 py-1.5 outline-none hover:bg-paper-200/40 focus-visible:bg-paper-200/40`} + title={asset.path} + > + <div className="flex min-w-0 items-center gap-2.5"> + <span className="shrink-0 text-ink-500"> + <AssetGlyph kind={asset.kind} /> + </span> + <span className="min-w-0 truncate text-sm text-ink-900">{asset.name}</span> + </div> + <span + className={[ + 'truncate text-right text-2xs', + usedIn === 0 ? 'text-ink-400/70' : 'text-ink-500' + ].join(' ')} + title={ + usedIn === 0 + ? 'Not referenced by any note' + : (usage.get(asset.path) ?? []).join('\n') + } + > + {usedIn === 0 ? 'unused' : `used in ${usedIn}`} + </span> + <span className="text-right text-2xs uppercase tracking-wide text-ink-400"> + {asset.kind} + </span> + <span className="text-right text-xs tabular-nums text-ink-500"> + {formatBytes(asset.size)} + </span> + <span className="text-right text-xs tabular-nums text-ink-500"> + {formatDate(asset.updatedAt)} + </span> + <button + type="button" + aria-label={`Delete ${asset.name}`} + title="Move to Trash" + onClick={async (e) => { + e.stopPropagation() + if (await confirmMoveToTrash(asset.name)) await deleteAsset(asset.path) + }} + className="flex h-6 w-6 items-center justify-center justify-self-end rounded text-ink-400 opacity-0 transition hover:bg-paper-300/60 hover:text-danger group-hover:opacity-100" + > + <TrashIcon width={14} height={14} /> + </button> + </div> + </li> + ) + })} + </ul> + </> )} </div> + {menu && ( + <ContextMenu + x={menu.x} + y={menu.y} + items={menuItems(menu.asset)} + onClose={() => setMenu(null)} + /> + )} </div> ) } diff --git a/packages/app-core/src/components/DatabaseView.tsx b/packages/app-core/src/components/DatabaseView.tsx index bd4079ad..d8c3c340 100644 --- a/packages/app-core/src/components/DatabaseView.tsx +++ b/packages/app-core/src/components/DatabaseView.tsx @@ -15,7 +15,7 @@ import { DatabaseTableView } from './DatabaseTableView' import { DatabaseBoardView } from './DatabaseBoardView' import { ContextMenu, type ContextMenuItem } from './ContextMenu' import { Button, IconButton } from './ui/Button' -import { DatabaseIcon, TableIcon, KanbanIcon, PlusIcon, PanelLeftIcon } from './icons' +import { DatabaseIcon, TableIcon, KanbanIcon, PlusIcon } from './icons' /** * Host for a CSV database tab: loads the database, renders the header @@ -34,8 +34,6 @@ export function DatabaseView({ const loadDatabase = useStore((s) => s.loadDatabase) const updateDatabaseRows = useStore((s) => s.updateDatabaseRows) const updateDatabaseSchema = useStore((s) => s.updateDatabaseSchema) - const sidebarOpen = useStore((s) => s.sidebarOpen) - const toggleSidebar = useStore((s) => s.toggleSidebar) const renameDatabase = useStore((s) => s.renameDatabase) const [viewMenu, setViewMenu] = useState<{ viewId: string; x: number; y: number } | null>(null) const [renamingView, setRenamingView] = useState<string | null>(null) @@ -78,11 +76,6 @@ export function DatabaseView({ return ( <div className="flex min-h-0 flex-1 flex-col bg-paper-100 text-ink-900"> <header className="glass-header flex h-12 shrink-0 items-center gap-2 px-4"> - {!sidebarOpen && isActive && ( - <IconButton size="sm" title="Show sidebar (⌘1)" onClick={() => toggleSidebar()}> - <PanelLeftIcon className="h-4 w-4" /> - </IconButton> - )} <DatabaseIcon className="h-4 w-4 shrink-0 text-ink-500" /> {editingTitle && canRenameTitle ? ( <input diff --git a/packages/app-core/src/components/EditorPane.tsx b/packages/app-core/src/components/EditorPane.tsx index 0dd1d8a0..cc69c6e3 100644 --- a/packages/app-core/src/components/EditorPane.tsx +++ b/packages/app-core/src/components/EditorPane.tsx @@ -3004,6 +3004,15 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { {hasTabs && ( <div className={tabStripHeaderClass}> <div className="flex shrink-0 items-center gap-0.5 self-center"> + {!sidebarOpen && ( + <IconBtn + title="Show sidebar (⌘1)" + onClick={toggleSidebar} + tooltipAlign="left" + > + <PanelLeftIcon width={16} height={16} /> + </IconBtn> + )} <IconBtn title={`Go back (${getKeymapDisplay( tabNavOverrides, @@ -3011,6 +3020,7 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { )})`} onClick={() => void jumpToPreviousNote()} disabled={!canGoBack} + tooltipAlign="left" > <ArrowLeftIcon width={16} height={16} /> </IconBtn> @@ -3021,6 +3031,7 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { )})`} onClick={() => void jumpToNextNote()} disabled={!canGoForward} + tooltipAlign="left" > <ArrowRightIcon width={16} height={16} /> </IconBtn> @@ -3056,11 +3067,6 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { {content && !zenMode && ( <header className="glass-header flex h-12 shrink-0 items-center justify-between gap-3 px-4"> <div className="flex min-w-0 flex-1 items-center gap-1"> - {!sidebarOpen && isActive && ( - <IconBtn title="Show sidebar (⌘1)" onClick={toggleSidebar}> - <PanelLeftIcon /> - </IconBtn> - )} <Breadcrumb note={content} autoFocus={isActive && pendingTitleFocusPath === content.path} @@ -3515,13 +3521,17 @@ function IconBtn({ onClick, title, active = false, - disabled = false + disabled = false, + tooltipAlign = 'center' }: { children: JSX.Element onClick: () => void title: string active?: boolean disabled?: boolean + /** 'left' anchors the tooltip to the button's left edge so it never spills + * off the left of the window (used by the leftmost toolbar buttons). */ + tooltipAlign?: 'center' | 'left' }): JSX.Element { return ( <button @@ -3540,7 +3550,12 @@ function IconBtn({ ].join(' ')} > <span className="pointer-events-none">{children}</span> - <span className="pointer-events-none absolute left-1/2 top-full z-30 mt-1.5 hidden -translate-x-1/2 whitespace-nowrap rounded-md border border-paper-300 bg-paper-50 px-2 py-1 text-xs font-medium text-ink-800 shadow-panel group-hover:block group-focus-visible:block"> + <span + className={[ + 'pointer-events-none absolute top-full z-30 mt-1.5 hidden whitespace-nowrap rounded-md border border-paper-300 bg-paper-50 px-2 py-1 text-xs font-medium text-ink-800 shadow-panel group-hover:block group-focus-visible:block', + tooltipAlign === 'left' ? 'left-0' : 'left-1/2 -translate-x-1/2' + ].join(' ')} + > {title} </span> </button> diff --git a/packages/app-core/src/lib/workspace-tabs.ts b/packages/app-core/src/lib/workspace-tabs.ts index 5f673f8b..246c5739 100644 --- a/packages/app-core/src/lib/workspace-tabs.ts +++ b/packages/app-core/src/lib/workspace-tabs.ts @@ -5,6 +5,7 @@ import { isTagsTabPath } from '@shared/tags' import { isTasksTabPath } from '@shared/tasks' import { isTrashTabPath } from '@shared/trash' import { isDatabaseTabPath } from '@shared/databases' +import { isAssetsViewTabPath } from '@shared/assets-view' import { isAssetTabPath } from './asset-tabs' import { isDiagramTabPath } from './diagram-tabs' import { allLeaves, type PaneLayout } from './pane-layout' @@ -17,6 +18,7 @@ export function isWorkspaceVirtualTabPath(path: string): boolean { isHelpTabPath(path) || isArchiveTabPath(path) || isTrashTabPath(path) || + isAssetsViewTabPath(path) || isAssetTabPath(path) || isDiagramTabPath(path) || isDatabaseTabPath(path) diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index 34f9e848..78b051e6 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -3001,7 +3001,8 @@ export const useStore = create<Store>((set, get) => { openAssetsView: async () => { const state = get() - await get().refreshAssets() + // Refresh both: assets for the list, notes for fresh assetEmbeds (usage). + await Promise.all([get().refreshAssets(), get().refreshNotes()]) await get().openNoteInPane(state.activePaneId, ASSETS_VIEW_TAB_PATH) ;(document.activeElement as HTMLElement | null)?.blur?.() set({ focusedPanel: 'editor' }) diff --git a/packages/bridge-contract/src/ipc.ts b/packages/bridge-contract/src/ipc.ts index d08b9e26..564fdb24 100644 --- a/packages/bridge-contract/src/ipc.ts +++ b/packages/bridge-contract/src/ipc.ts @@ -329,6 +329,9 @@ export interface NoteMeta { tags: string[] /** Outbound [[wikilink]] targets (note titles), unique. */ wikilinks: string[] + /** Outbound asset-embed targets (`![[asset]]` / `![](asset)`), unique. Used to + * show which notes use a given asset in the Assets view. */ + assetEmbeds: string[] /** True when the body references at least one local non-text asset * (PDF, image, audio, video, generic file). Surfaced in the sidebar * as a small paperclip hint so attachments are discoverable. */ From da530df8b13a1aedec737a078ca8b6ff55d0f17a Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Tue, 16 Jun 2026 17:26:15 -0500 Subject: [PATCH 34/66] Fix Vim/normal-mode key interaction in list views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Trash/Archive/Tasks/Tags/Quick Notes views register global capture-phase keydown handlers that fired single-key shortcuts straight through open confirm dialogs and context menus — Enter bypassed the delete-confirm and repeated Enter kept deleting — and ignored the Vim-mode setting entirely. - Add isAppOverlayOpen() so the views stand down while a prompt/confirm modal or context menu owns the keyboard. - Gate the single-key Vim shortcuts (j/k/x/d/gg/G/o/r/Space/1-3) behind vimMode; only arrows/Enter/Escape navigate when Vim is off. Reported by songgenqing. --- .../app-core/src/components/ArchiveView.tsx | 30 ++++++++++------- .../src/components/QuickNotesView.tsx | 24 ++++++++++---- packages/app-core/src/components/TagView.tsx | 23 +++++++++---- .../app-core/src/components/TasksView.tsx | 33 ++++++++++++------- .../app-core/src/components/TrashView.tsx | 25 ++++++++++---- .../app-core/src/lib/overlay-open.test.ts | 27 +++++++++++++++ packages/app-core/src/lib/overlay-open.ts | 15 +++++++++ 7 files changed, 135 insertions(+), 42 deletions(-) create mode 100644 packages/app-core/src/lib/overlay-open.test.ts create mode 100644 packages/app-core/src/lib/overlay-open.ts diff --git a/packages/app-core/src/components/ArchiveView.tsx b/packages/app-core/src/components/ArchiveView.tsx index cd030d94..95d6a2d7 100644 --- a/packages/app-core/src/components/ArchiveView.tsx +++ b/packages/app-core/src/components/ArchiveView.tsx @@ -10,6 +10,7 @@ import { buildMoveNotePrompt, parseMoveNoteTarget } from '../lib/move-note' import { promptApp } from '../lib/prompt-requests' import { advanceSequence, getKeymapBinding, matchesSequenceToken } from '../lib/keymaps' import { resolveSystemFolderLabels } from '../lib/system-folder-labels' +import { isAppOverlayOpen } from '../lib/overlay-open' function formatDate(ms: number): string { const d = new Date(ms) @@ -40,6 +41,7 @@ export function ArchiveView(): JSX.Element { const selectedPath = useStore((s) => s.selectedPath) const renameNote = useStore((s) => s.renameNote) const keymapOverrides = useStore((s) => s.keymapOverrides) + const vimMode = useStore((s) => s.vimMode) const setFocusedPanel = useStore((s) => s.setFocusedPanel) const systemFolderLabels = useStore((s) => s.systemFolderLabels) const workspaceMode = useStore((s) => s.workspaceMode) @@ -272,9 +274,9 @@ export function ArchiveView(): JSX.Element { useEffect(() => { if (!amActive) return const handler = (e: KeyboardEvent): void => { - if (document.querySelector('[data-ctx-menu]') || document.querySelector('[data-prompt-modal]')) { - return - } + // A modal/menu owns the keyboard while open — don't fire list shortcuts + // through it. (songgenqing report) + if (isAppOverlayOpen()) return const active = document.activeElement as HTMLElement | null if (active) { const tag = active.tagName @@ -284,6 +286,10 @@ export function ArchiveView(): JSX.Element { const key = e.key const overrides = keymapOverrides + // When Vim mode is off, the single-key Vim shortcuts (j/k/x/d/gg/G/o/r//…) + // are disabled — only arrows/Enter/Escape navigate. (songgenqing report) + const seq = (id: Parameters<typeof matchesSequenceToken>[2]): boolean => + vimMode && matchesSequenceToken(e, overrides, id) const consume = (): void => { e.preventDefault() e.stopImmediatePropagation() @@ -300,35 +306,36 @@ export function ArchiveView(): JSX.Element { return } - if (matchesSequenceToken(e, overrides, 'nav.filter')) { + if (seq('nav.filter')) { consume() filterRef.current?.focus() filterRef.current?.select() return } - if (matchesSequenceToken(e, overrides, 'nav.contextMenu') && current) { + if (seq('nav.contextMenu') && current) { consume() openMenuForCurrent() return } - if (matchesSequenceToken(e, overrides, 'nav.moveDown') || key === 'ArrowDown') { + if (seq('nav.moveDown') || key === 'ArrowDown') { consume() setCursorIndex((i) => Math.max(0, Math.min(filtered.length - 1, i + 1))) return } - if (matchesSequenceToken(e, overrides, 'nav.moveUp') || key === 'ArrowUp') { + if (seq('nav.moveUp') || key === 'ArrowUp') { consume() setCursorIndex((i) => Math.max(0, Math.min(filtered.length - 1, i - 1))) return } - if (matchesSequenceToken(e, overrides, 'nav.jumpBottom')) { + if (seq('nav.jumpBottom')) { consume() setCursorIndex(filtered.length - 1) return } if ( + vimMode && advanceSequence( e, getKeymapBinding(overrides, 'nav.jumpTop'), @@ -341,17 +348,17 @@ export function ArchiveView(): JSX.Element { ) { return } - if ((key === 'Enter' || matchesSequenceToken(e, overrides, 'nav.openResult')) && current) { + if ((key === 'Enter' || seq('nav.openResult')) && current) { consume() void openNote(current.path) return } - if (matchesSequenceToken(e, overrides, 'nav.unarchive') && current) { + if (seq('nav.unarchive') && current) { consume() void unarchiveNote(current) return } - if ((matchesSequenceToken(e, overrides, 'nav.delete') || key === 'd') && current) { + if ((seq('nav.delete') || (vimMode && key === 'd')) && current) { consume() void moveNoteToTrash(current) } @@ -369,6 +376,7 @@ export function ArchiveView(): JSX.Element { filter, filtered.length, keymapOverrides, + vimMode, moveNoteToTrash, openMenuForCurrent, openNote, diff --git a/packages/app-core/src/components/QuickNotesView.tsx b/packages/app-core/src/components/QuickNotesView.tsx index 1b67ec79..221e026b 100644 --- a/packages/app-core/src/components/QuickNotesView.tsx +++ b/packages/app-core/src/components/QuickNotesView.tsx @@ -5,6 +5,7 @@ import { CollectionViewHeader } from './CollectionViewHeader' import { resolveQuickNoteTitle } from '../lib/quick-note-title' import { advanceSequence, getKeymapBinding, matchesSequenceToken } from '../lib/keymaps' import { getSystemFolderLabel } from '../lib/system-folder-labels' +import { isAppOverlayOpen } from '../lib/overlay-open' function formatDate(ms: number): string { const d = new Date(ms) @@ -30,6 +31,7 @@ export function QuickNotesView(): JSX.Element { const quickNoteDateTitle = useStore((s) => s.quickNoteDateTitle) const quickNoteTitlePrefix = useStore((s) => s.quickNoteTitlePrefix) const keymapOverrides = useStore((s) => s.keymapOverrides) + const vimMode = useStore((s) => s.vimMode) const setFocusedPanel = useStore((s) => s.setFocusedPanel) const systemFolderLabels = useStore((s) => s.systemFolderLabels) const amActive = useStore(isQuickNotesViewActive) @@ -97,6 +99,9 @@ export function QuickNotesView(): JSX.Element { useEffect(() => { if (!amActive) return const handler = (e: KeyboardEvent): void => { + // A modal/menu owns the keyboard while open — don't fire list shortcuts + // through it. (songgenqing report) + if (isAppOverlayOpen()) return const active = document.activeElement as HTMLElement | null if (active) { const tag = active.tagName @@ -106,6 +111,10 @@ export function QuickNotesView(): JSX.Element { const key = e.key const overrides = keymapOverrides + // When Vim mode is off, the single-key Vim shortcuts (j/k/gg/G/o//…) are + // disabled — only arrows/Enter/Escape navigate. (songgenqing report) + const seq = (id: Parameters<typeof matchesSequenceToken>[2]): boolean => + vimMode && matchesSequenceToken(e, overrides, id) const consume = (): void => { e.preventDefault() e.stopImmediatePropagation() @@ -122,35 +131,36 @@ export function QuickNotesView(): JSX.Element { return } - if (matchesSequenceToken(e, overrides, 'nav.filter')) { + if (seq('nav.filter')) { consume() filterRef.current?.focus() filterRef.current?.select() return } - if (matchesSequenceToken(e, overrides, 'nav.newQuickNote')) { + if (seq('nav.newQuickNote')) { consume() void createQuickNote() return } - if (matchesSequenceToken(e, overrides, 'nav.moveDown') || key === 'ArrowDown') { + if (seq('nav.moveDown') || key === 'ArrowDown') { consume() setCursorIndex((i) => Math.max(0, Math.min(filtered.length - 1, i + 1))) return } - if (matchesSequenceToken(e, overrides, 'nav.moveUp') || key === 'ArrowUp') { + if (seq('nav.moveUp') || key === 'ArrowUp') { consume() setCursorIndex((i) => Math.max(0, Math.min(filtered.length - 1, i - 1))) return } - if (matchesSequenceToken(e, overrides, 'nav.jumpBottom')) { + if (seq('nav.jumpBottom')) { consume() setCursorIndex(filtered.length - 1) return } if ( + vimMode && advanceSequence( e, getKeymapBinding(overrides, 'nav.jumpTop'), @@ -163,7 +173,7 @@ export function QuickNotesView(): JSX.Element { ) { return } - if ((key === 'Enter' || matchesSequenceToken(e, overrides, 'nav.openResult')) && current) { + if ((key === 'Enter' || seq('nav.openResult')) && current) { consume() void openNote(current.path) } @@ -174,7 +184,7 @@ export function QuickNotesView(): JSX.Element { if (gTimer.current) clearTimeout(gTimer.current) window.removeEventListener('keydown', handler, true) } - }, [amActive, closeActiveNote, createQuickNote, current, filter, filtered.length, keymapOverrides, openNote]) + }, [amActive, closeActiveNote, createQuickNote, current, filter, filtered.length, keymapOverrides, vimMode, openNote]) return ( <div diff --git a/packages/app-core/src/components/TagView.tsx b/packages/app-core/src/components/TagView.tsx index cd4c350e..2397f853 100644 --- a/packages/app-core/src/components/TagView.tsx +++ b/packages/app-core/src/components/TagView.tsx @@ -6,6 +6,7 @@ import { TagIcon, CloseIcon, DocumentIcon } from './icons' import { advanceSequence, getKeymapBinding, matchesSequenceToken } from '../lib/keymaps' import { isPrimaryNotesAtRoot, noteFolderSubpath } from '../lib/vault-layout' import { isImeComposing } from '../lib/ime' +import { isAppOverlayOpen } from '../lib/overlay-open' function formatDate(ms: number): string { const d = new Date(ms) @@ -35,6 +36,7 @@ export function TagView(): JSX.Element { const closeTagView = useStore((s) => s.closeTagView) const selectNote = useStore((s) => s.selectNote) const keymapOverrides = useStore((s) => s.keymapOverrides) + const vimMode = useStore((s) => s.vimMode) const amActive = useStore(isTagsViewActive) const [filter, setFilter] = useState('') @@ -191,6 +193,9 @@ export function TagView(): JSX.Element { useEffect(() => { if (!amActive) return const handler = (e: KeyboardEvent): void => { + // A modal/menu owns the keyboard while open — don't fire list shortcuts + // through it. (songgenqing report) + if (isAppOverlayOpen()) return // While the Vim hint overlay is open it owns the keyboard; don't let // tag navigation (or Esc closing the view) steal its keys. (#151) if (document.querySelector('[data-vim-hint-overlay]')) return @@ -203,6 +208,10 @@ export function TagView(): JSX.Element { const k = e.key const overrides = keymapOverrides + // When Vim mode is off, the single-key Vim shortcuts (j/k/gg/G/o//…) are + // disabled — only arrows/Enter/Escape navigate. (songgenqing report) + const seq = (id: Parameters<typeof matchesSequenceToken>[2]): boolean => + vimMode && matchesSequenceToken(e, overrides, id) const consume = (): void => { e.preventDefault() e.stopImmediatePropagation() @@ -216,35 +225,36 @@ export function TagView(): JSX.Element { if (filter) setFilter('') return } - if (matchesSequenceToken(e, overrides, 'nav.filter')) { + if (seq('nav.filter')) { consume() filterRef.current?.focus() filterRef.current?.select() return } - if (matchesSequenceToken(e, overrides, 'nav.localEx')) { + if (seq('nav.localEx')) { consume() setExValue('') setExOpen(true) requestAnimationFrame(() => exRef.current?.focus()) return } - if (matchesSequenceToken(e, overrides, 'nav.moveDown') || k === 'ArrowDown') { + if (seq('nav.moveDown') || k === 'ArrowDown') { consume() moveCursor(1) return } - if (matchesSequenceToken(e, overrides, 'nav.moveUp') || k === 'ArrowUp') { + if (seq('nav.moveUp') || k === 'ArrowUp') { consume() moveCursor(-1) return } - if (matchesSequenceToken(e, overrides, 'nav.jumpBottom')) { + if (seq('nav.jumpBottom')) { consume() setCursorIndex(filtered.length - 1) return } if ( + vimMode && advanceSequence( e, getKeymapBinding(overrides, 'nav.jumpTop'), @@ -257,7 +267,7 @@ export function TagView(): JSX.Element { ) { return } - if ((k === 'Enter' || matchesSequenceToken(e, overrides, 'nav.openResult')) && current) { + if ((k === 'Enter' || seq('nav.openResult')) && current) { consume() void openCurrent() } @@ -271,6 +281,7 @@ export function TagView(): JSX.Element { filtered.length, current, keymapOverrides, + vimMode, openCurrent, closeTagView ]) diff --git a/packages/app-core/src/components/TasksView.tsx b/packages/app-core/src/components/TasksView.tsx index a161ca31..32d07ce7 100644 --- a/packages/app-core/src/components/TasksView.tsx +++ b/packages/app-core/src/components/TasksView.tsx @@ -8,6 +8,7 @@ import { TasksKanban } from './TasksKanban' import { CalendarIcon, CheckSquareIcon, KanbanIcon, ListIcon } from './icons' import { advanceSequence, getKeymapBinding, matchesSequenceToken } from '../lib/keymaps' import { isImeComposing } from '../lib/ime' +import { isAppOverlayOpen } from '../lib/overlay-open' type GroupKey = 'today' | 'upcoming' | 'waiting' | 'done' @@ -41,6 +42,7 @@ export function TasksView(): JSX.Element { const toggleTaskFromList = useStore((s) => s.toggleTaskFromList) const closeTasksView = useStore((s) => s.closeTasksView) const keymapOverrides = useStore((s) => s.keymapOverrides) + const vimMode = useStore((s) => s.vimMode) const viewMode = useStore((s) => s.tasksViewMode) const setViewMode = useStore((s) => s.setTasksViewMode) // Only the Tasks panel in the *active* pane should listen for j/k/etc. @@ -203,6 +205,9 @@ export function TasksView(): JSX.Element { useEffect(() => { if (!isActivePanel) return const handler = (e: KeyboardEvent): void => { + // A modal/menu owns the keyboard while open — don't fire list shortcuts + // through it. (songgenqing report) + if (isAppOverlayOpen()) return // While the Vim hint overlay is open it owns the keyboard; don't let // task navigation (or Esc closing the view) steal its keys. (#151) if (document.querySelector('[data-vim-hint-overlay]')) return @@ -215,6 +220,10 @@ export function TasksView(): JSX.Element { const key = e.key const overrides = keymapOverrides + // When Vim mode is off, the single-key Vim shortcuts (j/k/gg/G/o/Space/1-3/…) + // are disabled — only arrows/Enter/Escape navigate. (songgenqing report) + const seq = (id: Parameters<typeof matchesSequenceToken>[2]): boolean => + vimMode && matchesSequenceToken(e, overrides, id) const consume = (): void => { e.preventDefault() e.stopImmediatePropagation() @@ -229,31 +238,31 @@ export function TasksView(): JSX.Element { return } - // View switcher works regardless of sub-view. - if (key === '1') { + // View switcher works regardless of sub-view (Vim mode only). + if (vimMode && key === '1') { consume() setViewMode('list') return } - if (key === '2') { + if (vimMode && key === '2') { consume() setViewMode('calendar') return } - if (key === '3') { + if (vimMode && key === '3') { consume() setViewMode('kanban') return } - if (matchesSequenceToken(e, overrides, 'nav.filter')) { + if (seq('nav.filter')) { consume() filterRef.current?.focus() filterRef.current?.select() return } - if (matchesSequenceToken(e, overrides, 'nav.localEx')) { + if (seq('nav.localEx')) { consume() setExValue('') setExOpen(true) @@ -265,22 +274,23 @@ export function TasksView(): JSX.Element { // List-mode-only navigation. Calendar and Kanban have their own. if (viewMode !== 'list') return - if (matchesSequenceToken(e, overrides, 'nav.moveDown') || key === 'ArrowDown') { + if (seq('nav.moveDown') || key === 'ArrowDown') { consume() moveCursor(1) return } - if (matchesSequenceToken(e, overrides, 'nav.moveUp') || key === 'ArrowUp') { + if (seq('nav.moveUp') || key === 'ArrowUp') { consume() moveCursor(-1) return } - if (matchesSequenceToken(e, overrides, 'nav.jumpBottom')) { + if (seq('nav.jumpBottom')) { consume() setCursorIndex(taskRowIndices.length - 1) return } if ( + vimMode && advanceSequence( e, getKeymapBinding(overrides, 'nav.jumpTop'), @@ -294,12 +304,12 @@ export function TasksView(): JSX.Element { return } - if ((key === 'Enter' || matchesSequenceToken(e, overrides, 'nav.openResult')) && currentTask) { + if ((key === 'Enter' || seq('nav.openResult')) && currentTask) { consume() void openTaskAt(currentTask) return } - if ((key === ' ' || matchesSequenceToken(e, overrides, 'nav.toggleTask')) && currentTask) { + if (((vimMode && key === ' ') || seq('nav.toggleTask')) && currentTask) { consume() void toggleTaskFromList(currentTask) return @@ -315,6 +325,7 @@ export function TasksView(): JSX.Element { taskRowIndices.length, currentTask, keymapOverrides, + vimMode, openTaskAt, toggleTaskFromList, closeTasksView, diff --git a/packages/app-core/src/components/TrashView.tsx b/packages/app-core/src/components/TrashView.tsx index b831182f..d8d3753d 100644 --- a/packages/app-core/src/components/TrashView.tsx +++ b/packages/app-core/src/components/TrashView.tsx @@ -6,6 +6,7 @@ import { CollectionViewHeader } from './CollectionViewHeader' import { advanceSequence, getKeymapBinding, matchesSequenceToken } from '../lib/keymaps' import { getSystemFolderLabel } from '../lib/system-folder-labels' import { confirmApp } from '../lib/confirm-requests' +import { isAppOverlayOpen } from '../lib/overlay-open' function formatDate(ms: number): string { const d = new Date(ms) @@ -29,6 +30,7 @@ export function TrashView(): JSX.Element { const selectNote = useStore((s) => s.selectNote) const closeActiveNote = useStore((s) => s.closeActiveNote) const keymapOverrides = useStore((s) => s.keymapOverrides) + const vimMode = useStore((s) => s.vimMode) const setFocusedPanel = useStore((s) => s.setFocusedPanel) const systemFolderLabels = useStore((s) => s.systemFolderLabels) const amActive = useStore(isTrashViewActive) @@ -125,6 +127,9 @@ export function TrashView(): JSX.Element { useEffect(() => { if (!amActive) return const handler = (e: KeyboardEvent): void => { + // A modal/menu (e.g. the delete-confirm dialog) owns the keyboard while + // open — don't let list shortcuts fire through it. (songgenqing report) + if (isAppOverlayOpen()) return const active = document.activeElement as HTMLElement | null if (active) { const tag = active.tagName @@ -134,6 +139,10 @@ export function TrashView(): JSX.Element { const key = e.key const overrides = keymapOverrides + // When Vim mode is off, the single-key Vim shortcuts (j/k/x/d/gg/G/o/r//…) + // are disabled — only arrows/Enter/Escape navigate. (songgenqing report) + const seq = (id: Parameters<typeof matchesSequenceToken>[2]): boolean => + vimMode && matchesSequenceToken(e, overrides, id) const consume = (): void => { e.preventDefault() e.stopImmediatePropagation() @@ -150,29 +159,30 @@ export function TrashView(): JSX.Element { return } - if (matchesSequenceToken(e, overrides, 'nav.filter')) { + if (seq('nav.filter')) { consume() filterRef.current?.focus() filterRef.current?.select() return } - if (matchesSequenceToken(e, overrides, 'nav.moveDown') || key === 'ArrowDown') { + if (seq('nav.moveDown') || key === 'ArrowDown') { consume() setCursorIndex((i) => Math.max(0, Math.min(filtered.length - 1, i + 1))) return } - if (matchesSequenceToken(e, overrides, 'nav.moveUp') || key === 'ArrowUp') { + if (seq('nav.moveUp') || key === 'ArrowUp') { consume() setCursorIndex((i) => Math.max(0, Math.min(filtered.length - 1, i - 1))) return } - if (matchesSequenceToken(e, overrides, 'nav.jumpBottom')) { + if (seq('nav.jumpBottom')) { consume() setCursorIndex(filtered.length - 1) return } if ( + vimMode && advanceSequence( e, getKeymapBinding(overrides, 'nav.jumpTop'), @@ -185,17 +195,17 @@ export function TrashView(): JSX.Element { ) { return } - if ((key === 'Enter' || matchesSequenceToken(e, overrides, 'nav.openResult')) && current) { + if ((key === 'Enter' || seq('nav.openResult')) && current) { consume() void openCurrent() return } - if (matchesSequenceToken(e, overrides, 'nav.restore') && current) { + if (seq('nav.restore') && current) { consume() void restoreNote(current) return } - if ((matchesSequenceToken(e, overrides, 'nav.delete') || key === 'd') && current) { + if ((seq('nav.delete') || (vimMode && key === 'd')) && current) { consume() void deleteNoteForever(current) } @@ -213,6 +223,7 @@ export function TrashView(): JSX.Element { filter, filtered.length, keymapOverrides, + vimMode, openCurrent, restoreNote ]) diff --git a/packages/app-core/src/lib/overlay-open.test.ts b/packages/app-core/src/lib/overlay-open.test.ts new file mode 100644 index 00000000..0488d7e8 --- /dev/null +++ b/packages/app-core/src/lib/overlay-open.test.ts @@ -0,0 +1,27 @@ +// @vitest-environment jsdom +import { afterEach, describe, expect, it } from 'vitest' +import { isAppOverlayOpen } from './overlay-open' + +afterEach(() => { + document.body.innerHTML = '' +}) + +describe('isAppOverlayOpen', () => { + it('is false with no overlay in the DOM', () => { + expect(isAppOverlayOpen()).toBe(false) + }) + + it('is true while a prompt/confirm modal is open', () => { + const el = document.createElement('div') + el.setAttribute('data-prompt-modal', '') + document.body.appendChild(el) + expect(isAppOverlayOpen()).toBe(true) + }) + + it('is true while a context menu is open', () => { + const el = document.createElement('div') + el.setAttribute('data-ctx-menu', '') + document.body.appendChild(el) + expect(isAppOverlayOpen()).toBe(true) + }) +}) diff --git a/packages/app-core/src/lib/overlay-open.ts b/packages/app-core/src/lib/overlay-open.ts new file mode 100644 index 00000000..5223cc55 --- /dev/null +++ b/packages/app-core/src/lib/overlay-open.ts @@ -0,0 +1,15 @@ +/** + * True when a blocking overlay — a prompt/confirm modal or a context menu — is + * open. The built-in list views (Trash, Archive, Tasks, Tags, Quick Notes) + * register global capture-phase keydown handlers; they must bail while an + * overlay is open so it owns the keyboard. Otherwise keys fire *through* the + * overlay: e.g. the Trash delete-confirm dialog would be bypassed by Enter and + * repeated Enter would keep deleting. Both PromptModal and ConfirmModal carry + * `data-prompt-modal`; ContextMenu carries `data-ctx-menu`. + */ +export function isAppOverlayOpen(): boolean { + if (typeof document === 'undefined') return false + return !!( + document.querySelector('[data-prompt-modal]') || document.querySelector('[data-ctx-menu]') + ) +} From 0e4f7feab313dbf0836411d4edfe6ba3675dcfec Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Tue, 16 Jun 2026 17:26:41 -0500 Subject: [PATCH 35/66] Add markdown snippet auto-close in the editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Type an inline delimiter (`**` `__` `~~` ``` `==` `[[` `%%`) then Space to wrap the cursor, or a fence (``````, `~~~`, `$$`) then Enter to expand a block with the cursor inside. Skips already-closed pairs, closing fences, and escaped delimiters. Gated by a new 'Markdown snippets' setting (Settings → Writing, default on) and only active while typing — Vim off or Vim insert mode, never normal/visual mode. Wired into every editor surface via one shared helper. Ported from community PR #185 (songgenqing). --- .../app-core/src/components/EditorPane.tsx | 2 + .../src/components/ExternalFileApp.tsx | 2 + .../src/components/FloatingNoteApp.tsx | 2 + .../src/components/QuickCaptureApp.tsx | 2 + .../app-core/src/components/SettingsModal.tsx | 15 + .../src/components/TemplateEditorModal.tsx | 2 + .../src/lib/cm-markdown-snippets.test.ts | 150 ++++++++++ .../app-core/src/lib/cm-markdown-snippets.ts | 259 ++++++++++++++++++ .../src/lib/markdown-snippets-config.ts | 22 ++ packages/app-core/src/store.ts | 18 ++ 10 files changed, 474 insertions(+) create mode 100644 packages/app-core/src/lib/cm-markdown-snippets.test.ts create mode 100644 packages/app-core/src/lib/cm-markdown-snippets.ts create mode 100644 packages/app-core/src/lib/markdown-snippets-config.ts diff --git a/packages/app-core/src/components/EditorPane.tsx b/packages/app-core/src/components/EditorPane.tsx index cc69c6e3..09e04473 100644 --- a/packages/app-core/src/components/EditorPane.tsx +++ b/packages/app-core/src/components/EditorPane.tsx @@ -78,6 +78,7 @@ import { wikilinkRenderExtension } from '../lib/cm-wikilink-render' import { slashCommandSource, slashCommandRender } from '../lib/cm-slash-commands' import { dateShortcutSource } from '../lib/cm-date-shortcuts' import { wikilinkSource } from '../lib/cm-wikilinks' +import { appMarkdownSnippetExtension } from '../lib/markdown-snippets-config' import { LazyDiagramTabView, LazyPreview as Preview } from './LazyPreview' import { ConnectionsPanel } from './ConnectionsPanel' import { OutlinePanel } from './OutlinePanel' @@ -1354,6 +1355,7 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { const state = EditorState.create({ doc: initialBody, extensions: [ + appMarkdownSnippetExtension(), vimCompartment.of(s0.vimMode ? vim() : []), history(), drawSelection(), diff --git a/packages/app-core/src/components/ExternalFileApp.tsx b/packages/app-core/src/components/ExternalFileApp.tsx index 23dc7545..270eb21d 100644 --- a/packages/app-core/src/components/ExternalFileApp.tsx +++ b/packages/app-core/src/components/ExternalFileApp.tsx @@ -19,6 +19,7 @@ import { markdown, markdownLanguage } from '@codemirror/lang-markdown' import { resolveCodeLanguage } from '../lib/cm-code-languages' import { applyVimInsertEscape } from '../lib/vim-insert-escape' import { markdownListIndentPlugin } from '../lib/cm-markdown-list-indent' +import { appMarkdownSnippetExtension } from '../lib/markdown-snippets-config' import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language' import { searchKeymap } from '@codemirror/search' import type { ExternalFileContent } from '@shared/ipc' @@ -108,6 +109,7 @@ export function ExternalFileApp(): JSX.Element { // ref keeps the current text when the editor remounts on toggles. doc: bodyRef.current ?? '', extensions: [ + appMarkdownSnippetExtension(), new Compartment().of(prefs.vimMode ? vim() : []), history(), drawSelection(), diff --git a/packages/app-core/src/components/FloatingNoteApp.tsx b/packages/app-core/src/components/FloatingNoteApp.tsx index 4abc40ba..8bcbc0b6 100644 --- a/packages/app-core/src/components/FloatingNoteApp.tsx +++ b/packages/app-core/src/components/FloatingNoteApp.tsx @@ -31,6 +31,7 @@ import { markdown, markdownLanguage } from '@codemirror/lang-markdown' import { resolveCodeLanguage } from '../lib/cm-code-languages' import { applyVimInsertEscape } from '../lib/vim-insert-escape' import { markdownListIndentPlugin } from '../lib/cm-markdown-list-indent' +import { appMarkdownSnippetExtension } from '../lib/markdown-snippets-config' import { syntaxHighlighting, HighlightStyle, defaultHighlightStyle } from '@codemirror/language' import { tags as t } from '@lezer/highlight' import { searchKeymap } from '@codemirror/search' @@ -291,6 +292,7 @@ export function FloatingNoteApp({ notePath }: { notePath: string }): JSX.Element // toggles (the `content` closure would recreate it empty). doc: dirtyBodyRef.current ?? content?.body ?? '', extensions: [ + appMarkdownSnippetExtension(), new Compartment().of(prefs.vimMode ? vim() : []), history(), drawSelection(), diff --git a/packages/app-core/src/components/QuickCaptureApp.tsx b/packages/app-core/src/components/QuickCaptureApp.tsx index 1e305f7a..776d46da 100644 --- a/packages/app-core/src/components/QuickCaptureApp.tsx +++ b/packages/app-core/src/components/QuickCaptureApp.tsx @@ -44,6 +44,7 @@ import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirro import { markdown, markdownLanguage } from '@codemirror/lang-markdown' import { resolveCodeLanguage } from '../lib/cm-code-languages' import { markdownListIndentPlugin } from '../lib/cm-markdown-list-indent' +import { appMarkdownSnippetExtension } from '../lib/markdown-snippets-config' import { syntaxHighlighting, HighlightStyle, defaultHighlightStyle } from '@codemirror/language' import { tags as t } from '@lezer/highlight' import { searchKeymap } from '@codemirror/search' @@ -407,6 +408,7 @@ export function QuickCaptureApp(): JSX.Element { const state = EditorState.create({ doc: '', extensions: [ + appMarkdownSnippetExtension(), new Compartment().of(prefs.vimMode ? vim() : []), history(), drawSelection(), diff --git a/packages/app-core/src/components/SettingsModal.tsx b/packages/app-core/src/components/SettingsModal.tsx index 4fc310ff..91200b2a 100644 --- a/packages/app-core/src/components/SettingsModal.tsx +++ b/packages/app-core/src/components/SettingsModal.tsx @@ -235,6 +235,8 @@ export function SettingsModal(): JSX.Element { const setFzfBinaryPath = useStore((s) => s.setFzfBinaryPath) const livePreview = useStore((s) => s.livePreview) const setLivePreview = useStore((s) => s.setLivePreview) + const markdownSnippets = useStore((s) => s.markdownSnippets) + const setMarkdownSnippets = useStore((s) => s.setMarkdownSnippets) const tabsEnabled = useStore((s) => s.tabsEnabled) const setTabsEnabled = useStore((s) => s.setTabsEnabled) const wrapTabs = useStore((s) => s.wrapTabs) @@ -934,6 +936,12 @@ export function SettingsModal(): JSX.Element { description: 'Hide markdown syntax on lines you are not editing.', keywords: ['preview', 'markdown'] }, + { + id: 'markdown-snippets', + title: 'Markdown snippets', + description: 'Auto-close markdown delimiters as you type (** then Space, ``` then Enter).', + keywords: ['snippets', 'auto close', 'autoclose', 'auto-pair', 'brackets', 'markdown', 'completion'] + }, { id: 'note-tabs', title: 'Note tabs', @@ -1115,6 +1123,13 @@ export function SettingsModal(): JSX.Element { settingId="live-preview" onChange={setLivePreview} /> + <ToggleRow + label="Markdown snippets" + description="Auto-close markdown as you type: ** / __ / ~~ / ` / == / [[ / %% then Space wrap the cursor, and ``` / ~~~ / $$ then Enter expand a fenced block. In Vim mode this only applies in insert mode." + value={markdownSnippets} + settingId="markdown-snippets" + onChange={setMarkdownSnippets} + /> <ToggleRow label="Note tabs" description="Open notes in tabs and allow split-friendly tab workflows. Turn off to keep the simpler single-note behavior." diff --git a/packages/app-core/src/components/TemplateEditorModal.tsx b/packages/app-core/src/components/TemplateEditorModal.tsx index 99f97ce9..6c44eb1e 100644 --- a/packages/app-core/src/components/TemplateEditorModal.tsx +++ b/packages/app-core/src/components/TemplateEditorModal.tsx @@ -20,6 +20,7 @@ import { parseFrontmatter, slugifyTemplateName } from '@shared/template-files' import { renderTemplate } from '../lib/template-render' import { resolveCodeLanguage } from '../lib/cm-code-languages' import { markdownListIndentPlugin } from '../lib/cm-markdown-list-indent' +import { appMarkdownSnippetExtension } from '../lib/markdown-snippets-config' import { templateVariableSource, TEMPLATE_VARIABLES } from '../lib/cm-template-variables' import { templateSlashCommandSource, slashCommandRender } from '../lib/cm-slash-commands' import { completionNavKeymap } from '../lib/cm-completion-nav' @@ -109,6 +110,7 @@ export function TemplateEditorModal({ const state = EditorState.create({ doc: initialRaw ?? SKELETON, extensions: [ + appMarkdownSnippetExtension(), new Compartment().of(vimModeRef.current ? vim() : []), history(), drawSelection(), diff --git a/packages/app-core/src/lib/cm-markdown-snippets.test.ts b/packages/app-core/src/lib/cm-markdown-snippets.test.ts new file mode 100644 index 00000000..6e651d3c --- /dev/null +++ b/packages/app-core/src/lib/cm-markdown-snippets.test.ts @@ -0,0 +1,150 @@ +import { EditorState } from '@codemirror/state' +import { describe, expect, it } from 'vitest' +import { markdownSnippetExtension, markdownSnippetTransaction } from './cm-markdown-snippets' + +function createState(doc: string, pos = doc.length): EditorState { + return EditorState.create({ + doc, + selection: { anchor: pos }, + extensions: [markdownSnippetExtension()] + }) +} + +function typeInput(state: EditorState, text: string): EditorState { + const pos = state.selection.main.head + return state.update({ + changes: { from: pos, to: pos, insert: text }, + selection: { anchor: pos + text.length }, + userEvent: 'input.type' + }).state +} + +function typeChars(state: EditorState, text: string): EditorState { + let next = state + for (const char of text) next = typeInput(next, char) + return next +} + +function selectionOnlyUpdate(state: EditorState): EditorState { + return state.update({ + selection: { anchor: state.selection.main.head } + }).state +} + +function triggerSnippet(state: EditorState, key: string): EditorState | null { + const transaction = markdownSnippetTransaction(state, key) + if (!transaction) return null + return state.update(transaction).state +} + +function typeThenTrigger(doc: string, typed: string, key: string, pos = doc.length): EditorState | null { + return triggerSnippet(typeInput(createState(doc, pos), typed), key) +} + +function applySnippet(doc: string, key: string, pos = doc.length): EditorState | null { + return triggerSnippet(createState(doc, pos), key) +} + +describe('markdownSnippetTransaction', () => { + it('expands a backtick fence with Enter', () => { + const state = typeThenTrigger('', '```', 'Enter') + + expect(state?.doc.toString()).toBe('```\n\n```') + expect(state?.selection.main.head).toBe(4) + }) + + it('expands a backtick fence after character-by-character typing', () => { + const state = triggerSnippet(typeChars(createState(''), '```'), 'Enter') + + expect(state?.doc.toString()).toBe('```\n\n```') + expect(state?.selection.main.head).toBe(4) + }) + + it('keeps a pending block snippet across selection-only updates', () => { + const state = triggerSnippet(selectionOnlyUpdate(typeChars(createState(''), '```')), 'Enter') + + expect(state?.doc.toString()).toBe('```\n\n```') + expect(state?.selection.main.head).toBe(4) + }) + + it('does not expand block snippets with Space', () => { + expect(applySnippet('```', 'Space')).toBeNull() + expect(applySnippet('~~~', 'Space')).toBeNull() + expect(applySnippet('$$', 'Space')).toBeNull() + }) + + it('preserves indentation for block snippets', () => { + const state = typeThenTrigger('', ' $$', 'Enter') + + expect(state?.doc.toString()).toBe(' $$\n \n $$') + expect(state?.selection.main.head).toBe(7) + }) + + it('does not expand an already closed block', () => { + expect(applySnippet('```\nbody\n```', 'Enter', 3)).toBeNull() + }) + + it('does not treat a closing block delimiter as a new opener', () => { + expect(typeThenTrigger('```\ncode\n', '```', 'Enter')).toBeNull() + expect(typeThenTrigger('``` js\ncode\n', '```', 'Enter')).toBeNull() + expect(typeThenTrigger('```js\ncode\n', '```', 'Enter')).toBeNull() + expect(typeThenTrigger('$$\nmath\n', '$$', 'Enter')).toBeNull() + }) + + it('still expands a later block delimiter after a closed block', () => { + const state = typeThenTrigger('```\ncode\n```\n', '```', 'Enter') + + expect(state?.doc.toString()).toBe('```\ncode\n```\n```\n\n```') + expect(state?.selection.main.head).toBe(17) + }) + + it('expands after a prior fenced code block with an info string', () => { + const state = typeThenTrigger('```ts\nconst mode = "preview"\n```\n', '```', 'Enter') + + expect(state?.doc.toString()).toBe('```ts\nconst mode = "preview"\n```\n```\n\n```') + }) + + it('expands before an existing fenced code block with an info string', () => { + const doc = '\n```ts\nconst mode = "preview"\n```' + const state = typeThenTrigger(doc, '```', 'Enter', 0) + + expect(state?.doc.toString()).toBe('```\n\n```\n```ts\nconst mode = "preview"\n```') + }) + + it('expands inline strong markup with Space', () => { + const state = applySnippet('**', 'Space') + + expect(state?.doc.toString()).toBe('****') + expect(state?.selection.main.head).toBe(2) + }) + + it('expands wikilinks with Space', () => { + const state = applySnippet('[[', 'Space') + + expect(state?.doc.toString()).toBe('[[]]') + expect(state?.selection.main.head).toBe(2) + }) + + it('does not expand inline markup that is already closed', () => { + expect(applySnippet('****', 'Space', 2)).toBeNull() + }) + + it('does not treat closing delimiters as new openers', () => { + expect(applySnippet('**text**', 'Space')).toBeNull() + expect(applySnippet('`code`', 'Space')).toBeNull() + expect(applySnippet('~~done~~', 'Space')).toBeNull() + expect(applySnippet('%%comment%%', 'Space')).toBeNull() + }) + + it('still expands a later unmatched delimiter after a closed pair', () => { + const state = applySnippet('**text** **', 'Space') + + expect(state?.doc.toString()).toBe('**text** ****') + expect(state?.selection.main.head).toBe(11) + }) + + it('does not handle unrelated keys or text', () => { + expect(applySnippet('**', 'Enter')).toBeNull() + expect(applySnippet('hello', 'Space')).toBeNull() + }) +}) diff --git a/packages/app-core/src/lib/cm-markdown-snippets.ts b/packages/app-core/src/lib/cm-markdown-snippets.ts new file mode 100644 index 00000000..f68aad50 --- /dev/null +++ b/packages/app-core/src/lib/cm-markdown-snippets.ts @@ -0,0 +1,259 @@ +import { + Facet, + Prec, + StateField, + type EditorState, + type Extension, + type TransactionSpec +} from '@codemirror/state' +import { keymap, type EditorView } from '@codemirror/view' + +export type MarkdownSnippetMode = 'inline' | 'block' + +export interface MarkdownSnippetRule { + id: string + open: string + close: string + triggerKeys: readonly string[] + mode: MarkdownSnippetMode +} + +export interface MarkdownSnippetExtensionConfig { + rules?: readonly MarkdownSnippetRule[] + shouldHandle?: (view: EditorView) => boolean +} + +interface PendingBlockSnippet { + ruleId: string + lineFrom: number +} + +export const defaultMarkdownSnippetRules: readonly MarkdownSnippetRule[] = [ + { id: 'fenced-code-backtick', open: '```', close: '```', triggerKeys: ['Enter'], mode: 'block' }, + { id: 'fenced-code-tilde', open: '~~~', close: '~~~', triggerKeys: ['Enter'], mode: 'block' }, + { id: 'math-block', open: '$$', close: '$$', triggerKeys: ['Enter'], mode: 'block' }, + { id: 'strong-asterisk', open: '**', close: '**', triggerKeys: ['Space'], mode: 'inline' }, + { id: 'strong-underscore', open: '__', close: '__', triggerKeys: ['Space'], mode: 'inline' }, + { id: 'strikethrough', open: '~~', close: '~~', triggerKeys: ['Space'], mode: 'inline' }, + { id: 'inline-code', open: '`', close: '`', triggerKeys: ['Space'], mode: 'inline' }, + { id: 'highlight', open: '==', close: '==', triggerKeys: ['Space'], mode: 'inline' }, + { id: 'wikilink', open: '[[', close: ']]', triggerKeys: ['Space'], mode: 'inline' }, + { id: 'comment', open: '%%', close: '%%', triggerKeys: ['Space'], mode: 'inline' } +] + +const markdownSnippetRulesFacet = Facet.define< + readonly MarkdownSnippetRule[], + readonly MarkdownSnippetRule[] +>({ + combine: (values) => values.at(-1) ?? defaultMarkdownSnippetRules +}) + +function isBlockOpenerLine(rule: MarkdownSnippetRule, text: string): boolean { + const content = text.trimEnd().trimStart() + if (!content.startsWith(rule.open)) return false + const after = content.slice(rule.open.length) + if (rule.open === '$$') return after.trim() === '' + return true +} + +function isBlockCloserLine(rule: MarkdownSnippetRule, text: string): boolean { + return text.trim() === rule.close +} + +function hasUnclosedBlockOpenerAbove( + state: EditorState, + lineNumber: number, + rule: MarkdownSnippetRule +): boolean { + let open = false + for (let number = 1; number < lineNumber; number++) { + const text = state.doc.line(number).text + if (rule.open === rule.close) { + if (isBlockOpenerLine(rule, text)) open = !open + } else if (isBlockOpenerLine(rule, text)) { + open = true + } else if (isBlockCloserLine(rule, text)) { + open = false + } + } + return open +} + +function blockPendingAt( + state: EditorState, + pos: number, + rules: readonly MarkdownSnippetRule[] +): PendingBlockSnippet | null { + const line = state.doc.lineAt(pos) + if (pos !== line.to) return null + + for (const rule of rules) { + if (rule.mode !== 'block') continue + if (!isBlockOpenerLine(rule, line.text)) continue + if (hasUnclosedBlockOpenerAbove(state, line.number, rule)) continue + return { ruleId: rule.id, lineFrom: line.from } + } + return null +} + +const pendingBlockSnippetField = StateField.define<PendingBlockSnippet | null>({ + create: () => null, + update: (pending, tr) => { + if (!tr.docChanged) { + if (!pending) return null + const selection = tr.state.selection.main + if (!selection.empty) return null + const line = tr.state.doc.lineAt(selection.head) + if (line.from !== pending.lineFrom || selection.head !== line.to) return null + const rule = tr.state + .facet(markdownSnippetRulesFacet) + .find((candidate) => candidate.id === pending.ruleId) + if (!rule || !isBlockOpenerLine(rule, line.text)) return null + return pending + } + if (!tr.isUserEvent('input')) return null + const selection = tr.state.selection.main + if (!selection.empty) return null + return blockPendingAt(tr.state, selection.head, tr.state.facet(markdownSnippetRulesFacet)) + } +}) + +export function pendingMarkdownBlockSnippetLineFrom(state: EditorState): number | null { + return state.field(pendingBlockSnippetField, false)?.lineFrom ?? null +} + +export function hasPendingMarkdownBlockSnippet(state: EditorState): boolean { + return pendingMarkdownBlockSnippetLineFrom(state) != null +} + +export function isPendingMarkdownBlockSnippetStart(state: EditorState, from: number): boolean { + return pendingMarkdownBlockSnippetLineFrom(state) === state.doc.lineAt(from).from +} + +function blockSnippetTransaction( + state: EditorState, + pending: PendingBlockSnippet, + rules: readonly MarkdownSnippetRule[] +): TransactionSpec | null { + const rule = rules.find((candidate) => candidate.id === pending.ruleId) + if (!rule) return null + + const selection = state.selection.main + if (!selection.empty) return null + const line = state.doc.lineAt(selection.head) + if (line.from !== pending.lineFrom || selection.head !== line.to) return null + if (!isBlockOpenerLine(rule, line.text)) return null + if (hasUnclosedBlockOpenerAbove(state, line.number, rule)) return null + + const indentMatch = line.text.match(/^[\t ]*/) + const indent = indentMatch?.[0] ?? '' + + const insert = `${line.text}\n${indent}\n${indent}${rule.close}` + const cursor = line.from + line.text.length + 1 + indent.length + return { + changes: { from: line.from, to: line.to, insert }, + selection: { anchor: cursor } + } +} + +function hasOddBackslashRun(text: string, before: number): boolean { + let count = 0 + for (let i = before - 1; i >= 0 && text[i] === '\\'; i--) count++ + return count % 2 === 1 +} + +function countUnescapedOccurrences(text: string, token: string): number { + let count = 0 + for (let index = 0; index <= text.length - token.length; index++) { + if (text.slice(index, index + token.length) !== token) continue + if (!hasOddBackslashRun(text, index)) count++ + index += token.length - 1 + } + return count +} + +function isOpeningDelimiter(state: EditorState, rule: MarkdownSnippetRule, from: number): boolean { + const line = state.doc.lineAt(from) + const before = state.doc.sliceString(line.from, from) + if (hasOddBackslashRun(before, before.length)) return false + if (rule.open === rule.close && countUnescapedOccurrences(before, rule.open) % 2 === 1) { + return false + } + return true +} + +function inlineSnippetTransaction( + state: EditorState, + rule: MarkdownSnippetRule, + pos: number +): TransactionSpec | null { + if (pos < rule.open.length) return null + const from = pos - rule.open.length + if (state.doc.sliceString(from, pos) !== rule.open) return null + if (!isOpeningDelimiter(state, rule, from)) return null + if ( + rule.open.length > 0 && + [...rule.open].every((char) => char === rule.open[0]) && + from > 0 && + state.doc.sliceString(from - 1, from) === rule.open[0] + ) { + return null + } + if (state.doc.sliceString(pos, Math.min(state.doc.length, pos + rule.close.length)) === rule.close) { + return null + } + + return { + changes: { from, to: pos, insert: rule.open + rule.close }, + selection: { anchor: from + rule.open.length } + } +} + +export function markdownSnippetTransaction( + state: EditorState, + triggerKey: string +): TransactionSpec | null { + const selection = state.selection.main + if (!selection.empty) return null + + const rules = state.facet(markdownSnippetRulesFacet) + const pending = state.field(pendingBlockSnippetField, false) + if (pending) { + const rule = rules.find((candidate) => candidate.id === pending.ruleId) + if (rule?.triggerKeys.includes(triggerKey)) { + const transaction = blockSnippetTransaction(state, pending, rules) + if (transaction) return transaction + } + } + + for (const rule of rules) { + if (rule.mode === 'block') continue + if (!rule.triggerKeys.includes(triggerKey)) continue + const transaction = inlineSnippetTransaction(state, rule, selection.head) + if (transaction) return transaction + } + return null +} + +export function markdownSnippetExtension(config: MarkdownSnippetExtensionConfig = {}): Extension { + const rules = config.rules ?? defaultMarkdownSnippetRules + const triggerKeys = [...new Set(rules.flatMap((rule) => rule.triggerKeys))] + return [ + markdownSnippetRulesFacet.of(rules), + pendingBlockSnippetField, + Prec.high( + keymap.of( + triggerKeys.map((triggerKey) => ({ + key: triggerKey, + run: (view) => { + if (config.shouldHandle && !config.shouldHandle(view)) return false + const transaction = markdownSnippetTransaction(view.state, triggerKey) + if (!transaction) return false + view.dispatch(transaction) + return true + } + })) + ) + ) + ] +} diff --git a/packages/app-core/src/lib/markdown-snippets-config.ts b/packages/app-core/src/lib/markdown-snippets-config.ts new file mode 100644 index 00000000..72d98018 --- /dev/null +++ b/packages/app-core/src/lib/markdown-snippets-config.ts @@ -0,0 +1,22 @@ +import type { Extension } from '@codemirror/state' +import type { EditorView } from '@codemirror/view' +import { markdownSnippetExtension } from './cm-markdown-snippets' +import { isEditorInsertMode } from './vim-nav' +import { useStore } from '../store' + +/** + * Markdown snippet auto-close, wired to app state. Single source of truth for + * *when* snippets fire, shared by every editor surface: + * - respects the `markdownSnippets` pref (Settings → Writing), and + * - only fires while actually typing — Vim off, or Vim *insert* mode — never + * in Vim normal/visual mode, where Space/Enter belong to Vim. (songgenqing) + */ +export function appMarkdownSnippetExtension(): Extension { + return markdownSnippetExtension({ + shouldHandle: (view: EditorView) => { + const s = useStore.getState() + if (!s.markdownSnippets) return false + return !s.vimMode || isEditorInsertMode(view, s.vimMode) + } + }) +} diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index 78b051e6..9a9829b3 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -283,6 +283,9 @@ interface Prefs { /** Optional explicit binary path for fzf. Blank uses PATH lookup. */ fzfBinaryPath: string | null livePreview: boolean // hide markdown syntax on inactive lines + /** Auto-close markdown delimiters while typing: `**`+Space → `**|**`, + * ```` ``` ````+Enter expands a fenced block. Off restores plain typing. */ + markdownSnippets: boolean hideBuiltinTemplates: boolean // hide shipped built-in templates from the pickers tabsEnabled: boolean wrapTabs: boolean @@ -424,6 +427,7 @@ const DEFAULT_PREFS: Prefs = { ripgrepBinaryPath: null, fzfBinaryPath: null, livePreview: true, + markdownSnippets: true, hideBuiltinTemplates: false, tabsEnabled: true, wrapTabs: false, @@ -526,6 +530,10 @@ function normalizePrefs(p: Partial<Prefs>): Prefs { : DEFAULT_PREFS.fzfBinaryPath, livePreview: typeof p.livePreview === 'boolean' ? p.livePreview : DEFAULT_PREFS.livePreview, + markdownSnippets: + typeof p.markdownSnippets === 'boolean' + ? p.markdownSnippets + : DEFAULT_PREFS.markdownSnippets, hideBuiltinTemplates: typeof p.hideBuiltinTemplates === 'boolean' ? p.hideBuiltinTemplates @@ -1077,6 +1085,7 @@ function collectPrefs(s: { ripgrepBinaryPath: string | null fzfBinaryPath: string | null livePreview: boolean + markdownSnippets: boolean hideBuiltinTemplates: boolean tabsEnabled: boolean wrapTabs: boolean @@ -1135,6 +1144,7 @@ function collectPrefs(s: { ripgrepBinaryPath: s.ripgrepBinaryPath, fzfBinaryPath: s.fzfBinaryPath, livePreview: s.livePreview, + markdownSnippets: s.markdownSnippets, hideBuiltinTemplates: s.hideBuiltinTemplates, tabsEnabled: s.tabsEnabled, wrapTabs: s.wrapTabs, @@ -1496,6 +1506,8 @@ interface Store { ripgrepBinaryPath: string | null fzfBinaryPath: string | null livePreview: boolean + /** Auto-close markdown delimiters while typing. Persisted. */ + markdownSnippets: boolean hideBuiltinTemplates: boolean tabsEnabled: boolean wrapTabs: boolean @@ -1781,6 +1793,7 @@ interface Store { setRipgrepBinaryPath: (path: string | null) => void setFzfBinaryPath: (path: string | null) => void setLivePreview: (on: boolean) => void + setMarkdownSnippets: (on: boolean) => void setHideBuiltinTemplates: (hidden: boolean) => void setTabsEnabled: (on: boolean) => void setWrapTabs: (on: boolean) => void @@ -2806,6 +2819,7 @@ export const useStore = create<Store>((set, get) => { ripgrepBinaryPath: loadPrefs().ripgrepBinaryPath, fzfBinaryPath: loadPrefs().fzfBinaryPath, livePreview: loadPrefs().livePreview, + markdownSnippets: loadPrefs().markdownSnippets, hideBuiltinTemplates: loadPrefs().hideBuiltinTemplates, tabsEnabled: loadPrefs().tabsEnabled, wrapTabs: loadPrefs().wrapTabs, @@ -4275,6 +4289,10 @@ export const useStore = create<Store>((set, get) => { set({ livePreview: on }) savePrefs(collectPrefs(get())) }, + setMarkdownSnippets: (on) => { + set({ markdownSnippets: on }) + savePrefs(collectPrefs(get())) + }, setHideBuiltinTemplates: (hidden) => { set({ hideBuiltinTemplates: hidden }) savePrefs(collectPrefs(get())) From eaa3ae8a081abf0567ac9ddca4494759dae9b418 Mon Sep 17 00:00:00 2001 From: Weslley Almeida <weslleyalmeida6@gmail.com> Date: Tue, 16 Jun 2026 19:48:32 -0300 Subject: [PATCH 36/66] Add Flatpak packaging (#145) * Add GitHub Sponsors username to FUNDING.yml * docs: add GitHub Sponsors link to README * Add Flatpak packaging Adds packaging/flatpak/ mirroring the existing AUR packaging. The manifest downloads the official AppImage from the GitHub release and extracts it at build time (--appimage-extract, no FUSE required), then installs the unpacked Electron app against the Flatpak Electron base app and launches it through zypak. Flatpak ships a self-contained runtime, so it avoids the host FUSE / system-library issues that keep the AppImage from starting on Fedora Atomic, minimal installs and some Arch variants (same class of problem as #65). Includes the flatpak-builder manifest, zypak launcher, desktop entry, AppStream metainfo and a README documenting build and release-bump steps. --------- Co-authored-by: Adib Hanna <adibhanna@gmail.com> --- .github/FUNDING.yml | 1 + README.md | 1 + packaging/flatpak/README.md | 73 +++++++++++++++++++ .../flatpak/com.adibhanna.zennotes.desktop | 10 +++ .../com.adibhanna.zennotes.metainfo.xml | 28 +++++++ packaging/flatpak/com.adibhanna.zennotes.yml | 59 +++++++++++++++ packaging/flatpak/zennotes.sh | 4 + 7 files changed, 176 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 packaging/flatpak/README.md create mode 100644 packaging/flatpak/com.adibhanna.zennotes.desktop create mode 100644 packaging/flatpak/com.adibhanna.zennotes.metainfo.xml create mode 100644 packaging/flatpak/com.adibhanna.zennotes.yml create mode 100644 packaging/flatpak/zennotes.sh diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..ad7852f0 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: adibhanna diff --git a/README.md b/README.md index 4224ee80..cfd97ffb 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ On macOS, the first-party `zen` CLI also powers launcher workflows such as the R Grab the latest build from [GitHub Releases](https://github.com/ZenNotes/zennotes/releases/latest) — see [Install](#install) below. Website: [zennotes.org](https://zennotes.org) +Support development: [GitHub Sponsors](https://github.com/sponsors/adibhanna) Detailed in-repo documentation lives under [docs/README.md](docs/README.md). diff --git a/packaging/flatpak/README.md b/packaging/flatpak/README.md new file mode 100644 index 00000000..8f0a484c --- /dev/null +++ b/packaging/flatpak/README.md @@ -0,0 +1,73 @@ +# Linux packaging (Flatpak) + +This directory holds the Flatpak packaging for ZenNotes. + +## Why this exists + +The same class of problem reported in +[#65](https://github.com/ZenNotes/zennotes/issues/65) — the AppImage failing to +start on some distros — also hits other setups. AppImages rely on the host's +`libfuse2` and system libraries; on Fedora Atomic/Silverblue, some Arch +variants, and minimal installs the image simply won't launch. + +Flatpak sidesteps all of that: the app ships against a self-contained +[org.freedesktop.Platform](https://docs.flatpak.org/) runtime plus the +[Electron base app](https://github.com/flathub/org.electronjs.Electron2.BaseApp), +so it does not depend on host FUSE or system libraries, and it runs sandboxed. + +## How it works + +Like the AUR `PKGBUILD`, this manifest downloads the official AppImage from the +GitHub release and **extracts** it at build time (`--appimage-extract`, which +does not need FUSE). No source rebuild is required. The unpacked Electron app is +installed into `/app` and launched through +[`zypak`](https://github.com/refi64/zypak) (provided by the Electron base app), +which makes Chromium's sandbox work inside the Flatpak sandbox without the SUID +`chrome-sandbox` helper. + +Files: + +- `com.adibhanna.zennotes.yml` — flatpak-builder manifest +- `zennotes.sh` — launcher that wraps the binary with `zypak-wrapper` +- `com.adibhanna.zennotes.desktop` — desktop entry (Markdown + `zennotes://` handler) +- `com.adibhanna.zennotes.metainfo.xml` — AppStream metadata + +## Build & install locally + +Requires `flatpak` and `flatpak-builder`. + +```sh +cd packaging/flatpak + +# one-time: runtime, SDK and Electron base app (from Flathub) +flatpak install -y flathub org.freedesktop.Platform//25.08 \ + org.freedesktop.Sdk//25.08 org.electronjs.Electron2.BaseApp//25.08 + +flatpak-builder --user --install --force-clean build-dir com.adibhanna.zennotes.yml + +flatpak run com.adibhanna.zennotes +``` + +## Updating to a new release + +```sh +cd packaging/flatpak +# 1. bump the `url` in com.adibhanna.zennotes.yml to the new release tag +# 2. update the `sha256`: +curl -L -o /tmp/ZenNotes.AppImage \ + https://github.com/ZenNotes/zennotes/releases/download/v<version>/ZenNotes-<version>-linux-x86_64.AppImage +sha256sum /tmp/ZenNotes.AppImage +# 3. bump the <release> entry in com.adibhanna.zennotes.metainfo.xml +# 4. rebuild and smoke-test (see above) +``` + +## Notes & limitations + +- **Sandbox permissions:** `--filesystem=home` is granted so notes (plain + Markdown files) are reachable. Tighten it to a specific path (e.g. + `--filesystem=~/Notes`) if you prefer. +- **Auto-update is disabled** inside Flatpak (`electron-updater` cannot replace a + read-only `/app`). Update via `flatpak update` once published, or rebuild with + a new `url`/`sha256` for a local install. +- **Publishing to Flathub** would be a follow-up: it needs the `com.adibhanna.*` + app-id owner's sign-off plus screenshots in the AppStream metadata. diff --git a/packaging/flatpak/com.adibhanna.zennotes.desktop b/packaging/flatpak/com.adibhanna.zennotes.desktop new file mode 100644 index 00000000..a8e649a9 --- /dev/null +++ b/packaging/flatpak/com.adibhanna.zennotes.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=ZenNotes +Comment=Keyboard-first Markdown notes app +Exec=zennotes %U +Terminal=false +Type=Application +Icon=com.adibhanna.zennotes +StartupWMClass=ZenNotes +MimeType=text/markdown;x-scheme-handler/zennotes; +Categories=Office;Utility;TextEditor; diff --git a/packaging/flatpak/com.adibhanna.zennotes.metainfo.xml b/packaging/flatpak/com.adibhanna.zennotes.metainfo.xml new file mode 100644 index 00000000..bc3f2085 --- /dev/null +++ b/packaging/flatpak/com.adibhanna.zennotes.metainfo.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<component type="desktop-application"> + <id>com.adibhanna.zennotes</id> + <metadata_license>CC0-1.0</metadata_license> + <project_license>MIT</project_license> + <name>ZenNotes</name> + <summary>Keyboard-first Markdown notes app</summary> + <description> + <p> + ZenNotes is a keyboard-first Markdown notes app that keeps your notes as + ordinary Markdown files on disk. It adds Vim-friendly editing, split and + preview workflows, tasks, tags, archive/trash, diagrams, search, daily + notes and CSV databases (Notion-style Table and Board views over plain + .csv files) on top of the files you already own. + </p> + </description> + <launchable type="desktop-id">com.adibhanna.zennotes.desktop</launchable> + <url type="homepage">https://zennotes.org</url> + <url type="bugtracker">https://github.com/ZenNotes/zennotes/issues</url> + <url type="vcs-browser">https://github.com/ZenNotes/zennotes</url> + <developer id="com.adibhanna"> + <name>Adib Hanna</name> + </developer> + <content_rating type="oars-1.1"/> + <releases> + <release version="2.3.0" date="2026-06-13"/> + </releases> +</component> diff --git a/packaging/flatpak/com.adibhanna.zennotes.yml b/packaging/flatpak/com.adibhanna.zennotes.yml new file mode 100644 index 00000000..dfa2851c --- /dev/null +++ b/packaging/flatpak/com.adibhanna.zennotes.yml @@ -0,0 +1,59 @@ +app-id: com.adibhanna.zennotes +runtime: org.freedesktop.Platform +runtime-version: '25.08' +sdk: org.freedesktop.Sdk +base: org.electronjs.Electron2.BaseApp +base-version: '25.08' +command: zennotes +separate-locales: false + +finish-args: + - --share=ipc + - --share=network + - --socket=fallback-x11 + - --socket=wayland + - --device=dri + - --socket=pulseaudio + # Notes are plain Markdown files on disk — allow access to the home dir. + - --filesystem=home + - --talk-name=org.freedesktop.Notifications + +modules: + - name: zennotes + buildsystem: simple + build-commands: + # Unpack the official AppImage. `--appimage-extract` uses the embedded + # squashfs extractor and needs no FUSE, so it runs inside the build sandbox. + - chmod +x ZenNotes.AppImage + - ./ZenNotes.AppImage --appimage-extract + # Install icons from the AppImage's hicolor tree, renamed to the app-id. + - | + for s in 16 24 32 48 64 128 256 512; do + install -Dm644 "squashfs-root/usr/share/icons/hicolor/${s}x${s}/apps/ZenNotes.png" \ + "/app/share/icons/hicolor/${s}x${s}/apps/com.adibhanna.zennotes.png" + done + # Copy the unpacked Electron app into /app, dropping AppImage-only bits. + - mkdir -p /app/zennotes + - cp -a squashfs-root/. /app/zennotes/ + - rm -rf /app/zennotes/usr /app/zennotes/AppRun /app/zennotes/.DirIcon + /app/zennotes/ZenNotes.png /app/zennotes/ZenNotes.desktop + # The SUID chrome-sandbox is unusable in Flatpak; zypak handles sandboxing. + - rm -f /app/zennotes/chrome-sandbox + - chmod -R a+rX /app/zennotes + - install -Dm755 zennotes.sh /app/bin/zennotes + - install -Dm644 com.adibhanna.zennotes.desktop + /app/share/applications/com.adibhanna.zennotes.desktop + - install -Dm644 com.adibhanna.zennotes.metainfo.xml + /app/share/metainfo/com.adibhanna.zennotes.metainfo.xml + sources: + # Bump `url` + `sha256` on each release (see README.md). + - type: file + url: https://github.com/ZenNotes/zennotes/releases/download/v2.3.0/ZenNotes-2.3.0-linux-x86_64.AppImage + sha256: 22f1462bb9f72905461139adea1414cbe6d935338291fbb01fc89f978293c71c + dest-filename: ZenNotes.AppImage + - type: file + path: zennotes.sh + - type: file + path: com.adibhanna.zennotes.desktop + - type: file + path: com.adibhanna.zennotes.metainfo.xml diff --git a/packaging/flatpak/zennotes.sh b/packaging/flatpak/zennotes.sh new file mode 100644 index 00000000..d349c0b7 --- /dev/null +++ b/packaging/flatpak/zennotes.sh @@ -0,0 +1,4 @@ +#!/bin/sh +# Wrap the Electron binary with zypak so Chromium's sandbox works inside the +# Flatpak sandbox (the SUID chrome-sandbox is unavailable here). +exec zypak-wrapper /app/zennotes/ZenNotes "$@" From 028e72a7a01670ea8a6725a6210bbb3e652cf20d Mon Sep 17 00:00:00 2001 From: Lund <59971256+Lund1337@users.noreply.github.com> Date: Wed, 17 Jun 2026 02:12:08 +0300 Subject: [PATCH 37/66] feat: add tar.gz Linux target and simplify AUR PKGBUILD (#133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a tar.gz electron-builder Linux target that bundles the desktop file, icons, and LICENSE (via extraResources/extraFiles), and rewrite the AUR PKGBUILD to repackage that tarball instead of extracting the AppImage — dropping the chmod workarounds behind #70/#74/#92 (tar.gz preserves permissions). Fixes #103. Tarball layout verified end-to-end against an electron-builder build. --- apps/desktop/package.json | 21 +++++- packaging/aur/.SRCINFO | 4 +- packaging/aur/PKGBUILD | 85 ++++++++-------------- packaging/aur/arch-extras/zennotes.desktop | 10 +++ 4 files changed, 62 insertions(+), 58 deletions(-) create mode 100644 packaging/aur/arch-extras/zennotes.desktop diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 9f106a05..1e3f769e 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -228,11 +228,28 @@ "target": [ "AppImage", "deb", - "pacman" + "pacman", + "tar.gz" ], "icon": "build/icons", "maintainer": "Adib Hanna <adibhanna@gmail.com>", - "category": "Office" + "category": "Office", + "extraResources": [ + { + "from": "../../packaging/aur/arch-extras/zennotes.desktop", + "to": "arch-extras/zennotes.desktop" + }, + { + "from": "build/icons", + "to": "arch-extras/icons" + } + ], + "extraFiles": [ + { + "from": "../../LICENSE", + "to": "LICENSE" + } + ] }, "deb": { "afterInstall": "build/after-install.sh" diff --git a/packaging/aur/.SRCINFO b/packaging/aur/.SRCINFO index cfb1c973..f493b7cc 100644 --- a/packaging/aur/.SRCINFO +++ b/packaging/aur/.SRCINFO @@ -1,6 +1,6 @@ pkgbase = zennotes-bin pkgdesc = Keyboard-first, local-first Markdown notes with vim motions and live preview - pkgver = 2.2.0 + pkgver = 2.4.0 pkgrel = 1 url = https://github.com/ZenNotes/zennotes arch = x86_64 @@ -12,7 +12,7 @@ pkgbase = zennotes-bin provides = zennotes conflicts = zennotes options = !strip - source = ZenNotes-2.2.0.AppImage::https://github.com/ZenNotes/zennotes/releases/download/v2.2.0/ZenNotes-2.2.0-linux-x86_64.AppImage + source = ZenNotes-2.4.0-linux-x64.tar.gz::https://github.com/ZenNotes/zennotes/releases/download/v2.4.0/ZenNotes-2.4.0-linux-x64.tar.gz sha256sums = f7f6a67b193dad9961b3844a9ddb213ba866467999574b7f7bbaaff465895722 pkgname = zennotes-bin diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD index 35b38913..2b8d5c08 100644 --- a/packaging/aur/PKGBUILD +++ b/packaging/aur/PKGBUILD @@ -2,12 +2,6 @@ # # AUR package for ZenNotes (yay -S zennotes-bin). # -# This is a -bin package: it downloads the official AppImage from the GitHub -# release and *extracts* it at build time (--appimage-extract), then installs the -# unpacked app to /opt. Extraction does not need FUSE, so the resulting install -# runs on CachyOS / Arch without libfuse2 — sidestepping the AppImage-won't-start -# problem entirely. -# # Before publishing a new version: # 1. bump pkgver to match the GitHub release tag (without the leading "v"), # 2. run `updpkgsums` to fill in sha256sums (or `makepkg -g`), @@ -16,80 +10,63 @@ pkgname=zennotes-bin _appname=ZenNotes -pkgver=2.2.0 +pkgver=2.4.0 pkgrel=1 pkgdesc="Keyboard-first, local-first Markdown notes with vim motions and live preview" arch=('x86_64') url="https://github.com/ZenNotes/zennotes" license=('MIT') -# Electron bundles its own runtime; these are the shared libs it links against. depends=('gtk3' 'nss' 'alsa-lib' 'libxss') provides=('zennotes') conflicts=('zennotes') options=('!strip') -source=("${_appname}-${pkgver}.AppImage::${url}/releases/download/v${pkgver}/${_appname}-${pkgver}-linux-x86_64.AppImage") -# sha256 of the v2.2.0 x86_64 AppImage. Regenerate with `updpkgsums` on each bump. + +source=( + "${_appname}-${pkgver}-linux-x64.tar.gz::${url}/releases/download/v${pkgver}/${_appname}-${pkgver}-linux-x64.tar.gz" +) + sha256sums=('f7f6a67b193dad9961b3844a9ddb213ba866467999574b7f7bbaaff465895722') package() { cd "${srcdir}" - chmod +x "${_appname}-${pkgver}.AppImage" - # FUSE-free extraction — no libfuse2 required on the build host. - ./"${_appname}-${pkgver}.AppImage" --appimage-extract >/dev/null + local _extracted="${_appname}-${pkgver}-linux-x64" + local _extras="${_extracted}/resources/arch-extras" - # Install the unpacked app under /opt. + # Install application install -dm755 "${pkgdir}/opt/${pkgname}" - cp -a squashfs-root/. "${pkgdir}/opt/${pkgname}/" - - # `cp -a` re-applies the extracted squashfs root's mode (often 0700) onto - # /opt/zennotes-bin, leaving the tree untraversable for non-root users — so - # launching the desktop entry or `zennotes` fails with "command not found" - # or "not executable" (issues #70, #74). Force every directory and - # already-executable file world-traversable and everything world-readable. - # (chmod -R skips symlinks during recursion, so AppRun's real target is - # fixed via the regular file it points to.) - chmod -R a+rX "${pkgdir}/opt/${pkgname}" + # tar.gz preserves permissions (unlike squashfs extraction) + # chmod workarounds from #70/#74/#92 are no longer needed. + cp -a "${_extracted}/." "${pkgdir}/opt/${pkgname}/" - # `a+rX` only PRESERVES an existing execute bit — it won't restore one the - # source mode dropped during extraction/cp, so on some build hosts the - # Electron launcher lands non-executable and `/usr/bin/zennotes` reports - # "exists but is not an executable file" (#92). Force it on the launcher - # entry points; chmod follows the AppRun symlink to its real target. - for _exe in "${_appname}" AppRun chrome_crashpad_handler; do - if [ -e "${pkgdir}/opt/${pkgname}/${_exe}" ]; then - chmod a+x "${pkgdir}/opt/${pkgname}/${_exe}" - fi - done - - # The Chromium sandbox helper must be setuid-root to work without - # --no-sandbox. Keep this AFTER the chmod -R above so the setuid bit stands. - if [ -f "${pkgdir}/opt/${pkgname}/chrome-sandbox" ]; then + # Chromium sandbox + if [[ -f "${pkgdir}/opt/${pkgname}/chrome-sandbox" ]]; then chmod 4755 "${pkgdir}/opt/${pkgname}/chrome-sandbox" fi - # CLI launcher. + # CLI launcher install -dm755 "${pkgdir}/usr/bin" - ln -s "/opt/${pkgname}/AppRun" "${pkgdir}/usr/bin/zennotes" + ln -s "/opt/${pkgname}/${_appname}" "${pkgdir}/usr/bin/zennotes" - # Desktop entry — repoint Exec/Icon at the installed paths. - local desktop - desktop=$(find squashfs-root -maxdepth 1 -name '*.desktop' | head -n1) - if [ -n "${desktop}" ]; then - install -Dm644 "${desktop}" "${pkgdir}/usr/share/applications/zennotes.desktop" - sed -i \ - -e 's|^Exec=.*|Exec=zennotes %U|' \ - -e 's|^Icon=.*|Icon=zennotes|' \ + # Desktop entry + if [[ -f "${_extras}/zennotes.desktop" ]]; then + install -Dm644 \ + "${_extras}/zennotes.desktop" \ "${pkgdir}/usr/share/applications/zennotes.desktop" fi - # Icons shipped inside the AppImage. - local size icon - for size in 16 32 48 64 128 256 512; do - icon="squashfs-root/usr/share/icons/hicolor/${size}x${size}/apps/${_appname}.png" - if [ -f "${icon}" ]; then - install -Dm644 "${icon}" \ + # Icons + local size + for size in 16 24 32 48 64 128 256 512; do + if [[ -f "${_extras}/icons/${size}x${size}.png" ]]; then + install -Dm644 \ + "${_extras}/icons/${size}x${size}.png" \ "${pkgdir}/usr/share/icons/hicolor/${size}x${size}/apps/zennotes.png" fi done + + # LICENSE + install -Dm644 \ + "${_extracted}/LICENSE" \ + "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" } diff --git a/packaging/aur/arch-extras/zennotes.desktop b/packaging/aur/arch-extras/zennotes.desktop new file mode 100644 index 00000000..4f9a5eb8 --- /dev/null +++ b/packaging/aur/arch-extras/zennotes.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=ZenNotes +Comment=ZenNotes desktop shell +Exec=zennotes %U +Icon=zennotes +Terminal=false +Type=Application +Categories=Office; +MimeType=text/markdown;x-scheme-handler/zennotes; +StartupWMClass=ZenNotes From 613fd93ea1e07ebf6358808007642a97751b4c7b Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Tue, 16 Jun 2026 18:13:31 -0500 Subject: [PATCH 38/66] packaging(aur): SKIP sha256 until the v2.4.0 tarball is published MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rewritten PKGBUILD/.SRCINFO still carried the old v2.x AppImage sha256, which matches nothing now that the source is the tar.gz. The v2.4.0 tarball doesn't exist until release time, so there's no artifact to hash — use SKIP as a placeholder. `updpkgsums` pins the real checksum against the uploaded release asset before publishing to AUR. --- packaging/aur/.SRCINFO | 2 +- packaging/aur/PKGBUILD | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packaging/aur/.SRCINFO b/packaging/aur/.SRCINFO index f493b7cc..1fdc36ae 100644 --- a/packaging/aur/.SRCINFO +++ b/packaging/aur/.SRCINFO @@ -13,6 +13,6 @@ pkgbase = zennotes-bin conflicts = zennotes options = !strip source = ZenNotes-2.4.0-linux-x64.tar.gz::https://github.com/ZenNotes/zennotes/releases/download/v2.4.0/ZenNotes-2.4.0-linux-x64.tar.gz - sha256sums = f7f6a67b193dad9961b3844a9ddb213ba866467999574b7f7bbaaff465895722 + sha256sums = SKIP pkgname = zennotes-bin diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD index 2b8d5c08..8a62937a 100644 --- a/packaging/aur/PKGBUILD +++ b/packaging/aur/PKGBUILD @@ -25,7 +25,10 @@ source=( "${_appname}-${pkgver}-linux-x64.tar.gz::${url}/releases/download/v${pkgver}/${_appname}-${pkgver}-linux-x64.tar.gz" ) -sha256sums=('f7f6a67b193dad9961b3844a9ddb213ba866467999574b7f7bbaaff465895722') +# No artifact to hash yet — the v2.4.0 tarball is created at release time, so +# SKIP is a placeholder. Run `updpkgsums` against the uploaded release asset +# before publishing to AUR (step 2 above) to pin the real checksum. +sha256sums=('SKIP') package() { cd "${srcdir}" From bdcb315cf7b7fb5184846213c69c552fcc1b0c70 Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Tue, 16 Jun 2026 18:22:21 -0500 Subject: [PATCH 39/66] ci(aur): skip makepkg build until the release asset is published MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AUR check runs makepkg -s, which downloads the source tarball. The PKGBUILD points at the upcoming release's tarball (v2.4.0), which 404s until that release is cut — so the check failed for a non-bug reason. Probe the source URL first: when the asset isn't published yet, skip the build/lint with a notice (still validating PKGBUILD metadata + .SRCINFO sync); the full build runs once the release exists. --- .github/workflows/aur-check.yml | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/aur-check.yml b/.github/workflows/aur-check.yml index fbfe1e38..2f70d028 100644 --- a/.github/workflows/aur-check.yml +++ b/.github/workflows/aur-check.yml @@ -25,7 +25,7 @@ jobs: - name: Check out repository uses: actions/checkout@v4 - - name: Build the package + - name: Set up build user run: | set -euxo pipefail # makepkg refuses to run as root — build as an unprivileged user with @@ -35,11 +35,37 @@ jobs: install -d -o builder -g builder /tmp/build cp packaging/aur/PKGBUILD packaging/aur/.SRCINFO /tmp/build/ chown -R builder:builder /tmp/build + + # The PKGBUILD always points at the upcoming release's tarball, which does + # not exist until that release is published. Skip the full build (which has + # to download it) when the asset isn't up yet, rather than failing — the + # metadata + .SRCINFO are still checked, and the real build runs once the + # release exists. + - name: Check whether the release asset is published + id: asset + run: | + set -uo pipefail + # shellcheck disable=SC1091 + source packaging/aur/PKGBUILD + url="${source[0]#*::}" + echo "url=$url" + if curl -fsIL -o /dev/null "$url"; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "::notice title=AUR build skipped::Release asset not published yet ($url). PKGBUILD metadata + .SRCINFO are still validated; the full makepkg build runs once the release exists." + fi + + - name: Build the package + if: steps.asset.outputs.exists == 'true' + run: | + set -euxo pipefail cd /tmp/build sudo -u builder makepkg -s --noconfirm --noprogressbar ls -la ./*.pkg.tar.* - name: Lint (non-blocking) + if: steps.asset.outputs.exists == 'true' run: | cd /tmp/build namcap PKGBUILD || true @@ -47,6 +73,7 @@ jobs: - name: Verify .SRCINFO is in sync run: | + set -euxo pipefail cd /tmp/build sudo -u builder makepkg --printsrcinfo > .SRCINFO.generated if ! diff -u .SRCINFO .SRCINFO.generated; then From 2bf60457f7a79cea81def91069293c3c551a7ff0 Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Wed, 17 Jun 2026 07:45:31 -0500 Subject: [PATCH 40/66] =?UTF-8?q?feat:=20[[Note#heading]]=20wikilinks=20?= =?UTF-8?q?=E2=80=94=20resolve,=20jump=20to=20heading,=20autocomplete=20(#?= =?UTF-8?q?196)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `[[Note#heading]]` was matched as a note literally named "Note#heading", so it never resolved — the connections tab even offered to create it. The whole heading-link experience now works: - resolveWikilinkTarget (and suggestCreateNotePath) strip the #heading / ^block anchor, so [[Note#heading]] resolves to Note everywhere (editor click, connections, backlinks) and 'create from link' makes a valid Note.md. - Clicking, and `gd`, on [[Note#heading]] now scroll to the heading — shared openWikilinkHeading() → openNoteAtOffset — in both the editor and the preview. - Typing [[Note# autocompletes that note's headings ([[# uses the current note). - Ctrl+Y accepts the highlighted completion (Vim-style); Tab accepts a note link but keeps the caret inside [[…]] so you can chain into a #heading. Tests: wikilinks.test.ts (resolution / anchor / create-path, incl. the /Untitled#test edge case) and cm-wikilinks.test.ts (heading autocomplete). --- packages/app-core/src/components/Editor.tsx | 14 ++- .../app-core/src/components/EditorPane.tsx | 4 +- .../src/components/PinnedReferencePane.tsx | 4 +- packages/app-core/src/components/Preview.tsx | 10 +- .../app-core/src/lib/cm-completion-nav.ts | 61 ++++++++++-- .../app-core/src/lib/cm-wikilink-render.ts | 20 +++- .../app-core/src/lib/cm-wikilinks.test.ts | 50 +++++++++- packages/app-core/src/lib/cm-wikilinks.ts | 94 +++++++++++++++++++ .../app-core/src/lib/wikilink-navigation.ts | 27 ++++++ packages/app-core/src/lib/wikilinks.test.ts | 92 ++++++++++++++++++ packages/app-core/src/lib/wikilinks.ts | 39 +++++++- 11 files changed, 387 insertions(+), 28 deletions(-) create mode 100644 packages/app-core/src/lib/wikilink-navigation.ts create mode 100644 packages/app-core/src/lib/wikilinks.test.ts diff --git a/packages/app-core/src/components/Editor.tsx b/packages/app-core/src/components/Editor.tsx index e325aed9..7bfa58c6 100644 --- a/packages/app-core/src/components/Editor.tsx +++ b/packages/app-core/src/components/Editor.tsx @@ -20,8 +20,10 @@ import type { PaneLayout, PaneSplit } from '../lib/pane-layout' import { parseCreateNotePath, resolveWikilinkTarget, - suggestCreateNotePath + suggestCreateNotePath, + wikilinkHeadingAnchor } from '../lib/wikilinks' +import { openWikilinkHeading } from '../lib/wikilink-navigation' import { classifyLocalAssetHref, resolveAssetVaultRelativePath } from '../lib/local-assets' import { buildMoveNotePrompt, @@ -528,10 +530,16 @@ function registerVimCommands(): void { const notes = state.notes const resolved = resolveWikilinkTarget(notes, target) if (resolved) { - void state.selectNote(resolved.path).then(() => { + const focusEditorSoon = (): void => { state.setFocusedPanel('editor') requestAnimationFrame(() => useStore.getState().editorViewRef?.focus()) - }) + } + const headingAnchor = wikilinkHeadingAnchor(target) + if (headingAnchor) { + void openWikilinkHeading(resolved.path, headingAnchor).then(focusEditorSoon) + } else { + void state.selectNote(resolved.path).then(focusEditorSoon) + } return } diff --git a/packages/app-core/src/components/EditorPane.tsx b/packages/app-core/src/components/EditorPane.tsx index 09e04473..c79c63ea 100644 --- a/packages/app-core/src/components/EditorPane.tsx +++ b/packages/app-core/src/components/EditorPane.tsx @@ -77,7 +77,7 @@ import { hashtagExtension } from '../lib/cm-hashtags' import { wikilinkRenderExtension } from '../lib/cm-wikilink-render' import { slashCommandSource, slashCommandRender } from '../lib/cm-slash-commands' import { dateShortcutSource } from '../lib/cm-date-shortcuts' -import { wikilinkSource } from '../lib/cm-wikilinks' +import { wikilinkSource, wikilinkHeadingSource } from '../lib/cm-wikilinks' import { appMarkdownSnippetExtension } from '../lib/markdown-snippets-config' import { LazyDiagramTabView, LazyPreview as Preview } from './LazyPreview' import { ConnectionsPanel } from './ConnectionsPanel' @@ -1374,7 +1374,7 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { lineNumbersCompartment.of(lineNumberExtension(s0.lineNumberMode)), tooltips({ parent: document.body }), autocompletion({ - override: [slashCommandSource, dateShortcutSource, wikilinkSource], + override: [slashCommandSource, dateShortcutSource, wikilinkSource, wikilinkHeadingSource], addToOptions: [{ render: slashCommandRender.render, position: 0 }], icons: false, optionClass: (completion) => diff --git a/packages/app-core/src/components/PinnedReferencePane.tsx b/packages/app-core/src/components/PinnedReferencePane.tsx index df7a5e81..8c3ab283 100644 --- a/packages/app-core/src/components/PinnedReferencePane.tsx +++ b/packages/app-core/src/components/PinnedReferencePane.tsx @@ -40,7 +40,7 @@ import { livePreviewPlugin } from '../lib/cm-live-preview' import { headingFolding } from '../lib/cm-heading-fold' import { slashCommandSource, slashCommandRender } from '../lib/cm-slash-commands' import { dateShortcutSource } from '../lib/cm-date-shortcuts' -import { wikilinkSource } from '../lib/cm-wikilinks' +import { wikilinkSource, wikilinkHeadingSource } from '../lib/cm-wikilinks' import { completionNavKeymap } from '../lib/cm-completion-nav' import { classifyLocalAssetHref, type LocalAssetKind } from '../lib/local-assets' import { LazyPreview as Preview } from './LazyPreview' @@ -195,7 +195,7 @@ export function PinnedReferencePane(): JSX.Element | null { lineNumbersCompartment.of(lineNumberExtension(s0.lineNumberMode)), tooltips({ parent: document.body }), autocompletion({ - override: [slashCommandSource, dateShortcutSource, wikilinkSource], + override: [slashCommandSource, dateShortcutSource, wikilinkSource, wikilinkHeadingSource], addToOptions: [{ render: slashCommandRender.render, position: 0 }], icons: false, optionClass: (completion) => diff --git a/packages/app-core/src/components/Preview.tsx b/packages/app-core/src/components/Preview.tsx index 4b5d1bb0..1a311f15 100644 --- a/packages/app-core/src/components/Preview.tsx +++ b/packages/app-core/src/components/Preview.tsx @@ -4,7 +4,8 @@ import type { NoteMeta } from "@shared/ipc"; import { renderMarkdown } from "../lib/markdown"; import { useStore } from "../store"; import { resolveAuto, THEMES } from "../lib/themes"; -import { resolveWikilinkTarget } from "../lib/wikilinks"; +import { resolveWikilinkTarget, wikilinkHeadingAnchor } from "../lib/wikilinks"; +import { openWikilinkHeading } from "../lib/wikilink-navigation"; import { toggleTaskAtIndex } from "../lib/tasklists"; import { enhanceLocalAssetNodes, @@ -535,7 +536,12 @@ export const Preview = memo(function Preview({ if (anchor.classList.contains("wikilink")) { e.preventDefault(); const path = anchor.dataset.resolvedPath; - if (path) void selectNoteRef.current(path); + if (path) { + // Scroll to the #heading when the link carries one. (#196) + const headingAnchor = wikilinkHeadingAnchor(anchor.dataset.wikilink ?? ""); + if (headingAnchor) void openWikilinkHeading(path, headingAnchor); + else void selectNoteRef.current(path); + } return; } if (anchor.classList.contains("hashtag")) { diff --git a/packages/app-core/src/lib/cm-completion-nav.ts b/packages/app-core/src/lib/cm-completion-nav.ts index 86e68e55..17d4df74 100644 --- a/packages/app-core/src/lib/cm-completion-nav.ts +++ b/packages/app-core/src/lib/cm-completion-nav.ts @@ -1,4 +1,9 @@ -import { completionStatus, moveCompletionSelection } from '@codemirror/autocomplete' +import { + acceptCompletion, + completionStatus, + moveCompletionSelection, + selectedCompletion +} from '@codemirror/autocomplete' import { Prec } from '@codemirror/state' import { EditorView } from '@codemirror/view' @@ -48,13 +53,53 @@ export const completionNavKeymap = Prec.highest( EditorView.domEventHandlers({ keydown: (event, view) => { const direction = completionNavDirection(event) - if (!direction) return false - if (completionStatus(view.state) !== 'active') return false - const moved = moveCompletionSelection(direction === 'next')(view) - if (!moved) return false - event.preventDefault() - event.stopPropagation() - return true + if (direction) { + if (completionStatus(view.state) !== 'active') return false + if (!moveCompletionSelection(direction === 'next')(view)) return false + event.preventDefault() + event.stopPropagation() + return true + } + + const noMods = !event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey + + // Ctrl+Y — accept the highlighted completion (Vim-style). + if ( + event.ctrlKey && + !event.metaKey && + !event.altKey && + !event.shiftKey && + event.key.toLowerCase() === 'y' + ) { + if (completionStatus(view.state) !== 'active') return false + if (!acceptCompletion(view)) return false + event.preventDefault() + event.stopPropagation() + return true + } + + // Tab — accept, but for a note/asset wikilink keep the caret *inside* the + // `[[…]]` so you can keep typing (e.g. a `#heading` anchor). Headings, + // slash commands, etc. accept normally; with no completion Tab indents. + if (event.key === 'Tab' && noMods) { + if (completionStatus(view.state) !== 'active') return false + const completion = selectedCompletion(view.state) as + | { _kind?: string; _target?: string } + | null + if (!acceptCompletion(view)) return false + const keepsLinkOpen = completion?._kind === 'wikilink' && completion._target != null + if (keepsLinkOpen) { + const pos = view.state.selection.main.head + if (view.state.doc.sliceString(pos - 2, pos) === ']]') { + view.dispatch({ selection: { anchor: pos - 2 } }) + } + } + event.preventDefault() + event.stopPropagation() + return true + } + + return false } }) ) diff --git a/packages/app-core/src/lib/cm-wikilink-render.ts b/packages/app-core/src/lib/cm-wikilink-render.ts index 8040d3fb..2b1f8f42 100644 --- a/packages/app-core/src/lib/cm-wikilink-render.ts +++ b/packages/app-core/src/lib/cm-wikilink-render.ts @@ -19,7 +19,8 @@ import { type ViewUpdate } from '@codemirror/view' import { useStore } from '../store' -import { resolveWikilinkTarget } from './wikilinks' +import { resolveWikilinkTarget, wikilinkHeadingAnchor } from './wikilinks' +import { openWikilinkHeading } from './wikilink-navigation' // Same shape as the Preview pipeline (remarkWikilinks). const WIKILINK_RE = /(!?)\[\[([^\]|]+?)(?:\|([^\]]+))?\]\]/g @@ -112,15 +113,26 @@ const wikilinkRenderPlugin = ViewPlugin.fromClass( { decorations: (p) => p.decorations } ) -/** Open the note a wikilink points to (resolving the same way Preview does). */ +/** + * Open the note a wikilink points to, scrolling to its `#heading` when the + * target carries one (`[[Doc#Heading]]`). (#196) + */ function openWikilink(target: string): void { const state = useStore.getState() const resolved = resolveWikilinkTarget(state.notes, target) if (!resolved) return // unresolved link — leave note creation to other flows - void state.selectNote(resolved.path).then(() => { + + const focusEditorSoon = (): void => { useStore.getState().setFocusedPanel('editor') requestAnimationFrame(() => useStore.getState().editorViewRef?.focus()) - }) + } + + const anchor = wikilinkHeadingAnchor(target) + if (!anchor) { + void state.selectNote(resolved.path).then(focusEditorSoon) + return + } + void openWikilinkHeading(resolved.path, anchor).then(focusEditorSoon) } // Click a rendered wikilink to jump. Intercept on mousedown so CodeMirror diff --git a/packages/app-core/src/lib/cm-wikilinks.test.ts b/packages/app-core/src/lib/cm-wikilinks.test.ts index 59e65d34..1e0d7300 100644 --- a/packages/app-core/src/lib/cm-wikilinks.test.ts +++ b/packages/app-core/src/lib/cm-wikilinks.test.ts @@ -4,7 +4,7 @@ import { CompletionContext } from '@codemirror/autocomplete' import { EditorState } from '@codemirror/state' import { EditorView } from '@codemirror/view' import { describe, expect, it, vi } from 'vitest' -import { wikilinkSource } from './cm-wikilinks' +import { wikilinkSource, wikilinkHeadingSource } from './cm-wikilinks' const storeState = vi.hoisted(() => ({ activeNote: { @@ -19,7 +19,10 @@ const storeState = vi.hoisted(() => ({ wikilinks: [], hasAttachments: false, excerpt: '', - body: '' + body: '# Home\n\n## Tasks\n' + }, + noteContents: { + 'inbox/Zen Garden.md': { body: '# Intro\n\n## Setup\n\n## Usage Notes\n\nbody\n' } }, notes: [ { @@ -103,3 +106,46 @@ describe('wikilinkSource', () => { parent.remove() }) }) + +function headingResult(doc: string) { + const state = EditorState.create({ doc }) + return wikilinkHeadingSource(new CompletionContext(state, doc.length, true)) +} + +describe('wikilinkHeadingSource (#196 — heading autocomplete)', () => { + it('suggests the target note headings after #', async () => { + const result = await headingResult('[[Zen Garden#') + expect(result?.options.map((o) => o.label)).toEqual(['Intro', 'Setup', 'Usage Notes']) + }) + + it('anchors the completion just after the # so the heading lands inside the link', async () => { + const doc = '[[Zen Garden#Us' + const result = await headingResult(doc) + expect(result?.from).toBe('[[Zen Garden#'.length) + }) + + it('inserts the chosen heading and closes the link', async () => { + const parent = document.createElement('div') + document.body.append(parent) + const view = new EditorView({ parent, state: EditorState.create({ doc: '[[Zen Garden#Us' }) }) + const result = await wikilinkHeadingSource( + new CompletionContext(view.state, view.state.doc.length, true) + ) + const option = result?.options.find((o) => o.label === 'Usage Notes') + const apply = option?.apply + if (typeof apply !== 'function') throw new Error('expected a function apply handler') + apply(view, option!, result!.from, view.state.doc.length) + expect(view.state.doc.toString()).toBe('[[Zen Garden#Usage Notes]]') + view.destroy() + parent.remove() + }) + + it('falls back to the current note for [[#', async () => { + const result = await headingResult('[[#') + expect(result?.options.map((o) => o.label)).toEqual(['Home', 'Tasks']) + }) + + it('returns null without a # (note mode owns that)', async () => { + expect(await headingResult('[[Zen Garden')).toBeNull() + }) +}) diff --git a/packages/app-core/src/lib/cm-wikilinks.ts b/packages/app-core/src/lib/cm-wikilinks.ts index cfc0cccc..00acb54a 100644 --- a/packages/app-core/src/lib/cm-wikilinks.ts +++ b/packages/app-core/src/lib/cm-wikilinks.ts @@ -3,6 +3,8 @@ import type { EditorView } from '@codemirror/view' import type { AssetMeta, NoteMeta } from '@shared/ipc' import { useStore } from '../store' import { isPrimaryNotesAtRoot, noteFolderSubpath } from './vault-layout' +import { resolveWikilinkTarget } from './wikilinks' +import { parseOutline } from './outline' function normalize(value: string): string { return value.trim().toLowerCase() @@ -321,3 +323,95 @@ export function wikilinkSource(context: CompletionContext): CompletionResult | n filter: false } } + +/** + * Match `[[Note#<headingQuery>` so we can suggest the target note's headings. + * The note is everything before the first `#`; the heading query is whatever + * follows the last `#` (so nested `#a#b` still completes the deepest part). + */ +function wikilinkHeadingMatch(context: CompletionContext): { + from: number + notePart: string + query: string +} | null { + const { state, pos } = context + const line = state.doc.lineAt(pos) + const before = state.doc.sliceString(line.from, pos) + const openIndex = before.lastIndexOf('[[') + if (openIndex < 0) return null + + const inside = before.slice(openIndex + 2) + if (inside.includes(']]') || inside.includes('|')) return null + const firstHash = inside.indexOf('#') + if (firstHash < 0) return null // no heading anchor — `wikilinkSource` owns this + const lastHash = inside.lastIndexOf('#') + + return { + from: line.from + openIndex + 2 + lastHash + 1, + notePart: inside.slice(0, firstHash).trim(), + query: inside.slice(lastHash + 1) + } +} + +// Bodies fetched for heading completion are cached so typing the heading query +// doesn't re-read the file on every keystroke (`validFor` keeps the option list +// while the query stays anchor-shaped, so this mostly matters across notes). +const headingBodyCache = new Map<string, string>() + +/** + * Autocomplete headings inside a wikilink: typing `[[Note#` (or `[[#` for the + * current note) suggests that note's headings. (#196) + */ +export async function wikilinkHeadingSource( + context: CompletionContext +): Promise<CompletionResult | null> { + const match = wikilinkHeadingMatch(context) + if (!match) return null + + const state = useStore.getState() + const note = match.notePart + ? resolveWikilinkTarget(state.notes, match.notePart) + : state.activeNote + if (!note) return null + + let body = + state.noteContents[note.path]?.body ?? + (note as { body?: string }).body ?? // activeNote ([[#…]]) already carries its body + headingBodyCache.get(note.path) + if (body == null) { + try { + body = (await window.zen.readNote(note.path)).body + headingBodyCache.set(note.path, body) + } catch { + return null + } + } + + const seen = new Set<string>() + const options: Completion[] = [] + for (const heading of parseOutline(body)) { + const text = heading.text.trim() + const key = normalize(text) + if (!text || seen.has(key)) continue + seen.add(key) + options.push({ + label: text, + detail: `H${heading.level}`, + type: 'text', + apply: (view: EditorView, _completion: Completion, from: number, to: number) => { + const existingClose = view.state.doc.sliceString(to, to + 2) === ']]' + const insert = `${text}${existingClose ? '' : ']]'}` + view.dispatch({ + changes: { from, to, insert }, + selection: { anchor: from + text.length + (existingClose ? 0 : 2) } + }) + } + }) + if (options.length >= 100) break + } + if (options.length === 0) return null + + // Default filter (fuzzy) on the heading query; validFor lets CodeMirror keep + // and filter the list client-side while the query stays anchor-shaped. + return { from: match.from, options, validFor: /^[^\]|]*$/ } +} diff --git a/packages/app-core/src/lib/wikilink-navigation.ts b/packages/app-core/src/lib/wikilink-navigation.ts new file mode 100644 index 00000000..19527799 --- /dev/null +++ b/packages/app-core/src/lib/wikilink-navigation.ts @@ -0,0 +1,27 @@ +import { useStore } from '../store' +import { parseOutline } from './outline' + +/** + * Open `path` and scroll to the heading matching `headingAnchor` + * (case-insensitive, like Obsidian). Falls back to opening the note at the top + * when the heading isn't found. Shared by the editor's wikilink click and the + * preview pane so `[[Doc#Heading]]` lands on the heading. (#196) + */ +export async function openWikilinkHeading(path: string, headingAnchor: string): Promise<void> { + const s = useStore.getState() + let body = s.noteContents[path]?.body + if (body == null) { + try { + body = (await window.zen.readNote(path)).body + } catch { + body = '' + } + } + const needle = headingAnchor.trim().toLowerCase() + const heading = parseOutline(body).find((h) => h.text.trim().toLowerCase() === needle) + if (heading) { + await s.openNoteAtOffset(path, heading.from, { scrollMode: 'start' }) + } else { + await s.selectNote(path) + } +} diff --git a/packages/app-core/src/lib/wikilinks.test.ts b/packages/app-core/src/lib/wikilinks.test.ts new file mode 100644 index 00000000..37e45c8b --- /dev/null +++ b/packages/app-core/src/lib/wikilinks.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it } from 'vitest' +import { + parseCreateNotePath, + resolveWikilinkTarget, + stripWikilinkAnchor, + suggestCreateNotePath, + wikilinkHeadingAnchor +} from './wikilinks' + +const notes = [ + { path: 'inbox/My Document.md', title: 'My Document', folder: 'inbox' as const }, + { path: 'inbox/projects/Spec.md', title: 'Spec', folder: 'inbox' as const } +] + +describe('stripWikilinkAnchor (#196)', () => { + it('drops a #heading anchor', () => { + expect(stripWikilinkAnchor('My Document#My Heading')).toBe('My Document') + }) + + it('drops a ^block anchor', () => { + expect(stripWikilinkAnchor('My Document^abc123')).toBe('My Document') + }) + + it('keeps a plain target untouched', () => { + expect(stripWikilinkAnchor('My Document')).toBe('My Document') + }) + + it('keeps the path up to the first anchor', () => { + expect(stripWikilinkAnchor('projects/Spec#design')).toBe('projects/Spec') + }) +}) + +describe('wikilinkHeadingAnchor (#196)', () => { + it('returns the heading text after #', () => { + expect(wikilinkHeadingAnchor('My Document#My Heading')).toBe('My Heading') + }) + + it('is null without a heading anchor', () => { + expect(wikilinkHeadingAnchor('My Document')).toBeNull() + expect(wikilinkHeadingAnchor('My Document^block')).toBeNull() + }) +}) + +describe('resolveWikilinkTarget — heading/block anchors (#196)', () => { + it('resolves [[Doc#heading]] to the document', () => { + expect(resolveWikilinkTarget(notes, 'My Document#My Heading')?.path).toBe('inbox/My Document.md') + }) + + it('resolves [[Doc^block]] to the document', () => { + expect(resolveWikilinkTarget(notes, 'My Document^abc')?.path).toBe('inbox/My Document.md') + }) + + it('still resolves a plain [[Doc]]', () => { + expect(resolveWikilinkTarget(notes, 'My Document')?.path).toBe('inbox/My Document.md') + }) + + it('resolves a path-like [[folder/Doc#heading]]', () => { + expect(resolveWikilinkTarget(notes, 'projects/Spec#design')?.path).toBe('inbox/projects/Spec.md') + }) + + it('returns null for a bare [[#heading]] with no document', () => { + expect(resolveWikilinkTarget(notes, '#My Heading')).toBeNull() + }) +}) + +describe('suggestCreateNotePath — anchored targets (#196)', () => { + it('suggests the document, not the invalid anchored name', () => { + expect(suggestCreateNotePath('New Doc#Heading')).toBe('/New Doc.md') + }) +}) + +describe('leading-slash + anchor (#196 — [[/Untitled#test]])', () => { + const untitled = [{ path: 'inbox/Untitled.md', title: 'Untitled', folder: 'inbox' as const }] + + it('strips the anchor off an absolute-path target', () => { + expect(stripWikilinkAnchor('/Untitled#test')).toBe('/Untitled') + }) + + it('resolves [[/Untitled#test]] to inbox/Untitled.md', () => { + expect(resolveWikilinkTarget(untitled, '/Untitled#test')?.path).toBe('inbox/Untitled.md') + }) + + it('suggests a valid create path (no #, no extra slash)', () => { + expect(suggestCreateNotePath('/Untitled#test')).toBe('/Untitled.md') + }) + + it('parseCreateNotePath accepts the suggested path', () => { + expect(parseCreateNotePath(suggestCreateNotePath('/Untitled#test')).relPath).toBe( + 'inbox/Untitled.md' + ) + }) +}) diff --git a/packages/app-core/src/lib/wikilinks.ts b/packages/app-core/src/lib/wikilinks.ts index 79b77249..c2c6d23c 100644 --- a/packages/app-core/src/lib/wikilinks.ts +++ b/packages/app-core/src/lib/wikilinks.ts @@ -30,6 +30,30 @@ export function isPathLikeWikilinkTarget(target: string): boolean { return trimmed.startsWith('/') || trimmed.includes('/') || /\.md$/i.test(trimmed) } +/** + * Strip a `#heading` / `^block` anchor off a wikilink target — it points within + * the note, not at the note name. `[[Doc#Heading]]` resolves to `Doc`. Note + * names can't contain `#` or `^` (see INVALID_NOTE_PATH_CHARS), so the first one + * always starts the anchor. (#196) + */ +export function stripWikilinkAnchor(target: string): string { + const anchor = target.search(/[#^]/) + return anchor === -1 ? target : target.slice(0, anchor) +} + +/** + * The `#heading` text from a wikilink target, or null when there's no heading + * anchor. `[[Doc#My Heading]]` → `My Heading`; `[[Doc^block]]` / `[[Doc]]` → + * null (block refs aren't headings). Used to scroll to the heading on click. (#196) + */ +export function wikilinkHeadingAnchor(target: string): string | null { + const hash = target.indexOf('#') + if (hash < 0) return null + const caret = target.indexOf('^') + if (caret >= 0 && caret < hash) return null // a ^block anchor comes first + return target.slice(hash + 1).trim() || null +} + function resolveExplicitPath(notes: NoteRef[], target: string): NoteRef | null { const normalized = normalizeSlashes(target.trim()) if (!normalized) return null @@ -65,13 +89,17 @@ function resolvePathSuffix(notes: NoteRef[], target: string): NoteRef | null { } export function resolveWikilinkTarget<T extends NoteRef>(notes: T[], target: string): T | null { + // `[[Doc#Heading]]` / `[[Doc^block]]` point at a spot inside Doc — resolve the + // document, ignoring the anchor. (#196) + const doc = stripWikilinkAnchor(target) const visible = notes.filter((note) => note.folder !== 'trash') - if (isPathLikeWikilinkTarget(target)) { - return (resolveExplicitPath(visible, target) ?? - resolvePathSuffix(visible, target)) as T | null + if (isPathLikeWikilinkTarget(doc)) { + return (resolveExplicitPath(visible, doc) ?? + resolvePathSuffix(visible, doc)) as T | null } - const needle = normalizeForCompare(stripMdExtension(target)) + const needle = normalizeForCompare(stripMdExtension(doc)) + if (!needle) return null return visible.find((note) => normalizeForCompare(note.title) === needle) ?? null } @@ -102,7 +130,8 @@ export function extractWikilinkTargets(body: string): string[] { } export function suggestCreateNotePath(target: string): string { - const trimmed = normalizeSlashes(target.trim()).replace(/\/+$/, '') + // Creating from `[[Doc#Heading]]` makes `Doc`, not the invalid `Doc#Heading`. (#196) + const trimmed = normalizeSlashes(stripWikilinkAnchor(target).trim()).replace(/\/+$/, '') if (!trimmed) return '/Untitled.md' if (trimmed.startsWith('/')) { From 4cdbb22a2cac19e7a45397c088fb8a2489eedad8 Mon Sep 17 00:00:00 2001 From: Victor Ribeiro <victruu@outlook.com> Date: Fri, 12 Jun 2026 16:01:18 -0300 Subject: [PATCH 41/66] feat(ui): add middle-click to close tabs --- packages/app-core/src/components/EditorPane.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/app-core/src/components/EditorPane.tsx b/packages/app-core/src/components/EditorPane.tsx index c79c63ea..f9d3f4e6 100644 --- a/packages/app-core/src/components/EditorPane.tsx +++ b/packages/app-core/src/components/EditorPane.tsx @@ -2521,6 +2521,17 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { e.preventDefault() setTabMenu({ x: e.clientX, y: e.clientY, path: tab.path }) }} + onMouseDown={(e) => { + if (e.button === 1) { + e.preventDefault() + } + }} + onAuxClick={(e) => { + if (e.button === 1) { + e.preventDefault() + void closeTabInPane(paneId, tab.path) + } + }} > {tabDropIndicator?.path === tab.path && ( <div From 6290f82d7f315d91120648a5a0a47ced44328f19 Mon Sep 17 00:00:00 2001 From: Raihan Adam <hallwack.id@gmail.com> Date: Wed, 17 Jun 2026 08:39:48 -0500 Subject: [PATCH 42/66] packaging(nix): add Nix/NixOS flake and packages (#149) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a root flake plus packaging/nix/ with separate desktop and server packages that build ZenNotes from source, a dev shell, and install docs. Linux packaging only — no application-code changes. Co-authored-by: Krysteq <justkrysteq@proton.me> --- .gitignore | 3 + README.md | 2 + flake.lock | 27 +++++++ flake.nix | 46 +++++++++++ packaging/nix/README.md | 124 ++++++++++++++++++++++++++++++ packaging/nix/package-desktop.nix | 95 +++++++++++++++++++++++ packaging/nix/package-server.nix | 66 ++++++++++++++++ packaging/nix/release-data.json | 6 ++ 8 files changed, 369 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 packaging/nix/README.md create mode 100644 packaging/nix/package-desktop.nix create mode 100644 packaging/nix/package-server.nix create mode 100644 packaging/nix/release-data.json diff --git a/.gitignore b/.gitignore index aff36dcd..23a47f88 100644 --- a/.gitignore +++ b/.gitignore @@ -18,5 +18,8 @@ apps/server/bin data/ /vault/ /remotevault/ +# Nix build outputs +result +result-* # Release notes / launch copy are kept local only, never committed. docs/releases/ diff --git a/README.md b/README.md index cfd97ffb..b51fb8ca 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ Pick whatever suits your distro: ```sh sudo apt install ./ZenNotes-<version>-linux-amd64.deb ``` +- **Nix / NixOS:** + Read [packaging/nix/README.md](packaging/nix/README.md) for installation instructions - **Any distro — AppImage:** ```sh chmod +x ZenNotes-<version>-linux-x86_64.AppImage diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..6769406b --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1781074563, + "narHash": "sha256-md8WlXOlfnIeHeOScMTTHFyf2d6iaTwPl2apR5EQ3P4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9ae611a455b90cf061d8f332b977e387bda8e1ca", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..336b7f73 --- /dev/null +++ b/flake.nix @@ -0,0 +1,46 @@ +{ + description = "ZenNotes - Keyboard-first local Markdown notes"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = { nixpkgs, ... }: + let + systems = nixpkgs.lib.platforms.linux ++ nixpkgs.lib.platforms.darwin; + + forAllSystems = nixpkgs.lib.genAttrs systems; + in + { + packages = forAllSystems (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + rec { + zennotes-desktop = pkgs.callPackage ./packaging/nix/package-desktop.nix { }; + + zennotes-server = pkgs.callPackage ./packaging/nix/package-server.nix { }; + + default = zennotes-desktop; + } + ); + + devShell = forAllSystems (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + pkgs.mkShell { + buildInputs = with pkgs; [ + go + nodejs + electron + turbo + ]; + + shellHook = '' + export ELECTRON_SKIP_BINARY_DOWNLOAD=1 + ''; + } + ); + }; +} diff --git a/packaging/nix/README.md b/packaging/nix/README.md new file mode 100644 index 00000000..fdc35849 --- /dev/null +++ b/packaging/nix/README.md @@ -0,0 +1,124 @@ +# Linux packaging (Nix / NixOS) + +This directory holds the Nix packaging for ZenNotes. + +## Test it before installing + +You can use `nix run` to run the application without installing it: + +For the desktop app: +```sh +nix run github:ZenNotes/zennotes +``` + +For the server: +```sh +nix run github:ZenNotes/zennotes#zennotes-server +``` + +## Installing on NixOS + +For now as the package is not in the official nixpkgs repo you will need to add an input in your `flake.nix`, like this: + +```nix +inputs = { + # ... + + zennotes.url = "github:ZenNotes/zennotes"; +}; + +outputs = { nixpkgs, ... } @ inputs: +{ + # ... +}; +``` + +And then you can add it to your system packages: + +```nix +{ pkgs, inputs, ... }: + +{ + environment.systemPackages = [ + inputs.zennotes.packages.${pkgs.system}.zennotes-desktop + inputs.zennotes.packages.${pkgs.system}.zennotes-server + ]; +} +``` + + +If you don't use flakes you'll need to copy the `package-desktop.nix` file into your NixOS configuration and add it to your system packages: + +```nix +environment.systemPackages = [ + (pkgs.callPackage ./package-desktop.nix { }) +]; +``` + +Same goes for the server package: + +```nix +environment.systemPackages = [ + (pkgs.callPackage ./package-server.nix { }) +]; +``` + +## Updating to a new release + +1. Open `release-data.json` +2. Bump `version`: + +```json +{ + "version": "2.3.0", // => 2.4.0 + // ... +} +``` + +2. Update the source hash +To obtain a new hash (replace X.X.X with the desired version): + +```sh +nix-prefetch-github ZenNotes zennotes --rev "vX.X.X" +``` + +```json +{ + // ... + "hash": "sha256-+tLPVnnMbtMa5blSwHav9ZMlnkUsrdG62mMGxhbmy6g=", // update to new hash + // ... +} + +``` + +3. Update the npmDepsHash (if needed) and vendorHash (if needed) +To obtain a new npmDepsHash use this command in an updated project root: + +```sh +prefetch-npm-deps package-lock.json +``` + +```json +{ + // ... + "npmDepsHash": "sha256-7IpGnxVjaJvfSZyKjOylGMhFqa1bx8Ry5O1yqYfNnCE=", + "vendorHash": "sha256-wYBF7CjM6AvoWMWql9hFmIaj6pCmli4vOef6POyGkfU=" +} +``` + +4. Build and test + +```sh +nix build +./result/bin/zennotes-desktop +``` + +```sh +nix build .#server +./result/bin/zennotes-server +``` + +## Notes & limitations + +* Automatic updates inside ZenNotes are disabled because Nix packages are immutable. +* Updates should be performed through Nix by updating the package definition. diff --git a/packaging/nix/package-desktop.nix b/packaging/nix/package-desktop.nix new file mode 100644 index 00000000..df4b9cdd --- /dev/null +++ b/packaging/nix/package-desktop.nix @@ -0,0 +1,95 @@ +{ + stdenv, + lib, + buildNpmPackage, + fetchFromGitHub, + makeDesktopItem, + copyDesktopItems, + electron, + makeWrapper, + + installCLI ? false, + CLIcommand ? "zen", + commandLineArgs ? "", +}: +let + releaseData = lib.importJSON ./release-data.json; +in +buildNpmPackage (finalAttrs: { + pname = "zennotes-desktop"; + inherit (releaseData) version npmDepsHash; + + src = fetchFromGitHub { + owner = "ZenNotes"; + repo = "zennotes"; + tag = "v${finalAttrs.version}"; + inherit (releaseData) hash; + }; + + npmWorkspace = "apps/desktop"; + + env.ELECTRON_SKIP_BINARY_DOWNLOAD = "1"; + + nativeBuildInputs = [ + makeWrapper + ] + ++ lib.optionals stdenv.hostPlatform.isLinux [ + copyDesktopItems + ]; + + installPhase = '' + runHook preInstall + + mkdir -p $out/lib/node_modules/zennotes-monorepo + cp -r . $out/lib/node_modules/zennotes-monorepo/ + + for icon in apps/desktop/build/icons/*.png; do + size="$(basename "$icon" .png)" + install -Dm644 $icon $out/share/icons/hicolor/$size/apps/${finalAttrs.pname}.png + done + + mkdir -p $out/bin + makeWrapper ${electron}/bin/electron $out/bin/${finalAttrs.pname} \ + --add-flags "$out/lib/node_modules/zennotes-monorepo/apps/desktop" \ + --add-flags "\''${NIXOS_OZONE_WL:+\''${WAYLAND_DISPLAY:+--ozone-platform-hint=auto}}" \ + ${lib.optionalString (commandLineArgs != "") "--add-flags ${lib.escapeShellArg commandLineArgs}"} + + ${lib.optionalString installCLI '' + makeWrapper ${electron}/libexec/electron/electron $out/bin/${CLIcommand} \ + --set ELECTRON_RUN_AS_NODE 1 \ + --add-flags "$out/lib/node_modules/zennotes-monorepo/apps/desktop/out/main/cli.js" + ''} + + runHook postInstall + ''; + + desktopItems = [ + (makeDesktopItem { + name = finalAttrs.pname; + desktopName = "ZenNotes"; + exec = "${finalAttrs.pname} %U"; + icon = finalAttrs.pname; + comment = "Keyboard-first local Markdown notes"; + categories = [ + "Office" + "Utility" + "TextEditor" + ]; + startupWMClass = "ZenNotes"; + mimeTypes = [ + "text/markdown" + "x-scheme-handler/zennotes" + ]; + }) + ]; + + meta = { + description = "Keyboard-first local Markdown notes with Vim motions, diagrams, and MCP integration"; + homepage = "https://zennotes.org/"; + changelog = "https://github.com/ZenNotes/zennotes/releases/tag/v${finalAttrs.version}"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ justkrysteq ]; + mainProgram = finalAttrs.pname; + inherit (electron.meta) platforms; + }; +}) diff --git a/packaging/nix/package-server.nix b/packaging/nix/package-server.nix new file mode 100644 index 00000000..dced319a --- /dev/null +++ b/packaging/nix/package-server.nix @@ -0,0 +1,66 @@ +{ + lib, + buildGoModule, + fetchFromGitHub, + buildNpmPackage, +}: +let + releaseData = lib.importJSON ./release-data.json; + + src = fetchFromGitHub { + owner = "ZenNotes"; + repo = "zennotes"; + tag = "v${releaseData.version}"; + inherit (releaseData) hash; + }; + + web = buildNpmPackage { + pname = "zennotes-web"; + + inherit (releaseData) version npmDepsHash; + inherit src; + + npmWorkspace = "apps/web"; + + env.ELECTRON_SKIP_BINARY_DOWNLOAD = "1"; + + installPhase = '' + runHook preInstall + + mkdir -p "$out" + cp -R apps/web/dist/. "$out/" + + runHook postInstall + ''; + }; +in +buildGoModule (finalAttrs: { + pname = "zennotes-server"; + + inherit (releaseData) version vendorHash; + inherit src; + + modRoot = "apps/server"; + + subPackages = [ "cmd/zennotes-server" ]; + ldflags = [ + "-s" + "-w" + ]; + + preBuild = '' + rm -rf web/dist + mkdir -p web/dist + cp -R ${web}/. web/dist/ + ''; + + meta = { + description = "A server API for hosting remote ZenNotes vaults"; + homepage = "https://zennotes.org/"; + changelog = "https://github.com/ZenNotes/zennotes/releases/tag/v${finalAttrs.version}"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ justkrysteq ]; + mainProgram = finalAttrs.pname; + platforms = lib.platforms.linux ++ lib.platforms.darwin; + }; +}) diff --git a/packaging/nix/release-data.json b/packaging/nix/release-data.json new file mode 100644 index 00000000..593808ac --- /dev/null +++ b/packaging/nix/release-data.json @@ -0,0 +1,6 @@ +{ + "version": "2.3.0", + "hash": "sha256-+tLPVnnMbtMa5blSwHav9ZMlnkUsrdG62mMGxhbmy6g=", + "npmDepsHash": "sha256-7IpGnxVjaJvfSZyKjOylGMhFqa1bx8Ry5O1yqYfNnCE=", + "vendorHash": "sha256-wYBF7CjM6AvoWMWql9hFmIaj6pCmli4vOef6POyGkfU=" +} From b26395b23996a9e33f33bd65c7833386569b6b69 Mon Sep 17 00:00:00 2001 From: dannrs <danniramdhani@proton.me> Date: Sun, 14 Jun 2026 19:34:36 +0700 Subject: [PATCH 43/66] fix: set quick capture window title --- apps/desktop/src/main/index.ts | 1 + packages/app-core/src/components/QuickCaptureApp.tsx | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 6fe9aaf1..331377b0 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -2776,6 +2776,7 @@ function ensureQuickCaptureWindow(): BrowserWindow { height: 340, minWidth: 460, minHeight: 260, + title: 'ZenNotes Quick Capture', show: false, frame: false, titleBarStyle: mac ? 'hiddenInset' : 'hidden', diff --git a/packages/app-core/src/components/QuickCaptureApp.tsx b/packages/app-core/src/components/QuickCaptureApp.tsx index 776d46da..aee2832f 100644 --- a/packages/app-core/src/components/QuickCaptureApp.tsx +++ b/packages/app-core/src/components/QuickCaptureApp.tsx @@ -231,6 +231,11 @@ export function QuickCaptureApp(): JSX.Element { const [overlay, setOverlay] = useState<'none' | 'search' | 'command'>('none') const editorRef = useRef<EditorView | null>(null) + // Set a different title for the quick capture window. + useEffect(() => { + document.title = 'ZenNotes Quick Capture' + }, []) + // Apply theme + font CSS vars before paint. useEffect(() => { applyTheme(prefs) From 3bfd08c74fe4d908a9275cef4fadb539fe3b1f61 Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Wed, 17 Jun 2026 09:24:22 -0500 Subject: [PATCH 44/66] fix: Ctrl+D and other emacs chords work in Vim mode on macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeMirror's defaultKeymap binds the emacs-style control chords (Ctrl-d/a/e/f/b/v/h/k/o/t/p/n) as macOS-only bindings, and its key handler outranks the Vim plugin's — so on macOS those chords ran the emacs action (Ctrl-d deleted a character) instead of the Vim motion (<C-d> half-page down). Ctrl-u, which has no emacs binding, already worked, hence the up/down asymmetry. Linux/Windows were unaffected. Add vimAwareDefaultKeymap(): in Vim mode the emacs chords are stripped so Vim handles them natively; with Vim off they're kept (standard macOS editing keys). Applied to every Vim-capable editor surface — the main editor (dynamic via a compartment, so toggling Vim mode updates it live), quick capture, floating/external/template editors, and the pinned reference pane. --- .../app-core/src/components/EditorPane.tsx | 49 ++++++++++------ .../src/components/ExternalFileApp.tsx | 10 +++- .../src/components/FloatingNoteApp.tsx | 10 +++- .../src/components/PinnedReferencePane.tsx | 5 +- .../src/components/QuickCaptureApp.tsx | 10 +++- .../src/components/TemplateEditorModal.tsx | 10 +++- .../src/lib/cm-vim-default-keymap.test.ts | 34 +++++++++++ .../app-core/src/lib/cm-vim-default-keymap.ts | 58 +++++++++++++++++++ 8 files changed, 158 insertions(+), 28 deletions(-) create mode 100644 packages/app-core/src/lib/cm-vim-default-keymap.test.ts create mode 100644 packages/app-core/src/lib/cm-vim-default-keymap.ts diff --git a/packages/app-core/src/components/EditorPane.tsx b/packages/app-core/src/components/EditorPane.tsx index f9d3f4e6..8fd60e6b 100644 --- a/packages/app-core/src/components/EditorPane.tsx +++ b/packages/app-core/src/components/EditorPane.tsx @@ -39,7 +39,6 @@ import { import { Vim, getCM, vim } from '@replit/codemirror-vim' import type { AssetMeta, ImportedAsset, NoteComment, NoteFolder } from '@shared/ipc' import { - defaultKeymap, history, historyKeymap, indentWithTab, @@ -52,6 +51,7 @@ import { isImeComposing } from '../lib/ime' import { resolveCodeLanguage } from '../lib/cm-code-languages' import { markdownListIndentPlugin } from '../lib/cm-markdown-list-indent' import { completionNavKeymap } from '../lib/cm-completion-nav' +import { vimAwareDefaultKeymap } from '../lib/cm-vim-default-keymap' import { setYankToClipboardEnabled } from '../lib/cm-vim-clipboard' import { wireYankHighlight, yankHighlightExtension } from '../lib/cm-yank-highlight' import { frontmatterStyle } from '../lib/cm-frontmatter' @@ -220,6 +220,28 @@ const LARGE_DOC_LIVE_PREVIEW_DEFER_CHARS = 120_000 const LARGE_DOC_LIVE_PREVIEW_DEFER_MS = 3_000 const LARGE_DOC_EDITOR_HYDRATE_DELAY_MS = 180 +// The editor keymap depends on Vim mode: in Vim mode the macOS emacs-style +// chords are stripped from `defaultKeymap` so Vim's `<C-d>` & co. work (see +// cm-vim-default-keymap). Built behind a compartment and reconfigured on toggle. +function buildEditorKeymap(vimMode: boolean): Extension { + return keymap.of([ + { + key: 'Mod-f', + run: () => { + const state = useStore.getState() + if (state.vimMode) return false + state.setSearchOpen(true) + return true + } + }, + indentWithTab, + ...vimAwareDefaultKeymap(vimMode), + ...historyKeymap, + ...searchKeymap, + ...completionKeymap + ]) +} + function markdownEditingExtensions(): Extension[] { return [ markdown({ base: markdownLanguage, codeLanguages: resolveCodeLanguage, addKeymap: true }), @@ -701,6 +723,7 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { const lastProgrammaticPreviewTopRef = useRef<number | null>(null) const lastRestoredPathRef = useRef<string | null>(null) const vimCompartmentRef = useRef<Compartment | null>(null) + const editorKeymapCompartmentRef = useRef<Compartment | null>(null) const markdownCompartmentRef = useRef<Compartment | null>(null) const markdownSyntaxCompartmentRef = useRef<Compartment | null>(null) const livePreviewCompartmentRef = useRef<Compartment | null>(null) @@ -1333,12 +1356,14 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { } if (viewRef.current) return const vimCompartment = new Compartment() + const editorKeymapCompartment = new Compartment() const markdownCompartment = new Compartment() const markdownSyntaxCompartment = new Compartment() const livePreviewCompartment = new Compartment() const lineNumbersCompartment = new Compartment() const wordWrapCompartment = new Compartment() vimCompartmentRef.current = vimCompartment + editorKeymapCompartmentRef.current = editorKeymapCompartment markdownCompartmentRef.current = markdownCompartment markdownSyntaxCompartmentRef.current = markdownSyntaxCompartment livePreviewCompartmentRef.current = livePreviewCompartment @@ -1383,22 +1408,7 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { : 'slash-cmd-option' }), completionNavKeymap, - keymap.of([ - { - key: 'Mod-f', - run: () => { - const state = useStore.getState() - if (state.vimMode) return false - state.setSearchOpen(true) - return true - } - }, - indentWithTab, - ...defaultKeymap, - ...historyKeymap, - ...searchKeymap, - ...completionKeymap - ]), + editorKeymapCompartment.of(buildEditorKeymap(s0.vimMode)), EditorView.domEventHandlers({ mousedown: (event) => { const target = event.target as HTMLElement | null @@ -1678,7 +1688,10 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { const view = viewRef.current const comp = vimCompartmentRef.current if (!view || !comp) return - view.dispatch({ effects: comp.reconfigure(vimMode ? vim() : []) }) + const effects = [comp.reconfigure(vimMode ? vim() : [])] + const keymapComp = editorKeymapCompartmentRef.current + if (keymapComp) effects.push(keymapComp.reconfigure(buildEditorKeymap(vimMode))) + view.dispatch({ effects }) }, [vimMode]) useEffect(() => { const view = viewRef.current diff --git a/packages/app-core/src/components/ExternalFileApp.tsx b/packages/app-core/src/components/ExternalFileApp.tsx index 270eb21d..d1e81c2c 100644 --- a/packages/app-core/src/components/ExternalFileApp.tsx +++ b/packages/app-core/src/components/ExternalFileApp.tsx @@ -14,7 +14,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Annotation, Compartment, EditorState, type Transaction } from '@codemirror/state' import { EditorView, drawSelection, highlightActiveLine, keymap } from '@codemirror/view' import { Vim, vim } from '@replit/codemirror-vim' -import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands' +import { history, historyKeymap, indentWithTab } from '@codemirror/commands' +import { vimAwareDefaultKeymap } from '../lib/cm-vim-default-keymap' import { markdown, markdownLanguage } from '@codemirror/lang-markdown' import { resolveCodeLanguage } from '../lib/cm-code-languages' import { applyVimInsertEscape } from '../lib/vim-insert-escape' @@ -122,7 +123,12 @@ export function ExternalFileApp(): JSX.Element { syntaxHighlighting(defaultHighlightStyle, { fallback: true }), prefs.livePreview ? livePreviewPlugin : [], lineNumberExtension(prefs.lineNumberMode), - keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap, ...searchKeymap]), + keymap.of([ + indentWithTab, + ...vimAwareDefaultKeymap(prefs.vimMode), + ...historyKeymap, + ...searchKeymap + ]), EditorView.updateListener.of((upd) => { if (!upd.docChanged) return if (upd.transactions.some((tr: Transaction) => tr.annotation(programmatic))) return diff --git a/packages/app-core/src/components/FloatingNoteApp.tsx b/packages/app-core/src/components/FloatingNoteApp.tsx index 8bcbc0b6..530cfd91 100644 --- a/packages/app-core/src/components/FloatingNoteApp.tsx +++ b/packages/app-core/src/components/FloatingNoteApp.tsx @@ -26,7 +26,8 @@ import { lineNumbers } from '@codemirror/view' import { Vim, vim } from '@replit/codemirror-vim' -import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands' +import { history, historyKeymap, indentWithTab } from '@codemirror/commands' +import { vimAwareDefaultKeymap } from '../lib/cm-vim-default-keymap' import { markdown, markdownLanguage } from '@codemirror/lang-markdown' import { resolveCodeLanguage } from '../lib/cm-code-languages' import { applyVimInsertEscape } from '../lib/vim-insert-escape' @@ -305,7 +306,12 @@ export function FloatingNoteApp({ notePath }: { notePath: string }): JSX.Element syntaxHighlighting(defaultHighlightStyle, { fallback: true }), prefs.livePreview ? livePreviewPlugin : [], lineNumberExtension(prefs.lineNumberMode), - keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap, ...searchKeymap]), + keymap.of([ + indentWithTab, + ...vimAwareDefaultKeymap(prefs.vimMode), + ...historyKeymap, + ...searchKeymap + ]), EditorView.updateListener.of((upd) => { if (!upd.docChanged) return if (upd.transactions.some((tr: Transaction) => tr.annotation(programmatic))) return diff --git a/packages/app-core/src/components/PinnedReferencePane.tsx b/packages/app-core/src/components/PinnedReferencePane.tsx index 8c3ab283..e68eb91a 100644 --- a/packages/app-core/src/components/PinnedReferencePane.tsx +++ b/packages/app-core/src/components/PinnedReferencePane.tsx @@ -26,7 +26,8 @@ import { tooltips } from '@codemirror/view' import { vim } from '@replit/codemirror-vim' -import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands' +import { history, historyKeymap, indentWithTab } from '@codemirror/commands' +import { vimAwareDefaultKeymap } from '../lib/cm-vim-default-keymap' import { markdown, markdownLanguage } from '@codemirror/lang-markdown' import { resolveCodeLanguage } from '../lib/cm-code-languages' import { markdownListIndentPlugin } from '../lib/cm-markdown-list-indent' @@ -215,7 +216,7 @@ export function PinnedReferencePane(): JSX.Element | null { } }, indentWithTab, - ...defaultKeymap, + ...vimAwareDefaultKeymap(s0.vimMode), ...historyKeymap, ...searchKeymap, ...completionKeymap diff --git a/packages/app-core/src/components/QuickCaptureApp.tsx b/packages/app-core/src/components/QuickCaptureApp.tsx index aee2832f..1d910253 100644 --- a/packages/app-core/src/components/QuickCaptureApp.tsx +++ b/packages/app-core/src/components/QuickCaptureApp.tsx @@ -40,7 +40,8 @@ import { placeholder } from '@codemirror/view' import { Vim, vim } from '@replit/codemirror-vim' -import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands' +import { history, historyKeymap, indentWithTab } from '@codemirror/commands' +import { vimAwareDefaultKeymap } from '../lib/cm-vim-default-keymap' import { markdown, markdownLanguage } from '@codemirror/lang-markdown' import { resolveCodeLanguage } from '../lib/cm-code-languages' import { markdownListIndentPlugin } from '../lib/cm-markdown-list-indent' @@ -424,7 +425,12 @@ export function QuickCaptureApp(): JSX.Element { syntaxHighlighting(captureHighlight), syntaxHighlighting(defaultHighlightStyle, { fallback: true }), placeholder('Start writing…'), - keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap, ...searchKeymap]), + keymap.of([ + indentWithTab, + ...vimAwareDefaultKeymap(prefs.vimMode), + ...historyKeymap, + ...searchKeymap + ]), EditorView.updateListener.of((upd) => { if (!upd.docChanged) return const doc = upd.state.doc.toString() diff --git a/packages/app-core/src/components/TemplateEditorModal.tsx b/packages/app-core/src/components/TemplateEditorModal.tsx index 6c44eb1e..921f3d8a 100644 --- a/packages/app-core/src/components/TemplateEditorModal.tsx +++ b/packages/app-core/src/components/TemplateEditorModal.tsx @@ -9,7 +9,8 @@ import { useCallback, useMemo, useRef, useState } from 'react' import { Compartment, EditorState, type Transaction } from '@codemirror/state' import { EditorView, drawSelection, highlightActiveLine, keymap, tooltips } from '@codemirror/view' import { vim } from '@replit/codemirror-vim' -import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands' +import { history, historyKeymap, indentWithTab } from '@codemirror/commands' +import { vimAwareDefaultKeymap } from '../lib/cm-vim-default-keymap' import { markdown, markdownLanguage } from '@codemirror/lang-markdown' import { yamlFrontmatter } from '@codemirror/lang-yaml' import { syntaxHighlighting, HighlightStyle, defaultHighlightStyle } from '@codemirror/language' @@ -139,7 +140,12 @@ export function TemplateEditorModal({ optionClass: () => 'slash-cmd-option' }), completionNavKeymap, - keymap.of([indentWithTab, ...completionKeymap, ...defaultKeymap, ...historyKeymap]), + keymap.of([ + indentWithTab, + ...completionKeymap, + ...vimAwareDefaultKeymap(vimModeRef.current), + ...historyKeymap + ]), editorTheme, EditorView.updateListener.of((upd) => { if (upd.docChanged) setRaw(upd.state.doc.toString()) diff --git a/packages/app-core/src/lib/cm-vim-default-keymap.test.ts b/packages/app-core/src/lib/cm-vim-default-keymap.test.ts new file mode 100644 index 00000000..f8aecee5 --- /dev/null +++ b/packages/app-core/src/lib/cm-vim-default-keymap.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest' +import { vimAwareDefaultKeymap } from './cm-vim-default-keymap' + +// Regression guard for the macOS Vim `Ctrl-d` bug: defaultKeymap's emacs-style +// mac chords used to shadow Vim's <C-d> (half-page down) and delete a char. +describe('vimAwareDefaultKeymap', () => { + it('strips the macOS emacs chords in Vim mode so Vim handles them', () => { + const macs = new Set( + vimAwareDefaultKeymap(true) + .map((b) => b.mac) + .filter(Boolean) + ) + for (const chord of ['Ctrl-d', 'Ctrl-a', 'Ctrl-e', 'Ctrl-f', 'Ctrl-b', 'Ctrl-v', 'Ctrl-h']) { + expect(macs.has(chord)).toBe(false) + } + }) + + it('keeps the emacs chords when Vim is off (standard macOS editing keys)', () => { + const macs = new Set(vimAwareDefaultKeymap(false).map((b) => b.mac)) + expect(macs.has('Ctrl-d')).toBe(true) + expect(macs.has('Ctrl-e')).toBe(true) + }) + + it('never drops non-emacs bindings (Mod-a, arrows survive in Vim mode)', () => { + const vim = vimAwareDefaultKeymap(true) + expect(vim.some((b) => b.key === 'Mod-a')).toBe(true) + expect(vim.some((b) => b.key === 'ArrowDown')).toBe(true) + expect(vim.some((b) => b.key === 'Enter')).toBe(true) + }) + + it('removes exactly the 13 documented emacs chords, nothing more', () => { + expect(vimAwareDefaultKeymap(false).length - vimAwareDefaultKeymap(true).length).toBe(13) + }) +}) diff --git a/packages/app-core/src/lib/cm-vim-default-keymap.ts b/packages/app-core/src/lib/cm-vim-default-keymap.ts new file mode 100644 index 00000000..f9840e2a --- /dev/null +++ b/packages/app-core/src/lib/cm-vim-default-keymap.ts @@ -0,0 +1,58 @@ +import { defaultKeymap } from '@codemirror/commands' +import type { KeyBinding } from '@codemirror/view' + +/** + * macOS-only Vim keymap conflict (`Ctrl-d` deletes instead of half-page-down). + * + * `@codemirror/commands`' `defaultKeymap` folds in the emacs-style control + * chords as **mac-only** bindings (each entry's `mac` field). On macOS that + * makes the editor bind, among others: + * + * Ctrl-d → deleteCharForward Ctrl-a → cursorLineStart + * Ctrl-e → cursorLineEnd Ctrl-f → cursorCharRight + * Ctrl-b → cursorCharLeft Ctrl-v → cursorPageDown … + * + * These collide with Vim's own normal/visual-mode chords (`<C-d>` half-page + * down, `<C-a>` increment, `<C-v>` visual-block, `<C-f>`/`<C-b>` page, …). + * Because the keymap's key handler runs at higher precedence than the Vim + * plugin's, the emacs action wins — so in Vim mode on macOS `Ctrl-d` deletes a + * character instead of scrolling. (Linux/Windows are unaffected: these bindings + * are mac-only. `Ctrl-u`, which has no emacs binding, already worked — hence the + * up/down asymmetry users notice.) + * + * The fix is to drop these chords from the editor keymap while Vim mode is on, + * so Vim receives them and handles them natively. When Vim is off we keep them — + * they're standard macOS text-editing keys. + */ +const MAC_EMACS_CHORDS = new Set([ + 'Ctrl-b', + 'Ctrl-f', + 'Ctrl-p', + 'Ctrl-n', + 'Ctrl-a', + 'Ctrl-e', + 'Ctrl-d', + 'Ctrl-h', + 'Ctrl-k', + 'Ctrl-Alt-h', + 'Ctrl-o', + 'Ctrl-t', + 'Ctrl-v' +]) + +const isMacEmacsChord = (binding: KeyBinding): boolean => + typeof binding.mac === 'string' && MAC_EMACS_CHORDS.has(binding.mac) + +/** `defaultKeymap` with the macOS emacs-style control chords removed. */ +const defaultKeymapWithoutMacEmacs: readonly KeyBinding[] = defaultKeymap.filter( + (binding) => !isMacEmacsChord(binding) +) + +/** + * CodeMirror's `defaultKeymap`, made Vim-aware: in Vim mode the macOS + * emacs-style control chords are stripped so Vim's `<C-d>`/`<C-a>`/`<C-v>`/… + * bindings work; with Vim off the full keymap (including those chords) is used. + */ +export function vimAwareDefaultKeymap(vimMode: boolean): readonly KeyBinding[] { + return vimMode ? defaultKeymapWithoutMacEmacs : defaultKeymap +} From 15b37124d5763766238ad0e3814e2def64a0fc89 Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Wed, 17 Jun 2026 09:45:05 -0500 Subject: [PATCH 45/66] feat: render comment bodies as Markdown Comment bodies in the comments panel rendered as raw text, so Markdown a user typed (## headings, lists, **bold**, `code`) showed as literal source. They now render as sanitized Markdown via the same renderMarkdown pipeline the preview uses, with a compact `comment-prose` variant that scales headings and block spacing down to fit the narrow comment card. The input stays a plain textarea (you type Markdown source). --- .../app-core/src/components/CommentsPanel.tsx | 9 ++++- packages/app-core/src/styles/index.css | 35 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/app-core/src/components/CommentsPanel.tsx b/packages/app-core/src/components/CommentsPanel.tsx index 5675a1a8..73fe6b4b 100644 --- a/packages/app-core/src/components/CommentsPanel.tsx +++ b/packages/app-core/src/components/CommentsPanel.tsx @@ -10,6 +10,7 @@ import { import type { NoteComment, NoteContent } from '@shared/ipc' import { useStore } from '../store' import { commentQuote } from '../lib/comments' +import { renderMarkdown } from '../lib/markdown' import { usePanelResize } from '../lib/use-panel-resize' import { PanelResizeHandle } from './PanelResizeHandle' import { @@ -384,6 +385,9 @@ function CommentCard({ onDelete: () => void }): JSX.Element { const resolved = comment.resolvedAt != null + // Render the comment body as Markdown (sanitized). Cached by renderMarkdown, + // memoized per-body so card re-renders (hover/selection) don't re-parse. + const bodyHtml = useMemo(() => renderMarkdown(comment.body), [comment.body]) const showActionShortcuts = active && commentsFocused && !editing const handleCardClick = (event: MouseEvent<HTMLElement>): void => { const target = event.target as HTMLElement | null @@ -495,7 +499,10 @@ function CommentCard({ </div> </div> ) : ( - <p className="mt-3 whitespace-pre-wrap text-sm leading-5 text-ink-900">{comment.body}</p> + <div + className="comment-prose prose-zen mt-3 text-sm leading-5 text-ink-900" + dangerouslySetInnerHTML={{ __html: bodyHtml }} + /> )} </div> </div> diff --git a/packages/app-core/src/styles/index.css b/packages/app-core/src/styles/index.css index d322b6c1..5fe0e9ae 100644 --- a/packages/app-core/src/styles/index.css +++ b/packages/app-core/src/styles/index.css @@ -2097,6 +2097,41 @@ textarea, line-height: var(--z-editor-line-height, 1.7); color: theme("colors.ink.900"); } +/* Compact Markdown inside comment cards (CommentsPanel): reuse .prose-zen + element styling but drop the full-note container padding/width and scale + headings + block spacing down to fit a narrow, small-text comment. */ +.prose-zen.comment-prose { + width: 100%; + max-width: none; + margin: 0; + padding: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} +.prose-zen.comment-prose > :first-child { + margin-top: 0; +} +.prose-zen.comment-prose > :last-child { + margin-bottom: 0; +} +.prose-zen.comment-prose h1, +.prose-zen.comment-prose h2, +.prose-zen.comment-prose h3, +.prose-zen.comment-prose h4, +.prose-zen.comment-prose h5, +.prose-zen.comment-prose h6 { + font-size: 1.05em; + margin-top: 0.6em; + margin-bottom: 0.2em; + line-height: 1.3; +} +.prose-zen.comment-prose h1 { + font-size: 1.2em; +} +.prose-zen.comment-prose h2 { + font-size: 1.12em; +} .prose-zen h1, .prose-zen h2, .prose-zen h3, From 99358db29f5c5b26dc91999552759c893c55e182 Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Wed, 17 Jun 2026 09:45:06 -0500 Subject: [PATCH 46/66] feat: focus the title field when creating a note from the sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creating a note from the sidebar (the note-list "+" button, the folder context menu, and the empty-area menu) dropped the cursor straight into the body with the name left as "Untitled", so it had to be renamed afterward. These paths now pass focusTitle: true — matching the command palette's "New Note" — so focus lands on the title input with "Untitled" selected; Enter then moves to the body. Daily/weekly notes, wikilink-create, and trash-restore are intentionally unchanged. --- packages/app-core/src/components/NoteList.tsx | 2 +- packages/app-core/src/components/Sidebar.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/app-core/src/components/NoteList.tsx b/packages/app-core/src/components/NoteList.tsx index c91b16f4..d06dc64a 100644 --- a/packages/app-core/src/components/NoteList.tsx +++ b/packages/app-core/src/components/NoteList.tsx @@ -758,7 +758,7 @@ export function NoteList(): JSX.Element { <IconButton size="sm" title="New note" - onClick={() => void createAndOpen(newTarget.folder, newTarget.subpath)} + onClick={() => void createAndOpen(newTarget.folder, newTarget.subpath, { focusTitle: true })} > <PlusIcon /> </IconButton> diff --git a/packages/app-core/src/components/Sidebar.tsx b/packages/app-core/src/components/Sidebar.tsx index 09d0476a..c2c52bc9 100644 --- a/packages/app-core/src/components/Sidebar.tsx +++ b/packages/app-core/src/components/Sidebar.tsx @@ -1613,7 +1613,7 @@ export function Sidebar(): JSX.Element { { label: "New note", onSelect: async () => { - await createAndOpen(folder, subpath); + await createAndOpen(folder, subpath, { focusTitle: true }); }, }, { @@ -1839,7 +1839,7 @@ export function Sidebar(): JSX.Element { { label: "New note", onSelect: async () => { - await createAndOpen("inbox", ""); + await createAndOpen("inbox", "", { focusTitle: true }); }, }, { From 69f3c2df7ed7dab45cd1db702f93d392a379d982 Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Wed, 17 Jun 2026 10:26:56 -0500 Subject: [PATCH 47/66] fix: warn when inbox-mode vault hides root-level notes (#195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An Obsidian-style vault whose notes live at the root (and in custom folders) showed up empty when ZenNotes opened it in "inbox" mode — the scan only walks inbox/quick/archive/trash, so top-level content was silently hidden. This commonly happened when a flaky first directory read (iCloud-backed, symlinked folders) fell back to the inbox default. Add a sidebar banner that detects the mismatch — vault in inbox mode but its root holds markdown/non-system folders that only root mode surfaces (rootContentHiddenByInboxMode in the main process, exposed over a new IPC) — and offers a one-click "Switch to Vault root". Replaces the silent empty vault with a clear, actionable message. The banner lives in the sidebar (the unified layout's note tree), not the retired NoteList column. Refreshed on vault open and after settings changes. Covered by vault.test.ts cases. --- apps/desktop/src/main/index.ts | 8 ++++++ apps/desktop/src/main/vault.test.ts | 30 ++++++++++++++++++++ apps/desktop/src/main/vault.ts | 14 +++++++++ apps/desktop/src/preload/index.ts | 2 ++ apps/web/src/bridge/http-bridge.ts | 6 ++++ packages/app-core/src/components/Sidebar.tsx | 25 ++++++++++++++++ packages/app-core/src/store.ts | 22 +++++++++++++- packages/bridge-contract/src/bridge.ts | 3 ++ packages/bridge-contract/src/ipc.ts | 1 + 9 files changed, 110 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 331377b0..b910a1a6 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -82,6 +82,7 @@ import { searchVaultTextCapabilities, searchVaultText, setVaultSettings, + rootContentHiddenByInboxMode, type PersistedRemoteWorkspaceConfig, type PersistedRemoteWorkspaceProfile, type PersistedWindowState, @@ -2009,6 +2010,13 @@ function registerIpc(): void { return await setVaultSettings(v.root, next) }) + handle(IPC.VAULT_ROOT_CONTENT_HIDDEN, async () => { + // Local-vault only: a remote workspace manages its own layout server-side. + if (isRemoteWorkspaceActive()) return false + const v = requireVault() + return await rootContentHiddenByInboxMode(v.root) + }) + handle(IPC.VAULT_LIST_NOTES, async () => { if (isRemoteWorkspaceActive()) return await requireRemoteWorkspaceClient().listNotes() const v = requireVault() diff --git a/apps/desktop/src/main/vault.test.ts b/apps/desktop/src/main/vault.test.ts index ab41f116..e47826ac 100644 --- a/apps/desktop/src/main/vault.test.ts +++ b/apps/desktop/src/main/vault.test.ts @@ -22,6 +22,7 @@ import { renameFolder, restoreDeletedAsset, restoreFromTrash, + rootContentHiddenByInboxMode, searchVaultText, searchVaultTextCapabilities, setVaultSettings, @@ -41,6 +42,35 @@ afterEach(async () => { await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))) }) +describe('rootContentHiddenByInboxMode (#195)', () => { + it('flags an Obsidian-style vault (root notes + custom folders) stuck in inbox mode', async () => { + const root = await makeTempDir('zennotes-vault-hidden-') + await mkdir(root, { recursive: true }) + await writeFile(path.join(root, 'index.md'), '# Index\n') + await mkdir(path.join(root, 'concepts'), { recursive: true }) + const base = await getVaultSettings(root) + await setVaultSettings(root, { ...base, primaryNotesLocation: 'inbox' }) + expect(await rootContentHiddenByInboxMode(root)).toBe(true) + }) + + it('is false once the vault is switched to root mode', async () => { + const root = await makeTempDir('zennotes-vault-rootmode-') + await mkdir(root, { recursive: true }) + await writeFile(path.join(root, 'index.md'), '# Index\n') + const base = await getVaultSettings(root) + await setVaultSettings(root, { ...base, primaryNotesLocation: 'root' }) + expect(await rootContentHiddenByInboxMode(root)).toBe(false) + }) + + it('is false for an inbox-mode vault with no root content to hide', async () => { + const root = await makeTempDir('zennotes-vault-emptyroot-') + await mkdir(path.join(root, 'inbox'), { recursive: true }) + const base = await getVaultSettings(root) + await setVaultSettings(root, { ...base, primaryNotesLocation: 'inbox' }) + expect(await rootContentHiddenByInboxMode(root)).toBe(false) + }) +}) + describe('absolutePath', () => { it('rejects sibling-prefix escapes outside the vault root', async () => { const parent = await makeTempDir('zennotes-vault-parent-') diff --git a/apps/desktop/src/main/vault.ts b/apps/desktop/src/main/vault.ts index afb9759b..7cc09fec 100644 --- a/apps/desktop/src/main/vault.ts +++ b/apps/desktop/src/main/vault.ts @@ -963,6 +963,20 @@ async function inferPrimaryNotesLocation(root: string): Promise<PrimaryNotesLoca return DEFAULT_VAULT_SETTINGS.primaryNotesLocation } +/** + * True when the vault is in `inbox` primary-notes mode but its root holds + * markdown files or non-system folders that only `root` mode would surface — + * i.e. an Obsidian-style flat vault that was detected as Inbox (e.g. a flaky + * first directory read on an iCloud/symlinked folder fell back to the default) + * and is now silently hiding the user's notes. Drives the "Switch to Vault + * root" banner so an empty-looking vault is explained instead of silent. + */ +export async function rootContentHiddenByInboxMode(root: string): Promise<boolean> { + const settings = await getVaultSettings(root) + if (settings.primaryNotesLocation !== 'inbox') return false + return (await inferPrimaryNotesLocation(root)) === 'root' +} + async function vaultLooksEmpty(root: string): Promise<boolean> { let entries: Dirent[] try { diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index 739609fe..94134b54 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -262,6 +262,8 @@ const api: ZenBridge = { getVaultSettings: (): Promise<VaultSettings> => ipcRenderer.invoke(IPC.VAULT_GET_SETTINGS), setVaultSettings: (next: VaultSettings): Promise<VaultSettings> => ipcRenderer.invoke(IPC.VAULT_SET_SETTINGS, next), + rootContentHiddenByInboxMode: (): Promise<boolean> => + ipcRenderer.invoke(IPC.VAULT_ROOT_CONTENT_HIDDEN), listNotes: (): Promise<NoteMeta[]> => listNotesStreamed(), listNotesPage: (request: ListNotesPageRequest): Promise<ListNotesPageResponse> => diff --git a/apps/web/src/bridge/http-bridge.ts b/apps/web/src/bridge/http-bridge.ts index 31f19ea9..339e6950 100644 --- a/apps/web/src/bridge/http-bridge.ts +++ b/apps/web/src/bridge/http-bridge.ts @@ -280,6 +280,11 @@ function setVaultSettings(next: VaultSettings): Promise<VaultSettings> { }) } +function rootContentHiddenByInboxMode(): Promise<boolean> { + // Desktop-local concern; the web/server build never hides root content this way. + return Promise.resolve(false) +} + function listLocalVaults(): Promise<LocalVaultEntry[]> { return Promise.resolve([]) } @@ -1116,6 +1121,7 @@ export const httpBridge: ZenBridge = { browseServerDirectories, getVaultSettings, setVaultSettings, + rootContentHiddenByInboxMode, listNotes, listFolders, diff --git a/packages/app-core/src/components/Sidebar.tsx b/packages/app-core/src/components/Sidebar.tsx index c2c52bc9..d9b6680c 100644 --- a/packages/app-core/src/components/Sidebar.tsx +++ b/packages/app-core/src/components/Sidebar.tsx @@ -9,6 +9,7 @@ import { isTrashViewActive, useStore, } from "../store"; +import { Button } from "./ui/Button"; import { confirmMoveToTrash } from "../lib/confirm-trash"; import { buildMoveNotePrompt, parseMoveNoteTarget } from "../lib/move-note"; import { extractTags } from "../lib/tags"; @@ -364,6 +365,7 @@ export function Sidebar(): JSX.Element { const activeNote = useStore((s) => s.activeNote); const activeDirty = useStore((s) => s.activeDirty); const vaultSettings = useStore((s) => s.vaultSettings); + const rootContentHiddenByInboxMode = useStore((s) => s.rootContentHiddenByInboxMode); const view = useStore((s) => s.view); const assetFiles = useStore((s) => s.assetFiles); const setView = useStore((s) => s.setView); @@ -2707,6 +2709,29 @@ export function Sidebar(): JSX.Element { </div> </div> + {rootContentHiddenByInboxMode && ( + <div className="mx-3 mt-2 rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2.5"> + <p className="text-xs font-semibold text-ink-900">Notes at your vault root aren’t shown</p> + <p className="mt-1 text-xs leading-5 text-ink-600"> + This vault opened in <span className="font-medium text-ink-800">Inbox</span> mode, so + top-level files and folders are hidden. Switch to{" "} + <span className="font-medium text-ink-800">Vault root</span> to see them — the + Obsidian-style flat layout. + </p> + <div className="mt-2"> + <Button + variant="primary" + size="sm" + onClick={() => + void setVaultSettings({ ...vaultSettings, primaryNotesLocation: "root" }) + } + > + Switch to Vault root + </Button> + </div> + </div> + )} + {/* Main scrollable tree area */} <div ref={sidebarScrollRef} diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index 9a9829b3..cf454572 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -201,7 +201,12 @@ async function listNotesFromBridge(): Promise<NoteMeta[]> { async function refreshVaultIndexes(): Promise<void> { const state = useStore.getState() - await Promise.all([state.refreshNotes(), state.refreshAssets(), state.loadCustomTemplates()]) + await Promise.all([ + state.refreshNotes(), + state.refreshAssets(), + state.loadCustomTemplates(), + state.refreshRootContentHidden() + ]) } /** Find a template (built-in or custom) by id, or undefined if it's gone. */ @@ -1457,6 +1462,8 @@ interface Store { localVaults: LocalVaultEntry[] workspaceSetupError: string | null vaultSettings: VaultSettings + /** Vault is in `inbox` mode but its root holds notes only `root` mode shows. */ + rootContentHiddenByInboxMode: boolean notes: NoteMeta[] folders: FolderEntry[] assetFiles: AssetMeta[] @@ -1738,6 +1745,7 @@ interface Store { jumpToNextNote: () => Promise<void> applyChange: (ev: VaultChangeEvent) => Promise<void> refreshNotes: () => Promise<void> + refreshRootContentHidden: () => Promise<void> refreshAssets: () => Promise<void> deleteAsset: (relPath: string) => Promise<void> undoLastAssetAction: () => Promise<boolean> @@ -2778,6 +2786,7 @@ export const useStore = create<Store>((set, get) => { localVaults: [], workspaceSetupError: null, vaultSettings: DEFAULT_VAULT_SETTINGS, + rootContentHiddenByInboxMode: false, notes: [], folders: [], assetFiles: [], @@ -2905,10 +2914,21 @@ export const useStore = create<Store>((set, get) => { vaultSettings: settings }) await get().refreshNotes() + await get().refreshRootContentHidden() } catch (err) { console.error('setVaultSettings failed', err) } }, + refreshRootContentHidden: async () => { + try { + const hidden = await window.zen.rootContentHiddenByInboxMode() + if (get().rootContentHiddenByInboxMode !== hidden) { + set({ rootContentHiddenByInboxMode: hidden }) + } + } catch { + // Non-fatal: the banner is advisory; keep the previous value on error. + } + }, setNotes: (notes) => set({ notes }), setView: (view) => { set({ diff --git a/packages/bridge-contract/src/bridge.ts b/packages/bridge-contract/src/bridge.ts index 3111681a..d4666624 100644 --- a/packages/bridge-contract/src/bridge.ts +++ b/packages/bridge-contract/src/bridge.ts @@ -110,6 +110,9 @@ export interface ZenBridge { browseServerDirectories(path?: string): Promise<DirectoryBrowseResult> getVaultSettings(): Promise<VaultSettings> setVaultSettings(next: VaultSettings): Promise<VaultSettings> + /** True when the vault is in `inbox` mode but its root holds notes that only + * `root` mode would surface (drives the "Switch to Vault root" banner). */ + rootContentHiddenByInboxMode(): Promise<boolean> listNotes(): Promise<NoteMeta[]> listNotesPage?(request: ListNotesPageRequest): Promise<ListNotesPageResponse> diff --git a/packages/bridge-contract/src/ipc.ts b/packages/bridge-contract/src/ipc.ts index 564fdb24..069c088a 100644 --- a/packages/bridge-contract/src/ipc.ts +++ b/packages/bridge-contract/src/ipc.ts @@ -18,6 +18,7 @@ export const IPC = { VAULT_GET_CURRENT: 'vault:get-current', VAULT_GET_SETTINGS: 'vault:get-settings', VAULT_SET_SETTINGS: 'vault:set-settings', + VAULT_ROOT_CONTENT_HIDDEN: 'vault:root-content-hidden', VAULT_LIST_NOTES: 'vault:list-notes', VAULT_LIST_NOTES_STREAM: 'vault:list-notes-stream', VAULT_LIST_FOLDERS: 'vault:list-folders', From 8870c98374f7b90c5d8e7df6fec16955061e0d73 Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Wed, 17 Jun 2026 12:09:44 -0500 Subject: [PATCH 48/66] feat: per-folder accent colors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folders can now be given a preset accent color (10-color palette) from the sidebar context menu — "Change color…" / "Reset color" — alongside the existing folder icons. The color tints the folder's icon and name and stays readable in every state: a colored row that's selected uses a faint accent tint + ring instead of the saturated fill, so same-hue text doesn't vanish. Stored as `folderColors: Record<"folder:subpath", FolderColorId>` on VaultSettings (persisted to .zennotes/vault.json via the existing setVaultSettings — no new IPC), keyed exactly like folderIcons and rewritten in lock-step on folder rename/delete/duplicate (main process + store). --- apps/desktop/src/main/vault.ts | 89 +++++++++++- .../src/components/FolderColorPickerModal.tsx | 63 ++++++++ .../app-core/src/components/FolderColors.tsx | 48 ++++++ packages/app-core/src/components/Sidebar.tsx | 137 ++++++++++++++++-- .../app-core/src/lib/vault-layout.test.ts | 48 ++++-- packages/app-core/src/lib/vault-layout.ts | 117 ++++++++++++++- packages/app-core/src/store.test.ts | 12 +- packages/app-core/src/store.ts | 22 ++- packages/bridge-contract/src/ipc.ts | 18 ++- 9 files changed, 516 insertions(+), 38 deletions(-) create mode 100644 packages/app-core/src/components/FolderColorPickerModal.tsx create mode 100644 packages/app-core/src/components/FolderColors.tsx diff --git a/apps/desktop/src/main/vault.ts b/apps/desktop/src/main/vault.ts index 7cc09fec..62ca32e7 100644 --- a/apps/desktop/src/main/vault.ts +++ b/apps/desktop/src/main/vault.ts @@ -21,6 +21,7 @@ import { AssetMeta, DeletedAsset, type FolderIconId, + type FolderColorId, type PrimaryNotesLocation, type VaultSettings, FolderEntry, @@ -149,6 +150,34 @@ function isFolderIconId(value: unknown): value is FolderIconId { return typeof value === 'string' && VALID_FOLDER_ICON_IDS.has(value as FolderIconId) } +const VALID_FOLDER_COLOR_IDS = new Set<FolderColorId>([ + 'red', + 'orange', + 'amber', + 'green', + 'teal', + 'sky', + 'blue', + 'indigo', + 'violet', + 'pink' +]) + +function isFolderColorId(value: unknown): value is FolderColorId { + return typeof value === 'string' && VALID_FOLDER_COLOR_IDS.has(value as FolderColorId) +} + +function normalizeFolderColors(value: unknown): Record<string, FolderColorId> { + const out: Record<string, FolderColorId> = {} + if (value && typeof value === 'object') { + for (const [key, colorId] of Object.entries(value as Record<string, unknown>)) { + if (!key || !isFolderColorId(colorId)) continue + out[key] = colorId + } + } + return out +} + const DEFAULT_VAULT_SETTINGS: VaultSettings = { primaryNotesLocation: 'inbox', dailyNotes: { @@ -163,7 +192,8 @@ const DEFAULT_VAULT_SETTINGS: VaultSettings = { titlePattern: DEFAULT_WEEKLY_NOTE_TITLE_PATTERN, locale: DEFAULT_WEEKLY_NOTE_LOCALE }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } interface VaultTextSearchCandidate { @@ -715,7 +745,8 @@ function cloneVaultSettings(settings: VaultSettings): VaultSettings { legacyPatterns: settings.weeklyNotes.legacyPatterns?.map((pattern) => ({ ...pattern })), templateId: settings.weeklyNotes.templateId }, - folderIcons: { ...settings.folderIcons } + folderIcons: { ...settings.folderIcons }, + folderColors: { ...settings.folderColors } } } @@ -824,7 +855,8 @@ function normalizeVaultSettings( titlePattern: DEFAULT_WEEKLY_NOTE_TITLE_PATTERN, locale: DEFAULT_WEEKLY_NOTE_LOCALE }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } } const candidate = value as { @@ -846,6 +878,7 @@ function normalizeVaultSettings( templateId?: unknown } | null folderIcons?: Record<string, unknown> | null + folderColors?: Record<string, unknown> | null } const folderIcons: Record<string, FolderIconId> = {} if (candidate.folderIcons && typeof candidate.folderIcons === 'object') { @@ -880,7 +913,8 @@ function normalizeVaultSettings( legacyPatterns: normalizeWeeklyNoteLegacyPatterns(candidate.weeklyNotes?.legacyPatterns), templateId: normalizeTemplateId(candidate.weeklyNotes?.templateId) }, - folderIcons + folderIcons, + folderColors: normalizeFolderColors(candidate.folderColors) } } @@ -926,6 +960,44 @@ function removeFolderIcons( return next } +function rewriteFolderColorsForRename( + folderColors: Record<string, FolderColorId>, + folder: NoteFolder, + oldSubpath: string, + newSubpath: string +): Record<string, FolderColorId> { + const next: Record<string, FolderColorId> = {} + const exactKey = folderIconKey(folder, oldSubpath) + const prefix = `${exactKey}/` + for (const [key, value] of Object.entries(folderColors)) { + if (key === exactKey) { + next[folderIconKey(folder, newSubpath)] = value + continue + } + if (key.startsWith(prefix)) { + next[folderIconKey(folder, newSubpath) + key.slice(exactKey.length)] = value + continue + } + next[key] = value + } + return next +} + +function removeFolderColors( + folderColors: Record<string, FolderColorId>, + folder: NoteFolder, + subpath: string +): Record<string, FolderColorId> { + const next: Record<string, FolderColorId> = {} + const exactKey = folderIconKey(folder, subpath) + const prefix = `${exactKey}/` + for (const [key, value] of Object.entries(folderColors)) { + if (key === exactKey || key.startsWith(prefix)) continue + next[key] = value + } + return next +} + function duplicateFolderIcons( folderIcons: Record<string, FolderIconId>, folder: NoteFolder, @@ -3119,6 +3191,12 @@ export async function renameFolder( topFolder, oldClean, newClean + ), + folderColors: rewriteFolderColorsForRename( + settings.folderColors, + topFolder, + oldClean, + newClean ) } await setVaultSettings(root, nextSettings) @@ -3143,7 +3221,8 @@ export async function deleteFolder( const settings = await getVaultSettings(root) const nextSettings: VaultSettings = { ...settings, - folderIcons: removeFolderIcons(settings.folderIcons, topFolder, clean) + folderIcons: removeFolderIcons(settings.folderIcons, topFolder, clean), + folderColors: removeFolderColors(settings.folderColors, topFolder, clean) } await setVaultSettings(root, nextSettings) invalidateNoteMetaCache(root) diff --git a/packages/app-core/src/components/FolderColorPickerModal.tsx b/packages/app-core/src/components/FolderColorPickerModal.tsx new file mode 100644 index 00000000..d0502cf2 --- /dev/null +++ b/packages/app-core/src/components/FolderColorPickerModal.tsx @@ -0,0 +1,63 @@ +import type { FolderColorId } from '@shared/ipc' +import { FOLDER_COLOR_OPTIONS } from './FolderColors' +import { Modal } from './ui/Modal' +import { Button } from './ui/Button' + +export function FolderColorPickerModal({ + targetLabel, + currentColorId, + onSelect, + onReset, + onCancel +}: { + targetLabel: string + currentColorId: FolderColorId | null + onSelect: (colorId: FolderColorId) => void + onReset: () => void + onCancel: () => void +}): JSX.Element { + return ( + <Modal size="md" layer="modal" onClose={onCancel}> + <Modal.Header + title="Choose color" + description={ + <> + Pick an accent color for{' '} + <span className="font-medium text-ink-700">{targetLabel}</span>. + </> + } + /> + <Modal.Body className="grid grid-cols-2 gap-2 sm:grid-cols-3"> + {FOLDER_COLOR_OPTIONS.map((option) => { + const active = option.id === currentColorId + return ( + <button + key={option.id} + type="button" + onClick={() => onSelect(option.id)} + className={[ + 'flex items-center gap-3 rounded-xl border px-3 py-2 text-left transition-colors', + active + ? 'border-accent bg-accent/10 text-accent' + : 'border-paper-300 bg-paper-50 text-ink-800 hover:border-paper-400 hover:bg-paper-200/70' + ].join(' ')} + > + <span className={`h-4 w-4 shrink-0 rounded-full ${option.swatchClass}`} /> + <span className="truncate text-sm font-medium">{option.label}</span> + </button> + ) + })} + </Modal.Body> + <Modal.Footer> + <Button variant="secondary" onClick={onCancel}> + Cancel + </Button> + {currentColorId && ( + <Button variant="ghost" onClick={onReset}> + Reset color + </Button> + )} + </Modal.Footer> + </Modal> + ) +} diff --git a/packages/app-core/src/components/FolderColors.tsx b/packages/app-core/src/components/FolderColors.tsx new file mode 100644 index 00000000..baf32396 --- /dev/null +++ b/packages/app-core/src/components/FolderColors.tsx @@ -0,0 +1,48 @@ +import type { FolderColorId, NoteFolder } from '@shared/ipc' +import { folderIconKey } from '../lib/vault-layout' + +export interface FolderColorOption { + id: FolderColorId + label: string + /** Solid swatch background, used in the picker. */ + swatchClass: string + /** Text color applied to the folder's sidebar glyph. */ + glyphClass: string +} + +// Preset palette. Tailwind palette colors at the 500 weight read well on both +// the light (paper) and dark sidebar. Class names are literal so the Tailwind +// content scanner keeps them. +export const FOLDER_COLOR_OPTIONS: readonly FolderColorOption[] = [ + { id: 'red', label: 'Red', swatchClass: 'bg-red-500', glyphClass: 'text-red-500' }, + { id: 'orange', label: 'Orange', swatchClass: 'bg-orange-500', glyphClass: 'text-orange-500' }, + { id: 'amber', label: 'Amber', swatchClass: 'bg-amber-500', glyphClass: 'text-amber-500' }, + { id: 'green', label: 'Green', swatchClass: 'bg-green-500', glyphClass: 'text-green-500' }, + { id: 'teal', label: 'Teal', swatchClass: 'bg-teal-500', glyphClass: 'text-teal-500' }, + { id: 'sky', label: 'Sky', swatchClass: 'bg-sky-500', glyphClass: 'text-sky-500' }, + { id: 'blue', label: 'Blue', swatchClass: 'bg-blue-500', glyphClass: 'text-blue-500' }, + { id: 'indigo', label: 'Indigo', swatchClass: 'bg-indigo-500', glyphClass: 'text-indigo-500' }, + { id: 'violet', label: 'Violet', swatchClass: 'bg-violet-500', glyphClass: 'text-violet-500' }, + { id: 'pink', label: 'Pink', swatchClass: 'bg-pink-500', glyphClass: 'text-pink-500' } +] as const + +const FOLDER_COLOR_LOOKUP = new Map(FOLDER_COLOR_OPTIONS.map((option) => [option.id, option])) + +/** The chosen color for a folder, or null when none is set (default tint). */ +export function resolveFolderColorId( + folder: NoteFolder, + subpath: string, + folderColors: Record<string, FolderColorId> +): FolderColorId | null { + return folderColors[folderIconKey(folder, subpath)] ?? null +} + +/** The glyph text-color class for a folder, or null when no color is set. */ +export function resolveFolderColorGlyphClass( + folder: NoteFolder, + subpath: string, + folderColors: Record<string, FolderColorId> +): string | null { + const id = resolveFolderColorId(folder, subpath, folderColors) + return id ? FOLDER_COLOR_LOOKUP.get(id)?.glyphClass ?? null : null +} diff --git a/packages/app-core/src/components/Sidebar.tsx b/packages/app-core/src/components/Sidebar.tsx index d9b6680c..5041ca4a 100644 --- a/packages/app-core/src/components/Sidebar.tsx +++ b/packages/app-core/src/components/Sidebar.tsx @@ -13,7 +13,7 @@ import { Button } from "./ui/Button"; import { confirmMoveToTrash } from "../lib/confirm-trash"; import { buildMoveNotePrompt, parseMoveNoteTarget } from "../lib/move-note"; import { extractTags } from "../lib/tags"; -import type { AssetMeta, FolderEntry, FolderIconId, NoteFolder, NoteMeta } from "@shared/ipc"; +import type { AssetMeta, FolderColorId, FolderEntry, FolderIconId, NoteFolder, NoteMeta } from "@shared/ipc"; import type { NoteSortOrder } from "../store"; import { isArchiveTabPath } from "@shared/archive"; import { isTrashTabPath } from "@shared/trash"; @@ -79,6 +79,8 @@ import { resolveFolderIconOption, } from "./FolderIcons"; import { FolderIconPickerModal } from "./FolderIconPickerModal"; +import { resolveFolderColorId, resolveFolderColorGlyphClass } from "./FolderColors"; +import { FolderColorPickerModal } from "./FolderColorPickerModal"; import { getSidebarEdgePrefetchPaths, getSidebarEntryLimitIncludingIndex, @@ -154,17 +156,26 @@ function remoteWorkspaceLabel(baseUrl: string | null): string { function SidebarGlyph({ active, rowActive, + colorClass, children, }: { active: boolean; rowActive: boolean; + /** Custom resting tint (folder color); ignored while active/selected. */ + colorClass?: string; children: JSX.Element; }): JSX.Element { return ( <span className={[ "flex h-5 w-5 shrink-0 items-center justify-center transition-colors", - active ? "text-white" : rowActive ? "text-accent" : "text-ink-500 group-hover:text-ink-800", + colorClass + ? colorClass + : active + ? "text-white" + : rowActive + ? "text-accent" + : "text-ink-500 group-hover:text-ink-800", ].join(" ")} > {children} @@ -857,6 +868,11 @@ export function Sidebar(): JSX.Element { subpath: string; label: string; } | null>(null); + const [folderColorPicker, setFolderColorPicker] = useState<{ + folder: NoteFolder; + subpath: string; + label: string; + } | null>(null); const [sortMenu, setSortMenu] = useState<{ x: number; y: number } | null>( null, ); @@ -930,6 +946,46 @@ export function Sidebar(): JSX.Element { }, [setVaultSettings, vaultSettings], ); + + const openFolderColorPicker = useCallback( + (folder: NoteFolder, subpath: string, label: string) => { + setFolderMenu(null); + setFolderColorPicker({ folder, subpath, label }); + }, + [], + ); + + const saveFolderColor = useCallback( + async (folder: NoteFolder, subpath: string, colorId: FolderColorId) => { + const key = folderIconKey(folder, subpath); + const nextSettings = normalizeVaultSettings({ + ...vaultSettings, + folderColors: { + ...vaultSettings.folderColors, + [key]: colorId, + }, + }); + await setVaultSettings(nextSettings); + setFolderColorPicker(null); + }, + [setVaultSettings, vaultSettings], + ); + + const resetFolderColor = useCallback( + async (folder: NoteFolder, subpath: string) => { + const key = folderIconKey(folder, subpath); + if (!(key in vaultSettings.folderColors)) return; + const nextColors = { ...vaultSettings.folderColors }; + delete nextColors[key]; + const nextSettings = normalizeVaultSettings({ + ...vaultSettings, + folderColors: nextColors, + }); + await setVaultSettings(nextSettings); + setFolderColorPicker(null); + }, + [setVaultSettings, vaultSettings], + ); const [noteMenu, setNoteMenu] = useState<{ x: number; y: number; @@ -1570,6 +1626,10 @@ export function Sidebar(): JSX.Element { vaultSettings.folderIcons, iconKey, ); + const hasCustomColor = Object.prototype.hasOwnProperty.call( + vaultSettings.folderColors, + iconKey, + ); const iconItems: ContextMenuItem[] = [ { label: "Change icon…", @@ -1584,6 +1644,19 @@ export function Sidebar(): JSX.Element { await resetFolderIcon(folder, subpath); }, }, + { + label: "Change color…", + onSelect: async () => { + openFolderColorPicker(folder, subpath, label); + }, + }, + { + label: "Reset color", + disabled: !hasCustomColor, + onSelect: async () => { + await resetFolderColor(folder, subpath); + }, + }, ]; if (folder === "trash" && isTop) { @@ -1831,6 +1904,8 @@ export function Sidebar(): JSX.Element { primaryNotesAtRoot, openFolderIconPicker, resetFolderIcon, + openFolderColorPicker, + resetFolderColor, bulkSelectionMenuItems, selectedSidebarKeys, ]); @@ -3153,6 +3228,30 @@ export function Sidebar(): JSX.Element { onCancel={() => setFolderIconPicker(null)} /> )} + {folderColorPicker && ( + <FolderColorPickerModal + targetLabel={folderColorPicker.label} + currentColorId={resolveFolderColorId( + folderColorPicker.folder, + folderColorPicker.subpath, + vaultSettings.folderColors, + )} + onSelect={(colorId) => + void saveFolderColor( + folderColorPicker.folder, + folderColorPicker.subpath, + colorId, + ) + } + onReset={() => + void resetFolderColor( + folderColorPicker.folder, + folderColorPicker.subpath, + ) + } + onCancel={() => setFolderColorPicker(null)} + /> + )} {sortMenu && ( <ContextMenu x={sortMenu.x} @@ -3839,6 +3938,9 @@ function SubTree({ node.subpath, vaultSettings.folderIcons, ); + const folderColorClass = + resolveFolderColorGlyphClass(folder, node.subpath, vaultSettings.folderColors) ?? + undefined; // A `<Name>.base` folder is a database: render it with a database icon and a // title without the suffix; clicking the row opens the grid, while the // chevron still expands to reveal its record-page notes. (#185) @@ -3908,6 +4010,7 @@ function SubTree({ iconOption.icon ) } + glyphColorClass={folderColorClass} label={isDatabase ? formTitleFromDir(node.name) : node.name} isSymlink={node.isSymlink} count={countNotesInTree(node)} @@ -4347,6 +4450,7 @@ function TreeRow({ isSymlink = false, reserveLeadingSlot = true, showExpandChevron = true, + glyphColorClass, }: { icon: JSX.Element; label: string; @@ -4378,6 +4482,8 @@ function TreeRow({ reserveLeadingSlot?: boolean; /** Hide the chevron affordance while keeping row-click toggle behavior. */ showExpandChevron?: boolean; + /** Custom resting tint for the leading glyph (folder color). */ + glyphColorClass?: string; }): JSX.Element { const strongActive = active && (!sidebarFocused || !!vimHighlight); @@ -4401,11 +4507,16 @@ function TreeRow({ className={[ "group flex h-8 w-full items-center gap-1.5 rounded-lg px-1 text-left text-sm outline-none transition-colors focus:outline-none", active - ? vimHighlight - ? "vim-cursor-on-active bg-accent text-white" - : sidebarFocused - ? "text-accent" - : "bg-accent text-white" + ? glyphColorClass + ? // Colored folder: a saturated accent fill would put same-hue text on + // top (orange-on-orange). Use a faint tint + ring so the folder color + // stays readable while still reading as the active row. + `bg-accent/20 ring-1 ring-inset ring-accent/60${vimHighlight ? " vim-cursor-on-active" : ""}` + : vimHighlight + ? "vim-cursor-on-active bg-accent text-white" + : sidebarFocused + ? "text-accent" + : "bg-accent text-white" : dropTarget ? "bg-accent/20 text-ink-900 ring-1 ring-accent/60" : selected @@ -4460,10 +4571,18 @@ function TreeRow({ ) : reserveLeadingSlot ? ( <span className="h-5 w-5 shrink-0" /> ) : null} - <SidebarGlyph active={strongActive} rowActive={active || selected}> + <SidebarGlyph + active={strongActive} + rowActive={active || selected} + colorClass={glyphColorClass} + > {icon} </SidebarGlyph> - <span className="flex-1 truncate">{label}</span> + <span + className={["flex-1 truncate", glyphColorClass].filter(Boolean).join(" ")} + > + {label} + </span> {isSymlink && ( <span aria-label="Symlinked folder" diff --git a/packages/app-core/src/lib/vault-layout.test.ts b/packages/app-core/src/lib/vault-layout.test.ts index 40c212d6..425fd446 100644 --- a/packages/app-core/src/lib/vault-layout.test.ts +++ b/packages/app-core/src/lib/vault-layout.test.ts @@ -33,7 +33,8 @@ function settings(dailyDirectory: string, weeklyDirectory: string): VaultSetting primaryNotesLocation: 'inbox', dailyNotes: { enabled: true, directory: dailyDirectory }, weeklyNotes: { enabled: true, directory: weeklyDirectory }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } } @@ -70,7 +71,8 @@ describe('classifyDateNote', () => { titlePattern: "yyyy-'W'ww-EEE", locale: 'en-US' }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } as VaultSettings ) @@ -90,7 +92,8 @@ describe('classifyDateNote', () => { locale: 'en-US' }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } as VaultSettings ) @@ -123,7 +126,8 @@ describe('classifyDateNote', () => { locale: 'en-US' }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } as VaultSettings) expect(location).toEqual({ @@ -142,7 +146,8 @@ describe('classifyDateNote', () => { locale: 'en-US' }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } as VaultSettings expect(dailyNoteLocationForDate(new Date(2026, 5, 9), vaultSettings)).toEqual({ @@ -169,7 +174,8 @@ describe('classifyDateNote', () => { locale: 'en-US' }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } as VaultSettings) expect(location).toEqual({ @@ -188,7 +194,8 @@ describe('classifyDateNote', () => { titlePattern: "yyyy-'W'ww-EEE", locale: 'en-US' }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } as VaultSettings) expect(location).toEqual({ @@ -207,7 +214,8 @@ describe('classifyDateNote', () => { titlePattern: "yyyy-'W'ww", locale: 'en-US' }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } as VaultSettings) expect(location).toEqual({ @@ -226,7 +234,8 @@ describe('classifyDateNote', () => { titlePattern: "yyyy-'W'ww", locale: 'en-US' }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } as VaultSettings) expect(location).toEqual({ @@ -245,7 +254,8 @@ describe('classifyDateNote', () => { titlePattern: "yyyy-'W'ww", locale: 'en-US' }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } as VaultSettings expect(weeklyNoteLocationForDate(new Date(2026, 5, 9), vaultSettings)).toEqual({ @@ -271,7 +281,8 @@ describe('classifyDateNote', () => { locale: 'en-US' }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } as VaultSettings const info = classifyDateNote( @@ -293,7 +304,8 @@ describe('classifyDateNote', () => { locale: 'en-US' }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } as VaultSettings const info = classifyDateNote(note('inbox/2026/06-09.md', '06-09'), vaultSettings) @@ -312,7 +324,8 @@ describe('classifyDateNote', () => { locale: 'en-US' }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } as VaultSettings // ISO week 24 falls entirely in June, so a July folder cannot round-trip. @@ -337,7 +350,8 @@ describe('classifyDateNote', () => { ] }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } as VaultSettings const info = classifyDateNote( @@ -362,7 +376,8 @@ describe('classifyDateNote', () => { { directory: 'Weekly Notes', titlePattern: "yyyy-'W'ww", locale: 'en-US' } ] }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } as VaultSettings const info = classifyDateNote( @@ -403,7 +418,8 @@ describe('dateNoteFolderMayBelongToDatePattern', () => { ] }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } as VaultSettings expect(dateNoteFolderMayBelongToDatePattern('2026/Jun-Jun', vaultSettings)).toBe(true) diff --git a/packages/app-core/src/lib/vault-layout.ts b/packages/app-core/src/lib/vault-layout.ts index 5ecbe1e4..1dfa173a 100644 --- a/packages/app-core/src/lib/vault-layout.ts +++ b/packages/app-core/src/lib/vault-layout.ts @@ -9,6 +9,7 @@ import { type AssetMeta, type DateNotePatternSettings, type FolderIconId, + type FolderColorId, type NoteFolder, type NoteMeta, type VaultSettings @@ -62,6 +63,23 @@ function isFolderIconId(value: unknown): value is FolderIconId { return typeof value === 'string' && VALID_FOLDER_ICON_IDS.has(value as FolderIconId) } +const VALID_FOLDER_COLOR_IDS = new Set<FolderColorId>([ + 'red', + 'orange', + 'amber', + 'green', + 'teal', + 'sky', + 'blue', + 'indigo', + 'violet', + 'pink' +]) + +export function isFolderColorId(value: unknown): value is FolderColorId { + return typeof value === 'string' && VALID_FOLDER_COLOR_IDS.has(value as FolderColorId) +} + function pad(n: number): string { return n.toString().padStart(2, '0') } @@ -451,6 +469,14 @@ export function normalizeVaultSettings( normalizedFolderIcons[key] = value } } + const folderColors = settings?.folderColors + const normalizedFolderColors: Record<string, FolderColorId> = {} + if (folderColors && typeof folderColors === 'object') { + for (const [key, value] of Object.entries(folderColors)) { + if (!key || !isFolderColorId(value)) continue + normalizedFolderColors[key] = value + } + } const primaryNotesLocation = settings?.primaryNotesLocation === 'root' ? 'root' @@ -488,7 +514,8 @@ export function normalizeVaultSettings( ), templateId: normalizeTemplateId(settings?.weeklyNotes?.templateId) }, - folderIcons: normalizedFolderIcons + folderIcons: normalizedFolderIcons, + folderColors: normalizedFolderColors } } @@ -555,6 +582,94 @@ export function duplicateFolderIcons( return next } +// Folder color maps share the exact key scheme as folder icons (`folder:subpath`), +// so renames/deletes/duplicates rewrite them identically. Generic helpers keep +// the two in lock-step without duplicating the traversal logic. +function rewriteFolderKeyMap<T>( + map: Record<string, T>, + folder: NoteFolder, + oldSubpath: string, + newSubpath: string +): Record<string, T> { + const next: Record<string, T> = {} + const exactKey = folderIconKey(folder, oldSubpath) + const prefix = `${exactKey}/` + for (const [key, value] of Object.entries(map)) { + if (key === exactKey) { + next[folderIconKey(folder, newSubpath)] = value + continue + } + if (key.startsWith(prefix)) { + next[folderIconKey(folder, newSubpath) + key.slice(exactKey.length)] = value + continue + } + next[key] = value + } + return next +} + +function removeFolderKeyMap<T>( + map: Record<string, T>, + folder: NoteFolder, + subpath: string +): Record<string, T> { + const next: Record<string, T> = {} + const exactKey = folderIconKey(folder, subpath) + const prefix = `${exactKey}/` + for (const [key, value] of Object.entries(map)) { + if (key === exactKey || key.startsWith(prefix)) continue + next[key] = value + } + return next +} + +function duplicateFolderKeyMap<T>( + map: Record<string, T>, + folder: NoteFolder, + sourceSubpath: string, + targetSubpath: string +): Record<string, T> { + const next: Record<string, T> = { ...map } + const exactKey = folderIconKey(folder, sourceSubpath) + const prefix = `${exactKey}/` + for (const [key, value] of Object.entries(map)) { + if (key === exactKey) { + next[folderIconKey(folder, targetSubpath)] = value + continue + } + if (key.startsWith(prefix)) { + next[folderIconKey(folder, targetSubpath) + key.slice(exactKey.length)] = value + } + } + return next +} + +export function rewriteFolderColorsForRename( + folderColors: Record<string, FolderColorId>, + folder: NoteFolder, + oldSubpath: string, + newSubpath: string +): Record<string, FolderColorId> { + return rewriteFolderKeyMap(folderColors, folder, oldSubpath, newSubpath) +} + +export function removeFolderColors( + folderColors: Record<string, FolderColorId>, + folder: NoteFolder, + subpath: string +): Record<string, FolderColorId> { + return removeFolderKeyMap(folderColors, folder, subpath) +} + +export function duplicateFolderColors( + folderColors: Record<string, FolderColorId>, + folder: NoteFolder, + sourceSubpath: string, + targetSubpath: string +): Record<string, FolderColorId> { + return duplicateFolderKeyMap(folderColors, folder, sourceSubpath, targetSubpath) +} + export function isPrimaryNotesAtRoot( settings: VaultSettings | null | undefined ): boolean { diff --git a/packages/app-core/src/store.test.ts b/packages/app-core/src/store.test.ts index e802806d..9f137961 100644 --- a/packages/app-core/src/store.test.ts +++ b/packages/app-core/src/store.test.ts @@ -165,7 +165,8 @@ describe('daily note patterns', () => { locale: 'en-US' }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } }) @@ -202,7 +203,8 @@ describe('weekly note patterns', () => { titlePattern: "yyyy-'W'ww-EEE", locale: 'en-US' }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } }) @@ -223,7 +225,8 @@ describe('date note pattern history', () => { locale: 'en-US' }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } const nextSettings = { ...oldSettings, @@ -288,7 +291,8 @@ describe('date note pattern history', () => { titlePattern: "yyyy-'W'ww", locale: 'en-US' }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } const nextSettings = { ...oldSettings, diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index cf454572..51cb9e7a 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -74,16 +74,19 @@ import { workspaceRestorePrefetchContentPaths } from './lib/workspace-tabs' import { + duplicateFolderColors, duplicateFolderIcons, dailyNoteLocationForDate, folderForVaultRelativePath, findDailyNoteForDate, findWeeklyNoteForDate, isPrimaryNotesAtRoot, + removeFolderColors, removeFolderIcons, normalizeVaultSettings, noteFolderSubpath, weeklyNoteLocationForDate, + rewriteFolderColorsForRename, rewriteFolderIconsForRename } from './lib/vault-layout' import { renderTemplate, renderTitle } from './lib/template-render' @@ -5311,6 +5314,12 @@ export const useStore = create<Store>((set, get) => { oldSubpath, newSubpath ) + const nextFolderColors = rewriteFolderColorsForRename( + get().vaultSettings.folderColors, + folder, + oldSubpath, + newSubpath + ) set((s) => { const nextLayout = rewritePathsInTree(s.paneLayout, rewritePath) const ensured = ensureActivePane(nextLayout, s.activePaneId) @@ -5336,7 +5345,8 @@ export const useStore = create<Store>((set, get) => { pinnedRefPath: s.pinnedRefPath ? rewritePath(s.pinnedRefPath) : null, vaultSettings: { ...s.vaultSettings, - folderIcons: nextFolderIcons + folderIcons: nextFolderIcons, + folderColors: nextFolderColors }, ...activeFieldsFrom(ensured.layout, ensured.activePaneId, contents, dirty) } @@ -5368,6 +5378,7 @@ export const useStore = create<Store>((set, get) => { } const prefix = `${folder}/${subpath}/` const nextFolderIcons = removeFolderIcons(get().vaultSettings.folderIcons, folder, subpath) + const nextFolderColors = removeFolderColors(get().vaultSettings.folderColors, folder, subpath) set((s) => { const nextLayout = rewritePathsInTree(s.paneLayout, (p) => p.startsWith(prefix) ? null : p @@ -5391,7 +5402,8 @@ export const useStore = create<Store>((set, get) => { s.pinnedRefPath && s.pinnedRefPath.startsWith(prefix) ? null : s.pinnedRefPath, vaultSettings: { ...s.vaultSettings, - folderIcons: nextFolderIcons + folderIcons: nextFolderIcons, + folderColors: nextFolderColors }, ...activeFieldsFrom(ensured.layout, ensured.activePaneId, contents, dirty) } @@ -5410,6 +5422,12 @@ export const useStore = create<Store>((set, get) => { folder, subpath, newSubpath + ), + folderColors: duplicateFolderColors( + s.vaultSettings.folderColors, + folder, + subpath, + newSubpath ) } })) diff --git a/packages/bridge-contract/src/ipc.ts b/packages/bridge-contract/src/ipc.ts index 069c088a..e4cf20d0 100644 --- a/packages/bridge-contract/src/ipc.ts +++ b/packages/bridge-contract/src/ipc.ts @@ -247,6 +247,19 @@ export type FolderIconId = | 'chart' | 'home' +/** Preset folder accent colors (tints the folder's sidebar icon). */ +export type FolderColorId = + | 'red' + | 'orange' + | 'amber' + | 'green' + | 'teal' + | 'sky' + | 'blue' + | 'indigo' + | 'violet' + | 'pink' + export interface DateNotePatternSettings { /** Directory or date-based directory pattern inside the primary notes area. */ directory: string @@ -289,6 +302,8 @@ export interface VaultSettings { dailyNotes: DailyNotesSettings weeklyNotes: WeeklyNotesSettings folderIcons: Record<string, FolderIconId> + /** Per-folder accent color, keyed by `folder:subpath` (same key as folderIcons). */ + folderColors: Record<string, FolderColorId> } export const DEFAULT_DAILY_NOTES_DIRECTORY = 'Daily Notes' @@ -312,7 +327,8 @@ export const DEFAULT_VAULT_SETTINGS: VaultSettings = { titlePattern: DEFAULT_WEEKLY_NOTE_TITLE_PATTERN, locale: DEFAULT_WEEKLY_NOTE_LOCALE }, - folderIcons: {} + folderIcons: {}, + folderColors: {} } export interface NoteMeta { From b0bcbded31636b84a48fb6dc10d97b81c97c6977 Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Wed, 17 Jun 2026 12:30:46 -0500 Subject: [PATCH 49/66] feat: copy the whole note as Markdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a "Copy Note as Markdown" command (command palette) plus a Vim leader binding (Space l y, under the note-actions group) that copies the active note's full Markdown source — including unsaved edits — to the clipboard. Wired through a copyActiveNoteAsMarkdown store action, registered as the remappable vim.leaderCopyMarkdown keymap, surfaced in the leader-l which-key hint, and documented in the in-app help. --- packages/app-core/src/components/VimNav.tsx | 12 ++++++++++++ packages/app-core/src/lib/commands.ts | 10 ++++++++++ packages/app-core/src/lib/help.ts | 5 +++++ packages/app-core/src/lib/keymaps.ts | 12 ++++++++++++ packages/app-core/src/store.ts | 16 ++++++++++++++++ 5 files changed, 55 insertions(+) diff --git a/packages/app-core/src/components/VimNav.tsx b/packages/app-core/src/components/VimNav.tsx index faf9f836..ba98a2b6 100644 --- a/packages/app-core/src/components/VimNav.tsx +++ b/packages/app-core/src/components/VimNav.tsx @@ -161,6 +161,11 @@ export function VimNav(): JSX.Element | null { keyLabel: getKeymapDisplay(keymapOverrides, 'vim.leaderFormatNote'), label: 'Format note', detail: 'Run markdown formatting on the active note.' + }, + { + keyLabel: getKeymapDisplay(keymapOverrides, 'vim.leaderCopyMarkdown'), + label: 'Copy as Markdown', + detail: "Copy the whole note's Markdown to the clipboard." } ] } @@ -691,6 +696,13 @@ export function VimNav(): JSX.Element | null { void state.formatActiveNote() return } + if (matchesSequenceToken(e, overrides, 'vim.leaderCopyMarkdown')) { + e.preventDefault() + e.stopImmediatePropagation() + resetLeader() + void state.copyActiveNoteAsMarkdown() + return + } resetLeader() } diff --git a/packages/app-core/src/lib/commands.ts b/packages/app-core/src/lib/commands.ts index 63e703ac..2021cdd0 100644 --- a/packages/app-core/src/lib/commands.ts +++ b/packages/app-core/src/lib/commands.ts @@ -273,6 +273,16 @@ export function buildCommands(options?: { includeUnavailable?: boolean }): Comma window.zen.clipboardWriteText(`[[${title}]]`) } }, + { + id: 'note.copy-markdown', + title: 'Copy Note as Markdown', + category: 'Note', + keywords: 'copy clipboard markdown source document whole content text yank', + when: () => !!getState().activeNote, + run: async () => { + await getState().copyActiveNoteAsMarkdown() + } + }, { id: 'note.export-pdf', title: 'Export Note as PDF…', diff --git a/packages/app-core/src/lib/help.ts b/packages/app-core/src/lib/help.ts index 10cd99ec..5c184649 100644 --- a/packages/app-core/src/lib/help.ts +++ b/packages/app-core/src/lib/help.ts @@ -584,6 +584,11 @@ export const HELP_VIM_COMMANDS: HelpExCommand[] = [ summary: 'Leader-format in normal mode', detail: 'A quick keyboard path to format the active note from the editor.' }, + { + command: '<Space> l y', + summary: 'Leader-copy note as Markdown', + detail: "Copy the whole note's Markdown source to the clipboard from the editor (also available as the “Copy Note as Markdown” command)." + }, { command: '<Space> (pause)', summary: 'Show leader hints', diff --git a/packages/app-core/src/lib/keymaps.ts b/packages/app-core/src/lib/keymaps.ts index 92a39115..1ff53050 100644 --- a/packages/app-core/src/lib/keymaps.ts +++ b/packages/app-core/src/lib/keymaps.ts @@ -45,6 +45,7 @@ export type KeymapId = | "vim.leaderSwitchVault" | "vim.leaderNoteActions" | "vim.leaderFormatNote" + | "vim.leaderCopyMarkdown" | "vim.leaderQuickCapture" | "vim.leaderTemplatePicker" | "vim.leaderInsertTemplate" @@ -451,6 +452,17 @@ const KEYMAP_DEFINITIONS: KeymapDefinition[] = [ vimOnly: true, maxTokens: 1, }, + { + id: "vim.leaderCopyMarkdown", + kind: "sequence", + scope: "leader", + group: "vim", + title: "Leader note action: copy note as Markdown", + description: "Copy the whole note's Markdown source to the clipboard.", + defaultBinding: "y", + vimOnly: true, + maxTokens: 1, + }, { id: "vim.leaderQuickCapture", kind: "sequence", diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index 51cb9e7a..2ae76914 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -1783,6 +1783,7 @@ interface Store { archiveActive: () => Promise<void> unarchiveActive: () => Promise<void> exportActiveNotePdf: () => Promise<void> + copyActiveNoteAsMarkdown: () => Promise<void> setSearchOpen: (open: boolean) => void setVaultTextSearchOpen: (open: boolean) => void setCommandPaletteOpen: (open: boolean, mode?: CommandPaletteInitialMode) => void @@ -4210,6 +4211,21 @@ export const useStore = create<Store>((set, get) => { } }, + copyActiveNoteAsMarkdown: async () => { + const s = get() + const active = s.activeNote + if (!active) return + let body = s.noteContents[active.path]?.body + if (body == null) { + try { + body = (await window.zen.readNote(active.path)).body + } catch { + return + } + } + window.zen.clipboardWriteText(body) + }, + setSearchOpen: (open) => set({ searchOpen: open, From 287846d0d8452639da2cadb8b135f6ba4bcf4447 Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Wed, 17 Jun 2026 14:14:39 -0500 Subject: [PATCH 50/66] feat: Excalidraw drawings as a first-class file type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `.excalidraw` drawings alongside notes and databases: - Listed in the sidebar (not as assets), with the real Excalidraw icon; they open in an embedded, lazy-loaded @excalidraw/excalidraw editor that loads/saves the scene JSON and follows the app's light/dark theme. - Create from the sidebar ("New drawing" in the folder and empty-area menus) and from the breadcrumb. Rename and move preserve the `.excalidraw` extension instead of turning a drawing into a .md note. - The drawing header hides Markdown-only controls (edit/split/preview, connections, comments, outline, PDF export), keeping archive + trash. - New vault:create-excalidraw IPC (desktop); the web build degrades until the Go server learns to list/create drawings. Also adds a breadcrumb create menu: right-click — or press `m` while a crumb is focused (e.g. via hint mode) — any folder crumb to create a new note / drawing / folder in that folder. --- apps/desktop/src/main/index.ts | 12 + apps/desktop/src/main/vault.ts | 84 +- apps/desktop/src/preload/index.ts | 2 + apps/web/src/bridge/http-bridge.ts | 12 + package-lock.json | 1801 +++++++++++++++-- packages/app-core/package.json | 1 + .../app-core/src/components/EditorPane.tsx | 145 +- .../src/components/ExcalidrawView.tsx | 88 + .../src/components/LazyExcalidrawView.tsx | 20 + packages/app-core/src/components/Sidebar.tsx | 48 +- packages/app-core/src/components/VimNav.tsx | 23 + packages/app-core/src/components/icons.tsx | 13 + packages/app-core/src/store.ts | 20 +- packages/bridge-contract/src/bridge.ts | 2 + packages/bridge-contract/src/ipc.ts | 1 + packages/shared-domain/src/excalidraw.ts | 57 + 16 files changed, 2133 insertions(+), 196 deletions(-) create mode 100644 packages/app-core/src/components/ExcalidrawView.tsx create mode 100644 packages/app-core/src/components/LazyExcalidrawView.tsx create mode 100644 packages/shared-domain/src/excalidraw.ts diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index b910a1a6..8bf31274 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -45,6 +45,7 @@ import { archiveNote, createFolder, createNote, + createExcalidraw, deleteAsset, DEFAULT_QUICK_CAPTURE_HOTKEY, deleteFolder, @@ -2283,6 +2284,17 @@ function registerIpc(): void { } ) + handle( + IPC.VAULT_CREATE_EXCALIDRAW, + async (_e, folder: NoteFolder, subpath: string = '', title?: string) => { + if (isRemoteWorkspaceActive()) { + return await requireRemoteWorkspaceClient().createExcalidraw(folder, subpath, title) + } + const v = requireVault() + return await createExcalidraw(v.root, folder, subpath, title) + } + ) + handle(IPC.VAULT_RENAME_NOTE, async (_e, relPath: string, nextTitle: string) => { if (isRemoteWorkspaceActive()) { return await requireRemoteWorkspaceClient().renameNote(relPath, nextTitle) diff --git a/apps/desktop/src/main/vault.ts b/apps/desktop/src/main/vault.ts index 62ca32e7..541b9978 100644 --- a/apps/desktop/src/main/vault.ts +++ b/apps/desktop/src/main/vault.ts @@ -53,6 +53,7 @@ import { isDatabaseInternalPath, isFormDirName } from '@shared/databases' +import { isExcalidrawPath, emptyExcalidrawDocument } from '@shared/excalidraw' import { DEMO_TOUR_ASSETS, DEMO_TOUR_NOTES } from './demo-tour-data' const CONFIG_FILE = 'zennotes.config.json' @@ -1891,6 +1892,21 @@ async function isMarkdownNoteEntry(full: string, entry: Dirent): Promise<boolean return false } +// `.excalidraw` drawings are listed alongside notes (so they show in the sidebar +// tree); the sidebar/editor route them by extension to the drawing view. +async function isExcalidrawFileEntry(full: string, entry: Dirent): Promise<boolean> { + if (!isExcalidrawPath(entry.name)) return false + if (entry.isFile()) return true + if (entry.isSymbolicLink()) { + try { + return (await fs.stat(full)).isFile() + } catch { + return false + } + } + return false +} + async function realpathOrResolve(p: string): Promise<string> { try { return await fs.realpath(p) @@ -2257,6 +2273,28 @@ async function readMeta( return { ...cached.meta, siblingOrder: resolvedSiblingOrder, isSymlink: linked } } + // Excalidraw drawings are JSON, not Markdown — don't parse their body for + // tags/links/excerpt (that would be garbage) or even read it for meta. + if (isExcalidrawPath(relPath)) { + const meta: NoteMeta = { + path: relPath, + title: path.basename(abs, path.extname(abs)), + folder, + siblingOrder: resolvedSiblingOrder, + createdAt: stat.birthtimeMs || stat.ctimeMs, + updatedAt: stat.mtimeMs, + size: stat.size, + tags: [], + wikilinks: [], + assetEmbeds: [], + hasAttachments: false, + excerpt: '', + isSymlink: linked + } + noteMetaCache.set(cacheKey, { mtimeMs: stat.mtimeMs, size: stat.size, meta }) + return meta + } + let body = '' try { body = await fs.readFile(abs, 'utf8') @@ -2397,7 +2435,10 @@ export async function listNotes(root: string): Promise<NoteMeta[]> { ancestors.delete(childReal) continue } - if (await isMarkdownNoteEntry(full, entry)) { + if ( + (await isMarkdownNoteEntry(full, entry)) || + (await isExcalidrawFileEntry(full, entry)) + ) { noteFiles.push({ full, folder, siblingOrder: index, isSymlink: entry.isSymbolicLink() }) } } @@ -2854,6 +2895,35 @@ export async function createNote( return await readMeta(root, abs, folder) } +// Create a new `.excalidraw` drawing seeded with an empty scene. Mirrors +// createNote (unique title, same folder/subpath resolution) but writes the +// Excalidraw JSON instead of a Markdown stub. +export async function createExcalidraw( + root: string, + folder: NoteFolder, + subpath = '', + title?: string +): Promise<NoteMeta> { + const base = sanitizeNoteTitle(title) || 'Untitled drawing' + const clean = subpath.replace(/^\/+|\/+$/g, '') + const topRoot = await folderRoot(root, folder) + const dir = clean ? resolveSafe(topRoot, clean) : topRoot + await fs.mkdir(dir, { recursive: true }) + let finalTitle = base + for (let n = 2; ; n++) { + try { + await fs.access(path.join(dir, `${finalTitle}.excalidraw`)) + finalTitle = `${base} ${n}` + } catch { + break + } + } + const abs = path.join(dir, `${finalTitle}.excalidraw`) + await fs.writeFile(abs, JSON.stringify(emptyExcalidrawDocument(), null, 2), 'utf8') + invalidateNoteMetaCache(root, toPosix(path.relative(root, abs))) + return await readMeta(root, abs, folder) +} + /** * Move a markdown file that lives outside the vault into the vault's * primary notes area, de-duplicating the title on collision. The source @@ -2885,7 +2955,10 @@ export async function renameNote( if (!folder) throw new Error(`Note not in a known folder: ${rel}`) const dir = path.dirname(abs) const trimmed = sanitizeNoteTitle(nextTitle) - const target = path.join(dir, `${trimmed}.md`) + // Preserve the file's type on rename — a `.excalidraw` drawing must stay a + // drawing, not get turned into a `.md` note (which would render its JSON). + const ext = isExcalidrawPath(abs) ? '.excalidraw' : '.md' + const target = path.join(dir, `${trimmed}${ext}`) const willRename = target !== abs // Snapshot the vault before the rename so inbound [[wikilinks]] still // resolve to this note under its current name; we rewrite them afterwards. @@ -2985,7 +3058,9 @@ async function moveBetweenFolders( await fs.mkdir(destDir, { recursive: true }) const baseTitle = path.basename(filename, path.extname(filename)) const finalTitle = await uniqueTitle(destDir, baseTitle) - const destAbs = path.join(destDir, `${finalTitle}.md`) + // Preserve the file type when moving (a `.excalidraw` drawing stays a drawing). + const ext = isExcalidrawPath(filename) ? '.excalidraw' : '.md' + const destAbs = path.join(destDir, `${finalTitle}${ext}`) await fs.rename(abs, destAbs) const meta = await readMeta(root, destAbs, target) await moveNoteComments(root, rel, meta.path) @@ -3383,6 +3458,9 @@ export async function listAssets(root: string): Promise<AssetMeta[]> { } if (!entry.isFile()) continue if (entry.name.toLowerCase().endsWith('.md')) continue + // Excalidraw drawings are a first-class file type (listed with notes), not + // a generic attachment. + if (isExcalidrawPath(entry.name)) continue // Legacy co-located sidecar/.bak (pre-migration) — not a user asset. if (isDatabaseInternalPath(entry.name)) continue let stat diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index 94134b54..2c30c834 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -324,6 +324,8 @@ const api: ZenBridge = { ipcRenderer.invoke(IPC.VAULT_APPEND_NOTE, relPath, body, position), createNote: (folder: NoteFolder, title?: string, subpath?: string): Promise<NoteMeta> => ipcRenderer.invoke(IPC.VAULT_CREATE_NOTE, folder, title, subpath), + createExcalidraw: (folder: NoteFolder, subpath?: string, title?: string): Promise<NoteMeta> => + ipcRenderer.invoke(IPC.VAULT_CREATE_EXCALIDRAW, folder, subpath, title), renameNote: (relPath: string, nextTitle: string): Promise<NoteMeta> => ipcRenderer.invoke(IPC.VAULT_RENAME_NOTE, relPath, nextTitle), deleteNote: (relPath: string): Promise<void> => ipcRenderer.invoke(IPC.VAULT_DELETE_NOTE, relPath), diff --git a/apps/web/src/bridge/http-bridge.ts b/apps/web/src/bridge/http-bridge.ts index 339e6950..33706a0a 100644 --- a/apps/web/src/bridge/http-bridge.ts +++ b/apps/web/src/bridge/http-bridge.ts @@ -401,6 +401,17 @@ function createNote( }) } +function createExcalidraw( + folder: NoteFolder, + subpath?: string, + title?: string +): Promise<NoteMeta> { + return jsonRequest<NoteMeta>('/excalidraw/create', { + method: 'POST', + body: { folder, subpath, title } + }) +} + function renameNote(relPath: string, nextTitle: string): Promise<NoteMeta> { return jsonRequest<NoteMeta>('/notes/rename', { method: 'POST', @@ -1150,6 +1161,7 @@ export const httpBridge: ZenBridge = { writeNote, appendToNote, createNote, + createExcalidraw, renameNote, deleteNote, moveToTrash, diff --git a/package-lock.json b/package-lock.json index dc701fd8..01cbbdf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -492,6 +492,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -1916,6 +1925,245 @@ "node": ">=12" } }, + "node_modules/@excalidraw/excalidraw": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.18.1.tgz", + "integrity": "sha512-6i5Gt7IDTOH//qa0Z315Ly5iVRhjWpu2whrlQFqkuwrkKUWgRsMk0P5qdE7bpyDpai7jeLeWYkyj1eVAfni1lw==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "6.0.2", + "@excalidraw/laser-pointer": "1.3.1", + "@excalidraw/mermaid-to-excalidraw": "2.2.2", + "@excalidraw/random-username": "1.1.0", + "@radix-ui/react-popover": "1.1.6", + "@radix-ui/react-tabs": "1.0.2", + "browser-fs-access": "0.29.1", + "canvas-roundrect-polyfill": "0.0.1", + "clsx": "1.1.1", + "cross-env": "7.0.3", + "es6-promise-pool": "2.5.0", + "fractional-indexing": "3.2.0", + "fuzzy": "0.1.3", + "image-blob-reduce": "3.0.1", + "jotai": "2.11.0", + "jotai-scope": "0.7.2", + "lodash.debounce": "4.0.8", + "lodash.throttle": "4.1.1", + "nanoid": "3.3.3", + "open-color": "1.9.1", + "pako": "2.0.3", + "perfect-freehand": "1.2.0", + "pica": "7.1.1", + "png-chunk-text": "1.0.0", + "png-chunks-encode": "1.0.0", + "png-chunks-extract": "1.0.0", + "points-on-curve": "1.0.1", + "pwacompat": "2.0.17", + "roughjs": "4.6.4", + "sass": "1.51.0", + "tunnel-rat": "0.1.2" + }, + "peerDependencies": { + "react": "^17.0.2 || ^18.2.0 || ^19.0.0", + "react-dom": "^17.0.2 || ^18.2.0 || ^19.0.0" + } + }, + "node_modules/@excalidraw/excalidraw/node_modules/@braintree/sanitize-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz", + "integrity": "sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==", + "license": "MIT" + }, + "node_modules/@excalidraw/excalidraw/node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/@excalidraw/excalidraw/node_modules/points-on-curve": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-1.0.1.tgz", + "integrity": "sha512-3nmX4/LIiyuwGLwuUrfhTlDeQFlAhi7lyK/zcRNGhalwapDWgAGR82bUpmn2mA03vII3fvNCG8jAONzKXwpxAg==", + "license": "MIT" + }, + "node_modules/@excalidraw/excalidraw/node_modules/roughjs": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.4.tgz", + "integrity": "sha512-s6EZ0BntezkFYMf/9mGn7M8XGIoaav9QQBCnJROWB3brUWQ683Q2LbRD/hq0Z3bAJ/9NVpU/5LpiTWvQMyLDhw==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/@excalidraw/excalidraw/node_modules/roughjs/node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/@excalidraw/laser-pointer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@excalidraw/laser-pointer/-/laser-pointer-1.3.1.tgz", + "integrity": "sha512-psA1z1N2qeAfsORdXc9JmD2y4CmDwmuMRxnNdJHZexIcPwaNEyIpNcelw+QkL9rz9tosaN9krXuKaRqYpRAR6g==", + "license": "MIT" + }, + "node_modules/@excalidraw/markdown-to-text": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@excalidraw/markdown-to-text/-/markdown-to-text-0.1.2.tgz", + "integrity": "sha512-1nDXBNAojfi3oSFwJswKREkFm5wrSjqay81QlyRv2pkITG/XYB5v+oChENVBQLcxQwX4IUATWvXM5BcaNhPiIg==", + "license": "MIT" + }, + "node_modules/@excalidraw/mermaid-to-excalidraw": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@excalidraw/mermaid-to-excalidraw/-/mermaid-to-excalidraw-2.2.2.tgz", + "integrity": "sha512-5VKQq5CdRocC82vOIUpQ5ufJOVV9FpBTdHGA+ULqazeIVV+cr299877omQCibsdS3Bpitz2fsnTwnIXEmLVDSg==", + "license": "MIT", + "dependencies": { + "@excalidraw/markdown-to-text": "0.1.2", + "@mermaid-js/parser": "^0.6.3", + "mermaid": "^11.12.1", + "nanoid": "4.0.2" + } + }, + "node_modules/@excalidraw/mermaid-to-excalidraw/node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@excalidraw/mermaid-to-excalidraw/node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@excalidraw/mermaid-to-excalidraw/node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "license": "Apache-2.0" + }, + "node_modules/@excalidraw/mermaid-to-excalidraw/node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "license": "Apache-2.0" + }, + "node_modules/@excalidraw/mermaid-to-excalidraw/node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "license": "Apache-2.0" + }, + "node_modules/@excalidraw/mermaid-to-excalidraw/node_modules/@mermaid-js/parser": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.3.tgz", + "integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==", + "license": "MIT", + "dependencies": { + "langium": "3.3.1" + } + }, + "node_modules/@excalidraw/mermaid-to-excalidraw/node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@excalidraw/mermaid-to-excalidraw/node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, + "node_modules/@excalidraw/mermaid-to-excalidraw/node_modules/langium": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", + "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", + "license": "MIT", + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@excalidraw/mermaid-to-excalidraw/node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/@excalidraw/mermaid-to-excalidraw/node_modules/nanoid": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz", + "integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^14 || ^16 || >=18" + } + }, + "node_modules/@excalidraw/mermaid-to-excalidraw/node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "license": "MIT" + }, + "node_modules/@excalidraw/random-username": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@excalidraw/random-username/-/random-username-1.1.0.tgz", + "integrity": "sha512-nULYsQxkWHnbmHvcs+efMkJ4/9TtvNyFeLyHdeGxW0zHs6P+jYVqcRff9A6Vq9w9JXeDRnRh2VKvTtS19GW2qA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/@exodus/bytes": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", @@ -1934,6 +2182,44 @@ } } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -2799,173 +3085,941 @@ "integrity": "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==", "license": "MIT", "dependencies": { - "langium": "^4.0.0" + "langium": "^4.0.0" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@prinsss/dvi2html": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@prinsss/dvi2html/-/dvi2html-0.0.1.tgz", + "integrity": "sha512-AdlM+1uAXH278q50vfpwIU7ykA5FKuxi6mx5haGZCmVTeNw6nxcbfyTIBRmlviesu4W+YJeFFfdv+gJ68O47aw==", + "license": "GPL-3.0", + "dependencies": { + "buffer": "^6.0.3" + } + }, + "node_modules/@prinsss/dvi2html/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz", + "integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.1.tgz", + "integrity": "sha512-uuiFbs+YCKjn3X1DTSx9G7BHApu4GHbi3kgiwsnFUbOKCrwejAJv4eE4Vc8C0Oaxt9T0aV4ox0WCOdx+39Xo+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-slot": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-context": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.0.tgz", + "integrity": "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.1.tgz", + "integrity": "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", + "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.0.tgz", + "integrity": "sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", + "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", + "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.6.tgz", + "integrity": "sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", + "integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", + "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.2.tgz", + "integrity": "sha512-HLK+CqD/8pN6GfJm3U+cqpqhSKYAWiOJDe+A+8MfxBnOue39QEeMa43csUn2CXCHQT0/mewh1LrrG4tfkM9DMA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-collection": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-direction": "1.0.0", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-controllable-state": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", + "integrity": "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.0.tgz", + "integrity": "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz", + "integrity": "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.1.tgz", + "integrity": "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-slot": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", + "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", + "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz", + "integrity": "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz", + "integrity": "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.2.tgz", + "integrity": "sha512-gOUwh+HbjCuL0UCo8kZ+kdUEG8QtpdO4sMQduJ34ZEz0r4922g9REOBM+vIsfwtGxSug4Yb1msJMJYN2Bk8TpQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-direction": "1.0.0", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-roving-focus": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", + "integrity": "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-context": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.0.tgz", + "integrity": "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz", + "integrity": "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.0.tgz", + "integrity": "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=18" + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0" }, "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.1.tgz", + "integrity": "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", + "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==", "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", + "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", "license": "MIT", - "engines": { - "node": ">= 8" + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz", + "integrity": "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==", "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.0" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/@npmcli/fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", - "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", - "dev": true, - "license": "ISC", + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz", + "integrity": "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==", + "license": "MIT", "dependencies": { - "@gar/promisify": "^1.1.3", - "semver": "^7.3.5" + "@babel/runtime": "^7.13.10" }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "engines": { - "node": ">=10" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@npmcli/move-file": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", - "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "dev": true, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", "license": "MIT", "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" + "@radix-ui/react-use-callback-ref": "1.1.0" }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@prinsss/dvi2html": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@prinsss/dvi2html/-/dvi2html-0.0.1.tgz", - "integrity": "sha512-AdlM+1uAXH278q50vfpwIU7ykA5FKuxi6mx5haGZCmVTeNw6nxcbfyTIBRmlviesu4W+YJeFFfdv+gJ68O47aw==", - "license": "GPL-3.0", - "dependencies": { - "buffer": "^6.0.3" + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@prinsss/dvi2html/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true } - ], + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", "license": "MIT", "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, + "node_modules/@radix-ui/rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", + "license": "MIT" + }, "node_modules/@replit/codemirror-vim": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/@replit/codemirror-vim/-/codemirror-vim-6.3.0.tgz", @@ -3881,7 +4935,6 @@ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -3891,7 +4944,7 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" @@ -4296,7 +5349,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -4583,6 +5635,18 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -4842,7 +5906,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4952,7 +6015,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -4961,6 +6023,12 @@ "node": ">=8" } }, + "node_modules/browser-fs-access": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.29.1.tgz", + "integrity": "sha512-LSvVX5e21LRrXqVMhqtAwj5xPgDb+fXAIH80NsnCQ9xuZPs2xWsOREi24RKgZa1XOiQRbcmVrv87+ulOKsgjxw==", + "license": "Apache-2.0" + }, "node_modules/browserslist": { "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", @@ -5336,6 +6404,12 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas-roundrect-polyfill": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/canvas-roundrect-polyfill/-/canvas-roundrect-polyfill-0.0.1.tgz", + "integrity": "sha512-yWq+R3U3jE+coOeEb3a3GgE2j/0MMiDKM/QpLb6h9ihf5fGY9UXtvK9o4vNqjWXoZz7/3EaSVU3IX53TvFFUOw==", + "license": "MIT" + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -5589,6 +6663,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/clsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", + "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/codemirror": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", @@ -5912,6 +6995,24 @@ "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "license": "MIT" }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6898,6 +7999,12 @@ "license": "MIT", "optional": true }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -7703,6 +8810,15 @@ "license": "MIT", "optional": true }, + "node_modules/es6-promise-pool": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/es6-promise-pool/-/es6-promise-pool-2.5.0.tgz", + "integrity": "sha512-VHErXfzR/6r/+yyzPKeBvO0lgjfC5cbDCQWjWwMZWSb6YU39TGIl51OUmCfWCq4ylMdJSB8zkz2vIuIeIxXApA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -8114,7 +9230,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -8248,6 +9363,15 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/fractional-indexing": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fractional-indexing/-/fractional-indexing-3.2.0.tgz", + "integrity": "sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ==", + "license": "CC0-1.0", + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -8303,7 +9427,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -8342,6 +9465,14 @@ "interval-arithmetic-eval": "^0.5.1" } }, + "node_modules/fuzzy": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", + "integrity": "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/gauge": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", @@ -8407,6 +9538,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -8569,6 +9709,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/glur": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/glur/-/glur-1.1.2.tgz", + "integrity": "sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA==", + "license": "MIT" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -9182,6 +10328,21 @@ ], "license": "BSD-3-Clause" }, + "node_modules/image-blob-reduce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/image-blob-reduce/-/image-blob-reduce-3.0.1.tgz", + "integrity": "sha512-/VmmWgIryG/wcn4TVrV7cC4mlfUC/oyiKIfSg5eVM3Ten/c1c34RJhMYKCWTnoSMHSqXLt3tsrBR4Q2HInvN+Q==", + "license": "MIT", + "dependencies": { + "pica": "^7.1.0" + } + }, + "node_modules/immutable": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.8.tgz", + "integrity": "sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==", + "license": "MIT" + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -9277,7 +10438,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -9328,7 +10488,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9348,7 +10507,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -9378,7 +10536,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -9490,6 +10647,38 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/jotai": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.11.0.tgz", + "integrity": "sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=17.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/jotai-scope": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/jotai-scope/-/jotai-scope-0.7.2.tgz", + "integrity": "sha512-Gwed97f3dDObrO43++2lRcgOqw4O2sdr4JCjP/7eHK1oPACDJ7xKHGScpJX9XaflU+KBHXF+VhwECnzcaQiShg==", + "license": "MIT", + "peerDependencies": { + "jotai": ">=2.9.2", + "react": ">=17.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9792,6 +10981,12 @@ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -9833,6 +11028,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" + }, "node_modules/lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", @@ -11308,6 +12509,16 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multimath": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/multimath/-/multimath-2.0.0.tgz", + "integrity": "sha512-toRx66cAMJ+Ccz7pMIg38xSIrtnbozk0dchXezwQDMgQmbGpfxjtv68H+L00iFL8hxDaVjrmwAFSb3I6bg8Q2g==", + "license": "MIT", + "dependencies": { + "glur": "^1.1.2", + "object-assign": "^4.1.1" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -11667,7 +12878,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11794,6 +13004,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open-color": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/open-color/-/open-color-1.9.1.tgz", + "integrity": "sha512-vCseG/EQ6/RcvxhUcGJiHViOgrtz4x0XbZepXvKik66TMGkvbmjeJrKFyBEx6daG5rNyyd14zYXhz0hZVwQFOw==", + "license": "MIT" + }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -11873,6 +13089,12 @@ "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", "license": "MIT" }, + "node_modules/pako": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.3.tgz", + "integrity": "sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parse5": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", @@ -12009,6 +13231,25 @@ "dev": true, "license": "MIT" }, + "node_modules/perfect-freehand": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.2.0.tgz", + "integrity": "sha512-h/0ikF1M3phW7CwpZ5MMvKnfpHficWoOEyr//KVNTxV4F6deRK1eYMtHyBKEAKFK0aXIEUK9oBvlF6PNXMDsAw==", + "license": "MIT" + }, + "node_modules/pica": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/pica/-/pica-7.1.1.tgz", + "integrity": "sha512-WY73tMvNzXWEld2LicT9Y260L43isrZ85tPuqRyvtkljSDLmnNFQmZICt4xUJMVulmcc6L9O7jbBrtx3DOz/YQ==", + "license": "MIT", + "dependencies": { + "glur": "^1.1.2", + "inherits": "^2.0.3", + "multimath": "^2.0.0", + "object-assign": "^4.1.1", + "webworkify": "^1.5.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -12019,7 +13260,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -12083,6 +13323,49 @@ "node": ">=10.4.0" } }, + "node_modules/png-chunk-text": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-chunk-text/-/png-chunk-text-1.0.0.tgz", + "integrity": "sha512-DEROKU3SkkLGWNMzru3xPVgxyd48UGuMSZvioErCure6yhOc/pRH2ZV+SEn7nmaf7WNf3NdIpH+UTrRdKyq9Lw==", + "license": "MIT" + }, + "node_modules/png-chunks-encode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-chunks-encode/-/png-chunks-encode-1.0.0.tgz", + "integrity": "sha512-J1jcHgbQRsIIgx5wxW9UmCymV3wwn4qCCJl6KYgEU/yHCh/L2Mwq/nMOkRPtmV79TLxRZj5w3tH69pvygFkDqA==", + "license": "MIT", + "dependencies": { + "crc-32": "^0.3.0", + "sliced": "^1.0.1" + } + }, + "node_modules/png-chunks-encode/node_modules/crc-32": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-0.3.0.tgz", + "integrity": "sha512-kucVIjOmMc1f0tv53BJ/5WIX+MGLcKuoBhnGqQrgKJNqLByb/sVMWfW/Aw6hw0jgcqjJ2pi9E5y32zOIpaUlsA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/png-chunks-extract": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-chunks-extract/-/png-chunks-extract-1.0.0.tgz", + "integrity": "sha512-ZiVwF5EJ0DNZyzAqld8BP1qyJBaGOFaq9zl579qfbkcmOwWLLO4I9L8i2O4j3HkI6/35i0nKG2n+dZplxiT89Q==", + "license": "MIT", + "dependencies": { + "crc-32": "^0.3.0" + } + }, + "node_modules/png-chunks-extract/node_modules/crc-32": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-0.3.0.tgz", + "integrity": "sha512-kucVIjOmMc1f0tv53BJ/5WIX+MGLcKuoBhnGqQrgKJNqLByb/sVMWfW/Aw6hw0jgcqjJ2pi9E5y32zOIpaUlsA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/points-on-curve": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", @@ -12370,6 +13653,12 @@ "node": ">=6" } }, + "node_modules/pwacompat": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/pwacompat/-/pwacompat-2.0.17.tgz", + "integrity": "sha512-6Du7IZdIy7cHiv7AhtDy4X2QRM8IAD5DII69mt5qWibC2d15ZU8DmBG1WdZKekG11cChSu4zkSUGPF9sweOl6w==", + "license": "Apache-2.0" + }, "node_modules/qs": { "version": "6.15.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", @@ -12483,6 +13772,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -12501,6 +13791,75 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/read-binary-file-arch": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", @@ -13079,6 +14438,71 @@ "truncate-utf8-bytes": "^1.0.0" } }, + "node_modules/sass": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.51.0.tgz", + "integrity": "sha512-haGdpTgywJTvHC2b91GSq+clTKGbtkkZmVAb82jZQN/wTy6qs8DdFm2lhEQbEwrY0QDRgSQ3xDurqM977C3noA==", + "license": "MIT", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/sass/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/sax": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", @@ -13364,6 +14788,13 @@ "node": ">=8" } }, + "node_modules/sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==", + "deprecated": "Unsupported", + "license": "MIT" + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -14197,7 +15628,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -14310,6 +15740,43 @@ "license": "0BSD", "peer": true }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", + "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", + "license": "MIT", + "dependencies": { + "zustand": "^4.3.2" + } + }, + "node_modules/tunnel-rat/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/turbo": { "version": "2.9.6", "resolved": "https://registry.npmjs.org/turbo/-/turbo-2.9.6.tgz", @@ -14604,6 +16071,59 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utf8-byte-length": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", @@ -14966,6 +16486,12 @@ "node": ">=20" } }, + "node_modules/webworkify": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/webworkify/-/webworkify-1.5.0.tgz", + "integrity": "sha512-AMcUeyXAhbACL8S2hqqdqOLqvJ8ylmIbNwUIqQujRSouf4+eUFaXbG6F1Rbu+srlJMmxQWsiU7mOJi0nMBfM1g==", + "license": "MIT" + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -15325,6 +16851,7 @@ "@codemirror/search": "^6.5.8", "@codemirror/state": "^6.5.0", "@codemirror/view": "^6.35.3", + "@excalidraw/excalidraw": "^0.18.1", "@lezer/highlight": "^1.2.1", "@replit/codemirror-vim": "^6.3.0", "dompurify": "^3.3.4", diff --git a/packages/app-core/package.json b/packages/app-core/package.json index 7f7db1d7..0d4d88e3 100644 --- a/packages/app-core/package.json +++ b/packages/app-core/package.json @@ -28,6 +28,7 @@ "@codemirror/search": "^6.5.8", "@codemirror/state": "^6.5.0", "@codemirror/view": "^6.35.3", + "@excalidraw/excalidraw": "^0.18.1", "@lezer/highlight": "^1.2.1", "@replit/codemirror-vim": "^6.3.0", "dompurify": "^3.3.4", diff --git a/packages/app-core/src/components/EditorPane.tsx b/packages/app-core/src/components/EditorPane.tsx index 8fd60e6b..2c6aa5fc 100644 --- a/packages/app-core/src/components/EditorPane.tsx +++ b/packages/app-core/src/components/EditorPane.tsx @@ -85,8 +85,11 @@ import { OutlinePanel } from './OutlinePanel' import { CalendarPanel } from './CalendarPanel' import { CommentsPanel, type CommentDraft } from './CommentsPanel' import { ContextMenu, type ContextMenuItem } from './ContextMenu' +import { promptApp } from '../lib/prompt-requests' import { TasksView } from './TasksView' import { DatabaseView } from './DatabaseView' +import { LazyExcalidrawView } from './LazyExcalidrawView' +import { isExcalidrawPath } from '@shared/excalidraw' import { TagView } from './TagView' import { HelpView } from './HelpView' import { ArchiveView } from './ArchiveView' @@ -2664,47 +2667,55 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { const toolbar = useMemo(() => { if (!content) return null const folder = content.folder + // Excalidraw drawings only get the file-level actions (archive/trash) — the + // Markdown-specific controls (edit/split/preview, connections, comments, + // outline, calendar, PDF export) don't apply to a canvas. + const isDrawing = isExcalidrawPath(content.path) return ( <div className="flex items-center gap-1 text-ink-500"> - <ToggleGroup mode={mode} onChange={applyPaneMode} /> - <div className="mx-2 h-4 w-px bg-paper-300" /> - <IconBtn - title={connectionsOpen ? 'Hide connections' : 'Show connections'} - active={connectionsOpen} - onClick={toggleConnectionsPanel} - > - <PanelRightIcon /> - </IconBtn> - <IconBtn - title={ - commentsOpen - ? 'Hide comments' - : `Show comments${openCommentCount > 0 ? ` (${openCommentCount})` : ''}` - } - active={commentsOpen} - onClick={toggleCommentsPanel} - > - <FeedbackIcon /> - </IconBtn> - <IconBtn - title={outlineOpen ? 'Hide outline' : 'Show outline'} - active={outlineOpen} - onClick={toggleOutlinePanel} - > - <ListTreeIcon /> - </IconBtn> - {calendarAvailable && ( - <IconBtn - title={calendarOpen ? 'Hide calendar' : 'Show calendar'} - active={calendarOpen} - onClick={toggleCalendarPanel} - > - <CalendarIcon /> - </IconBtn> + {!isDrawing && ( + <> + <ToggleGroup mode={mode} onChange={applyPaneMode} /> + <div className="mx-2 h-4 w-px bg-paper-300" /> + <IconBtn + title={connectionsOpen ? 'Hide connections' : 'Show connections'} + active={connectionsOpen} + onClick={toggleConnectionsPanel} + > + <PanelRightIcon /> + </IconBtn> + <IconBtn + title={ + commentsOpen + ? 'Hide comments' + : `Show comments${openCommentCount > 0 ? ` (${openCommentCount})` : ''}` + } + active={commentsOpen} + onClick={toggleCommentsPanel} + > + <FeedbackIcon /> + </IconBtn> + <IconBtn + title={outlineOpen ? 'Hide outline' : 'Show outline'} + active={outlineOpen} + onClick={toggleOutlinePanel} + > + <ListTreeIcon /> + </IconBtn> + {calendarAvailable && ( + <IconBtn + title={calendarOpen ? 'Hide calendar' : 'Show calendar'} + active={calendarOpen} + onClick={toggleCalendarPanel} + > + <CalendarIcon /> + </IconBtn> + )} + <IconBtn title="Export as PDF (⇧⌘E)" onClick={() => void exportActiveNotePdf()}> + <FileDownIcon /> + </IconBtn> + </> )} - <IconBtn title="Export as PDF (⇧⌘E)" onClick={() => void exportActiveNotePdf()}> - <FileDownIcon /> - </IconBtn> {folder === 'trash' ? ( <IconBtn title="Restore" onClick={() => void restoreActive()}> <ArrowUpRightIcon /> @@ -3154,6 +3165,8 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { <LazyDiagramTabView diagram={diagramFromTabPath(activeTab)} /> ) : activeTab && isDatabaseTabPath(activeTab) ? ( <DatabaseView tabPath={activeTab} isActive={isActive} /> + ) : activeTab && isExcalidrawPath(activeTab) ? ( + <LazyExcalidrawView path={activeTab} /> ) : content ? ( <div className={[ @@ -3637,6 +3650,12 @@ function Breadcrumb({ const setView = useStore((s) => s.setView) const systemFolderLabels = useStore((s) => s.systemFolderLabels) const vaultSettings = useStore((s) => s.vaultSettings) + const createAndOpen = useStore((s) => s.createAndOpen) + const createDrawingAndOpen = useStore((s) => s.createDrawingAndOpen) + const createFolder = useStore((s) => s.createFolder) + const [crumbMenu, setCrumbMenu] = useState<{ x: number; y: number; subpath: string } | null>( + null + ) const [editing, setEditing] = useState(false) const [value, setValue] = useState(note.title) const [warning, setWarning] = useState('') @@ -3662,10 +3681,11 @@ function Breadcrumb({ const segments = noteFolderSubpath(note, vaultSettings) .split('/') .filter(Boolean) - const ancestors: { label: string; onClick: () => void }[] = [] + const ancestors: { label: string; subpath: string; onClick: () => void }[] = [] if (!(topFolder === 'inbox' && isPrimaryNotesAtRoot(vaultSettings))) { ancestors.push({ label: getSystemFolderLabel(topFolder, systemFolderLabels), + subpath: '', onClick: () => setView({ kind: 'folder', folder: topFolder, subpath: '' }) }) } @@ -3675,6 +3695,7 @@ function Breadcrumb({ const subpath = acc ancestors.push({ label: seg, + subpath, onClick: () => setView({ kind: 'folder', folder: topFolder, subpath }) }) } @@ -3703,15 +3724,59 @@ function Breadcrumb({ {ancestors.map((c, i) => ( <span key={i} className="flex shrink-0 items-center gap-1"> <button + data-crumb-menu="" onClick={c.onClick} + onContextMenu={(e) => { + e.preventDefault() + e.stopPropagation() + setCrumbMenu({ x: e.clientX, y: e.clientY, subpath: c.subpath }) + }} className="truncate rounded px-1 hover:bg-paper-200/70 hover:text-ink-800" - title={`Go to ${c.label}`} + title={`Go to ${c.label} — right-click (or m) to create here`} > {c.label} </button> <span className="text-ink-400">›</span> </span> ))} + {crumbMenu && ( + <ContextMenu + x={crumbMenu.x} + y={crumbMenu.y} + onClose={() => setCrumbMenu(null)} + items={[ + { + label: 'New note', + onSelect: () => + void createAndOpen(topFolder, crumbMenu.subpath, { focusTitle: true }) + }, + { + label: 'New drawing', + onSelect: () => void createDrawingAndOpen(topFolder, crumbMenu.subpath) + }, + { + label: 'New folder', + onSelect: async () => { + const name = await promptApp({ + title: 'New folder', + placeholder: 'Folder name', + okLabel: 'Create', + validate: (v) => (v.includes('/') ? 'Folder name cannot contain "/"' : null) + }) + if (!name) return + const clean = name.trim().replace(/^\/+|\/+$/g, '') + if (!clean) return + const next = crumbMenu.subpath ? `${crumbMenu.subpath}/${clean}` : clean + try { + await createFolder(topFolder, next) + } catch (err) { + window.alert((err as Error).message) + } + } + } + ]} + /> + )} {editingNow ? ( <input ref={inputRef} diff --git a/packages/app-core/src/components/ExcalidrawView.tsx b/packages/app-core/src/components/ExcalidrawView.tsx new file mode 100644 index 00000000..83729a26 --- /dev/null +++ b/packages/app-core/src/components/ExcalidrawView.tsx @@ -0,0 +1,88 @@ +import { useEffect, useRef, useState } from 'react' +import type { ComponentProps } from 'react' +import { Excalidraw, serializeAsJSON } from '@excalidraw/excalidraw' +import '@excalidraw/excalidraw/index.css' +import { parseExcalidrawDocument } from '@shared/excalidraw' +import { useStore } from '../store' +import { THEMES } from '../lib/themes' + +type InitialData = ComponentProps<typeof Excalidraw>['initialData'] + +/** + * The embedded Excalidraw drawing editor for a `.excalidraw` file. Loaded lazily + * (see LazyExcalidrawView) so the heavy bundle never touches startup. Reads the + * scene JSON from disk on open and debounce-saves it back on every change. + */ +export function ExcalidrawView({ path }: { path: string }): JSX.Element { + const [initialData, setInitialData] = useState<InitialData | undefined>(undefined) + const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null) + const lastSaved = useRef<string>('') + const pathRef = useRef(path) + pathRef.current = path + + // Match the app's light/dark theme. + const themeId = useStore((s) => s.themeId) + const excalidrawTheme = THEMES.find((t) => t.id === themeId)?.mode === 'dark' ? 'dark' : 'light' + + useEffect(() => { + let cancelled = false + setInitialData(undefined) + window.zen + .readNote(path) + .then((res) => { + if (cancelled) return + lastSaved.current = res?.body ?? '' + const doc = parseExcalidrawDocument(res?.body ?? '') + setInitialData({ + elements: doc.elements, + appState: doc.appState, + files: doc.files + } as InitialData) + }) + .catch(() => { + if (!cancelled) setInitialData({} as InitialData) + }) + return () => { + cancelled = true + } + }, [path]) + + useEffect( + () => () => { + if (saveTimer.current) clearTimeout(saveTimer.current) + }, + [] + ) + + if (initialData === undefined) { + return ( + <div className="flex min-h-0 flex-1 items-center justify-center text-sm text-ink-500"> + Loading drawing… + </div> + ) + } + + return ( + <div className="min-h-0 w-full flex-1" style={{ height: '100%' }}> + <Excalidraw + initialData={initialData} + theme={excalidrawTheme} + onChange={(elements, appState, files) => { + if (saveTimer.current) clearTimeout(saveTimer.current) + saveTimer.current = setTimeout(() => { + let json: string + try { + json = serializeAsJSON(elements, appState, files, 'local') + } catch { + return + } + // Skip no-op writes (Excalidraw fires onChange on load and on hover). + if (json === lastSaved.current) return + lastSaved.current = json + void window.zen.writeNote(pathRef.current, json) + }, 700) + }} + /> + </div> + ) +} diff --git a/packages/app-core/src/components/LazyExcalidrawView.tsx b/packages/app-core/src/components/LazyExcalidrawView.tsx new file mode 100644 index 00000000..90e3f3a0 --- /dev/null +++ b/packages/app-core/src/components/LazyExcalidrawView.tsx @@ -0,0 +1,20 @@ +import { lazy, Suspense } from 'react' + +const ExcalidrawViewImpl = lazy(() => + import('./ExcalidrawView').then((mod) => ({ default: mod.ExcalidrawView })) +) + +/** Lazy boundary for the Excalidraw editor so its heavy bundle loads on demand. */ +export function LazyExcalidrawView({ path }: { path: string }): JSX.Element { + return ( + <Suspense + fallback={ + <div className="flex min-h-0 flex-1 items-center justify-center text-sm text-ink-500"> + Loading drawing… + </div> + } + > + <ExcalidrawViewImpl path={path} /> + </Suspense> + ) +} diff --git a/packages/app-core/src/components/Sidebar.tsx b/packages/app-core/src/components/Sidebar.tsx index 5041ca4a..103afb4b 100644 --- a/packages/app-core/src/components/Sidebar.tsx +++ b/packages/app-core/src/components/Sidebar.tsx @@ -27,6 +27,7 @@ import { CloseIcon, DatabaseIcon, DocumentIcon, + ExcalidrawIcon, ExpandAllIcon, PaperclipIcon, FolderPlusIcon, @@ -73,6 +74,7 @@ import { formTitleFromDir, isFormDirName, } from "@shared/databases"; +import { isExcalidrawPath } from "@shared/excalidraw"; import { FolderGlyphIcon, resolveFolderIconId, @@ -398,6 +400,7 @@ export function Sidebar(): JSX.Element { const tagsViewActive = useStore(isTagsViewActive); const setSearchOpen = useStore((s) => s.setSearchOpen); const createAndOpen = useStore((s) => s.createAndOpen); + const createDrawingAndOpen = useStore((s) => s.createDrawingAndOpen); const createDatabase = useStore((s) => s.createDatabase); const createNoteInChosenFolder = useStore((s) => s.createNoteInChosenFolder); const openTemplatePaletteForFolder = useStore((s) => s.openTemplatePaletteForFolder); @@ -1691,6 +1694,12 @@ export function Sidebar(): JSX.Element { await createAndOpen(folder, subpath, { focusTitle: true }); }, }, + { + label: "New drawing", + onSelect: async () => { + await createDrawingAndOpen(folder, subpath); + }, + }, { label: "New from template", onSelect: () => { @@ -1885,6 +1894,7 @@ export function Sidebar(): JSX.Element { allFolders, vault, createAndOpen, + createDrawingAndOpen, createDatabase, openTemplatePaletteForFolder, openArchiveView, @@ -1919,6 +1929,12 @@ export function Sidebar(): JSX.Element { await createAndOpen("inbox", "", { focusTitle: true }); }, }, + { + label: "New drawing", + onSelect: async () => { + await createDrawingAndOpen("inbox", ""); + }, + }, { label: "New from template", onSelect: () => { @@ -1950,7 +1966,7 @@ export function Sidebar(): JSX.Element { }, }, ], - [createAndOpen, createDatabase, openTemplatePaletteForFolder, createFolderAction], + [createAndOpen, createDrawingAndOpen, createDatabase, openTemplatePaletteForFolder, createFolderAction], ); const noteMenuItems = useMemo<ContextMenuItem[]>(() => { @@ -4237,19 +4253,23 @@ const NoteLeaf = memo(function NoteLeaf({ > {showSidebarChevrons && <span className="h-5 w-5 shrink-0" />} <SidebarGlyph active={strongActive} rowActive={active || selected}> - <svg - width="14" - height="14" - viewBox="0 0 24 24" - fill="none" - stroke="currentColor" - strokeWidth="1.75" - strokeLinecap="round" - strokeLinejoin="round" - > - <path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V9Z" /> - <path d="M14 3v6h6" /> - </svg> + {isExcalidrawPath(note.path) ? ( + <ExcalidrawIcon width={14} height={14} /> + ) : ( + <svg + width="14" + height="14" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + strokeWidth="1.75" + strokeLinecap="round" + strokeLinejoin="round" + > + <path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V9Z" /> + <path d="M14 3v6h6" /> + </svg> + )} </SidebarGlyph> <span className="flex-1 truncate">{note.title}</span> {note.isSymlink && ( diff --git a/packages/app-core/src/components/VimNav.tsx b/packages/app-core/src/components/VimNav.tsx index ba98a2b6..c8babe22 100644 --- a/packages/app-core/src/components/VimNav.tsx +++ b/packages/app-core/src/components/VimNav.tsx @@ -778,6 +778,29 @@ export function VimNav(): JSX.Element | null { return } + // A focused breadcrumb folder crumb (e.g. reached via hint mode) owns the + // context-menu key — open *its* create menu, not the sidebar item's. + { + const activeCrumb = document.activeElement as HTMLElement | null + if ( + activeCrumb?.hasAttribute('data-crumb-menu') && + (matchesSequenceToken(e, overrides, 'nav.contextMenu') || wantsNativeContextMenuKey(e)) + ) { + e.preventDefault() + e.stopImmediatePropagation() + const rect = activeCrumb.getBoundingClientRect() + activeCrumb.dispatchEvent( + new MouseEvent('contextmenu', { + bubbles: true, + cancelable: true, + clientX: Math.round(rect.left), + clientY: Math.round(rect.bottom + 2) + }) + ) + return + } + } + // ------- Sidebar navigation (explicit) ----------------------------- // When focusedPanel is 'sidebar', always handle here — even if the // editor still holds stale DOM focus from a previous interaction. diff --git a/packages/app-core/src/components/icons.tsx b/packages/app-core/src/components/icons.tsx index c5b9b956..0f58c1c1 100644 --- a/packages/app-core/src/components/icons.tsx +++ b/packages/app-core/src/components/icons.tsx @@ -44,6 +44,19 @@ export const DatabaseIcon = (p: IconProps): JSX.Element => ( </I> ) +export const ExcalidrawIcon = (p: IconProps): JSX.Element => ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="16" + height="16" + viewBox="0 0 107 101" + fill="currentColor" + {...p} + > + <path fillRule="nonzero" transform="matrix(1 0 0 1 -26.41 -29.49)" d="M119.81 105.98a.549.549 0 0 0-.53-.12c-4.19-6.19-9.52-12.06-14.68-17.73l-.85-.93c0-.11-.05-.21-.12-.3a.548.548 0 0 0-.34-.2l-.17-.18-.12-.09c-.15-.32-.53-.56-.95-.35-1.58.81-3 1.97-4.4 3.04-1.87 1.43-3.7 2.92-5.42 4.52-.7.65-1.39 1.33-1.97 2.09-.28.37-.07.72.27.87-1.22 1.2-2.45 2.45-3.68 3.74-.11.12-.17.28-.16.44.01.16.09.31.22.41l2.16 1.65s.01.03.03.04c3.09 3.05 8.51 7.28 14.25 11.76.85.67 1.71 1.34 2.57 2.01.39.47.76.94 1.12 1.4.19.25.55.3.8.11.13.1.26.21.39.31a.57.57 0 0 0 .8-.1c.07-.09.1-.2.11-.31.04 0 .07.03.1.03.15 0 .31-.06.42-.18l10.18-11.12a.56.56 0 0 0-.04-.8l.01-.01Zm-29.23-3.85c.07.09.14.17.21.25 1.16.98 2.4 2.04 3.66 3.12l-5.12-3.91s-.32-.22-.52-.36c-.11-.08-.21-.16-.31-.24l-.38-.32s.07-.07.1-.11l.35-.35c1.72-1.74 4.67-4.64 6.19-6.06-1.61 1.62-4.87 6.37-4.17 7.98h-.01Zm17.53 13.81-4.22-3.22c-1.65-1.71-3.43-3.4-5.24-5.03 2.28 1.76 4.23 3.25 4.52 3.51 2.21 1.97 2.11 1.61 3.63 2.91l1.83 1.33c-.18.16-.36.33-.53.49l.01.01Zm1.06.81-.08-.06c.16-.13.33-.25.49-.38l-.4.44h-.01ZM42.24 51.45c.14.72.27 1.43.4 2.11.69 3.7 1.33 7.03 2.55 9.56l.48 1.92c.19.73.46 1.64.71 1.83 2.85 2.52 7.22 6.28 11.89 9.82.21.16.5.15.7-.01.01.02.03.03.04.04.11.1.24.15.38.15.16 0 .31-.06.42-.19 5.98-6.65 10.43-12.12 13.6-16.7.2-.25.3-.54.29-.84.2-.24.41-.48.6-.68a.558.558 0 0 0-.1-.86.578.578 0 0 0-.17-.36c-1.39-1.34-2.42-2.31-3.46-3.28-1.84-1.72-3.74-3.5-7.77-7.51-.02-.02-.05-.04-.07-.06a.555.555 0 0 0-.22-.14c-1.11-.39-3.39-.78-6.26-1.28-4.22-.72-10-1.72-15.2-3.27h-.04v-.01s-.02 0-.03.02h-.01l.04-.02s-.31.01-.37.04c-.08.04-.14.09-.19.15-.05.06-.09.12-.47.2-.38.08.08 0 .11 0h-.11v.03c.07.34.05.58.16.97-.02.1.21 1.02.24 1.11l1.83 7.26h.03Zm30.95 6.54s-.03.04-.04.05l-.64-.71c.22.21.44.42.68.66Zm-7.09 9.39s-.07.08-.1.12l-.02-.02c.04-.03.08-.07.13-.1h-.01Zm-7.07 8.47Zm3.02-28.57c.35.35 1.74 1.65 2.06 1.97-1.45-.66-5.06-2.34-6.74-2.88 1.65.29 3.93.66 4.68.91Zm-19.18-2.77c.84 1.44 1.5 6.49 2.16 11.4-.37-1.58-.69-3.12-.99-4.6-.52-2.56-1-4.85-1.67-6.88.14.01.31.03.49.05 0 .01 0 .02.02.03h-.01Zm-.29-1.21c-.23-.02-.44-.04-.62-.05-.02-.04-.03-.08-.04-.12l.66.18v-.01Zm-2.22.45v-.02.02ZM118.9 42.57c.04-.23-1.1-1.24-.74-1.26.85-.04.86-1.35 0-1.31-1.13.06-2.27.32-3.37.53-1.98.37-3.95.78-5.92 1.21-4.39.94-8.77 1.93-13.1 3.11-1.36.37-2.86.7-4.11 1.36-.42.22-.4.67-.17.95-.09.05-.18.08-.28.09-.37.07-.74.13-1.11.19a.566.566 0 0 0-.39.86c-2.32 3.1-4.96 6.44-7.82 9.95-2.81 3.21-5.73 6.63-8.72 10.14-9.41 11.06-20.08 23.6-31.9 34.64-.23.21-.24.57-.03.8.05.06.12.1.19.13-.16.15-.32.3-.48.44-.1.09-.14.2-.16.32-.08.08-.16.17-.23.25-.21.23-.2.59.03.8.23.21.59.2.8-.03.04-.04.08-.09.12-.13a.84.84 0 0 1 1.22 0c.69.74 1.34 1.44 1.95 2.09l-1.38-1.15a.57.57 0 0 0-.8.07c-.2.24-.17.6.07.8l14.82 12.43c.11.09.24.13.37.13.15 0 .29-.06.4-.17l.36-.36a.56.56 0 0 0 .63-.12c20.09-20.18 36.27-35.43 54.8-49.06.17-.12.25-.32.23-.51a.57.57 0 0 0 .48-.39c3.42-10.46 4.08-19.72 4.28-24.27 0-.03.01-.05.02-.07.02-.05.03-.1.04-.14.03-.11.05-.19.05-.19.26-.78.17-1.53-.15-2.15v.02ZM82.98 58.94c.9-1.03 1.79-2.04 2.67-3.02-5.76 7.58-15.3 19.26-28.81 33.14 9.2-10.18 18.47-20.73 26.14-30.12Zm-32.55 52.81-.03-.03c.11.02.19.04.2.04a.47.47 0 0 0-.17 0v-.01Zm6.9 6.42-.05-.04.03-.03c.02 0 .03.02.04.02 0 .02-.02.03-.03.05h.01Zm8.36-7.21 1.38-1.44c.01.01.02.03.03.05-.47.46-.94.93-1.42 1.39h.01Zm2.24-2.21c.26-.3.56-.65.87-1.02.01-.01.02-.03.04-.04 3.29-3.39 6.68-6.82 10.18-10.25.02-.02.05-.04.07-.06.86-.66 1.82-1.39 2.72-2.08-4.52 4.32-9.11 8.78-13.88 13.46v-.01Zm21.65-55.88c-1.86 2.42-3.9 5.56-5.63 8.07-5.46 7.91-23.04 27.28-23.43 27.65-2.71 2.62-10.88 10.46-16.09 15.37-.14.13-.25.24-.34.35a.794.794 0 0 1 .03-1.13c24.82-23.4 39.88-42.89 46-51.38-.13.33-.24.69-.55 1.09l.01-.02Zm16.51 7.1-.01.02c0-.02-.02-.07.01-.02Zm-.91-5.13Zm-5.89 9.45c-2.26-1.31-3.32-3.27-2.71-5.25l.19-.66c.08-.19.17-.38.28-.57.59-.98 1.49-1.85 2.52-2.36.05-.02.1-.03.15-.04a.795.795 0 0 1-.04-.43c.05-.31.25-.58.66-.58.67 0 2.75.62 3.54 1.3.24.19.47.4.68.63.3.35.74.92.96 1.33.13.06.23.62.38.91.14.46.2.93.18 1.4 0 .02 0 .02.01.03-.03.07 0 .37-.04.4-.1.72-.36 1.43-.75 2.05-.04.05-.07.11-.11.16 0 .01-.02.02-.03.04-.3.43-.65.83-1.08 1.13-1.26.89-2.73 1.16-4.2.79a6.33 6.33 0 0 1-.57-.25l-.02-.03Zm16.27-1.63c-.49 2.05-1.09 4.19-1.8 6.38-.03.08-.03.16-.03.23-.1.01-.19.05-.27.11-4.44 3.26-8.73 6.62-12.98 10.11 3.67-3.32 7.39-6.62 11.23-9.95a6.409 6.409 0 0 0 2.11-3.74l.56-3.37.03-.1c.25-.71 1.34-.4 1.17.33h-.02Z" /> + </svg> +) + export const InboxIcon = (p: IconProps): JSX.Element => ( <I {...p}> <path d="M4 13v5a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5" /> diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index 2ae76914..fa2fe8bb 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -23,6 +23,7 @@ import type { WorkspaceMode } from '@shared/ipc' import type { VaultTask } from '@shared/tasks' +import { isExcalidrawPath } from '@shared/excalidraw' import { TASKS_TAB_PATH, isTasksTabPath } from '@shared/tasks' import type { DatabaseDoc, DatabaseSidecar } from '@shared/databases' import { @@ -1762,6 +1763,7 @@ interface Store { subpath?: string, options?: { focusTitle?: boolean; title?: string } ) => Promise<void> + createDrawingAndOpen: (folder: NoteFolder, subpath?: string) => Promise<void> /** * Create a note after asking where to put it: a destination prompt that * defaults to `initialPath` (empty = vault root), so the user can press Enter @@ -3715,8 +3717,11 @@ export const useStore = create<Store>((set, get) => { await get().refreshNotes() return } - const pathIsMarkdown = ev.path.toLowerCase().endsWith('.md') - if (ev.scope !== 'vault-settings' && !pathIsMarkdown) { + // Excalidraw drawings are notes (they live in the notes tree), so treat + // their change events as note events, not asset events. + const pathIsNote = + ev.path.toLowerCase().endsWith('.md') || isExcalidrawPath(ev.path) + if (ev.scope !== 'vault-settings' && !pathIsNote) { await get().refreshAssets() return } @@ -4019,6 +4024,17 @@ export const useStore = create<Store>((set, get) => { } }, + createDrawingAndOpen: async (folder, subpath = '') => { + try { + const meta = await window.zen.createExcalidraw(folder, subpath) + await get().refreshNotes() + set({ view: { kind: 'folder', folder, subpath } }) + await get().selectNote(meta.path) + } catch (err) { + console.error('createExcalidraw failed', err) + } + }, + createNoteInChosenFolder: async (opts) => { const state = get() const entered = await promptApp( diff --git a/packages/bridge-contract/src/bridge.ts b/packages/bridge-contract/src/bridge.ts index d4666624..b881cdab 100644 --- a/packages/bridge-contract/src/bridge.ts +++ b/packages/bridge-contract/src/bridge.ts @@ -151,6 +151,8 @@ export interface ZenBridge { writeNote(relPath: string, body: string): Promise<NoteMeta> appendToNote(relPath: string, body: string, position: 'start' | 'end'): Promise<NoteMeta> createNote(folder: NoteFolder, title?: string, subpath?: string): Promise<NoteMeta> + /** Create a new `.excalidraw` drawing seeded with an empty scene. */ + createExcalidraw(folder: NoteFolder, subpath?: string, title?: string): Promise<NoteMeta> renameNote(relPath: string, nextTitle: string): Promise<NoteMeta> deleteNote(relPath: string): Promise<void> moveToTrash(relPath: string): Promise<NoteMeta> diff --git a/packages/bridge-contract/src/ipc.ts b/packages/bridge-contract/src/ipc.ts index e4cf20d0..42258695 100644 --- a/packages/bridge-contract/src/ipc.ts +++ b/packages/bridge-contract/src/ipc.ts @@ -38,6 +38,7 @@ export const IPC = { VAULT_WRITE_NOTE: 'vault:write-note', VAULT_APPEND_NOTE: 'vault:append-note', VAULT_CREATE_NOTE: 'vault:create-note', + VAULT_CREATE_EXCALIDRAW: 'vault:create-excalidraw', VAULT_RENAME_NOTE: 'vault:rename-note', VAULT_DELETE_NOTE: 'vault:delete-note', VAULT_MOVE_TO_TRASH: 'vault:move-to-trash', diff --git a/packages/shared-domain/src/excalidraw.ts b/packages/shared-domain/src/excalidraw.ts new file mode 100644 index 00000000..bd2f47d1 --- /dev/null +++ b/packages/shared-domain/src/excalidraw.ts @@ -0,0 +1,57 @@ +// Excalidraw drawings are stored as standalone `.excalidraw` files (the native +// Excalidraw JSON scene format). They are a first-class file type alongside +// Markdown notes and `.base` databases: listed in the sidebar with their own +// icon, opened in a dedicated editor tab, and saved back as JSON. + +export const EXCALIDRAW_EXT = '.excalidraw' + +export function isExcalidrawPath(path: string | null | undefined): boolean { + return typeof path === 'string' && path.toLowerCase().endsWith(EXCALIDRAW_EXT) +} + +/** Display title for a drawing (filename without the `.excalidraw` extension). */ +export function excalidrawTitleFromPath(path: string): string { + const base = path.split('/').pop() ?? path + return base.toLowerCase().endsWith(EXCALIDRAW_EXT) + ? base.slice(0, -EXCALIDRAW_EXT.length) + : base +} + +/** The on-disk Excalidraw scene shape (a subset of the official format). */ +export interface ExcalidrawDocument { + type: 'excalidraw' + version: number + source: string + elements: unknown[] + appState: Record<string, unknown> + files: Record<string, unknown> +} + +export function emptyExcalidrawDocument(): ExcalidrawDocument { + return { + type: 'excalidraw', + version: 2, + source: 'zennotes', + elements: [], + appState: {}, + files: {} + } +} + +/** Parse on-disk JSON into a scene, falling back to an empty doc when invalid. */ +export function parseExcalidrawDocument(raw: string): ExcalidrawDocument { + try { + const parsed = JSON.parse(raw) as Partial<ExcalidrawDocument> + return { + ...emptyExcalidrawDocument(), + ...parsed, + type: 'excalidraw', + elements: Array.isArray(parsed.elements) ? parsed.elements : [], + appState: + parsed.appState && typeof parsed.appState === 'object' ? parsed.appState : {}, + files: parsed.files && typeof parsed.files === 'object' ? parsed.files : {} + } + } catch { + return emptyExcalidrawDocument() + } +} From 704ad859a1ff4e866ff4d142edc0e8a3bde8014f Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Wed, 17 Jun 2026 14:28:34 -0500 Subject: [PATCH 51/66] feat(server): Excalidraw drawings on the self-hosted/web build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings the desktop's `.excalidraw` support to the Go server so drawings work on the self-hosted and web client: - ListNotes includes `.excalidraw` files (shown in the sidebar); ListAssets excludes them so they aren't treated as attachments. - readMeta/ReadNote skip Markdown parsing for drawings — a scene's hex color (e.g. #1971c2) no longer registers as a #tag, and the raw JSON never leaks into the excerpt. - Rename, move, duplicate, and move-between-folders preserve the `.excalidraw` extension instead of turning a drawing into a .md note. - New CreateExcalidraw method + POST /excalidraw/create endpoint, backing the web bridge's existing createExcalidraw call (which previously 404'd). - Root-mode detection recognizes a vault of only drawings; full-text search stays .md-only (drawings remain findable by title). Tests: vault listing/create/rename-move parity + an end-to-end HTTP test (login → POST /api/excalidraw/create → appears in /api/notes). --- .../internal/httpserver/excalidraw_test.go | 71 ++++++++++ apps/server/internal/httpserver/server.go | 19 +++ apps/server/internal/vault/vault.go | 129 +++++++++++++----- apps/server/internal/vault/vault_test.go | 105 ++++++++++++++ 4 files changed, 293 insertions(+), 31 deletions(-) create mode 100644 apps/server/internal/httpserver/excalidraw_test.go diff --git a/apps/server/internal/httpserver/excalidraw_test.go b/apps/server/internal/httpserver/excalidraw_test.go new file mode 100644 index 00000000..cf2d8ca9 --- /dev/null +++ b/apps/server/internal/httpserver/excalidraw_test.go @@ -0,0 +1,71 @@ +package httpserver + +import ( + "bytes" + "encoding/json" + "net/http" + "strings" + "testing" + + "github.com/ZenNotes/zennotes/apps/server/internal/config" +) + +// TestCreateExcalidrawEndpoint exercises the full HTTP wiring: log in, POST +// /api/excalidraw/create, and confirm the drawing comes back as a `.excalidraw` +// note that then shows up in /api/notes (and not in /api/assets). +func TestCreateExcalidrawEndpoint(t *testing.T) { + root := t.TempDir() + server, _ := newTestServer(t, config.Config{ + VaultPath: root, + DefaultVaultPath: root, + Bind: "127.0.0.1:7878", + AuthToken: "secret-token", + BrowseRoots: []string{root}, + }) + jar := loginAndJar(t, server, "secret-token") + client := &http.Client{Jar: jar} + + body, _ := json.Marshal(map[string]string{"folder": "inbox", "title": "My Sketch"}) + resp, err := client.Post(server.URL+"/api/excalidraw/create", "application/json", bytes.NewReader(body)) + if err != nil { + t.Fatalf("POST /api/excalidraw/create: %v", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("create status: %d", resp.StatusCode) + } + var created struct { + Path string `json:"path"` + Title string `json:"title"` + } + if err := json.NewDecoder(resp.Body).Decode(&created); err != nil { + t.Fatalf("decode create response: %v", err) + } + if !strings.HasSuffix(created.Path, ".excalidraw") { + t.Fatalf("created path = %q, want a .excalidraw file", created.Path) + } + if created.Title != "My Sketch" { + t.Errorf("created title = %q, want My Sketch", created.Title) + } + + listResp, err := client.Get(server.URL + "/api/notes") + if err != nil { + t.Fatalf("GET /api/notes: %v", err) + } + defer listResp.Body.Close() + var notes []struct { + Path string `json:"path"` + } + if err := json.NewDecoder(listResp.Body).Decode(¬es); err != nil { + t.Fatalf("decode notes: %v", err) + } + found := false + for _, n := range notes { + if n.Path == created.Path { + found = true + } + } + if !found { + t.Errorf("created drawing %q not returned by /api/notes", created.Path) + } +} diff --git a/apps/server/internal/httpserver/server.go b/apps/server/internal/httpserver/server.go index 4beb8c4d..bd822a0c 100644 --- a/apps/server/internal/httpserver/server.go +++ b/apps/server/internal/httpserver/server.go @@ -181,6 +181,7 @@ func (s *Server) registerProtectedRoutes(r chi.Router) { r.Post("/comments/write", s.writeComments) r.Post("/notes/write", s.writeNote) r.Post("/notes/create", s.createNote) + r.Post("/excalidraw/create", s.createExcalidraw) r.Post("/notes/rename", s.renameNote) r.Post("/notes/delete", s.deleteNote) r.Post("/notes/trash", s.trashNote) @@ -592,6 +593,24 @@ func (s *Server) createNote(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, meta) } +func (s *Server) createExcalidraw(w http.ResponseWriter, r *http.Request) { + var req struct { + Folder vault.NoteFolder `json:"folder"` + Title string `json:"title"` + Subpath string `json:"subpath"` + } + if err := readJSON(r, &req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + meta, err := s.currentVault().CreateExcalidraw(req.Folder, req.Title, req.Subpath) + if err != nil { + writeError(w, err) + return + } + writeJSON(w, http.StatusOK, meta) +} + func (s *Server) renameNote(w http.ResponseWriter, r *http.Request) { var req struct { Path string `json:"path"` diff --git a/apps/server/internal/vault/vault.go b/apps/server/internal/vault/vault.go index 0a2d85ba..a4a8e03f 100644 --- a/apps/server/internal/vault/vault.go +++ b/apps/server/internal/vault/vault.go @@ -42,6 +42,37 @@ func isFormDirName(name string) bool { return strings.HasSuffix(strings.ToLower(name), formDirSuffix) } +// excalidrawExt marks a standalone Excalidraw drawing — the native Excalidraw +// JSON scene format. Drawings are a first-class file type alongside Markdown +// notes: listed in the sidebar (not as assets) and opened in a dedicated editor. +const excalidrawExt = ".excalidraw" + +// isExcalidrawName reports whether a filename is an Excalidraw drawing. +func isExcalidrawName(name string) bool { + return strings.EqualFold(filepath.Ext(name), excalidrawExt) +} + +// noteExt returns the on-disk extension for a note-like file, preserving +// `.excalidraw` for drawings and defaulting to `.md` otherwise. Rename/move/ +// duplicate use it so a drawing never silently becomes a Markdown note. +func noteExt(name string) string { + if isExcalidrawName(name) { + return excalidrawExt + } + return ".md" +} + +// emptyExcalidrawJSON mirrors emptyExcalidrawDocument() in +// packages/shared-domain/src/excalidraw.ts (JSON.stringify, 2-space indent). +const emptyExcalidrawJSON = `{ + "type": "excalidraw", + "version": 2, + "source": "zennotes", + "elements": [], + "appState": {}, + "files": {} +}` + // ErrAssetTooLarge is returned when an asset upload exceeds the // vault's MaxAssetBytes limit. var ErrAssetTooLarge = errors.New("asset exceeds maximum size") @@ -487,7 +518,7 @@ func (v *Vault) inferPrimaryNotesLocation() PrimaryNotesLocation { if _, reserved := reservedRootNames[name]; reserved { continue } - if entry.IsDir() || strings.EqualFold(filepath.Ext(name), ".md") { + if entry.IsDir() || strings.EqualFold(filepath.Ext(name), ".md") || isExcalidrawName(name) { return PrimaryNotesRoot } } @@ -787,7 +818,7 @@ func (v *Vault) ListNotes() ([]NoteMeta, error) { } } } - if !strings.EqualFold(filepath.Ext(d.Name()), ".md") { + if !strings.EqualFold(filepath.Ext(d.Name()), ".md") && !isExcalidrawName(d.Name()) { return nil } files = append(files, noteFile{folder: folder, path: path}) @@ -946,7 +977,7 @@ func (v *Vault) ListAssets() ([]AssetMeta, error) { } continue } - if !entry.Type().IsRegular() || strings.EqualFold(filepath.Ext(name), ".md") { + if !entry.Type().IsRegular() || strings.EqualFold(filepath.Ext(name), ".md") || isExcalidrawName(name) { continue } info, err := entry.Info() @@ -1005,6 +1036,30 @@ func kindForExt(ext string) string { // --- Read / Write --- +// buildNoteMeta assembles NoteMeta for a note-like file. Excalidraw drawings +// store JSON, not Markdown, so their tags/wikilinks/excerpt are skipped — a hex +// color like "#1971c2" in the scene must not register as a #tag. +func buildNoteMeta(relPosix, title string, folder NoteFolder, info os.FileInfo, bodyStr string) NoteMeta { + meta := NoteMeta{ + Path: relPosix, + Title: title, + Folder: folder, + CreatedAt: info.ModTime().UnixMilli(), + UpdatedAt: info.ModTime().UnixMilli(), + Size: info.Size(), + Tags: []string{}, + Wikilinks: []string{}, + } + if isExcalidrawName(relPosix) { + return meta + } + meta.Tags = ExtractTags(bodyStr) + meta.Wikilinks = ExtractWikilinks(bodyStr) + meta.HasAttachments = BodyHasLocalAsset(bodyStr) + meta.Excerpt = BuildExcerpt(bodyStr) + return meta +} + func (v *Vault) readMeta(folder NoteFolder, abs string) (NoteMeta, error) { info, err := os.Stat(abs) if err != nil { @@ -1037,18 +1092,7 @@ func (v *Vault) readMeta(folder NoteFolder, abs string) (NoteMeta, error) { title := strings.TrimSuffix(filepath.Base(abs), filepath.Ext(abs)) - meta := NoteMeta{ - Path: relPosix, - Title: title, - Folder: folder, - CreatedAt: info.ModTime().UnixMilli(), - UpdatedAt: info.ModTime().UnixMilli(), - Size: info.Size(), - Tags: ExtractTags(bodyStr), - Wikilinks: ExtractWikilinks(bodyStr), - HasAttachments: BodyHasLocalAsset(bodyStr), - Excerpt: BuildExcerpt(bodyStr), - } + meta := buildNoteMeta(relPosix, title, folder, info, bodyStr) v.metaCacheMu.Lock() v.metaCache[abs] = noteMetaCacheEntry{ mtimeMs: statMtimeMs, @@ -1078,18 +1122,7 @@ func (v *Vault) ReadNote(rel string) (NoteContent, error) { bodyStr := string(body) rel = filepath.ToSlash(rel) title := strings.TrimSuffix(filepath.Base(abs), filepath.Ext(abs)) - meta := NoteMeta{ - Path: rel, - Title: title, - Folder: folder, - CreatedAt: info.ModTime().UnixMilli(), - UpdatedAt: info.ModTime().UnixMilli(), - Size: info.Size(), - Tags: ExtractTags(bodyStr), - Wikilinks: ExtractWikilinks(bodyStr), - HasAttachments: BodyHasLocalAsset(bodyStr), - Excerpt: BuildExcerpt(bodyStr), - } + meta := buildNoteMeta(rel, title, folder, info, bodyStr) return NoteContent{NoteMeta: meta, Body: bodyStr}, nil } @@ -1369,6 +1402,40 @@ func (v *Vault) CreateNote(folder NoteFolder, title, subpath string) (NoteMeta, return v.readMeta(folder, abs) } +// CreateExcalidraw writes a new empty `.excalidraw` drawing under folder/subpath +// and returns its meta. Mirrors CreateNote but seeds an empty Excalidraw scene. +func (v *Vault) CreateExcalidraw(folder NoteFolder, title, subpath string) (NoteMeta, error) { + v.mu.Lock() + defer v.mu.Unlock() + if !IsValidFolder(folder) { + return NoteMeta{}, fmt.Errorf("invalid folder: %s", folder) + } + if title == "" { + title = defaultTitle() + } + title = sanitizeFileStem(title) + dir, err := v.folderRoot(folder) + if err != nil { + return NoteMeta{}, err + } + if subpath != "" { + sub, err := SafeJoin(dir, subpath) + if err != nil { + return NoteMeta{}, err + } + dir = sub + } + if err := os.MkdirAll(dir, v.dirMode); err != nil { + return NoteMeta{}, err + } + abs := uniquePath(dir, title, excalidrawExt) + if err := os.WriteFile(abs, []byte(emptyExcalidrawJSON), v.fileMode); err != nil { + return NoteMeta{}, err + } + v.invalidateTextSearchCache() + return v.readMeta(folder, abs) +} + func (v *Vault) RenameNote(rel, nextTitle string) (NoteMeta, error) { // Snapshot the vault before the rename (ListNotes takes its own read lock) // so inbound [[wikilinks]] still resolve to this note under its current name. @@ -1397,7 +1464,7 @@ func (v *Vault) renameNoteFile(rel, nextTitle string) (NoteMeta, error) { return NoteMeta{}, errors.New("empty title") } dir := filepath.Dir(abs) - newAbs := uniquePath(dir, nextTitle, ".md") + newAbs := uniquePath(dir, nextTitle, noteExt(abs)) if err := os.Rename(abs, newAbs); err != nil { return NoteMeta{}, err } @@ -1517,7 +1584,7 @@ func (v *Vault) moveBetweenFolders(rel string, target NoteFolder) (NoteMeta, err if err := os.MkdirAll(destDir, v.dirMode); err != nil { return NoteMeta{}, err } - newAbs := uniquePath(destDir, title, ".md") + newAbs := uniquePath(destDir, title, noteExt(abs)) if err := os.Rename(abs, newAbs); err != nil { return NoteMeta{}, err } @@ -1557,7 +1624,7 @@ func (v *Vault) DuplicateNote(rel string) (NoteMeta, error) { } folder, _ := v.folderOf(abs) title := strings.TrimSuffix(filepath.Base(abs), filepath.Ext(abs)) + " copy" - newAbs := uniquePath(filepath.Dir(abs), sanitizeFileStem(title), ".md") + newAbs := uniquePath(filepath.Dir(abs), sanitizeFileStem(title), noteExt(abs)) if err := copyFile(abs, newAbs, v.fileMode); err != nil { return NoteMeta{}, err } @@ -1597,7 +1664,7 @@ func (v *Vault) MoveNote(rel string, target NoteFolder, targetSubpath string) (N return NoteMeta{}, err } title := strings.TrimSuffix(filepath.Base(abs), filepath.Ext(abs)) - newAbs := uniquePath(destDir, title, ".md") + newAbs := uniquePath(destDir, title, noteExt(abs)) if err := os.Rename(abs, newAbs); err != nil { return NoteMeta{}, err } diff --git a/apps/server/internal/vault/vault_test.go b/apps/server/internal/vault/vault_test.go index 4014d0a1..634558a4 100644 --- a/apps/server/internal/vault/vault_test.go +++ b/apps/server/internal/vault/vault_test.go @@ -762,3 +762,108 @@ func hasNotePath(notes []NoteMeta, path string) bool { } return false } + +func TestExcalidrawListedAsNoteNotAsset(t *testing.T) { + root := t.TempDir() + v, err := New(root, Options{}) + if err != nil { + t.Fatal(err) + } + // A drawing whose JSON body contains a hex color (#1971c2) that must NOT + // be mistaken for a #tag, plus an image that should stay an asset. + scene := `{"type":"excalidraw","version":2,"elements":[{"strokeColor":"#1971c2"}],"appState":{},"files":{}}` + if err := os.WriteFile(filepath.Join(root, "inbox", "Sketch.excalidraw"), []byte(scene), 0o600); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(root, "assets"), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(root, "assets", "pic.png"), []byte("PNG"), 0o600); err != nil { + t.Fatal(err) + } + + notes, err := v.ListNotes() + if err != nil { + t.Fatal(err) + } + if !hasNotePath(notes, "inbox/Sketch.excalidraw") { + t.Error("ListNotes dropped the .excalidraw drawing") + } + for _, n := range notes { + if n.Path == "inbox/Sketch.excalidraw" { + if n.Title != "Sketch" { + t.Errorf("drawing title = %q, want Sketch", n.Title) + } + if len(n.Tags) != 0 { + t.Errorf("drawing leaked tags from JSON hex colors: %v", n.Tags) + } + } + } + + assets, err := v.ListAssets() + if err != nil { + t.Fatal(err) + } + sawImage := false + for _, a := range assets { + if strings.HasSuffix(a.Path, ".excalidraw") { + t.Errorf("ListAssets leaked a drawing: %s", a.Path) + } + if a.Path == "assets/pic.png" { + sawImage = true + } + } + if !sawImage { + t.Error("ListAssets dropped a real asset") + } +} + +func TestCreateExcalidrawSeedsEmptyScene(t *testing.T) { + root := t.TempDir() + v, err := New(root, Options{}) + if err != nil { + t.Fatal(err) + } + meta, err := v.CreateExcalidraw(FolderInbox, "My Drawing", "") + if err != nil { + t.Fatal(err) + } + if !strings.HasSuffix(meta.Path, ".excalidraw") { + t.Errorf("created path = %q, want a .excalidraw file", meta.Path) + } + content, err := v.ReadNote(meta.Path) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(content.Body, `"type": "excalidraw"`) { + t.Errorf("seeded scene missing excalidraw type: %s", content.Body) + } +} + +func TestRenameAndMovePreserveExcalidrawExt(t *testing.T) { + root := t.TempDir() + v, err := New(root, Options{}) + if err != nil { + t.Fatal(err) + } + created, err := v.CreateExcalidraw(FolderInbox, "Diagram", "") + if err != nil { + t.Fatal(err) + } + + renamed, err := v.RenameNote(created.Path, "Flowchart") + if err != nil { + t.Fatal(err) + } + if !strings.HasSuffix(renamed.Path, ".excalidraw") { + t.Errorf("rename dropped the extension: %q", renamed.Path) + } + + moved, err := v.MoveNote(renamed.Path, FolderArchive, "") + if err != nil { + t.Fatal(err) + } + if !strings.HasSuffix(moved.Path, ".excalidraw") { + t.Errorf("move dropped the extension: %q", moved.Path) + } +} From 180be75e9fc4bb116801c5059bccdc4e36857865 Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Wed, 17 Jun 2026 15:33:30 -0500 Subject: [PATCH 52/66] feat: Favorites + Notion-style sidebar reorganization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Favorites — pin notes and folders to the top of the sidebar: - Right-click a note or subfolder → Add/Remove from Favorites, or press the Vim leader binding `<leader> l s` on the active note (which-key + remappable). - A FAVORITES section lists them with their real icons; click or Vim-Enter to open. Stored per-vault in VaultSettings.favorites (note path or folder key). - Favorites survive note rename/move and folder rename, and are pruned on folder delete (rewrite logic lives in the store; the Go server carries favorites through folder ops so the web client persists them too). Sidebar reorganized into clear labeled sections (Notion-style): - FAVORITES (top), QUICK ACCESS (Tasks / Quick Notes / Daily+Weekly), NOTES (the tree), TAGS (flows after the notes), and SYSTEM (Archive / Trash / Assets) pinned to the bottom. - The header `+` now opens a create menu for every supported type (note, drawing, template, database, folder) instead of only a note. - Fix: Vim j/k now stops on Daily/Weekly note rows (they were missing navigation indices); Assets is Vim-activatable; removed stray top margin that made the gap above Daily Notes uneven; dropped the Tags chevron. --- apps/desktop/src/main/vault.ts | 25 +- apps/server/internal/vault/types.go | 3 + apps/server/internal/vault/vault.go | 28 ++ apps/server/internal/vault/vault_test.go | 57 +++ packages/app-core/src/components/Sidebar.tsx | 402 ++++++++++++------ packages/app-core/src/components/VimNav.tsx | 14 + packages/app-core/src/lib/keymaps.ts | 12 + .../app-core/src/lib/vault-layout.test.ts | 119 +++++- packages/app-core/src/lib/vault-layout.ts | 107 ++++- packages/app-core/src/store.test.ts | 12 +- packages/app-core/src/store.ts | 66 +++ packages/bridge-contract/src/ipc.ts | 11 +- 12 files changed, 699 insertions(+), 157 deletions(-) diff --git a/apps/desktop/src/main/vault.ts b/apps/desktop/src/main/vault.ts index 541b9978..7f21681d 100644 --- a/apps/desktop/src/main/vault.ts +++ b/apps/desktop/src/main/vault.ts @@ -194,7 +194,8 @@ const DEFAULT_VAULT_SETTINGS: VaultSettings = { locale: DEFAULT_WEEKLY_NOTE_LOCALE }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } interface VaultTextSearchCandidate { @@ -747,7 +748,8 @@ function cloneVaultSettings(settings: VaultSettings): VaultSettings { templateId: settings.weeklyNotes.templateId }, folderIcons: { ...settings.folderIcons }, - folderColors: { ...settings.folderColors } + folderColors: { ...settings.folderColors }, + favorites: [...settings.favorites] } } @@ -857,7 +859,8 @@ function normalizeVaultSettings( locale: DEFAULT_WEEKLY_NOTE_LOCALE }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } } const candidate = value as { @@ -880,6 +883,7 @@ function normalizeVaultSettings( } | null folderIcons?: Record<string, unknown> | null folderColors?: Record<string, unknown> | null + favorites?: unknown } const folderIcons: Record<string, FolderIconId> = {} if (candidate.folderIcons && typeof candidate.folderIcons === 'object') { @@ -915,10 +919,23 @@ function normalizeVaultSettings( templateId: normalizeTemplateId(candidate.weeklyNotes?.templateId) }, folderIcons, - folderColors: normalizeFolderColors(candidate.folderColors) + folderColors: normalizeFolderColors(candidate.folderColors), + favorites: normalizeFavorites(candidate.favorites) } } +function normalizeFavorites(value: unknown): string[] { + if (!Array.isArray(value)) return [] + const out: string[] = [] + const seen = new Set<string>() + for (const entry of value) { + if (typeof entry !== 'string' || !entry || seen.has(entry)) continue + seen.add(entry) + out.push(entry) + } + return out +} + function folderIconKey(folder: NoteFolder, subpath: string): string { return `${folder}:${subpath}` } diff --git a/apps/server/internal/vault/types.go b/apps/server/internal/vault/types.go index 9b1d7e63..b6439af8 100644 --- a/apps/server/internal/vault/types.go +++ b/apps/server/internal/vault/types.go @@ -84,6 +84,9 @@ type VaultSettings struct { DailyNotes DailyNotesSettings `json:"dailyNotes"` WeeklyNotes WeeklyNotesSettings `json:"weeklyNotes"` FolderIcons map[string]FolderIconID `json:"folderIcons"` + // Favorites are note paths or `folder:subpath` keys pinned to the top of + // the sidebar. Persisted so the web client's favorites survive a round-trip. + Favorites []string `json:"favorites"` } // NoteMeta — vault-relative note metadata. Mirrors shared/ipc.ts NoteMeta. diff --git a/apps/server/internal/vault/vault.go b/apps/server/internal/vault/vault.go index a4a8e03f..bb332f20 100644 --- a/apps/server/internal/vault/vault.go +++ b/apps/server/internal/vault/vault.go @@ -270,6 +270,8 @@ func cloneSettings(settings VaultSettings) VaultSettings { for key, value := range settings.FolderIcons { folderIcons[key] = value } + favorites := make([]string, len(settings.Favorites)) + copy(favorites, settings.Favorites) dailyLegacyPatterns := make([]DateNotePatternSettings, len(settings.DailyNotes.LegacyPatterns)) copy(dailyLegacyPatterns, settings.DailyNotes.LegacyPatterns) weeklyLegacyPatterns := make([]DateNotePatternSettings, len(settings.WeeklyNotes.LegacyPatterns)) @@ -293,6 +295,7 @@ func cloneSettings(settings VaultSettings) VaultSettings { TemplateID: settings.WeeklyNotes.TemplateID, }, FolderIcons: folderIcons, + Favorites: favorites, } } @@ -424,9 +427,26 @@ func normalizeVaultSettings(value VaultSettings, fallbackPrimary PrimaryNotesLoc TemplateID: value.WeeklyNotes.TemplateID, }, FolderIcons: folderIcons, + Favorites: normalizeFavorites(value.Favorites), } } +func normalizeFavorites(value []string) []string { + out := []string{} + seen := map[string]struct{}{} + for _, entry := range value { + if entry == "" { + continue + } + if _, ok := seen[entry]; ok { + continue + } + seen[entry] = struct{}{} + out = append(out, entry) + } + return out +} + func folderIconKey(folder NoteFolder, subpath string) string { return fmt.Sprintf("%s:%s", folder, subpath) } @@ -1729,6 +1749,9 @@ func (v *Vault) RenameFolder(folder NoteFolder, oldSub, newSub string) (string, DailyNotes: settings.DailyNotes, WeeklyNotes: settings.WeeklyNotes, FolderIcons: rewriteFolderIconsForRename(settings.FolderIcons, folder, oldSub, newSub), + // Favorites are carried through verbatim; the client rewrites stale + // favorite keys after the rename and re-persists them. + Favorites: settings.Favorites, }) if err != nil { return "", err @@ -1764,6 +1787,9 @@ func (v *Vault) DeleteFolder(folder NoteFolder, subpath string) error { DailyNotes: settings.DailyNotes, WeeklyNotes: settings.WeeklyNotes, FolderIcons: removeFolderIcons(settings.FolderIcons, folder, subpath), + // Favorites are carried through verbatim; the client prunes the deleted + // folder's favorites and re-persists them. + Favorites: settings.Favorites, }) return err } @@ -1797,6 +1823,8 @@ func (v *Vault) DuplicateFolder(folder NoteFolder, subpath string) (string, erro DailyNotes: settings.DailyNotes, WeeklyNotes: settings.WeeklyNotes, FolderIcons: duplicateFolderIcons(settings.FolderIcons, folder, subpath, relPath), + // A duplicated folder isn't auto-favorited; carry existing favorites through. + Favorites: settings.Favorites, }) if err != nil { return "", err diff --git a/apps/server/internal/vault/vault_test.go b/apps/server/internal/vault/vault_test.go index 634558a4..a168fa1b 100644 --- a/apps/server/internal/vault/vault_test.go +++ b/apps/server/internal/vault/vault_test.go @@ -763,6 +763,63 @@ func hasNotePath(notes []NoteMeta, path string) bool { return false } +func TestFavoritesRoundTripAndDedupe(t *testing.T) { + root := t.TempDir() + v, err := New(root, Options{}) + if err != nil { + t.Fatal(err) + } + saved, err := v.SetSettings(VaultSettings{ + // Mix of a note path and a folder key, with a duplicate and an empty entry. + Favorites: []string{"inbox/Idea.md", "inbox:Projects", "inbox/Idea.md", ""}, + }) + if err != nil { + t.Fatal(err) + } + want := []string{"inbox/Idea.md", "inbox:Projects"} + if len(saved.Favorites) != len(want) { + t.Fatalf("favorites = %v, want %v", saved.Favorites, want) + } + for i, f := range want { + if saved.Favorites[i] != f { + t.Errorf("favorites[%d] = %q, want %q", i, saved.Favorites[i], f) + } + } + // Persisted to disk and reloaded. + reloaded, err := v.GetSettings() + if err != nil { + t.Fatal(err) + } + if len(reloaded.Favorites) != len(want) { + t.Errorf("reloaded favorites = %v, want %v", reloaded.Favorites, want) + } +} + +func TestFavoritesSurviveFolderRename(t *testing.T) { + root := t.TempDir() + v, err := New(root, Options{}) + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(root, "inbox", "Projects"), 0o700); err != nil { + t.Fatal(err) + } + if _, err := v.SetSettings(VaultSettings{Favorites: []string{"inbox:Projects", "inbox/Idea.md"}}); err != nil { + t.Fatal(err) + } + if _, err := v.RenameFolder("inbox", "Projects", "Work"); err != nil { + t.Fatal(err) + } + // The server carries favorites through verbatim (the client rewrites keys). + settings, err := v.GetSettings() + if err != nil { + t.Fatal(err) + } + if len(settings.Favorites) != 2 { + t.Fatalf("folder rename dropped favorites: %v", settings.Favorites) + } +} + func TestExcalidrawListedAsNoteNotAsset(t *testing.T) { root := t.TempDir() v, err := New(root, Options{}) diff --git a/packages/app-core/src/components/Sidebar.tsx b/packages/app-core/src/components/Sidebar.tsx index 103afb4b..3d324ed9 100644 --- a/packages/app-core/src/components/Sidebar.tsx +++ b/packages/app-core/src/components/Sidebar.tsx @@ -54,11 +54,14 @@ import { classifyDateNote, dateNoteFolderMayBelongToDatePattern, dateNoteDirectoryDisplayLabel, + favoriteFolderKey, folderIconKey, + isFavoriteFolderKey, isPrimaryNotesAtRoot, folderForVaultRelativePath, normalizeVaultSettings, noteFolderSubpath, + parseFavoriteFolderKey, } from "../lib/vault-layout"; import { hasZenItem, @@ -248,6 +251,11 @@ type SidebarSelectionItem = | { kind: "note"; path: string } | { kind: "folder"; folder: NoteFolder; subpath: string }; +/** A favorite resolved to a live note or folder for rendering. */ +type FavoriteItem = + | { kind: "note"; key: string; path: string; title: string; isDrawing: boolean } + | { kind: "folder"; key: string; folder: NoteFolder; subpath: string; label: string }; + function noteSelectionKey(path: string): string { return `note:${encodeURIComponent(path)}`; } @@ -401,6 +409,7 @@ export function Sidebar(): JSX.Element { const setSearchOpen = useStore((s) => s.setSearchOpen); const createAndOpen = useStore((s) => s.createAndOpen); const createDrawingAndOpen = useStore((s) => s.createDrawingAndOpen); + const toggleFavorite = useStore((s) => s.toggleFavorite); const createDatabase = useStore((s) => s.createDatabase); const createNoteInChosenFolder = useStore((s) => s.createNoteInChosenFolder); const openTemplatePaletteForFolder = useStore((s) => s.openTemplatePaletteForFolder); @@ -1094,6 +1103,41 @@ export function Sidebar(): JSX.Element { return next; }, [notes, allFolders, assetFiles, vaultSettings]); + // Resolve favorite keys to live notes/folders. Keys whose target no longer + // exists (renamed away, deleted, trashed) are silently skipped — the Favorites + // section never shows a broken row. Order follows the stored favorites list. + const favoriteItems = useMemo<FavoriteItem[]>(() => { + const out: FavoriteItem[] = []; + for (const key of vaultSettings.favorites) { + if (isFavoriteFolderKey(key)) { + const parsed = parseFavoriteFolderKey(key); + if (!parsed || !parsed.subpath) continue; + const exists = allFolders.some( + (f) => f.folder === parsed.folder && f.subpath === parsed.subpath, + ); + if (!exists) continue; + out.push({ + kind: "folder", + key, + folder: parsed.folder, + subpath: parsed.subpath, + label: parsed.subpath.split("/").slice(-1)[0], + }); + } else { + const note = notes.find((n) => n.path === key); + if (!note || note.folder === "trash") continue; + out.push({ + kind: "note", + key, + path: note.path, + title: note.title, + isDrawing: isExcalidrawPath(note.path), + }); + } + } + return out; + }, [vaultSettings.favorites, notes, allFolders]); + // Daily/weekly notes grouped for the pinned date-nav: daily by year → month → // day, weekly by year → week, all newest-first. const dateNav = useMemo(() => { @@ -1758,6 +1802,14 @@ export function Sidebar(): JSX.Element { if (!isTop) { items.push({ kind: "separator" }); + const favKey = favoriteFolderKey(folder, subpath); + const isFav = vaultSettings.favorites.includes(favKey); + items.push({ + label: isFav ? "Remove from Favorites" : "Add to Favorites", + onSelect: async () => { + await toggleFavorite(favKey); + }, + }); items.push({ label: "Duplicate", onSelect: async () => { @@ -1916,6 +1968,7 @@ export function Sidebar(): JSX.Element { resetFolderIcon, openFolderColorPicker, resetFolderColor, + toggleFavorite, bulkSelectionMenuItems, selectedSidebarKeys, ]); @@ -1996,6 +2049,13 @@ export function Sidebar(): JSX.Element { }); } if (n.folder !== "trash") { + const isFav = vaultSettings.favorites.includes(n.path); + items.push({ + label: isFav ? "Remove from Favorites" : "Add to Favorites", + onSelect: async () => { + await toggleFavorite(n.path); + }, + }); items.push({ label: "Rename…", onSelect: async () => { @@ -2156,6 +2216,8 @@ export function Sidebar(): JSX.Element { absolutePathLabel, tabsEnabled, openNoteInTab, + toggleFavorite, + vaultSettings.favorites, bulkSelectionMenuItems, selectedSidebarKeys, folderLabels.archive, @@ -2697,8 +2759,11 @@ export function Sidebar(): JSX.Element { )} <div className="flex items-center gap-0.5"> <IconBtn - title="New note (choose folder)" - onClick={() => void createNoteInChosenFolder()} + title="Create… (note, drawing, folder, database)" + onClick={(e) => { + const r = e.currentTarget.getBoundingClientRect(); + setRootMenu({ x: r.left, y: r.bottom + 4 }); + }} > <PlusIcon /> </IconBtn> @@ -2848,6 +2913,82 @@ export function Sidebar(): JSX.Element { } }} > + {favoriteItems.length > 0 && ( + <> + <SidebarSectionHeading label="Favorites" /> + {favoriteItems.map((item) => { + const idx = idxCounter.current.value++; + const vimHighlight = vimCursor === idx; + if (item.kind === "note") { + return ( + <FavoriteRow + key={item.key} + label={item.title || "Untitled"} + icon={ + item.isDrawing ? ( + <ExcalidrawIcon width={13} height={13} /> + ) : ( + <DocumentIcon width={13} height={13} /> + ) + } + active={selectedPath === item.path} + onClick={() => { + setFocusedPanel("editor"); + handleSelectNote(item.path); + }} + onContextMenu={(e) => { + const note = notes.find((n) => n.path === item.path); + if (note) openNoteMenu(e, note); + }} + sidebarIdx={idx} + vimHighlight={vimHighlight} + sidebarFocused={isSidebarFocused} + dataAttrs={{ + "data-sidebar-type": "note", + "data-sidebar-path": item.path, + }} + /> + ); + } + return ( + <FavoriteRow + key={item.key} + label={item.label} + icon={ + resolveFolderIconOption( + item.folder, + item.subpath, + vaultSettings.folderIcons, + ).icon + } + active={isFolderActive(item.folder, item.subpath)} + onClick={() => { + setFocusedPanel("editor"); + setView({ + kind: "folder", + folder: item.folder, + subpath: item.subpath, + }); + }} + onContextMenu={(e) => + openFolderMenu(e, item.folder, item.subpath) + } + sidebarIdx={idx} + vimHighlight={vimHighlight} + sidebarFocused={isSidebarFocused} + dataAttrs={{ + "data-sidebar-type": "folder", + "data-sidebar-folder": item.folder, + "data-sidebar-subpath": item.subpath, + }} + /> + ); + })} + </> + )} + + <SidebarSectionHeading label="Quick access" /> + <TaskSidebarRow active={tasksViewActive} onClick={() => void openTasksView()} @@ -2923,55 +3064,10 @@ export function Sidebar(): JSX.Element { onNoteContextMenu={openNoteMenu} dragPayloadForItem={dragPayloadForItem} onRootContextMenu={(e, subpath) => openFolderMenu(e, "inbox", subpath)} + idxCounter={idxCounter.current} + vimCursor={vimCursor} /> - <div className="mt-1"> - <ArchiveSidebarRow - label={folderLabels.archive} - icon={resolveFolderIconOption("archive", "", vaultSettings.folderIcons).icon} - count={countNotesInTree(trees.archive)} - active={ - archiveViewActive || - (view.kind === "folder" && view.folder === "archive") || - !!selectedPath?.startsWith("archive/") - } - onClick={() => { - void openArchiveView(); - }} - onContextMenu={(e) => openFolderMenu(e, "archive", "")} - sidebarIdx={idxCounter.current.value++} - vimHighlight={vimCursor === idxCounter.current.value - 1} - sidebarFocused={isSidebarFocused} - /> - - <TrashSidebarRow - label={folderLabels.trash} - icon={resolveFolderIconOption("trash", "", vaultSettings.folderIcons).icon} - count={countNotesInTree(trees.trash)} - active={trashViewActive || !!selectedPath?.startsWith("trash/")} - onClick={() => { - void openTrashView(); - }} - onContextMenu={(e) => openFolderMenu(e, "trash", "")} - sidebarIdx={idxCounter.current.value++} - vimHighlight={vimCursor === idxCounter.current.value - 1} - sidebarFocused={isSidebarFocused} - /> - - <TrashSidebarRow - label="Assets" - icon={<PaperclipIcon width={16} height={16} />} - count={assetCount} - active={assetsViewActive} - onClick={() => { - void openAssetsView(); - }} - sidebarIdx={idxCounter.current.value++} - vimHighlight={vimCursor === idxCounter.current.value - 1} - sidebarFocused={isSidebarFocused} - /> - </div> - <SidebarSectionHeading label="Notes" onDropPayload={ @@ -3048,10 +3144,9 @@ export function Sidebar(): JSX.Element { /> )} - {/* Tag pills — pushed to the bottom of the tree area, just above - the footer, via mt-auto (the wrapper is min-h-full flex-col). */} + {/* Tags flow with the content, right after the notes tree. */} {tags.length > 0 && ( - <div className="mt-auto pt-5"> + <div className="pt-4"> <button type="button" onClick={() => setTagsCollapsed(!tagsCollapsed)} @@ -3059,19 +3154,6 @@ export function Sidebar(): JSX.Element { aria-expanded={!tagsCollapsed} className="flex w-full items-center gap-1 rounded px-2 pb-2 text-xs font-medium uppercase tracking-wide text-ink-500 transition-colors hover:text-ink-800" > - {showSidebarChevrons && ( - <span - aria-hidden - className="inline-block transition-transform" - style={{ - transform: tagsCollapsed - ? "rotate(-90deg)" - : "rotate(0deg)", - }} - > - ▾ - </span> - )} <span>Tags</span> <span className="ml-1 text-ink-500 normal-case tracking-normal"> {tags.length} @@ -3130,7 +3212,51 @@ export function Sidebar(): JSX.Element { </div> )} </div> - )} + )} + + {/* System (Archive / Trash / Assets) pinned to the bottom. */} + <div className="mt-auto pt-4"> + <SidebarSectionHeading label="System" /> + <SystemRow + icon={resolveFolderIconOption("archive", "", vaultSettings.folderIcons).icon} + label={folderLabels.archive} + count={countNotesInTree(trees.archive)} + active={ + archiveViewActive || + (view.kind === "folder" && view.folder === "archive") || + !!selectedPath?.startsWith("archive/") + } + onClick={() => void openArchiveView()} + onContextMenu={(e) => openFolderMenu(e, "archive", "")} + sidebarIdx={idxCounter.current.value++} + vimHighlight={vimCursor === idxCounter.current.value - 1} + sidebarFocused={isSidebarFocused} + sidebarType="archive" + /> + <SystemRow + icon={resolveFolderIconOption("trash", "", vaultSettings.folderIcons).icon} + label={folderLabels.trash} + count={countNotesInTree(trees.trash)} + active={trashViewActive || !!selectedPath?.startsWith("trash/")} + onClick={() => void openTrashView()} + onContextMenu={(e) => openFolderMenu(e, "trash", "")} + sidebarIdx={idxCounter.current.value++} + vimHighlight={vimCursor === idxCounter.current.value - 1} + sidebarFocused={isSidebarFocused} + sidebarType="trash" + /> + <SystemRow + icon={<PaperclipIcon width={16} height={16} />} + label="Assets" + count={assetCount} + active={assetsViewActive} + onClick={() => void openAssetsView()} + sidebarIdx={idxCounter.current.value++} + vimHighlight={vimCursor === idxCounter.current.value - 1} + sidebarFocused={isSidebarFocused} + sidebarType="assets" + /> + </div> </div> </div> @@ -3151,7 +3277,7 @@ export function Sidebar(): JSX.Element { sidebarIdx={idxCounter.current.value++} vimHighlight={vimCursor === idxCounter.current.value - 1} sidebarFocused={isSidebarFocused} - sidebarData={{ type: "assets" }} + sidebarData={{ type: "files" }} /> )} {(!hasAssetsDir || !canRevealInFileManager) && <div />} @@ -4704,29 +4830,31 @@ function TaskSidebarRow({ ); } -function ArchiveSidebarRow({ +// A single favorited note/folder row. Sets the same data-sidebar-* attributes +// as a regular note/folder row so the shared Vim activation (Enter) and the +// `m` context-menu key work without special-casing. +function FavoriteRow({ label, icon, - count, active, onClick, onContextMenu, sidebarIdx, vimHighlight, sidebarFocused = false, + dataAttrs, }: { label: string; icon: JSX.Element; - count: number; active: boolean; onClick: () => void; onContextMenu?: (e: React.MouseEvent) => void; sidebarIdx?: number; vimHighlight?: boolean; sidebarFocused?: boolean; + dataAttrs: Record<string, string | number>; }): JSX.Element { const strongActive = active && (!sidebarFocused || !!vimHighlight); - return ( <div role="button" @@ -4752,37 +4880,24 @@ function ArchiveSidebarRow({ : "text-ink-800 hover:bg-paper-200/70", ].join(" ")} style={{ paddingLeft: 4 }} - {...(sidebarIdx != null - ? { - "data-sidebar-idx": sidebarIdx, - "data-sidebar-type": "archive", - } - : { - "data-sidebar-type": "archive", - })} + {...(sidebarIdx != null ? { "data-sidebar-idx": sidebarIdx } : {})} + {...dataAttrs} > <SidebarGlyph active={strongActive} rowActive={active}> {icon} </SidebarGlyph> <span className="flex-1 truncate">{label}</span> {sidebarFocused && vimHighlight && ( - <RowKeyHint active={active} keyLabel="m" compact={count > 0} /> - )} - {count > 0 && ( - <span - className={[ - "shrink-0 pr-2 text-xs", - strongActive ? "text-white/80" : "text-ink-500", - ].join(" ")} - > - {count} - </span> + <RowKeyHint active={active} keyLabel="m" compact /> )} </div> ); } -function TrashSidebarRow({ +// A System row (Archive / Trash / Assets): a full-width vertical row with a +// leading glyph, label, and trailing count. Vim-navigable and activatable via +// its data-sidebar-type. +function SystemRow({ label, icon, count, @@ -4792,6 +4907,7 @@ function TrashSidebarRow({ sidebarIdx, vimHighlight, sidebarFocused = false, + sidebarType, }: { label: string; icon: JSX.Element; @@ -4802,6 +4918,7 @@ function TrashSidebarRow({ sidebarIdx?: number; vimHighlight?: boolean; sidebarFocused?: boolean; + sidebarType: string; }): JSX.Element { const strongActive = active && (!sidebarFocused || !!vimHighlight); @@ -4831,13 +4948,8 @@ function TrashSidebarRow({ ].join(" ")} style={{ paddingLeft: 4 }} {...(sidebarIdx != null - ? { - "data-sidebar-idx": sidebarIdx, - "data-sidebar-type": "trash", - } - : { - "data-sidebar-type": "trash", - })} + ? { "data-sidebar-idx": sidebarIdx, "data-sidebar-type": sidebarType } + : { "data-sidebar-type": sidebarType })} > <SidebarGlyph active={strongActive} rowActive={active}> {icon} @@ -5092,6 +5204,8 @@ function DateNotesNav({ onNoteContextMenu, dragPayloadForItem, onRootContextMenu, + idxCounter, + vimCursor, }: { dateNav: DateNavData; expanded: Set<string>; @@ -5112,6 +5226,8 @@ function DateNotesNav({ onNoteContextMenu: (e: React.MouseEvent, note: NoteMeta) => void; dragPayloadForItem: (item: SidebarSelectionItem) => DragPayload; onRootContextMenu?: (e: React.MouseEvent, subpath: string) => void; + idxCounter: { value: number }; + vimCursor: number; }): JSX.Element | null { const rows: JSX.Element[] = []; const monthName = (year: number, month: number): string => @@ -5125,39 +5241,58 @@ function DateNotesNav({ icon: JSX.Element, active: boolean, chevron: boolean, + // The date directory this group belongs to, so Vim Enter navigates there. + sidebarSubpath: string, onContextMenu?: (e: React.MouseEvent) => void, - ): JSX.Element => ( - <TreeRow - key={key} - icon={icon} - label={label} - count={count} - active={active} - expandable - collapsed={!expanded.has(key)} - depth={depth} - onToggle={() => onToggle(key)} - onSelect={onSelect} - onContextMenu={onContextMenu} - reserveLeadingSlot={chevron} - showExpandChevron={chevron} - /> - ); - const leaf = (note: NoteMeta, depth: number): JSX.Element => ( - <NoteLeaf - key={note.path} - note={note} - depth={depth} - showSidebarChevrons={showSidebarChevrons} - active={note.path === selectedPath} - selected={selectedKeys.has(noteSelectionKey(note.path))} - sidebarFocused={sidebarFocused} - onSelectItem={onSelectItem} - onSelectNote={onSelectNote} - onContextMenuNote={onNoteContextMenu} - dragPayloadForItem={dragPayloadForItem} - /> - ); + ): JSX.Element => { + const idx = idxCounter.value++; + return ( + <TreeRow + key={key} + icon={icon} + label={label} + count={count} + active={active} + expandable + collapsed={!expanded.has(key)} + depth={depth} + onToggle={() => onToggle(key)} + onSelect={onSelect} + onContextMenu={onContextMenu} + reserveLeadingSlot={chevron} + showExpandChevron={chevron} + sidebarIdx={idx} + vimHighlight={vimCursor === idx} + sidebarFocused={sidebarFocused} + sidebarData={{ + type: "folder", + folder: "inbox", + subpath: sidebarSubpath, + key: "", + }} + /> + ); + }; + const leaf = (note: NoteMeta, depth: number): JSX.Element => { + const idx = idxCounter.value++; + return ( + <NoteLeaf + key={note.path} + note={note} + depth={depth} + showSidebarChevrons={showSidebarChevrons} + active={note.path === selectedPath} + selected={selectedKeys.has(noteSelectionKey(note.path))} + sidebarFocused={sidebarFocused} + onSelectItem={onSelectItem} + onSelectNote={onSelectNote} + onContextMenuNote={onNoteContextMenu} + dragPayloadForItem={dragPayloadForItem} + sidebarIdx={idx} + vimHighlight={vimCursor === idx} + /> + ); + }; if (dateNav.dailyEnabled && dateNav.dailyTotal > 0) { rows.push( @@ -5170,6 +5305,7 @@ function DateNotesNav({ dailyIcon, isFolderActive("inbox", dateNav.dailyDir), false, + dateNav.dailyDir, onRootContextMenu ? (e) => onRootContextMenu(e, dateNav.dailyDir) : undefined, ), ); @@ -5186,6 +5322,7 @@ function DateNotesNav({ <FolderGlyphIcon open={expanded.has(yKey)} />, false, showSidebarChevrons, + dateNav.dailyDir, ), ); if (expanded.has(yKey)) { @@ -5201,6 +5338,7 @@ function DateNotesNav({ <FolderGlyphIcon open={expanded.has(mKey)} />, false, showSidebarChevrons, + dateNav.dailyDir, ), ); if (expanded.has(mKey)) for (const n of mg.notes) rows.push(leaf(n, 3)); @@ -5221,6 +5359,7 @@ function DateNotesNav({ weeklyIcon, isFolderActive("inbox", dateNav.weeklyDir), false, + dateNav.weeklyDir, onRootContextMenu ? (e) => onRootContextMenu(e, dateNav.weeklyDir) : undefined, ), ); @@ -5237,6 +5376,7 @@ function DateNotesNav({ <FolderGlyphIcon open={expanded.has(yKey)} />, false, showSidebarChevrons, + dateNav.weeklyDir, ), ); if (expanded.has(yKey)) for (const n of yg.notes) rows.push(leaf(n, 2)); @@ -5245,7 +5385,7 @@ function DateNotesNav({ } if (rows.length === 0) return null; - return <div className="mt-1 flex flex-col">{rows}</div>; + return <div className="flex flex-col">{rows}</div>; } function RowKeyHint({ diff --git a/packages/app-core/src/components/VimNav.tsx b/packages/app-core/src/components/VimNav.tsx index c8babe22..36b2aeb9 100644 --- a/packages/app-core/src/components/VimNav.tsx +++ b/packages/app-core/src/components/VimNav.tsx @@ -166,6 +166,11 @@ export function VimNav(): JSX.Element | null { keyLabel: getKeymapDisplay(keymapOverrides, 'vim.leaderCopyMarkdown'), label: 'Copy as Markdown', detail: "Copy the whole note's Markdown to the clipboard." + }, + { + keyLabel: getKeymapDisplay(keymapOverrides, 'vim.leaderToggleFavorite'), + label: 'Toggle favorite', + detail: 'Add or remove the active note from Favorites.' } ] } @@ -703,6 +708,13 @@ export function VimNav(): JSX.Element | null { void state.copyActiveNoteAsMarkdown() return } + if (matchesSequenceToken(e, overrides, 'vim.leaderToggleFavorite')) { + e.preventDefault() + e.stopImmediatePropagation() + resetLeader() + void state.toggleFavoriteActiveNote() + return + } resetLeader() } @@ -1761,6 +1773,8 @@ export function VimNav(): JSX.Element | null { void state.openArchiveView() } else if (itemType === 'trash') { void state.openTrashView() + } else if (itemType === 'assets') { + void state.openAssetsView() } } diff --git a/packages/app-core/src/lib/keymaps.ts b/packages/app-core/src/lib/keymaps.ts index 1ff53050..0762727c 100644 --- a/packages/app-core/src/lib/keymaps.ts +++ b/packages/app-core/src/lib/keymaps.ts @@ -46,6 +46,7 @@ export type KeymapId = | "vim.leaderNoteActions" | "vim.leaderFormatNote" | "vim.leaderCopyMarkdown" + | "vim.leaderToggleFavorite" | "vim.leaderQuickCapture" | "vim.leaderTemplatePicker" | "vim.leaderInsertTemplate" @@ -463,6 +464,17 @@ const KEYMAP_DEFINITIONS: KeymapDefinition[] = [ vimOnly: true, maxTokens: 1, }, + { + id: "vim.leaderToggleFavorite", + kind: "sequence", + scope: "leader", + group: "vim", + title: "Leader note action: toggle favorite", + description: "Add or remove the active note from Favorites.", + defaultBinding: "s", + vimOnly: true, + maxTokens: 1, + }, { id: "vim.leaderQuickCapture", kind: "sequence", diff --git a/packages/app-core/src/lib/vault-layout.test.ts b/packages/app-core/src/lib/vault-layout.test.ts index 425fd446..75cf30b6 100644 --- a/packages/app-core/src/lib/vault-layout.test.ts +++ b/packages/app-core/src/lib/vault-layout.test.ts @@ -7,8 +7,16 @@ import { dailyNoteLocationForDate, dateNoteFolderMayBelongToDatePattern, dateNoteDirectoryDisplayLabel, + favoriteFolderKey, folderForVaultRelativePath, + isFavoriteFolderKey, noteFolderSubpath, + normalizeVaultSettings, + parseFavoriteFolderKey, + removeFavoritesForFolder, + rewriteFavoriteNotePath, + rewriteFavoritesForFolderRename, + toggleFavorite, weeklyNoteLocationForDate } from './vault-layout' @@ -34,7 +42,8 @@ function settings(dailyDirectory: string, weeklyDirectory: string): VaultSetting dailyNotes: { enabled: true, directory: dailyDirectory }, weeklyNotes: { enabled: true, directory: weeklyDirectory }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } } @@ -72,7 +81,8 @@ describe('classifyDateNote', () => { locale: 'en-US' }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } as VaultSettings ) @@ -93,7 +103,8 @@ describe('classifyDateNote', () => { }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } as VaultSettings ) @@ -127,7 +138,8 @@ describe('classifyDateNote', () => { }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } as VaultSettings) expect(location).toEqual({ @@ -147,7 +159,8 @@ describe('classifyDateNote', () => { }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } as VaultSettings expect(dailyNoteLocationForDate(new Date(2026, 5, 9), vaultSettings)).toEqual({ @@ -175,7 +188,8 @@ describe('classifyDateNote', () => { }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } as VaultSettings) expect(location).toEqual({ @@ -195,7 +209,8 @@ describe('classifyDateNote', () => { locale: 'en-US' }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } as VaultSettings) expect(location).toEqual({ @@ -215,7 +230,8 @@ describe('classifyDateNote', () => { locale: 'en-US' }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } as VaultSettings) expect(location).toEqual({ @@ -235,7 +251,8 @@ describe('classifyDateNote', () => { locale: 'en-US' }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } as VaultSettings) expect(location).toEqual({ @@ -255,7 +272,8 @@ describe('classifyDateNote', () => { locale: 'en-US' }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } as VaultSettings expect(weeklyNoteLocationForDate(new Date(2026, 5, 9), vaultSettings)).toEqual({ @@ -282,7 +300,8 @@ describe('classifyDateNote', () => { }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } as VaultSettings const info = classifyDateNote( @@ -305,7 +324,8 @@ describe('classifyDateNote', () => { }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } as VaultSettings const info = classifyDateNote(note('inbox/2026/06-09.md', '06-09'), vaultSettings) @@ -325,7 +345,8 @@ describe('classifyDateNote', () => { }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } as VaultSettings // ISO week 24 falls entirely in June, so a July folder cannot round-trip. @@ -351,7 +372,8 @@ describe('classifyDateNote', () => { }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } as VaultSettings const info = classifyDateNote( @@ -377,7 +399,8 @@ describe('classifyDateNote', () => { ] }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } as VaultSettings const info = classifyDateNote( @@ -419,7 +442,8 @@ describe('dateNoteFolderMayBelongToDatePattern', () => { }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } as VaultSettings expect(dateNoteFolderMayBelongToDatePattern('2026/Jun-Jun', vaultSettings)).toBe(true) @@ -468,3 +492,66 @@ describe('folderForVaultRelativePath — case-insensitive system folders (#186)' expect(noteFolderSubpath({ folder: 'inbox', path: 'Inbox/n.md' }, inboxMode)).toBe('') }) }) + +describe('favorites', () => { + it('discriminates folder keys from note paths by the colon', () => { + expect(isFavoriteFolderKey('inbox:Projects')).toBe(true) + expect(isFavoriteFolderKey('inbox:')).toBe(true) // top-level folder key + expect(isFavoriteFolderKey('inbox/Idea.md')).toBe(false) + expect(favoriteFolderKey('inbox', 'Projects')).toBe('inbox:Projects') + expect(parseFavoriteFolderKey('inbox:Projects/Sub')).toEqual({ + folder: 'inbox', + subpath: 'Projects/Sub' + }) + expect(parseFavoriteFolderKey('inbox/Idea.md')).toBeNull() + }) + + it('toggles a key on and off', () => { + expect(toggleFavorite([], 'inbox/A.md')).toEqual(['inbox/A.md']) + expect(toggleFavorite(['inbox/A.md', 'x:y'], 'inbox/A.md')).toEqual(['x:y']) + // Added at the end, preserving order. + expect(toggleFavorite(['x:y'], 'inbox/A.md')).toEqual(['x:y', 'inbox/A.md']) + }) + + it('rewrites a note favorite when the note is renamed/moved', () => { + const favs = ['inbox/A.md', 'inbox:Projects'] + expect(rewriteFavoriteNotePath(favs, 'inbox/A.md', 'inbox/B.md')).toEqual([ + 'inbox/B.md', + 'inbox:Projects' + ]) + // Unaffected favorites return the same reference (no needless persist). + expect(rewriteFavoriteNotePath(favs, 'inbox/Z.md', 'inbox/Q.md')).toBe(favs) + }) + + it('repoints favorites when a folder is renamed', () => { + const favs = ['inbox:Projects', 'inbox:Projects/Sub', 'inbox/Projects/note.md', 'quick:Keep'] + const next = rewriteFavoritesForFolderRename( + favs, + 'inbox', + 'Projects', + 'Work', + 'inbox/Projects/', + 'inbox/Work/' + ) + expect(next).toEqual(['inbox:Work', 'inbox:Work/Sub', 'inbox/Work/note.md', 'quick:Keep']) + }) + + it('removes favorites for a deleted folder and its notes', () => { + const favs = ['inbox:Projects', 'inbox:Projects/Sub', 'inbox/Projects/note.md', 'inbox/Other.md'] + expect( + removeFavoritesForFolder(favs, 'inbox', 'Projects', 'inbox/Projects/') + ).toEqual(['inbox/Other.md']) + }) + + it('normalizes favorites: drops empties/dupes, keeps order', () => { + const settings = normalizeVaultSettings({ + favorites: ['inbox/A.md', '', 'inbox/A.md', 'inbox:Projects'] + } as unknown as VaultSettings) + expect(settings.favorites).toEqual(['inbox/A.md', 'inbox:Projects']) + }) + + it('defaults favorites to an empty array', () => { + const settings = normalizeVaultSettings({} as unknown as VaultSettings) + expect(settings.favorites).toEqual([]) + }) +}) diff --git a/packages/app-core/src/lib/vault-layout.ts b/packages/app-core/src/lib/vault-layout.ts index 1dfa173a..70c6b070 100644 --- a/packages/app-core/src/lib/vault-layout.ts +++ b/packages/app-core/src/lib/vault-layout.ts @@ -477,6 +477,15 @@ export function normalizeVaultSettings( normalizedFolderColors[key] = value } } + const normalizedFavorites: string[] = [] + if (Array.isArray(settings?.favorites)) { + const seen = new Set<string>() + for (const entry of settings.favorites) { + if (typeof entry !== 'string' || !entry || seen.has(entry)) continue + seen.add(entry) + normalizedFavorites.push(entry) + } + } const primaryNotesLocation = settings?.primaryNotesLocation === 'root' ? 'root' @@ -515,7 +524,8 @@ export function normalizeVaultSettings( templateId: normalizeTemplateId(settings?.weeklyNotes?.templateId) }, folderIcons: normalizedFolderIcons, - folderColors: normalizedFolderColors + folderColors: normalizedFolderColors, + favorites: normalizedFavorites } } @@ -523,6 +533,101 @@ export function folderIconKey(folder: NoteFolder, subpath: string): string { return `${folder}:${subpath}` } +/** + * Favorites store either a note's vault-relative path (e.g. `inbox/Idea.md`) or a + * folder key `folder:subpath`. Folder keys always contain a `:`; note paths never + * do, so the colon discriminates the two. + */ +export function favoriteFolderKey(folder: NoteFolder, subpath: string): string { + return folderIconKey(folder, subpath) +} + +export function isFavoriteFolderKey(key: string): boolean { + return key.includes(':') +} + +/** Parse a folder favorite key back into its `{ folder, subpath }` parts. */ +export function parseFavoriteFolderKey( + key: string +): { folder: NoteFolder; subpath: string } | null { + const idx = key.indexOf(':') + if (idx === -1) return null + const folder = key.slice(0, idx) as NoteFolder + return { folder, subpath: key.slice(idx + 1) } +} + +/** Toggle a favorite key, returning the next list (added at the end, or removed). */ +export function toggleFavorite(favorites: string[], key: string): string[] { + return favorites.includes(key) + ? favorites.filter((f) => f !== key) + : [...favorites, key] +} + +/** Rewrite a note favorite when its path changes (rename or move). */ +export function rewriteFavoriteNotePath( + favorites: string[], + oldPath: string, + newPath: string +): string[] { + if (oldPath === newPath || !favorites.includes(oldPath)) return favorites + return favorites.map((f) => (f === oldPath ? newPath : f)) +} + +/** + * Rewrite favorites after a folder rename/move: the folder's own key plus every + * note favorite whose path lived under that folder are repointed at the new + * location. `oldRelPrefix`/`newRelPrefix` are the vault-relative folder paths + * (with trailing slash) used to rewrite note paths. + */ +export function rewriteFavoritesForFolderRename( + favorites: string[], + folder: NoteFolder, + oldSubpath: string, + newSubpath: string, + oldRelPrefix: string, + newRelPrefix: string +): string[] { + const exactKey = favoriteFolderKey(folder, oldSubpath) + const keyPrefix = `${exactKey}/` + const newExactKey = favoriteFolderKey(folder, newSubpath) + return favorites.map((f) => { + if (f === exactKey) return newExactKey + if (f.startsWith(keyPrefix)) { + return favoriteFolderKey(folder, newSubpath + f.slice(exactKey.length)) + } + if (oldRelPrefix && f.startsWith(oldRelPrefix)) { + return newRelPrefix + f.slice(oldRelPrefix.length) + } + return f + }) +} + +/** Remove a single favorite key (note path or folder key) if present. */ +export function removeFavorite(favorites: string[], key: string): string[] { + return favorites.includes(key) ? favorites.filter((f) => f !== key) : favorites +} + +/** + * Remove favorites for a deleted folder: its own key, any descendant folder + * keys, and any note favorites whose path lived under it (`relPrefix` is the + * vault-relative folder path with trailing slash). + */ +export function removeFavoritesForFolder( + favorites: string[], + folder: NoteFolder, + subpath: string, + relPrefix: string +): string[] { + const exactKey = favoriteFolderKey(folder, subpath) + const keyPrefix = `${exactKey}/` + return favorites.filter( + (f) => + f !== exactKey && + !f.startsWith(keyPrefix) && + !(relPrefix && f.startsWith(relPrefix)) + ) +} + export function rewriteFolderIconsForRename( folderIcons: Record<string, FolderIconId>, folder: NoteFolder, diff --git a/packages/app-core/src/store.test.ts b/packages/app-core/src/store.test.ts index 9f137961..9eb9c8fd 100644 --- a/packages/app-core/src/store.test.ts +++ b/packages/app-core/src/store.test.ts @@ -166,7 +166,8 @@ describe('daily note patterns', () => { }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } }) @@ -204,7 +205,8 @@ describe('weekly note patterns', () => { locale: 'en-US' }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } }) @@ -226,7 +228,8 @@ describe('date note pattern history', () => { }, weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } const nextSettings = { ...oldSettings, @@ -292,7 +295,8 @@ describe('date note pattern history', () => { locale: 'en-US' }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } const nextSettings = { ...oldSettings, diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index fa2fe8bb..2b03d7e2 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -82,10 +82,14 @@ import { findDailyNoteForDate, findWeeklyNoteForDate, isPrimaryNotesAtRoot, + removeFavoritesForFolder, removeFolderColors, removeFolderIcons, normalizeVaultSettings, noteFolderSubpath, + rewriteFavoriteNotePath, + rewriteFavoritesForFolderRename, + toggleFavorite as toggleFavoriteKey, weeklyNoteLocationForDate, rewriteFolderColorsForRename, rewriteFolderIconsForRename @@ -1658,6 +1662,15 @@ interface Store { setVault: (v: VaultInfo | null) => void setVaultSettings: (next: VaultSettings) => Promise<void> + /** + * Toggle a favorite (a note path or a `folder:subpath` key) and persist it. + * Favorites pin to the top of the sidebar. + */ + toggleFavorite: (key: string) => Promise<void> + /** Toggle favorite for the active editor note (Vim leader command). */ + toggleFavoriteActiveNote: () => Promise<void> + /** @internal Replace the favorites list and persist (no note refresh). */ + applyFavorites: (nextFavorites: string[]) => Promise<void> setNotes: (notes: NoteMeta[]) => void setView: (view: View) => void /** Open the Tasks panel as a tab in the active pane. If the tab is @@ -2925,6 +2938,36 @@ export const useStore = create<Store>((set, get) => { console.error('setVaultSettings failed', err) } }, + applyFavorites: async (nextFavorites) => { + const current = get().vaultSettings + if ( + current.favorites.length === nextFavorites.length && + current.favorites.every((f, i) => f === nextFavorites[i]) + ) { + return // unchanged — skip the disk write + } + // Favorites don't affect note listing, so update optimistically and persist + // without a full refreshNotes. + set({ vaultSettings: { ...current, favorites: nextFavorites } }) + try { + const saved = normalizeVaultSettings( + await window.zen.setVaultSettings({ ...get().vaultSettings, favorites: nextFavorites }) + ) + set({ vaultSettings: saved }) + } catch (err) { + console.error('applyFavorites failed', err) + set({ vaultSettings: current }) // revert on failure + } + }, + toggleFavorite: async (key) => { + if (!key) return + await get().applyFavorites(toggleFavoriteKey(get().vaultSettings.favorites, key)) + }, + toggleFavoriteActiveNote: async () => { + const path = get().activeNote?.path ?? get().selectedPath + if (!path) return + await get().toggleFavorite(path) + }, refreshRootContentHidden: async () => { try { const hidden = await window.zen.rootContentHiddenByInboxMode() @@ -3998,6 +4041,9 @@ export const useStore = create<Store>((set, get) => { try { const meta = await window.zen.renameNote(oldPath, nextTitle) set((s) => renameNoteState(s, oldPath, meta)) + await get().applyFavorites( + rewriteFavoriteNotePath(get().vaultSettings.favorites, oldPath, meta.path) + ) await get().refreshNotes() } catch (err) { console.error('renameNote failed', err) @@ -5384,6 +5430,19 @@ export const useStore = create<Store>((set, get) => { } }) + // Repoint favorites at the renamed folder (its own key, descendant folder + // keys, and note favorites that lived under it) and persist. + await get().applyFavorites( + rewriteFavoritesForFolderRename( + get().vaultSettings.favorites, + folder, + oldSubpath, + newSubpath, + oldPrefix, + newPrefix + ) + ) + await get().refreshNotes() const v = get().view @@ -5440,6 +5499,10 @@ export const useStore = create<Store>((set, get) => { ...activeFieldsFrom(ensured.layout, ensured.activePaneId, contents, dirty) } }) + // Drop favorites for the deleted folder and the notes that lived under it. + await get().applyFavorites( + removeFavoritesForFolder(get().vaultSettings.favorites, folder, subpath, prefix) + ) }, duplicateFolder: async (folder, subpath) => { @@ -5507,6 +5570,9 @@ export const useStore = create<Store>((set, get) => { ...activeFieldsFrom(ensured.layout, ensured.activePaneId, contents, dirty) } }) + await get().applyFavorites( + rewriteFavoriteNotePath(get().vaultSettings.favorites, relPath, meta.path) + ) } catch (err) { console.error('moveNote failed', err) } diff --git a/packages/bridge-contract/src/ipc.ts b/packages/bridge-contract/src/ipc.ts index 42258695..236da613 100644 --- a/packages/bridge-contract/src/ipc.ts +++ b/packages/bridge-contract/src/ipc.ts @@ -305,6 +305,14 @@ export interface VaultSettings { folderIcons: Record<string, FolderIconId> /** Per-folder accent color, keyed by `folder:subpath` (same key as folderIcons). */ folderColors: Record<string, FolderColorId> + /** + * Favorited notes and folders, pinned to the top of the sidebar. Each entry is + * either a note's vault-relative path (e.g. `inbox/Idea.md`) or a folder key + * `folder:subpath` (e.g. `inbox:Projects`). Folder keys always contain a `:`; + * note paths never do (`:` is a forbidden filename char), so the two are + * distinguishable. Order is the display order in the Favorites section. + */ + favorites: string[] } export const DEFAULT_DAILY_NOTES_DIRECTORY = 'Daily Notes' @@ -329,7 +337,8 @@ export const DEFAULT_VAULT_SETTINGS: VaultSettings = { locale: DEFAULT_WEEKLY_NOTE_LOCALE }, folderIcons: {}, - folderColors: {} + folderColors: {}, + favorites: [] } export interface NoteMeta { From 731abb7429eea66a8a8787452d008391df34e80c Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Wed, 17 Jun 2026 16:27:31 -0500 Subject: [PATCH 53/66] feat(sync): foundation for bidirectional synced vaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lays the groundwork for a third workspace mode, `synced` — a local vault that bidirectionally syncs with a server hub — while leaving `local` and the thin-client `remote` modes untouched. No user-facing behavior yet. Server (M2): - GET /api/sync/manifest returns {path, hash, size, mtime} per note/.excalidraw for content-addressed diffing. sha256 hashes are cached in the existing note-meta-cache (keyed by mtime+size, version bumped 1->2), so an unchanged vault answers from stat() calls alone. supportsSyncManifest capability added. - Refactored the notes walk into a shared collectNoteFiles(). Tests for hash stability/change, database+internal exclusion, and the HTTP endpoint (auth). Desktop plumbing (M1): - WorkspaceMode / WindowWorkspaceMode gain 'synced'; PersistedConfig gains syncedVaults[] (root + server profile + conflict policy + asset mode) with a normalizer. WindowVaultRegistry.setSyncedVault treats synced as disk-backed (shares the chokidar watcher) with an onLocalChange fan-out hook for the engine. store.workspaceModeFrom handles synced. Reconcile core (M3, pure): - Three-way diff (local/remote/base content hashes): push, pull, bidirectional delete-propagation, keep-both conflicts, first-sync union merge, hash-only (clock-skew-safe) decisions. 16 unit tests cover every decision-table row. --- apps/desktop/src/main/sync/reconcile.test.ts | 105 ++++++++++ apps/desktop/src/main/sync/reconcile.ts | 115 +++++++++++ apps/desktop/src/main/vault.ts | 55 +++++- apps/desktop/src/main/window-vaults.ts | 40 +++- .../internal/httpserver/excalidraw_test.go | 47 +++++ apps/server/internal/httpserver/server.go | 11 ++ apps/server/internal/vault/types.go | 10 + apps/server/internal/vault/vault.go | 183 +++++++++++++++--- apps/server/internal/vault/vault_test.go | 93 +++++++++ packages/app-core/src/store.ts | 2 +- packages/bridge-contract/src/ipc.ts | 9 +- 11 files changed, 629 insertions(+), 41 deletions(-) create mode 100644 apps/desktop/src/main/sync/reconcile.test.ts create mode 100644 apps/desktop/src/main/sync/reconcile.ts diff --git a/apps/desktop/src/main/sync/reconcile.test.ts b/apps/desktop/src/main/sync/reconcile.test.ts new file mode 100644 index 00000000..13c9f6a1 --- /dev/null +++ b/apps/desktop/src/main/sync/reconcile.test.ts @@ -0,0 +1,105 @@ +import { describe, it, expect } from 'vitest' +import { reconcile, isNeverSync, type FileState, type BaseEntry, type SyncAction } from './reconcile' + +function L(entries: Record<string, string>): Map<string, FileState> { + return new Map(Object.entries(entries).map(([p, h]) => [p, { hash: h, size: h.length }])) +} +function B(entries: Record<string, string>): Map<string, BaseEntry> { + return new Map(Object.entries(entries).map(([p, h]) => [p, { contentHash: h, size: h.length }])) +} +/** Run reconcile on a single path and return the one action (or undefined). */ +function one( + local: Record<string, string>, + remote: Record<string, string>, + base: Record<string, string> +): SyncAction | undefined { + const actions = reconcile(L(local), L(remote), B(base)) + expect(actions.length).toBeLessThanOrEqual(1) + return actions[0] +} + +describe('reconcile — decision table', () => { + it('unchanged on both sides → no action', () => { + expect(one({ 'a.md': 'x' }, { 'a.md': 'x' }, { 'a.md': 'x' })).toBeUndefined() + }) + it('edited locally only → push', () => { + expect(one({ 'a.md': 'x2' }, { 'a.md': 'x' }, { 'a.md': 'x' })).toEqual({ kind: 'push', path: 'a.md' }) + }) + it('edited remotely only → pull', () => { + expect(one({ 'a.md': 'x' }, { 'a.md': 'x2' }, { 'a.md': 'x' })).toEqual({ kind: 'pull', path: 'a.md' }) + }) + it('both edited to identical bytes → converge', () => { + expect(one({ 'a.md': 'y' }, { 'a.md': 'y' }, { 'a.md': 'x' })).toEqual({ kind: 'converge', path: 'a.md' }) + }) + it('both edited differently → conflict(both-edited)', () => { + expect(one({ 'a.md': 'L' }, { 'a.md': 'R' }, { 'a.md': 'x' })).toEqual({ + kind: 'conflict', + path: 'a.md', + reason: 'both-edited' + }) + }) + it('deleted locally, remote unchanged → deleteRemote', () => { + expect(one({}, { 'a.md': 'x' }, { 'a.md': 'x' })).toEqual({ kind: 'deleteRemote', path: 'a.md' }) + }) + it('deleted remotely, local unchanged → deleteLocal', () => { + expect(one({ 'a.md': 'x' }, {}, { 'a.md': 'x' })).toEqual({ kind: 'deleteLocal', path: 'a.md' }) + }) + it('deleted locally but edited remotely → conflict(delete-vs-edit)', () => { + expect(one({}, { 'a.md': 'x2' }, { 'a.md': 'x' })).toEqual({ + kind: 'conflict', + path: 'a.md', + reason: 'delete-vs-edit' + }) + }) + it('edited locally but deleted remotely → conflict(edit-vs-delete)', () => { + expect(one({ 'a.md': 'x2' }, {}, { 'a.md': 'x' })).toEqual({ + kind: 'conflict', + path: 'a.md', + reason: 'edit-vs-delete' + }) + }) + it('new local, no base → push', () => { + expect(one({ 'a.md': 'x' }, {}, {})).toEqual({ kind: 'push', path: 'a.md' }) + }) + it('new remote, no base → pull', () => { + expect(one({}, { 'a.md': 'x' }, {})).toEqual({ kind: 'pull', path: 'a.md' }) + }) + it('first sync, same path identical bytes → converge', () => { + expect(one({ 'a.md': 'x' }, { 'a.md': 'x' }, {})).toEqual({ kind: 'converge', path: 'a.md' }) + }) + it('first sync, same path different bytes → conflict(first-sync-clash)', () => { + expect(one({ 'a.md': 'L' }, { 'a.md': 'R' }, {})).toEqual({ + kind: 'conflict', + path: 'a.md', + reason: 'first-sync-clash' + }) + }) + it('deleted on both sides → forget the base entry', () => { + expect(one({}, {}, { 'a.md': 'x' })).toEqual({ kind: 'forget', path: 'a.md' }) + }) +}) + +describe('reconcile — NEVER_SYNC + union', () => { + it('never syncs sync-state.json / note-meta-cache', () => { + expect(isNeverSync('.zennotes/sync-state.json')).toBe(true) + expect(isNeverSync('.zennotes/note-meta-cache-v1.json')).toBe(true) + expect(isNeverSync('inbox/note.md')).toBe(false) + const actions = reconcile(L({ '.zennotes/sync-state.json': 'a' }), L({}), B({})) + expect(actions).toEqual([]) + }) + it('first-sync union merge across both sides', () => { + const actions = reconcile( + L({ 'inbox/A.md': 'a', 'inbox/Shared.md': 'L' }), + L({ 'inbox/B.md': 'b', 'inbox/Shared.md': 'R' }), + B({}) + ) + const byPath = new Map(actions.map((x) => [x.path, x])) + expect(byPath.get('inbox/A.md')).toEqual({ kind: 'push', path: 'inbox/A.md' }) + expect(byPath.get('inbox/B.md')).toEqual({ kind: 'pull', path: 'inbox/B.md' }) + expect(byPath.get('inbox/Shared.md')).toEqual({ + kind: 'conflict', + path: 'inbox/Shared.md', + reason: 'first-sync-clash' + }) + }) +}) diff --git a/apps/desktop/src/main/sync/reconcile.ts b/apps/desktop/src/main/sync/reconcile.ts new file mode 100644 index 00000000..f99cf44a --- /dev/null +++ b/apps/desktop/src/main/sync/reconcile.ts @@ -0,0 +1,115 @@ +/** + * Pure three-way reconcile for synced vaults. + * + * For every path we compare three content hashes: + * L = local hash (null if the file is absent locally) + * R = remote hash (null if absent on the server) + * B = base hash (the bytes at the last successful sync; null if never synced) + * + * Because the server has no content hashes of its own and no tombstones (a delete + * is just a missing file), the base snapshot is what lets us tell "deleted here" + * apart from "new there". Decisions are hash-only — never mtime — so clock skew + * across machines can't corrupt them. + */ + +export interface FileState { + hash: string + size: number +} + +export interface BaseEntry { + contentHash: string + size: number +} + +export type ConflictReason = + | 'both-edited' // edited differently on both sides + | 'first-sync-clash' // same path, different bytes, never synced before + | 'delete-vs-edit' // deleted locally, edited remotely → resurrect from server + | 'edit-vs-delete' // edited locally, deleted remotely → resurrect to server + +export type SyncAction = + | { kind: 'push'; path: string } // local → server + | { kind: 'pull'; path: string } // server → local + | { kind: 'deleteRemote'; path: string } // remove on server + | { kind: 'deleteLocal'; path: string } // remove locally + | { kind: 'conflict'; path: string; reason: ConflictReason } + | { kind: 'converge'; path: string } // L === R but ≠ B: update base, no transfer + | { kind: 'forget'; path: string } // deleted on both sides: drop the base entry + +/** Paths that must never participate in sync (per-device / cache files). */ +const NEVER_SYNC_PREFIXES = ['.zennotes/sync-state.json', '.zennotes/note-meta-cache'] + +export function isNeverSync(path: string): boolean { + return NEVER_SYNC_PREFIXES.some((p) => path === p || path.startsWith(p)) +} + +export function reconcile( + local: Map<string, FileState>, + remote: Map<string, FileState>, + base: Map<string, BaseEntry> +): SyncAction[] { + const paths = new Set<string>([...local.keys(), ...remote.keys(), ...base.keys()]) + const actions: SyncAction[] = [] + + for (const path of paths) { + if (isNeverSync(path)) continue + const L = local.get(path)?.hash ?? null + const R = remote.get(path)?.hash ?? null + const B = base.get(path)?.contentHash ?? null + + if (L !== null && R !== null) { + if (L === B && R === B) continue // unchanged both sides + if (L !== B && R === B) { + actions.push({ kind: 'push', path }) + continue + } + if (L === B && R !== B) { + actions.push({ kind: 'pull', path }) + continue + } + // both changed (or never synced) + if (L === R) { + actions.push({ kind: 'converge', path }) + continue + } + actions.push({ + kind: 'conflict', + path, + reason: B === null ? 'first-sync-clash' : 'both-edited' + }) + continue + } + + if (L !== null && R === null) { + if (B === null) { + actions.push({ kind: 'push', path }) // new local + continue + } + if (L === B) { + actions.push({ kind: 'deleteLocal', path }) // deleted remotely, local unchanged + continue + } + actions.push({ kind: 'conflict', path, reason: 'edit-vs-delete' }) + continue + } + + if (L === null && R !== null) { + if (B === null) { + actions.push({ kind: 'pull', path }) // new remote + continue + } + if (R === B) { + actions.push({ kind: 'deleteRemote', path }) // deleted locally, remote unchanged + continue + } + actions.push({ kind: 'conflict', path, reason: 'delete-vs-edit' }) + continue + } + + // L === null && R === null + if (B !== null) actions.push({ kind: 'forget', path }) // deleted on both sides + } + + return actions +} diff --git a/apps/desktop/src/main/vault.ts b/apps/desktop/src/main/vault.ts index 7f21681d..5e2e74f1 100644 --- a/apps/desktop/src/main/vault.ts +++ b/apps/desktop/src/main/vault.ts @@ -40,7 +40,8 @@ import { VaultTextSearchBackendResolved, VaultTextSearchToolPaths, VaultTextSearchMatch, - VaultInfo + VaultInfo, + type WorkspaceMode } from '@shared/ipc' import { DEMO_TOUR_DIR } from '@shared/demo-tour' import { @@ -255,10 +256,24 @@ export interface PersistedRemoteWorkspaceProfile extends PersistedRemoteWorkspac export type PersistedLocalVault = LocalVaultEntry +export type SyncConflictPolicy = 'keep-both' | 'last-write-wins' +export type SyncAssetMode = 'lazy' | 'full' + +/** A local vault folder bound to a server profile for bidirectional sync. */ +export interface PersistedSyncedVault { + root: string + /** id into remoteWorkspaceProfiles — the server hub this folder syncs with. */ + serverProfileId: string + conflictPolicy: SyncConflictPolicy + assetMode: SyncAssetMode + lastSyncAt: number | null +} + export interface PersistedConfig { - workspaceMode: 'local' | 'remote' + workspaceMode: WorkspaceMode vaultRoot: string | null localVaults: PersistedLocalVault[] + syncedVaults: PersistedSyncedVault[] remoteWorkspace: PersistedRemoteWorkspaceConfig | null remoteWorkspaceProfileId: string | null remoteWorkspaceProfiles: PersistedRemoteWorkspaceProfile[] @@ -278,6 +293,7 @@ const DEFAULT_CONFIG: PersistedConfig = { workspaceMode: 'local', vaultRoot: null, localVaults: [], + syncedVaults: [], remoteWorkspace: null, remoteWorkspaceProfileId: null, remoteWorkspaceProfiles: [], @@ -399,17 +415,50 @@ function normalizePersistedConfig(value: unknown): PersistedConfig { localVaultsByRoot.set(entry.root, entry) } } + const normalizeSyncedVault = (raw: unknown): PersistedSyncedVault | null => { + if (!raw || typeof raw !== 'object') return null + const value = raw as Record<string, unknown> + const rawRoot = typeof value.root === 'string' ? value.root.trim() : '' + const serverProfileId = + typeof value.serverProfileId === 'string' ? value.serverProfileId.trim() : '' + if (!rawRoot || !serverProfileId) return null + // Drop bindings whose server profile no longer exists. + if (!remoteWorkspaceProfiles.some((p) => p.id === serverProfileId)) return null + return { + root: path.resolve(rawRoot), + serverProfileId, + conflictPolicy: value.conflictPolicy === 'last-write-wins' ? 'last-write-wins' : 'keep-both', + assetMode: value.assetMode === 'full' ? 'full' : 'lazy', + lastSyncAt: + typeof value.lastSyncAt === 'number' && Number.isFinite(value.lastSyncAt) + ? value.lastSyncAt + : null + } + } + const syncedVaultsByRoot = new Map<string, PersistedSyncedVault>() + const rawSyncedVaults = Array.isArray(candidate.syncedVaults) ? candidate.syncedVaults : [] + for (const raw of rawSyncedVaults) { + const entry = normalizeSyncedVault(raw) + if (entry) syncedVaultsByRoot.set(entry.root, entry) + } const quickCaptureHotkey = typeof candidate.quickCaptureHotkey === 'string' ? candidate.quickCaptureHotkey.trim() : DEFAULT_QUICK_CAPTURE_HOTKEY const quickCapturePinned = candidate.quickCapturePinned === true + const workspaceMode: WorkspaceMode = + candidate.workspaceMode === 'remote' + ? 'remote' + : candidate.workspaceMode === 'synced' + ? 'synced' + : 'local' return { - workspaceMode: candidate.workspaceMode === 'remote' ? 'remote' : 'local', + workspaceMode, vaultRoot: typeof candidate.vaultRoot === 'string' ? candidate.vaultRoot : null, localVaults: [...localVaultsByRoot.values()].sort( (a, b) => b.lastOpenedAt - a.lastOpenedAt || a.name.localeCompare(b.name) ), + syncedVaults: [...syncedVaultsByRoot.values()], remoteWorkspace: legacyRemoteWorkspace, remoteWorkspaceProfileId: typeof candidate.remoteWorkspaceProfileId === 'string' && diff --git a/apps/desktop/src/main/window-vaults.ts b/apps/desktop/src/main/window-vaults.ts index 7f6a65e2..c7279659 100644 --- a/apps/desktop/src/main/window-vaults.ts +++ b/apps/desktop/src/main/window-vaults.ts @@ -1,7 +1,13 @@ import path from 'node:path' import type { VaultChangeEvent, VaultInfo } from '@shared/ipc' -export type WindowWorkspaceMode = 'local' | 'remote' +export type WindowWorkspaceMode = 'local' | 'remote' | 'synced' + +/** Both `local` and `synced` are backed by real files on disk and share the + * per-root file watcher; `remote` is a thin client with no local files. */ +function isDiskBacked(mode: WindowWorkspaceMode | undefined): boolean { + return mode === 'local' || mode === 'synced' +} export interface VaultWatcherLike { start(root: string, onEvent: (ev: VaultChangeEvent) => void): void @@ -23,6 +29,8 @@ export interface WindowVaultRegistryOptions { makeWatcher: () => VaultWatcherLike invalidateVault: (root: string, ev: VaultChangeEvent) => void sendVaultChange: (windowId: number, ev: VaultChangeEvent) => void + /** Fan-out of every disk change under a watched root, for the sync engine. */ + onLocalChange?: (root: string, ev: VaultChangeEvent) => void } export class WindowVaultRegistry { @@ -32,15 +40,30 @@ export class WindowVaultRegistry { constructor(private readonly options: WindowVaultRegistryOptions) {} setLocalVault(windowId: number, vault: VaultInfo): void { + this.attachDiskVault(windowId, vault, 'local') + } + + /** A synced vault is disk-backed exactly like a local vault (same watcher); + * the only difference is the mode tag and an attached background sync engine + * (wired by the caller in index.ts). */ + setSyncedVault(windowId: number, vault: VaultInfo): void { + this.attachDiskVault(windowId, vault, 'synced') + } + + private attachDiskVault( + windowId: number, + vault: VaultInfo, + mode: 'local' | 'synced' + ): void { const root = normalizeRoot(vault.root) const previous = this.sessions.get(windowId) - if (previous?.mode === 'local' && previous.vault) { + if (previous && isDiskBacked(previous.mode) && previous.vault) { const previousRoot = normalizeRoot(previous.vault.root) if (previousRoot !== root) this.detachLocalWindow(windowId, previousRoot) } this.sessions.set(windowId, { - mode: 'local', + mode, vault: { ...vault, root } }) @@ -54,6 +77,7 @@ export class WindowVaultRegistry { } watcher.start(root, (ev) => { this.options.invalidateVault(root, ev) + this.options.onLocalChange?.(root, ev) this.sendLocalVaultChange(root, ev) }) this.localVaultWatches.set(root, registration) @@ -63,7 +87,7 @@ export class WindowVaultRegistry { setRemoteVault(windowId: number, vault: VaultInfo | null): void { const previous = this.sessions.get(windowId) - if (previous?.mode === 'local' && previous.vault) { + if (previous && isDiskBacked(previous.mode) && previous.vault) { this.detachLocalWindow(windowId, normalizeRoot(previous.vault.root)) } this.sessions.set(windowId, { @@ -74,7 +98,7 @@ export class WindowVaultRegistry { clearWindow(windowId: number): void { const previous = this.sessions.get(windowId) - if (previous?.mode === 'local' && previous.vault) { + if (previous && isDiskBacked(previous.mode) && previous.vault) { this.detachLocalWindow(windowId, normalizeRoot(previous.vault.root)) } this.sessions.delete(windowId) @@ -104,6 +128,10 @@ export class WindowVaultRegistry { return this.sessions.get(windowId)?.mode === 'remote' } + isSyncedWindow(windowId: number): boolean { + return this.sessions.get(windowId)?.mode === 'synced' + } + hasRemoteWindows(): boolean { for (const session of this.sessions.values()) { if (session.mode === 'remote') return true @@ -121,7 +149,7 @@ export class WindowVaultRegistry { isPathInsideWindowVault(windowId: number, absPath: string): boolean { const session = this.sessions.get(windowId) - if (session?.mode !== 'local' || !session.vault) return false + if (!isDiskBacked(session?.mode) || !session?.vault) return false return isPathInsideRoot(path.resolve(absPath), normalizeRoot(session.vault.root)) } diff --git a/apps/server/internal/httpserver/excalidraw_test.go b/apps/server/internal/httpserver/excalidraw_test.go index cf2d8ca9..1235d8d9 100644 --- a/apps/server/internal/httpserver/excalidraw_test.go +++ b/apps/server/internal/httpserver/excalidraw_test.go @@ -69,3 +69,50 @@ func TestCreateExcalidrawEndpoint(t *testing.T) { t.Errorf("created drawing %q not returned by /api/notes", created.Path) } } + +func TestSyncManifestEndpoint(t *testing.T) { + root := t.TempDir() + server, _ := newTestServer(t, config.Config{ + VaultPath: root, + DefaultVaultPath: root, + Bind: "127.0.0.1:7878", + AuthToken: "secret-token", + BrowseRoots: []string{root}, + }) + + // Requires auth. + unauth, err := http.Get(server.URL + "/api/sync/manifest") + if err != nil { + t.Fatal(err) + } + unauth.Body.Close() + if unauth.StatusCode != http.StatusUnauthorized { + t.Fatalf("manifest without auth = %d, want 401", unauth.StatusCode) + } + + jar := loginAndJar(t, server, "secret-token") + client := &http.Client{Jar: jar} + resp, err := client.Get(server.URL + "/api/sync/manifest") + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("manifest status = %d", resp.StatusCode) + } + var manifest []struct { + Path string `json:"path"` + Hash string `json:"hash"` + } + if err := json.NewDecoder(resp.Body).Decode(&manifest); err != nil { + t.Fatalf("decode manifest: %v", err) + } + if len(manifest) == 0 { + t.Fatal("manifest empty (expected at least the seeded welcome note)") + } + for _, e := range manifest { + if !strings.HasPrefix(e.Hash, "sha256:") { + t.Errorf("entry %s missing sha256 hash: %q", e.Path, e.Hash) + } + } +} diff --git a/apps/server/internal/httpserver/server.go b/apps/server/internal/httpserver/server.go index bd822a0c..2c36c1e4 100644 --- a/apps/server/internal/httpserver/server.go +++ b/apps/server/internal/httpserver/server.go @@ -170,6 +170,7 @@ func (s *Server) registerProtectedRoutes(r chi.Router) { r.Get("/fs/browse", s.browseDirectories) r.Get("/notes", s.listNotes) + r.Get("/sync/manifest", s.syncManifest) r.Get("/folders", s.listFolders) r.Get("/assets", s.listAssets) r.Get("/assets/exists", s.assetsExists) @@ -297,6 +298,7 @@ func (s *Server) capabilities(w http.ResponseWriter, _ *http.Request) { "supportsVaultSelection": true, "supportsDirectoryBrowsing": true, "supportsWatch": true, + "supportsSyncManifest": true, }) } @@ -495,6 +497,15 @@ func (s *Server) listNotes(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, notes) } +func (s *Server) syncManifest(w http.ResponseWriter, _ *http.Request) { + manifest, err := s.currentVault().Manifest() + if err != nil { + writeError(w, err) + return + } + writeJSON(w, http.StatusOK, manifest) +} + func (s *Server) listFolders(w http.ResponseWriter, _ *http.Request) { folders, err := s.currentVault().ListFolders() if err != nil { diff --git a/apps/server/internal/vault/types.go b/apps/server/internal/vault/types.go index b6439af8..0613f30b 100644 --- a/apps/server/internal/vault/types.go +++ b/apps/server/internal/vault/types.go @@ -140,6 +140,16 @@ type AssetMeta struct { UpdatedAt int64 `json:"updatedAt"` } +// ManifestEntry — one file in the sync manifest: vault-relative path, sha256 +// content hash ("sha256:<hex>"), size, and mtime (UnixMilli). Mirrors the +// client's SyncManifestEntry. +type ManifestEntry struct { + Path string `json:"path"` + Hash string `json:"hash"` + Size int64 `json:"size"` + Mtime int64 `json:"mtime"` +} + // ImportedAsset — mirrors shared/ipc.ts ImportedAsset. type ImportedAsset struct { Name string `json:"name"` diff --git a/apps/server/internal/vault/vault.go b/apps/server/internal/vault/vault.go index bb332f20..f7246fad 100644 --- a/apps/server/internal/vault/vault.go +++ b/apps/server/internal/vault/vault.go @@ -2,6 +2,7 @@ package vault import ( "crypto/rand" + "crypto/sha256" "encoding/hex" "encoding/json" "errors" @@ -26,10 +27,12 @@ const ( internalVaultDir = ".zennotes" vaultSettingsFile = "vault.json" noteMetaCacheFile = "note-meta-cache-v1.json" - noteMetaCacheVersion = 1 - noteCommentsDir = "comments" - noteCommentsSuffix = ".comments.json" - noteMetaReadLimit = 64 + // Bumped 1->2 when contentHash was added to cache entries; older caches + // (without hashes) are discarded by the version check on load. + noteMetaCacheVersion = 2 + noteCommentsDir = "comments" + noteCommentsSuffix = ".comments.json" + noteMetaReadLimit = 64 // formDirSuffix marks a database folder (`<Name>.base/`), a self-contained // folder holding data.csv, schema.json, and record-page notes. Databases are // a desktop-only feature; the server hides these folders (it neither serves @@ -183,9 +186,10 @@ type textSearchCache struct { } type noteMetaCacheEntry struct { - mtimeMs float64 - size int64 - meta NoteMeta + mtimeMs float64 + size int64 + meta NoteMeta + contentHash string // "sha256:<hex>" of the raw file bytes; "" if unknown } type persistedNoteMetaCache struct { @@ -194,10 +198,17 @@ type persistedNoteMetaCache struct { } type persistedNoteMetaEntry struct { - Path string `json:"path"` - MtimeMs float64 `json:"mtimeMs"` - Size int64 `json:"size"` - Meta NoteMeta `json:"meta"` + Path string `json:"path"` + MtimeMs float64 `json:"mtimeMs"` + Size int64 `json:"size"` + Meta NoteMeta `json:"meta"` + ContentHash string `json:"contentHash,omitempty"` +} + +// hashContent returns the sync content hash of raw file bytes. +func hashContent(body []byte) string { + sum := sha256.Sum256(body) + return "sha256:" + hex.EncodeToString(sum[:]) } func mtimeMs(info fs.FileInfo) float64 { @@ -688,9 +699,10 @@ func (v *Vault) hydratePersistedNoteMetaCache() { continue } entries[abs] = noteMetaCacheEntry{ - mtimeMs: entry.MtimeMs, - size: entry.Size, - meta: entry.Meta, + mtimeMs: entry.MtimeMs, + size: entry.Size, + meta: entry.Meta, + contentHash: entry.ContentHash, } } if len(entries) == 0 { @@ -737,10 +749,11 @@ func (v *Vault) persistNoteMetaCacheSnapshot(metas []NoteMeta) { metaCopy := cached.meta metaCopy.SiblingOrder = meta.SiblingOrder entries = append(entries, persistedNoteMetaEntry{ - Path: meta.Path, - MtimeMs: cached.mtimeMs, - Size: cached.size, - Meta: metaCopy, + Path: meta.Path, + MtimeMs: cached.mtimeMs, + Size: cached.size, + Meta: metaCopy, + ContentHash: cached.contentHash, }) } v.metaCacheMu.Unlock() @@ -789,16 +802,16 @@ func isSkippableWalkErr(err error) bool { return errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission) } -func (v *Vault) ListNotes() ([]NoteMeta, error) { - v.mu.RLock() - defer v.mu.RUnlock() - v.hydratePersistedNoteMetaCache() - - type noteFile struct { - folder NoteFolder - path string - } +type noteFile struct { + folder NoteFolder + path string +} +// collectNoteFiles walks every note/.excalidraw file across all top-level +// folders, applying the same skip rules as ListNotes (database folders, hidden +// dirs, primary-root hiding). Shared by ListNotes and Manifest. Callers hold +// v.mu (at least RLock). +func (v *Vault) collectNoteFiles() ([]noteFile, error) { files := []noteFile{} for _, folder := range AllFolders { folderRoot, err := v.folderRoot(folder) @@ -848,6 +861,18 @@ func (v *Vault) ListNotes() ([]NoteMeta, error) { return nil, err } } + return files, nil +} + +func (v *Vault) ListNotes() ([]NoteMeta, error) { + v.mu.RLock() + defer v.mu.RUnlock() + v.hydratePersistedNoteMetaCache() + + files, err := v.collectNoteFiles() + if err != nil { + return nil, err + } results := make([]NoteMeta, len(files)) ok := make([]bool, len(files)) @@ -888,6 +913,103 @@ func (v *Vault) ListNotes() ([]NoteMeta, error) { return out, nil } +// Manifest returns a content-addressed listing of every note/.excalidraw file: +// path + sha256 content hash + size + mtime. The sync client diffs this against +// its base snapshot to decide pushes/pulls without downloading bodies. Hashes +// are cached in the note-meta-cache (keyed by mtime+size), so an unchanged vault +// answers from stat() calls alone. +func (v *Vault) Manifest() ([]ManifestEntry, error) { + v.mu.RLock() + defer v.mu.RUnlock() + v.hydratePersistedNoteMetaCache() + + files, err := v.collectNoteFiles() + if err != nil { + return nil, err + } + + hashes := make([]string, len(files)) + sizes := make([]int64, len(files)) + mtimes := make([]int64, len(files)) + okFlags := make([]bool, len(files)) + + limit := noteMetaReadLimit + if len(files) < limit { + limit = len(files) + } + sem := make(chan struct{}, limit) + var wg sync.WaitGroup + for index, file := range files { + wg.Add(1) + go func(index int, file noteFile) { + defer wg.Done() + sem <- struct{}{} + defer func() { <-sem }() + info, err := os.Stat(file.path) + if err != nil { + return + } + h := v.cachedContentHash(file.path, info) + if h == "" { + return + } + hashes[index] = h + sizes[index] = info.Size() + mtimes[index] = info.ModTime().UnixMilli() + okFlags[index] = true + }(index, file) + } + wg.Wait() + + out := make([]ManifestEntry, 0, len(files)) + for index, file := range files { + if !okFlags[index] { + continue + } + rel, err := filepath.Rel(v.root, file.path) + if err != nil { + continue + } + out = append(out, ManifestEntry{ + Path: filepath.ToSlash(rel), + Hash: hashes[index], + Size: sizes[index], + Mtime: mtimes[index], + }) + } + return out, nil +} + +// cachedContentHash returns the sha256 content hash of a file, reusing the +// note-meta-cache when the file's mtime+size are unchanged. On a miss it reads +// and hashes the file once and stores a hash-only cache entry (zero meta, so +// readMeta still recomputes full metadata for a genuinely changed file). +func (v *Vault) cachedContentHash(abs string, info fs.FileInfo) string { + statMtimeMs := mtimeMs(info) + v.metaCacheMu.Lock() + cached, ok := v.metaCache[abs] + if ok && sameMtimeMs(cached.mtimeMs, statMtimeMs) && cached.size == info.Size() && cached.contentHash != "" { + h := cached.contentHash + v.metaCacheMu.Unlock() + return h + } + v.metaCacheMu.Unlock() + + body, err := os.ReadFile(abs) + if err != nil { + return "" + } + h := hashContent(body) + v.metaCacheMu.Lock() + v.metaCache[abs] = noteMetaCacheEntry{ + mtimeMs: statMtimeMs, + size: info.Size(), + contentHash: h, + } + v.metaCacheMu.Unlock() + return h +} + func assignSiblingOrder[T any](list []T, key func(T) string, set func(*T, int)) { counts := map[string]int{} for i := range list { @@ -1115,9 +1237,10 @@ func (v *Vault) readMeta(folder NoteFolder, abs string) (NoteMeta, error) { meta := buildNoteMeta(relPosix, title, folder, info, bodyStr) v.metaCacheMu.Lock() v.metaCache[abs] = noteMetaCacheEntry{ - mtimeMs: statMtimeMs, - size: info.Size(), - meta: meta, + mtimeMs: statMtimeMs, + size: info.Size(), + meta: meta, + contentHash: hashContent(body), } v.metaCacheMu.Unlock() return meta, nil diff --git a/apps/server/internal/vault/vault_test.go b/apps/server/internal/vault/vault_test.go index a168fa1b..ba2fa53b 100644 --- a/apps/server/internal/vault/vault_test.go +++ b/apps/server/internal/vault/vault_test.go @@ -763,6 +763,99 @@ func hasNotePath(notes []NoteMeta, path string) bool { return false } +func TestManifestHashesStableAndChange(t *testing.T) { + root := t.TempDir() + v, err := New(root, Options{}) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(root, "inbox", "A.md"), []byte("# A"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(root, "inbox", "B.excalidraw"), []byte(`{"type":"excalidraw"}`), 0o600); err != nil { + t.Fatal(err) + } + + m1, err := v.Manifest() + if err != nil { + t.Fatal(err) + } + if len(m1) < 2 { + t.Fatalf("manifest = %d entries, want >= 2: %+v", len(m1), m1) + } + byPath := map[string]ManifestEntry{} + for _, e := range m1 { + if !strings.HasPrefix(e.Hash, "sha256:") { + t.Errorf("entry %s hash missing sha256 prefix: %q", e.Path, e.Hash) + } + if e.Size == 0 { + t.Errorf("entry %s has zero size", e.Path) + } + byPath[e.Path] = e + } + if _, ok := byPath["inbox/A.md"]; !ok { + t.Error("manifest missing inbox/A.md") + } + if _, ok := byPath["inbox/B.excalidraw"]; !ok { + t.Error("manifest missing inbox/B.excalidraw") + } + + // Re-running yields identical hashes (cache hit, no drift). + m2, err := v.Manifest() + if err != nil { + t.Fatal(err) + } + for _, e := range m2 { + if byPath[e.Path].Hash != e.Hash { + t.Errorf("hash for %s changed across runs: %q vs %q", e.Path, byPath[e.Path].Hash, e.Hash) + } + } + + // Editing a note changes only its hash. + if err := os.WriteFile(filepath.Join(root, "inbox", "A.md"), []byte("# A edited"), 0o600); err != nil { + t.Fatal(err) + } + m3, err := v.Manifest() + if err != nil { + t.Fatal(err) + } + for _, e := range m3 { + if e.Path == "inbox/A.md" && e.Hash == byPath["inbox/A.md"].Hash { + t.Error("edited note kept the same hash") + } + if e.Path == "inbox/B.excalidraw" && e.Hash != byPath["inbox/B.excalidraw"].Hash { + t.Error("untouched drawing hash changed") + } + } +} + +func TestManifestExcludesDatabasesAndInternal(t *testing.T) { + root := t.TempDir() + v, err := New(root, Options{}) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(root, "inbox", "Keep.md"), []byte("# Keep"), 0o600); err != nil { + t.Fatal(err) + } + base := filepath.Join(root, "inbox", "DB.base") + if err := os.MkdirAll(base, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(base, "Rec.md"), []byte("# Rec"), 0o600); err != nil { + t.Fatal(err) + } + m, err := v.Manifest() + if err != nil { + t.Fatal(err) + } + for _, e := range m { + if strings.Contains(e.Path, ".base") || strings.HasPrefix(e.Path, ".zennotes") { + t.Errorf("manifest leaked excluded path: %s", e.Path) + } + } +} + func TestFavoritesRoundTripAndDedupe(t *testing.T) { root := t.TempDir() v, err := New(root, Options{}) diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index 2b03d7e2..04738022 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -2111,7 +2111,7 @@ function findMatchingRemoteProfile( } function workspaceModeFrom(info: RemoteWorkspaceInfo | null): WorkspaceMode { - return info?.mode === 'remote' ? 'remote' : 'local' + return info?.mode === 'remote' || info?.mode === 'synced' ? info.mode : 'local' } async function ensureWebServerSession( diff --git a/packages/bridge-contract/src/ipc.ts b/packages/bridge-contract/src/ipc.ts index 236da613..489a91f0 100644 --- a/packages/bridge-contract/src/ipc.ts +++ b/packages/bridge-contract/src/ipc.ts @@ -525,6 +525,8 @@ export interface ServerCapabilities { supportsVaultSelection: boolean supportsDirectoryBrowsing: boolean supportsWatch: boolean + /** Server exposes GET /api/sync/manifest (content hashes) for synced vaults. */ + supportsSyncManifest?: boolean } export interface ServerSessionStatus { @@ -533,7 +535,12 @@ export interface ServerSessionStatus { supportsSessionLogin: boolean } -export type WorkspaceMode = 'local' | 'remote' +/** + * `local` = files on disk, no server. `remote` = thin HTTP client, no local + * copy. `synced` = a local vault that bidirectionally syncs with a server hub + * (behaves like `local` for all file ops; a background engine replicates it). + */ +export type WorkspaceMode = 'local' | 'remote' | 'synced' export interface RemoteWorkspaceInfo { mode: WorkspaceMode From 757098a98499dcf0b08e32bbe2a2eedd6f8eb710 Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Wed, 17 Jun 2026 16:35:11 -0500 Subject: [PATCH 54/66] feat(sync): the bidirectional sync engine (M3 I/O) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds the background replicator on top of the pure reconcile core: - sync-state.ts — per-device base snapshot at .zennotes/sync-state.json (atomic writes, intent log for crash recovery). - scan-local.ts — walks/hashes local note/.excalidraw files, mirroring the server's collectNoteFiles (inbox/root + quick/archive/trash, excluding databases, hidden, .zennotes). - conflict.ts — keep-both conflict-copy naming ("Note (conflict <dev> <ts>).md"). - engine.ts — debounced, single-flight reconcile loop triggered by the local watcher fan-out, the server /api/watch WS, and a periodic backstop; executes push/pull/delete/converge/conflict via the existing RemoteServerClient with atomic, path-safe local writes. Offline/error/conflict status surface. - RemoteServerClient.getSyncManifest(); SyncManifestEntry + SyncStatus types. Verified: 22 unit tests (reconcile decision table + an end-to-end engine test against an in-memory server over real local files: first-sync union merge, pull, push, delete-propagation, keep-both conflict with no data loss, idempotency). Not yet wired into app startup (next: activation + status UI). --- apps/desktop/src/main/remote/server-client.ts | 5 + apps/desktop/src/main/sync/conflict.ts | 41 +++ apps/desktop/src/main/sync/engine.test.ts | 155 +++++++++ apps/desktop/src/main/sync/engine.ts | 308 ++++++++++++++++++ apps/desktop/src/main/sync/scan-local.ts | 96 ++++++ apps/desktop/src/main/sync/sync-state.ts | 87 +++++ packages/bridge-contract/src/ipc.ts | 23 ++ 7 files changed, 715 insertions(+) create mode 100644 apps/desktop/src/main/sync/conflict.ts create mode 100644 apps/desktop/src/main/sync/engine.test.ts create mode 100644 apps/desktop/src/main/sync/engine.ts create mode 100644 apps/desktop/src/main/sync/scan-local.ts create mode 100644 apps/desktop/src/main/sync/sync-state.ts diff --git a/apps/desktop/src/main/remote/server-client.ts b/apps/desktop/src/main/remote/server-client.ts index cacfd990..7e31ca9b 100644 --- a/apps/desktop/src/main/remote/server-client.ts +++ b/apps/desktop/src/main/remote/server-client.ts @@ -9,6 +9,7 @@ import type { NoteFolder, NoteMeta, ServerCapabilities, + SyncManifestEntry, VaultChangeEvent, VaultDemoTourResult, VaultInfo, @@ -72,6 +73,10 @@ export class RemoteServerClient { return this.jsonRequest<NoteMeta[]>('/api/notes') } + async getSyncManifest(): Promise<SyncManifestEntry[]> { + return this.jsonRequest<SyncManifestEntry[]>('/api/sync/manifest') + } + async listFolders(): Promise<FolderEntry[]> { return this.jsonRequest<FolderEntry[]>('/api/folders') } diff --git a/apps/desktop/src/main/sync/conflict.ts b/apps/desktop/src/main/sync/conflict.ts new file mode 100644 index 00000000..67e59654 --- /dev/null +++ b/apps/desktop/src/main/sync/conflict.ts @@ -0,0 +1,41 @@ +/** + * Conflict resolution policy. The default `keep-both` never overwrites: the + * losing version is written next to the original as a conflict copy. The + * `last-write-wins` policy is scaffolded for a future opt-in. + */ + +export type ConflictPolicy = 'keep-both' | 'last-write-wins' + +/** Two-digit zero-padded. */ +function pad(n: number): string { + return n < 10 ? `0${n}` : String(n) +} + +/** `YYYY-MM-DD HHmmss` in local time, for conflict-copy filenames. */ +export function formatConflictTimestamp(now: Date): string { + return ( + `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ` + + `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}` + ) +} + +/** + * Build the conflict-copy path for `originalPath`, preserving its folder and + * extension: `inbox/Note.md` → `inbox/Note (conflict 1a2b3c4d 2026-06-17 150405).md`. + */ +export function conflictCopyPath(originalPath: string, deviceId: string, now: Date): string { + const slash = originalPath.lastIndexOf('/') + const dot = originalPath.lastIndexOf('.') + const hasExt = dot > slash + const ext = hasExt ? originalPath.slice(dot) : '' + const stem = hasExt ? originalPath.slice(0, dot) : originalPath + const device = deviceId.slice(0, 8) + return `${stem} (conflict ${device} ${formatConflictTimestamp(now)})${ext}` +} + +/** A conflict copy this engine produced (so it never conflicts with itself). */ +const CONFLICT_MARKER = /\(conflict [0-9a-f]{8} \d{4}-\d{2}-\d{2} \d{6}\)/ + +export function isConflictCopyPath(path: string): boolean { + return CONFLICT_MARKER.test(path) +} diff --git a/apps/desktop/src/main/sync/engine.test.ts b/apps/desktop/src/main/sync/engine.test.ts new file mode 100644 index 00000000..b7bc0901 --- /dev/null +++ b/apps/desktop/src/main/sync/engine.test.ts @@ -0,0 +1,155 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { promises as fs } from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import type { RemoteServerClient } from '../remote/server-client' +import { SyncEngine } from './engine' +import { hashBytes } from './scan-local' + +/** Minimal in-memory stand-in for the Go server. */ +class FakeServer { + files = new Map<string, string>() + baseUrl = 'http://fake' + + async getSyncManifest(): Promise<{ path: string; hash: string; size: number; mtime: number }[]> { + return [...this.files].map(([p, body]) => ({ + path: p, + hash: hashBytes(Buffer.from(body, 'utf8')), + size: Buffer.byteLength(body), + mtime: 0 + })) + } + async readNote(p: string): Promise<{ path: string; body: string }> { + return { path: p, body: this.files.get(p) ?? '' } + } + async writeNote(p: string, body: string): Promise<unknown> { + this.files.set(p, body) + return {} + } + async deleteNote(p: string): Promise<void> { + this.files.delete(p) + } + watchVaultChanges(): () => void { + return () => {} + } +} + +function makeEngine(root: string, server: FakeServer): SyncEngine { + return new SyncEngine({ + root, + client: server as unknown as RemoteServerClient, + serverProfileId: 'p1', + conflictPolicy: 'keep-both' + }) +} + +async function writeLocal(root: string, rel: string, body: string): Promise<void> { + const abs = path.join(root, rel) + await fs.mkdir(path.dirname(abs), { recursive: true }) + await fs.writeFile(abs, body, 'utf8') +} +async function readLocal(root: string, rel: string): Promise<string | null> { + try { + return await fs.readFile(path.join(root, rel), 'utf8') + } catch { + return null + } +} +async function localNotePaths(root: string): Promise<string[]> { + const out: string[] = [] + const walk = async (dir: string): Promise<void> => { + for (const e of await fs.readdir(dir, { withFileTypes: true })) { + if (e.name.startsWith('.')) continue + const full = path.join(dir, e.name) + if (e.isDirectory()) await walk(full) + else if (e.name.endsWith('.md')) out.push(path.relative(root, full).split(path.sep).join('/')) + } + } + await walk(root) + return out.sort() +} + +describe('SyncEngine — end to end (fake server, real local fs)', () => { + let root: string + beforeEach(async () => { + root = await fs.mkdtemp(path.join(os.tmpdir(), 'zen-sync-')) + await fs.mkdir(path.join(root, '.zennotes'), { recursive: true }) + await fs.mkdir(path.join(root, 'inbox'), { recursive: true }) + }) + afterEach(async () => { + await fs.rm(root, { recursive: true, force: true }) + }) + + it('first sync = union merge of local + server', async () => { + const server = new FakeServer() + server.files.set('inbox/FromServer.md', 'server note') + await writeLocal(root, 'inbox/FromLocal.md', 'local note') + + await makeEngine(root, server).runOnce() + + // Local gained the server note; server gained the local note. + expect(await readLocal(root, 'inbox/FromServer.md')).toBe('server note') + expect(server.files.get('inbox/FromLocal.md')).toBe('local note') + expect(await localNotePaths(root)).toEqual(['inbox/FromLocal.md', 'inbox/FromServer.md']) + }) + + it('pulls a note added on the server after first sync', async () => { + const server = new FakeServer() + const engine = makeEngine(root, server) + await engine.runOnce() // establish base (empty) + server.files.set('inbox/New.md', 'remote-only') + await engine.runOnce() + expect(await readLocal(root, 'inbox/New.md')).toBe('remote-only') + }) + + it('pushes a local edit to the server', async () => { + const server = new FakeServer() + server.files.set('inbox/A.md', 'v1') + const engine = makeEngine(root, server) + await engine.runOnce() // pulls A=v1 to local, base set + await writeLocal(root, 'inbox/A.md', 'v2') + await engine.runOnce() + expect(server.files.get('inbox/A.md')).toBe('v2') + }) + + it('propagates a local delete to the server', async () => { + const server = new FakeServer() + server.files.set('inbox/A.md', 'v1') + const engine = makeEngine(root, server) + await engine.runOnce() // local now has A + await fs.rm(path.join(root, 'inbox/A.md')) + await engine.runOnce() + expect(server.files.has('inbox/A.md')).toBe(false) + }) + + it('keeps both on a concurrent conflicting edit (never loses data)', async () => { + const server = new FakeServer() + server.files.set('inbox/A.md', 'base') + const engine = makeEngine(root, server) + await engine.runOnce() // synced: local A = base + // Edit differently on both sides before the next sync. + await writeLocal(root, 'inbox/A.md', 'local edit') + server.files.set('inbox/A.md', 'server edit') + await engine.runOnce() + + // Local content is canonical at the path; the server edit is preserved as a copy. + expect(await readLocal(root, 'inbox/A.md')).toBe('local edit') + const paths = await localNotePaths(root) + const conflictCopy = paths.find((p) => p.includes('(conflict')) + expect(conflictCopy).toBeTruthy() + expect(await readLocal(root, conflictCopy as string)).toBe('server edit') + // Both files exist on the server too (converged). + expect(server.files.get('inbox/A.md')).toBe('local edit') + expect(server.files.get(conflictCopy as string)).toBe('server edit') + }) + + it('is idempotent: a second sync with no changes does nothing', async () => { + const server = new FakeServer() + server.files.set('inbox/A.md', 'x') + const engine = makeEngine(root, server) + await engine.runOnce() + const before = new Map(server.files) + await engine.runOnce() + expect([...server.files]).toEqual([...before]) + }) +}) diff --git a/apps/desktop/src/main/sync/engine.ts b/apps/desktop/src/main/sync/engine.ts new file mode 100644 index 00000000..55c39b66 --- /dev/null +++ b/apps/desktop/src/main/sync/engine.ts @@ -0,0 +1,308 @@ +import { promises as fs } from 'node:fs' +import path from 'node:path' +import type { SyncStatus, VaultChangeEvent } from '@shared/ipc' +import type { RemoteServerClient } from '../remote/server-client' +import { + reconcile, + type BaseEntry, + type ConflictReason, + type FileState, + type SyncAction +} from './reconcile' +import { loadSyncState, saveSyncState, type SyncState } from './sync-state' +import { conflictCopyPath, type ConflictPolicy } from './conflict' +import { hashBytes, readPrimaryNotesAtRoot, scanLocalVault } from './scan-local' + +export interface SyncEngineOptions { + root: string + client: RemoteServerClient + serverProfileId: string + conflictPolicy: ConflictPolicy + onStatus?: (status: SyncStatus) => void +} + +const DEBOUNCE_MS = 750 +const PERIODIC_MS = 5 * 60 * 1000 + +/** + * The background sync engine for one synced vault. Triggered by local file + * changes (chokidar fan-out), server `/api/watch` events, and a periodic + * backstop; runs a debounced, single-flight three-way reconcile and replicates + * the resulting actions via the existing RemoteServerClient. Pure decisions live + * in reconcile.ts; this file is the I/O + scheduling shell. + */ +export class SyncEngine { + private state: SyncState | null = null + private status: SyncStatus = { + kind: 'idle', + pendingPush: 0, + pendingPull: 0, + conflicts: [], + lastSyncAt: 0, + lastError: null + } + private running = false + private syncing = false + private rerun = false + private debounceTimer: ReturnType<typeof setTimeout> | null = null + private periodicTimer: ReturnType<typeof setInterval> | null = null + private stopWatch: (() => void) | null = null + + constructor(private readonly opts: SyncEngineOptions) {} + + get root(): string { + return this.opts.root + } + + async start(): Promise<void> { + if (this.running) return + this.running = true + this.state = await loadSyncState( + this.opts.root, + this.opts.serverProfileId, + this.opts.client.baseUrl + ) + this.status = { ...this.status, lastSyncAt: this.state.lastSyncAt } + this.stopWatch = this.opts.client.watchVaultChanges(() => this.schedule()) + this.periodicTimer = setInterval(() => this.schedule(), PERIODIC_MS) + this.schedule(true) + } + + stop(): void { + this.running = false + this.stopWatch?.() + this.stopWatch = null + if (this.debounceTimer) clearTimeout(this.debounceTimer) + if (this.periodicTimer) clearInterval(this.periodicTimer) + this.debounceTimer = null + this.periodicTimer = null + } + + getStatus(): SyncStatus { + return this.status + } + + /** Load state (if needed) and run a single reconcile. Used by syncNow paths + * and tests; the scheduler calls reconcileOnce directly via runSync. */ + async runOnce(): Promise<void> { + if (!this.state) { + this.state = await loadSyncState( + this.opts.root, + this.opts.serverProfileId, + this.opts.client.baseUrl + ) + } + await this.reconcileOnce() + } + + /** Fan-out target for local disk changes under this vault root. */ + onLocalChange(_ev: VaultChangeEvent): void { + this.schedule() + } + + /** Force an immediate reconcile (manual "Sync now"). */ + syncNow(): void { + this.schedule(true) + } + + private schedule(immediate = false): void { + if (!this.running) return + if (this.debounceTimer) clearTimeout(this.debounceTimer) + this.debounceTimer = setTimeout(() => void this.runSync(), immediate ? 0 : DEBOUNCE_MS) + } + + private setStatus(patch: Partial<SyncStatus>): void { + this.status = { ...this.status, ...patch } + this.opts.onStatus?.(this.status) + } + + private async runSync(): Promise<void> { + if (!this.running) return + if (this.syncing) { + this.rerun = true + return + } + this.syncing = true + this.setStatus({ kind: 'syncing', lastError: null }) + try { + await this.reconcileOnce() + this.setStatus({ kind: 'idle', lastSyncAt: Date.now(), pendingPush: 0, pendingPull: 0 }) + } catch (err) { + const message = err instanceof Error ? err.message : String(err) + this.setStatus({ kind: isOffline(message) ? 'offline' : 'error', lastError: message }) + } finally { + this.syncing = false + if (this.rerun) { + this.rerun = false + this.schedule() + } + } + } + + private async reconcileOnce(): Promise<void> { + const state = this.state + if (!state) return + const primaryAtRoot = await readPrimaryNotesAtRoot(this.opts.root) + const [localMap, remoteList] = await Promise.all([ + scanLocalVault(this.opts.root, primaryAtRoot), + this.opts.client.getSyncManifest() + ]) + const remoteMap = new Map<string, FileState>( + remoteList.map((e) => [e.path, { hash: e.hash, size: e.size }]) + ) + const baseMap = new Map<string, BaseEntry>( + Object.entries(state.entries).map(([p, e]) => [p, { contentHash: e.contentHash, size: e.size }]) + ) + + const actions = reconcile(localMap, remoteMap, baseMap) + let pushes = 0 + let pulls = 0 + const conflicts: string[] = [] + for (const action of actions) { + try { + await this.execute(action, localMap, remoteMap, state, conflicts) + if (action.kind === 'push') pushes++ + if (action.kind === 'pull') pulls++ + } catch (err) { + // One file failing (e.g. oversize, gone) must not abort the whole sync. + this.setStatus({ lastError: `${action.kind} ${action.path}: ${(err as Error).message}` }) + } + } + state.lastSyncAt = Date.now() + await saveSyncState(this.opts.root, state) + if (conflicts.length) { + this.setStatus({ conflicts: dedupe([...this.status.conflicts, ...conflicts]) }) + } + this.status = { ...this.status, pendingPush: pushes, pendingPull: pulls } + } + + private async execute( + action: SyncAction, + localMap: Map<string, FileState>, + remoteMap: Map<string, FileState>, + state: SyncState, + conflicts: string[] + ): Promise<void> { + const client = this.opts.client + const root = this.opts.root + switch (action.kind) { + case 'push': { + const body = await readLocal(root, action.path) + await client.writeNote(action.path, body) + setBase(state, action.path, localMap.get(action.path)) + break + } + case 'pull': { + const content = await client.readNote(action.path) + await writeLocalAtomic(root, action.path, content.body) + setBase(state, action.path, remoteMap.get(action.path)) + break + } + case 'deleteRemote': { + await client.deleteNote(action.path) + delete state.entries[action.path] + break + } + case 'deleteLocal': { + await removeLocal(root, action.path) + delete state.entries[action.path] + break + } + case 'converge': { + setBase(state, action.path, localMap.get(action.path) ?? remoteMap.get(action.path)) + break + } + case 'forget': { + delete state.entries[action.path] + break + } + case 'conflict': { + await this.resolveConflict(action.path, action.reason, localMap, remoteMap, state, conflicts) + break + } + } + } + + private async resolveConflict( + p: string, + reason: ConflictReason, + localMap: Map<string, FileState>, + remoteMap: Map<string, FileState>, + state: SyncState, + conflicts: string[] + ): Promise<void> { + const client = this.opts.client + const root = this.opts.root + // delete-vs-edit: resurrect from the server (the edit wins over the delete). + if (reason === 'delete-vs-edit') { + const content = await client.readNote(p) + await writeLocalAtomic(root, p, content.body) + setBase(state, p, remoteMap.get(p)) + conflicts.push(p) + return + } + // edit-vs-delete: resurrect to the server (the edit wins over the delete). + if (reason === 'edit-vs-delete') { + const body = await readLocal(root, p) + await client.writeNote(p, body) + setBase(state, p, localMap.get(p)) + conflicts.push(p) + return + } + // both-edited / first-sync-clash: keep both. Local stays canonical; the + // server's differing version is written next to it as a conflict copy. + const remoteContent = await client.readNote(p) + const copyPath = conflictCopyPath(p, state.deviceId, new Date()) + const remoteBytes = Buffer.from(remoteContent.body, 'utf8') + await writeLocalAtomic(root, copyPath, remoteContent.body) + await client.writeNote(copyPath, remoteContent.body) + const localBody = await readLocal(root, p) + await client.writeNote(p, localBody) // server's path now matches local (canonical) + setBase(state, p, localMap.get(p)) + state.entries[copyPath] = { contentHash: hashBytes(remoteBytes), size: remoteBytes.length } + conflicts.push(copyPath) + } +} + +function setBase(state: SyncState, p: string, entry: FileState | undefined): void { + if (!entry) { + delete state.entries[p] + return + } + state.entries[p] = { contentHash: entry.hash, size: entry.size } +} + +function dedupe(items: string[]): string[] { + return [...new Set(items)] +} + +function isOffline(message: string): boolean { + return /Could not connect|ECONN|fetch failed|ENOTFOUND|ETIMEDOUT|network/i.test(message) +} + +// --- local file helpers (path-safe, atomic writes) --- + +function safeAbs(root: string, rel: string): string { + const abs = path.resolve(root, rel) + const base = path.resolve(root) + if (abs !== base && !abs.startsWith(base + path.sep)) { + throw new Error(`refusing path outside vault: ${rel}`) + } + return abs +} + +async function readLocal(root: string, rel: string): Promise<string> { + return fs.readFile(safeAbs(root, rel), 'utf8') +} + +async function writeLocalAtomic(root: string, rel: string, body: string): Promise<void> { + const abs = safeAbs(root, rel) + await fs.mkdir(path.dirname(abs), { recursive: true }) + const tmp = `${abs}.${process.pid}.${Date.now()}.synctmp` + await fs.writeFile(tmp, body, 'utf8') + await fs.rename(tmp, abs) +} + +async function removeLocal(root: string, rel: string): Promise<void> { + await fs.rm(safeAbs(root, rel), { force: true }) +} diff --git a/apps/desktop/src/main/sync/scan-local.ts b/apps/desktop/src/main/sync/scan-local.ts new file mode 100644 index 00000000..116bdd36 --- /dev/null +++ b/apps/desktop/src/main/sync/scan-local.ts @@ -0,0 +1,96 @@ +import { promises as fs } from 'node:fs' +import type { Dirent } from 'node:fs' +import path from 'node:path' +import { createHash } from 'node:crypto' +import { isNeverSync, type FileState } from './reconcile' + +/** sha256 content hash matching the server's hashContent(). */ +export function hashBytes(body: Buffer): string { + return 'sha256:' + createHash('sha256').update(body).digest('hex') +} + +// Top-level names hidden from the vault-root walk in root mode (mirrors the Go +// shouldHidePrimaryRootName) so they aren't double-listed. +const HIDDEN_PRIMARY_ROOT_NAMES = new Set([ + 'quick', + 'archive', + 'trash', + 'assets', + 'attachements', + '_assets', + '.zennotes' +]) +const TOP_FOLDERS = ['quick', 'archive', 'trash'] + +function isNoteFile(name: string): boolean { + const lower = name.toLowerCase() + return lower.endsWith('.md') || lower.endsWith('.excalidraw') +} + +function isFormDir(name: string): boolean { + return name.toLowerCase().endsWith('.base') +} + +async function walkFolder( + vaultRoot: string, + folderRoot: string, + primaryRoot: boolean, + out: Map<string, FileState> +): Promise<void> { + let entries: Dirent[] + try { + entries = await fs.readdir(folderRoot, { withFileTypes: true }) + } catch { + return // folder may not exist (e.g. no quick/ yet) + } + for (const entry of entries) { + const name = entry.name + if (name.startsWith('.')) continue // hidden, incl. .zennotes + if (primaryRoot && HIDDEN_PRIMARY_ROOT_NAMES.has(name.toLowerCase())) continue + const full = path.join(folderRoot, name) + if (entry.isDirectory()) { + if (isFormDir(name)) continue // database folder — synced as a unit later + await walkFolder(vaultRoot, full, false, out) + continue + } + if (!isNoteFile(name)) continue + const rel = path.relative(vaultRoot, full).split(path.sep).join('/') + if (isNeverSync(rel)) continue + try { + const body = await fs.readFile(full) + out.set(rel, { hash: hashBytes(body), size: body.length }) + } catch { + // skip unreadable file + } + } +} + +/** + * Walk every synced note/.excalidraw file in a vault and hash it, mirroring the + * server's collectNoteFiles: the primary notes area (root in root mode, else + * `inbox/`) plus `quick`/`archive`/`trash`, excluding databases, hidden files, + * and `.zennotes/`. + */ +export async function scanLocalVault( + root: string, + primaryNotesAtRoot: boolean +): Promise<Map<string, FileState>> { + const out = new Map<string, FileState>() + const inboxRoot = primaryNotesAtRoot ? root : path.join(root, 'inbox') + await walkFolder(root, inboxRoot, primaryNotesAtRoot, out) + for (const folder of TOP_FOLDERS) { + await walkFolder(root, path.join(root, folder), false, out) + } + return out +} + +/** Read primaryNotesLocation from the local vault settings; defaults to inbox. */ +export async function readPrimaryNotesAtRoot(root: string): Promise<boolean> { + try { + const raw = await fs.readFile(path.join(root, '.zennotes', 'vault.json'), 'utf8') + const parsed = JSON.parse(raw) as { primaryNotesLocation?: string } + return parsed.primaryNotesLocation === 'root' + } catch { + return false + } +} diff --git a/apps/desktop/src/main/sync/sync-state.ts b/apps/desktop/src/main/sync/sync-state.ts new file mode 100644 index 00000000..6011e315 --- /dev/null +++ b/apps/desktop/src/main/sync/sync-state.ts @@ -0,0 +1,87 @@ +import { promises as fs } from 'node:fs' +import path from 'node:path' +import { randomUUID } from 'node:crypto' + +/** Per-path base: the content + server state recorded at the last successful sync. */ +export interface SyncEntry { + /** sha256 of the bytes that were in sync (local == remote at that moment). */ + contentHash: string + size: number +} + +/** + * The base snapshot for a synced vault, persisted at + * `<root>/.zennotes/sync-state.json`. It is per-device and never itself synced. + * `pendingPull`/`pendingPush` form an intent log: paths whose transfer was + * started but not confirmed, re-derived from the manifest on the next run so a + * crash mid-sync can never be mistaken for a user edit. + */ +export interface SyncState { + version: number + deviceId: string + serverProfileId: string + baseUrl: string + lastSyncAt: number + entries: Record<string, SyncEntry> + pendingPull: string[] + pendingPush: string[] +} + +export const SYNC_STATE_VERSION = 1 + +export function syncStatePath(root: string): string { + return path.join(root, '.zennotes', 'sync-state.json') +} + +export function emptySyncState(serverProfileId: string, baseUrl: string): SyncState { + return { + version: SYNC_STATE_VERSION, + deviceId: randomUUID(), + serverProfileId, + baseUrl, + lastSyncAt: 0, + entries: {}, + pendingPull: [], + pendingPush: [] + } +} + +export async function loadSyncState( + root: string, + serverProfileId: string, + baseUrl: string +): Promise<SyncState> { + try { + const raw = await fs.readFile(syncStatePath(root), 'utf8') + const parsed = JSON.parse(raw) as Partial<SyncState> + if ( + parsed.version === SYNC_STATE_VERSION && + typeof parsed.deviceId === 'string' && + parsed.entries && + typeof parsed.entries === 'object' + ) { + return { + version: SYNC_STATE_VERSION, + deviceId: parsed.deviceId, + serverProfileId: parsed.serverProfileId ?? serverProfileId, + baseUrl: parsed.baseUrl ?? baseUrl, + lastSyncAt: typeof parsed.lastSyncAt === 'number' ? parsed.lastSyncAt : 0, + entries: parsed.entries as Record<string, SyncEntry>, + pendingPull: Array.isArray(parsed.pendingPull) ? parsed.pendingPull : [], + pendingPush: Array.isArray(parsed.pendingPush) ? parsed.pendingPush : [] + } + } + } catch { + // missing or invalid → fresh state (first sync = union merge) + } + return emptySyncState(serverProfileId, baseUrl) +} + +/** Atomically persist the base snapshot (temp file + rename). */ +export async function saveSyncState(root: string, state: SyncState): Promise<void> { + const target = syncStatePath(root) + await fs.mkdir(path.dirname(target), { recursive: true }) + const tmp = `${target}.${process.pid}.${Date.now()}.tmp` + await fs.writeFile(tmp, JSON.stringify(state), 'utf8') + await fs.rename(tmp, target) +} diff --git a/packages/bridge-contract/src/ipc.ts b/packages/bridge-contract/src/ipc.ts index 489a91f0..01313f05 100644 --- a/packages/bridge-contract/src/ipc.ts +++ b/packages/bridge-contract/src/ipc.ts @@ -529,6 +529,29 @@ export interface ServerCapabilities { supportsSyncManifest?: boolean } +/** One file in the server sync manifest. Mirrors the Go ManifestEntry. */ +export interface SyncManifestEntry { + path: string + hash: string + size: number + mtime: number +} + +export type SyncStatusKind = 'idle' | 'syncing' | 'offline' | 'error' + +/** Live sync status for a synced vault, pushed to the renderer. */ +export interface SyncStatus { + kind: SyncStatusKind + /** Files changed locally still to push (best-effort, from the last run). */ + pendingPush: number + /** Files changed remotely still to pull (best-effort, from the last run). */ + pendingPull: number + /** Conflict-copy paths created (kept-both). */ + conflicts: string[] + lastSyncAt: number + lastError: string | null +} + export interface ServerSessionStatus { authenticated: boolean authRequired: boolean From aa22df51347718bd3c85c80634343a86d9821fb2 Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Wed, 17 Jun 2026 16:59:02 -0500 Subject: [PATCH 55/66] =?UTF-8?q?feat(sync):=20app=20integration=20?= =?UTF-8?q?=E2=80=94=20activation,=20IPC,=20and=20status=20UI=20(M3+M4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the sync engine into the running app: - index.ts activation: a synced vault opens disk-backed (all file ops stay local — isRemoteWorkspaceActive stays false) plus a background SyncEngine, started from loadCurrentVaultFromConfig and torn down when switching to local/remote. setSyncedVaultForWindow mirrors setVaultForWindow; the local watcher fan-out (onLocalChange) drives the engine. - New IPC: sync:get-status / now / on-status / attach-server / detach-server / open-vault, exposed through preload + the ZenBridge interface; the web bridge gets inert stubs (the browser is itself the thin client). - Store: syncStatus slice, onSyncStatus subscription + initial fetch, and syncNow / attachSyncServer / detachSyncServer actions. - UI: a cloud sync-status button in the sidebar header (synced / syncing / offline / error + a conflict badge; click to sync now) and vault-menu items to "Back up & sync to <server>" (attach a local vault) or "Stop syncing". Typechecks clean across all packages; 36 desktop/sync tests + app-core tests green; web builds. Runtime not yet exercised end-to-end (next: assets, then databases/comments/settings, then the conflict-resolution UI). --- apps/desktop/src/main/index.ts | 149 ++++++++++++++++++- apps/desktop/src/preload/index.ts | 24 +++ apps/web/src/bridge/http-bridge.ts | 12 ++ packages/app-core/src/components/Sidebar.tsx | 94 +++++++++++- packages/app-core/src/components/icons.tsx | 26 ++++ packages/app-core/src/store.ts | 44 ++++++ packages/bridge-contract/src/bridge.ts | 14 ++ packages/bridge-contract/src/ipc.ts | 6 + 8 files changed, 367 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 8bf31274..86b1acbd 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -33,6 +33,7 @@ import type { RemoteWorkspaceProfile, RemoteWorkspaceProfileInput, ServerCapabilities, + SyncStatus, VaultSettings, VaultChangeEvent, VaultInfo, @@ -86,6 +87,7 @@ import { rootContentHiddenByInboxMode, type PersistedRemoteWorkspaceConfig, type PersistedRemoteWorkspaceProfile, + type PersistedSyncedVault, type PersistedWindowState, rememberLocalVault, updateConfig, @@ -106,6 +108,7 @@ import { getRemoteWorkspaceSecret, setRemoteWorkspaceSecret } from './secret-store' +import { SyncEngine } from './sync/engine' import { scanAllTasks, scanTasksForPath } from './tasks' import { readDatabase, @@ -182,13 +185,17 @@ let mainWindow: BrowserWindow | null = null let mainWindowReadyForAppEvents = false let creatingMainWindow: Promise<BrowserWindow> | null = null let currentVault: VaultInfo | null = null -let currentWorkspaceMode: 'local' | 'remote' = 'local' +let currentWorkspaceMode: 'local' | 'remote' | 'synced' = 'local' let remoteWorkspaceConfig: PersistedRemoteWorkspaceConfig | null = null let currentRemoteWorkspaceProfileId: string | null = null let remoteWorkspaceClient: RemoteServerClient | null = null let remoteServerCapabilities: ServerCapabilities | null = null let stopRemoteVaultWatch: (() => void) | null = null const ipcWindowContext = new AsyncLocalStorage<BrowserWindow>() +// The active synced vault's background sync engine (one at a time; keyed by +// root so the local-change fan-out only reaches the matching engine). +let activeSyncEngine: SyncEngine | null = null + const windowVaults = new WindowVaultRegistry({ makeWatcher: () => new VaultWatcher(), invalidateVault: (root, ev) => { @@ -199,8 +206,45 @@ const windowVaults = new WindowVaultRegistry({ const win = BrowserWindow.fromId(windowId) if (!win || win.isDestroyed()) return win.webContents.send(IPC.VAULT_ON_CHANGE, ev) + }, + onLocalChange: (root, ev) => { + if (activeSyncEngine && path.resolve(activeSyncEngine.root) === path.resolve(root)) { + activeSyncEngine.onLocalChange(ev) + } } }) + +function broadcastSyncStatus(status: SyncStatus): void { + for (const win of BrowserWindow.getAllWindows()) { + if (!win.isDestroyed()) win.webContents.send(IPC.SYNC_ON_STATUS, status) + } +} + +function stopSyncEngine(): void { + activeSyncEngine?.stop() + activeSyncEngine = null +} + +/** Start (or restart) the background sync engine for a synced vault. */ +async function startSyncEngine(synced: PersistedSyncedVault): Promise<void> { + const cfg = await loadConfig() + const profile = findRemoteProfileById(cfg.remoteWorkspaceProfiles, synced.serverProfileId) + if (!profile) { + throw new Error('The server for this synced vault is no longer configured.') + } + const authToken = await getRemoteWorkspaceSecret(profile.id) + const client = new RemoteServerClient({ baseUrl: profile.baseUrl, authToken }) + stopSyncEngine() + const engine = new SyncEngine({ + root: path.resolve(synced.root), + client, + serverProfileId: synced.serverProfileId, + conflictPolicy: synced.conflictPolicy, + onStatus: broadcastSyncStatus + }) + activeSyncEngine = engine + await engine.start() +} const DEFAULT_WINDOW_WIDTH = 1280 const DEFAULT_WINDOW_HEIGHT = 820 const MIN_WINDOW_WIDTH = 900 @@ -1195,6 +1239,7 @@ async function setVaultForWindow( windowVaults.setLocalVault(win.id, vault) currentVault = vault currentWorkspaceMode = 'local' + stopSyncEngine() if (!windowVaults.hasRemoteWindows()) { remoteWorkspaceClient = null remoteWorkspaceConfig = null @@ -1214,6 +1259,38 @@ async function setVaultForWindow( return vault } +/** Open a vault in `synced` mode (disk-backed like local, plus the engine). */ +async function setSyncedVaultForWindow( + win: BrowserWindow, + synced: PersistedSyncedVault, + options: { persist?: boolean } = {} +): Promise<VaultInfo> { + const root = path.resolve(synced.root) + await ensureVaultLayout(root) + const vault = vaultInfo(root) + windowVaults.setSyncedVault(win.id, vault) + currentVault = vault + currentWorkspaceMode = 'synced' + if (!windowVaults.hasRemoteWindows()) { + remoteWorkspaceClient = null + remoteWorkspaceConfig = null + currentRemoteWorkspaceProfileId = null + remoteServerCapabilities = null + stopRemoteWatch() + } + await startSyncEngine(synced) + if (options.persist !== false) { + await updateConfig((cfg) => ({ + ...cfg, + workspaceMode: 'synced', + vaultRoot: root, + localVaults: rememberLocalVault(cfg.localVaults, vault), + remoteWorkspaceProfileId: null + })) + } + return vault +} + async function setVault(root: string): Promise<VaultInfo> { const win = currentIpcWindow() ?? mainWindow if (win && !win.isDestroyed()) return await setVaultForWindow(win, root) @@ -1222,6 +1299,7 @@ async function setVault(root: string): Promise<VaultInfo> { const vault = vaultInfo(path.resolve(root)) currentVault = vault currentWorkspaceMode = 'local' + stopSyncEngine() remoteWorkspaceClient = null remoteWorkspaceConfig = null currentRemoteWorkspaceProfileId = null @@ -1322,6 +1400,7 @@ async function setRemoteWorkspace( const win = currentIpcWindow() ?? mainWindow currentWorkspaceMode = 'remote' + stopSyncEngine() currentVault = vault if (win && !win.isDestroyed()) { windowVaults.setRemoteVault(win.id, vault) @@ -1691,6 +1770,19 @@ async function loadCurrentVaultFromConfig(): Promise<VaultInfo | null> { return null } } + if (cfg.workspaceMode === 'synced' && cfg.vaultRoot) { + const synced = cfg.syncedVaults.find( + (s) => path.resolve(s.root) === path.resolve(cfg.vaultRoot!) + ) + if (synced && win && !win.isDestroyed()) { + try { + return await setSyncedVaultForWindow(win, synced, { persist: false }) + } catch { + // Fall through to opening the folder as a plain local vault if the + // engine can't start (e.g. server profile gone) — never block startup. + } + } + } if (cfg.vaultRoot) { try { if (win && !win.isDestroyed()) { @@ -1950,6 +2042,61 @@ function registerIpc(): void { return await connectRemoteWorkspaceProfile(id) }) + handle(IPC.SYNC_GET_STATUS, async () => activeSyncEngine?.getStatus() ?? null) + + handle(IPC.SYNC_NOW, async () => { + activeSyncEngine?.syncNow() + }) + + // Attach the currently open local vault to a saved server and switch it to + // synced mode (bidirectional backup/sync). + handle(IPC.SYNC_ATTACH_SERVER, async (_e, profileId: string) => { + const win = currentIpcWindow() ?? mainWindow + if (!win || win.isDestroyed()) throw new Error('No window is associated with this call.') + const vault = windowVaults.vaultForWindow(win.id) ?? currentVault + if (!vault) throw new Error('No vault is open to sync.') + const root = path.resolve(vault.root) + const cfg = await loadConfig() + const profile = findRemoteProfileById(cfg.remoteWorkspaceProfiles, profileId) + if (!profile) throw new Error('Add and save a server connection first.') + const synced: PersistedSyncedVault = { + root, + serverProfileId: profileId, + conflictPolicy: 'keep-both', + assetMode: 'lazy', + lastSyncAt: null + } + await updateConfig((c) => ({ + ...c, + syncedVaults: [...c.syncedVaults.filter((s) => path.resolve(s.root) !== root), synced] + })) + return await setSyncedVaultForWindow(win, synced) + }) + + // Detach: stop syncing and reopen the folder as a plain local vault. + handle(IPC.SYNC_DETACH_SERVER, async () => { + const win = currentIpcWindow() ?? mainWindow + const vault = win && !win.isDestroyed() ? windowVaults.vaultForWindow(win.id) : currentVault + if (!vault) return null + const root = path.resolve(vault.root) + await updateConfig((c) => ({ + ...c, + syncedVaults: c.syncedVaults.filter((s) => path.resolve(s.root) !== root) + })) + if (win && !win.isDestroyed()) return await setVaultForWindow(win, root) + return await setVault(root) + }) + + // Open an already-configured synced vault by its root. + handle(IPC.SYNC_OPEN_VAULT, async (_e, root: string) => { + const win = currentIpcWindow() ?? mainWindow + if (!win || win.isDestroyed()) throw new Error('No window is associated with this call.') + const cfg = await loadConfig() + const synced = cfg.syncedVaults.find((s) => path.resolve(s.root) === path.resolve(root)) + if (!synced) throw new Error('That synced vault is not configured.') + return await setSyncedVaultForWindow(win, synced) + }) + handle(IPC.VAULT_GET_CURRENT, async () => { return await loadCurrentVaultFromConfig() }) diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index 2c30c834..87a0e987 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -36,6 +36,7 @@ import type { RemoteWorkspaceProfileInput, ServerCapabilities, ServerSessionStatus, + SyncStatus, VaultChangeEvent, VaultDemoTourResult, VaultInfo, @@ -228,6 +229,29 @@ const api: ZenBridge = { return result }, + getSyncStatus: (): Promise<SyncStatus | null> => ipcRenderer.invoke(IPC.SYNC_GET_STATUS), + syncNow: (): Promise<void> => ipcRenderer.invoke(IPC.SYNC_NOW), + attachSyncServer: async (profileId: string): Promise<VaultInfo> => { + const vault = await ipcRenderer.invoke(IPC.SYNC_ATTACH_SERVER, profileId) + await refreshRemoteWorkspaceInfo() + return vault + }, + detachSyncServer: async (): Promise<VaultInfo | null> => { + const vault = await ipcRenderer.invoke(IPC.SYNC_DETACH_SERVER) + await refreshRemoteWorkspaceInfo() + return vault + }, + openSyncedVault: async (root: string): Promise<VaultInfo> => { + const vault = await ipcRenderer.invoke(IPC.SYNC_OPEN_VAULT, root) + await refreshRemoteWorkspaceInfo() + return vault + }, + onSyncStatus: (cb: (status: SyncStatus) => void): (() => void) => { + const listener = (_: unknown, status: SyncStatus): void => cb(status) + ipcRenderer.on(IPC.SYNC_ON_STATUS, listener) + return () => ipcRenderer.removeListener(IPC.SYNC_ON_STATUS, listener) + }, + getCurrentVault: async (): Promise<VaultInfo | null> => { const vault = await ipcRenderer.invoke(IPC.VAULT_GET_CURRENT) await refreshRemoteWorkspaceInfo() diff --git a/apps/web/src/bridge/http-bridge.ts b/apps/web/src/bridge/http-bridge.ts index 33706a0a..1ac98228 100644 --- a/apps/web/src/bridge/http-bridge.ts +++ b/apps/web/src/bridge/http-bridge.ts @@ -47,6 +47,7 @@ import type { RemoteWorkspaceProfile, RemoteWorkspaceProfileInput, ServerCapabilities, + SyncStatus, ServerSessionStatus, VaultSettings, TikzRenderResponse, @@ -1123,6 +1124,17 @@ export const httpBridge: ZenBridge = { deleteRemoteWorkspaceProfile: (_id: string) => deleteRemoteWorkspaceProfile(), connectRemoteWorkspaceProfile: (_id: string) => connectRemoteWorkspaceProfile(), + // The web client is itself the server's thin client — it never syncs a local + // copy, so sync controls are inert here. + getSyncStatus: () => Promise.resolve(null), + syncNow: () => Promise.resolve(), + attachSyncServer: (_profileId: string) => + Promise.reject(new Error('Syncing is a desktop feature.')), + detachSyncServer: () => Promise.resolve(null), + openSyncedVault: (_root: string) => + Promise.reject(new Error('Syncing is a desktop feature.')), + onSyncStatus: (_cb: (status: SyncStatus) => void) => () => {}, + getCurrentVault, listLocalVaults, openLocalVault, diff --git a/packages/app-core/src/components/Sidebar.tsx b/packages/app-core/src/components/Sidebar.tsx index 3d324ed9..03786b68 100644 --- a/packages/app-core/src/components/Sidebar.tsx +++ b/packages/app-core/src/components/Sidebar.tsx @@ -13,7 +13,7 @@ import { Button } from "./ui/Button"; import { confirmMoveToTrash } from "../lib/confirm-trash"; import { buildMoveNotePrompt, parseMoveNoteTarget } from "../lib/move-note"; import { extractTags } from "../lib/tags"; -import type { AssetMeta, FolderColorId, FolderEntry, FolderIconId, NoteFolder, NoteMeta } from "@shared/ipc"; +import type { AssetMeta, FolderColorId, FolderEntry, FolderIconId, NoteFolder, NoteMeta, SyncStatus } from "@shared/ipc"; import type { NoteSortOrder } from "../store"; import { isArchiveTabPath } from "@shared/archive"; import { isTrashTabPath } from "@shared/trash"; @@ -25,6 +25,9 @@ import { ChevronRightIcon, CheckSquareIcon, CloseIcon, + CloudIcon, + CloudCheckIcon, + CloudOffIcon, DatabaseIcon, DocumentIcon, ExcalidrawIcon, @@ -451,6 +454,10 @@ export function Sidebar(): JSX.Element { const remoteWorkspaceInfo = useStore((s) => s.remoteWorkspaceInfo); const localVaults = useStore((s) => s.localVaults); const remoteWorkspaceProfiles = useStore((s) => s.remoteWorkspaceProfiles); + const syncStatus = useStore((s) => s.syncStatus); + const attachSyncServer = useStore((s) => s.attachSyncServer); + const detachSyncServer = useStore((s) => s.detachSyncServer); + const syncNow = useStore((s) => s.syncNow); const openVaultPicker = useStore((s) => s.openVaultPicker); const openLocalVault = useStore((s) => s.openLocalVault); const closeVault = useStore((s) => s.closeVault); @@ -2481,6 +2488,34 @@ export function Sidebar(): JSX.Element { }); } + // Sync controls: a synced vault can stop syncing; a plain local vault can be + // attached to any saved server for bidirectional backup/sync. + if (syncStatus) { + items.push({ kind: "separator" }); + items.push({ + label: "Stop syncing (keep local copy)", + icon: <CloudIcon className="h-4 w-4" />, + onSelect: async () => { + await detachSyncServer(); + }, + }); + } else if (workspaceMode === "local" && remoteWorkspaceProfiles.length > 0) { + items.push({ kind: "separator" }); + for (const profile of remoteWorkspaceProfiles) { + items.push({ + label: `Back up & sync to ${profile.name}`, + icon: <CloudIcon className="h-4 w-4" />, + onSelect: async () => { + try { + await attachSyncServer(profile.id); + } catch (err) { + window.alert((err as Error).message); + } + }, + }); + } + } + return items; }, [ canCloseCurrentVault, @@ -2493,6 +2528,11 @@ export function Sidebar(): JSX.Element { openVaultPicker, refreshLocalVaults, vaultSwitcherEntries, + syncStatus, + workspaceMode, + remoteWorkspaceProfiles, + attachSyncServer, + detachSyncServer, ]); // A folder only shows the strong "selected" accent highlight when @@ -2758,6 +2798,9 @@ export function Sidebar(): JSX.Element { </div> )} <div className="flex items-center gap-0.5"> + {syncStatus && ( + <SyncStatusButton status={syncStatus} onSyncNow={() => void syncNow()} /> + )} <IconBtn title="Create… (note, drawing, folder, database)" onClick={(e) => { @@ -5138,6 +5181,55 @@ function SidebarFooterAction({ ); } +function SyncStatusButton({ + status, + onSyncNow, +}: { + status: SyncStatus; + onSyncNow: () => void; +}): JSX.Element { + const hasConflicts = status.conflicts.length > 0; + const view = + status.kind === "syncing" + ? { icon: <CloudIcon className="h-4 w-4 animate-pulse" />, title: "Syncing…", tint: "text-accent" } + : status.kind === "offline" + ? { + icon: <CloudOffIcon className="h-4 w-4" />, + title: "Offline — changes will sync when reconnected", + tint: "text-ink-400", + } + : status.kind === "error" + ? { + icon: <CloudOffIcon className="h-4 w-4" />, + title: status.lastError ?? "Sync error", + tint: "text-red-500", + } + : { + icon: <CloudCheckIcon className="h-4 w-4" />, + title: hasConflicts + ? `Synced • ${status.conflicts.length} conflict cop${status.conflicts.length === 1 ? "y" : "ies"} kept` + : "Synced", + tint: hasConflicts ? "text-amber-500" : "text-emerald-500", + }; + return ( + <button + type="button" + onClick={onSyncNow} + aria-label={view.title} + title={`${view.title} — click to sync now`} + className={[ + "group relative flex h-7 w-7 items-center justify-center rounded-md transition-colors hover:bg-paper-200", + view.tint, + ].join(" ")} + > + {view.icon} + {hasConflicts && status.kind !== "syncing" && ( + <span className="absolute right-0.5 top-0.5 h-1.5 w-1.5 rounded-full bg-amber-500" /> + )} + </button> + ); +} + function IconBtn({ children, onClick, diff --git a/packages/app-core/src/components/icons.tsx b/packages/app-core/src/components/icons.tsx index 0f58c1c1..50c0d802 100644 --- a/packages/app-core/src/components/icons.tsx +++ b/packages/app-core/src/components/icons.tsx @@ -29,6 +29,32 @@ export const SearchIcon = (p: IconProps): JSX.Element => ( </I> ) +const CLOUD_PATH = 'M17.5 19a4.5 4.5 0 0 0 .5-8.97A6 6 0 0 0 6.34 9.2 4 4 0 0 0 7 17h10.5z' + +/** Plain cloud — the sync affordance. */ +export const CloudIcon = (p: IconProps): JSX.Element => ( + <I {...p}> + <path d={CLOUD_PATH} /> + </I> +) + +/** Cloud with a check — "synced / up to date". */ +export const CloudCheckIcon = (p: IconProps): JSX.Element => ( + <I {...p}> + <path d="M17.5 17a4.5 4.5 0 0 0 .5-8.97A6 6 0 0 0 6.34 7.2 4 4 0 0 0 6.5 15" /> + <path d="m9 16 2 2 4-4" /> + </I> +) + +/** Cloud with a slash — "offline". */ +export const CloudOffIcon = (p: IconProps): JSX.Element => ( + <I {...p}> + <path d="M6.34 7.2A4 4 0 0 0 7 15h8" /> + <path d="M18 14.5a4.5 4.5 0 0 0-2.9-7.4" /> + <path d="m3 3 18 18" /> + </I> +) + export const TableIcon = (p: IconProps): JSX.Element => ( <I {...p}> <rect x="3" y="3" width="18" height="18" rx="2" /> diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index 04738022..d5584678 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -16,6 +16,7 @@ import type { RemoteWorkspaceProfile, RemoteWorkspaceProfileInput, ServerCapabilities, + SyncStatus, VaultSettings, VaultTextSearchBackendPreference, VaultChangeEvent, @@ -1467,6 +1468,8 @@ interface Store { workspaceMode: WorkspaceMode remoteWorkspaceInfo: RemoteWorkspaceInfo | null remoteWorkspaceProfiles: RemoteWorkspaceProfile[] + /** Live sync status for a synced vault (null when not syncing). */ + syncStatus: SyncStatus | null localVaults: LocalVaultEntry[] workspaceSetupError: string | null vaultSettings: VaultSettings @@ -2005,6 +2008,14 @@ interface Store { deleteRemoteWorkspaceProfile: (id: string) => Promise<void> refreshRemoteWorkspaceProfiles: () => Promise<RemoteWorkspaceProfile[]> refreshLocalVaults: () => Promise<LocalVaultEntry[]> + /** Pull the current sync status for the active synced vault. */ + refreshSyncStatus: () => Promise<void> + /** Trigger an immediate reconcile of the synced vault. */ + syncNow: () => Promise<void> + /** Attach the open local vault to a saved server (switch to synced mode). */ + attachSyncServer: (profileId: string) => Promise<void> + /** Stop syncing and reopen the folder as a plain local vault. */ + detachSyncServer: () => Promise<void> persistWorkspace: () => void flushDirtyNotes: () => Promise<void> refreshWorkspaceContext: () => Promise<RemoteWorkspaceInfo | null> @@ -2801,6 +2812,7 @@ export const useStore = create<Store>((set, get) => { vault: null, workspaceMode: 'local', remoteWorkspaceInfo: null, + syncStatus: null, remoteWorkspaceProfiles: [], localVaults: [], workspaceSetupError: null, @@ -5596,6 +5608,34 @@ export const useStore = create<Store>((set, get) => { } }, + refreshSyncStatus: async () => { + try { + const status = await window.zen.getSyncStatus() + set({ syncStatus: status }) + } catch { + set({ syncStatus: null }) + } + }, + syncNow: async () => { + try { + await window.zen.syncNow() + } catch (err) { + console.error('syncNow failed', err) + } + }, + attachSyncServer: async (profileId) => { + await window.zen.attachSyncServer(profileId) + await get().refreshSyncStatus() + await get().refreshWorkspaceContext() + await get().refreshNotes() + }, + detachSyncServer: async () => { + await window.zen.detachSyncServer() + set({ syncStatus: null }) + await get().refreshWorkspaceContext() + await get().refreshNotes() + }, + refreshRemoteWorkspaceProfiles: async () => { if (!window.zen.getCapabilities().supportsRemoteWorkspace) { set({ remoteWorkspaceProfiles: [] }) @@ -5719,6 +5759,10 @@ export const useStore = create<Store>((set, get) => { window.zen.onVaultChange((ev) => { void get().applyChange(ev) }) + window.zen.onSyncStatus((status) => { + set({ syncStatus: status }) + }) + void get().refreshSyncStatus() }, openVaultPicker: async () => { diff --git a/packages/bridge-contract/src/bridge.ts b/packages/bridge-contract/src/bridge.ts index b881cdab..bee5da77 100644 --- a/packages/bridge-contract/src/bridge.ts +++ b/packages/bridge-contract/src/bridge.ts @@ -23,6 +23,7 @@ import type { RemoteWorkspaceProfileInput, ServerCapabilities, ServerSessionStatus, + SyncStatus, VaultSettings, TikzRenderResponse, VaultChangeEvent, @@ -101,6 +102,19 @@ export interface ZenBridge { id: string ): Promise<{ vault: VaultInfo | null; capabilities: ServerCapabilities }> + /** Current sync status for the active synced vault, or null if not synced. */ + getSyncStatus(): Promise<SyncStatus | null> + /** Trigger an immediate reconcile. */ + syncNow(): Promise<void> + /** Attach the open local vault to a saved server and switch it to synced mode. */ + attachSyncServer(profileId: string): Promise<VaultInfo> + /** Stop syncing and reopen the folder as a plain local vault. */ + detachSyncServer(): Promise<VaultInfo | null> + /** Open an already-configured synced vault by its root. */ + openSyncedVault(root: string): Promise<VaultInfo> + /** Subscribe to sync-status pushes; returns an unsubscribe function. */ + onSyncStatus(cb: (status: SyncStatus) => void): () => void + getCurrentVault(): Promise<VaultInfo | null> listLocalVaults(): Promise<LocalVaultEntry[]> openLocalVault(root: string): Promise<VaultInfo | null> diff --git a/packages/bridge-contract/src/ipc.ts b/packages/bridge-contract/src/ipc.ts index 01313f05..218cab6f 100644 --- a/packages/bridge-contract/src/ipc.ts +++ b/packages/bridge-contract/src/ipc.ts @@ -9,6 +9,12 @@ export const IPC = { WORKSPACE_SAVE_REMOTE_PROFILE: 'workspace:save-remote-profile', WORKSPACE_DELETE_REMOTE_PROFILE: 'workspace:delete-remote-profile', WORKSPACE_CONNECT_REMOTE_PROFILE: 'workspace:connect-remote-profile', + SYNC_GET_STATUS: 'sync:get-status', + SYNC_NOW: 'sync:now', + SYNC_ON_STATUS: 'sync:on-status', + SYNC_ATTACH_SERVER: 'sync:attach-server', + SYNC_DETACH_SERVER: 'sync:detach-server', + SYNC_OPEN_VAULT: 'sync:open-vault', VAULT_LIST_LOCAL: 'vault:list-local', VAULT_OPEN_LOCAL: 'vault:open-local', VAULT_CLOSE: 'vault:close', From 4fd805b46c9cf8edabed0e14f05eeab1209b17b6 Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Wed, 17 Jun 2026 17:06:47 -0500 Subject: [PATCH 56/66] =?UTF-8?q?feat(sync):=20full-vault=20coverage=20?= =?UTF-8?q?=E2=80=94=20databases,=20comments,=20settings,=20assets=20(M5+M?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unifies all syncable content under one manifest instead of notes-only: - Server: Manifest() now walks collectSyncFiles() — the whole vault tree (notes, drawings, databases, assets, comments, vault.json) excluding only the per-device sync-state, the meta cache, dotfiles, unrelated dot-dirs, and temp files. New POST /api/sync/write (multipart) writes raw bytes to an exact path for binary files (Vault.WriteSyncFile, atomic + size-capped). - Client: scanLocalVault() walks the whole tree to match (also removing the earlier inbox/root-mode parity concern). The engine classifies each path as text (.md/.excalidraw/.json/.csv/.txt) — transferred via writeNote/readNote — or binary, transferred byte-for-byte via writeSyncFile/readSyncFile. Conflict copies handle both. Databases (CSV/JSON), comments, and settings are text, so they sync through the existing endpoints with no special-casing. Tests: 25 sync unit tests (added binary push/pull byte-equality, databases/ comments/settings sync, and a never-sync guard for sync-state/meta-cache); the server manifest test now asserts syncables-in / internal-out. All green; typechecks + web build clean. Note: assets sync as a full mirror for now; the lazy on-demand fetch is a follow-up optimization. --- apps/desktop/src/main/remote/server-client.ts | 27 +++++ apps/desktop/src/main/sync/engine.test.ts | 84 +++++++++++--- apps/desktop/src/main/sync/engine.ts | 70 +++++++++--- apps/desktop/src/main/sync/scan-local.ts | 73 +++--------- apps/server/internal/httpserver/server.go | 32 ++++++ apps/server/internal/vault/vault.go | 107 ++++++++++++++++-- apps/server/internal/vault/vault_test.go | 50 ++++++-- 7 files changed, 333 insertions(+), 110 deletions(-) diff --git a/apps/desktop/src/main/remote/server-client.ts b/apps/desktop/src/main/remote/server-client.ts index 7e31ca9b..815c8e2d 100644 --- a/apps/desktop/src/main/remote/server-client.ts +++ b/apps/desktop/src/main/remote/server-client.ts @@ -77,6 +77,33 @@ export class RemoteServerClient { return this.jsonRequest<SyncManifestEntry[]>('/api/sync/manifest') } + /** Push raw bytes to an exact vault path (binary file sync). */ + async writeSyncFile(relPath: string, bytes: Buffer): Promise<void> { + const form = new FormData() + form.set('path', relPath) + form.set('file', new Blob([bytes])) + const headers: Record<string, string> = {} + if (this.authToken) headers['Authorization'] = `Bearer ${this.authToken}` + const response = await fetch(`${this.baseUrl}/api/sync/write`, { + method: 'POST', + headers, + body: form + }) + if (!response.ok) { + const text = await response.text().catch(() => '') + throw new Error( + `Sync write failed (${response.status} ${response.statusText}) for ${relPath}${text ? `: ${text}` : ''}` + ) + } + } + + /** Read raw bytes for an exact vault path (binary file sync). */ + async readSyncFile(relPath: string): Promise<Buffer> { + const response = await this.fetchAssetResponse(relPath) + const buf = await response.arrayBuffer() + return Buffer.from(buf) + } + async listFolders(): Promise<FolderEntry[]> { return this.jsonRequest<FolderEntry[]>('/api/folders') } diff --git a/apps/desktop/src/main/sync/engine.test.ts b/apps/desktop/src/main/sync/engine.test.ts index b7bc0901..0b0435d6 100644 --- a/apps/desktop/src/main/sync/engine.test.ts +++ b/apps/desktop/src/main/sync/engine.test.ts @@ -6,26 +6,32 @@ import type { RemoteServerClient } from '../remote/server-client' import { SyncEngine } from './engine' import { hashBytes } from './scan-local' -/** Minimal in-memory stand-in for the Go server. */ +/** Minimal in-memory stand-in for the Go server (stores raw bytes). */ class FakeServer { - files = new Map<string, string>() + files = new Map<string, Buffer>() baseUrl = 'http://fake' async getSyncManifest(): Promise<{ path: string; hash: string; size: number; mtime: number }[]> { - return [...this.files].map(([p, body]) => ({ + return [...this.files].map(([p, bytes]) => ({ path: p, - hash: hashBytes(Buffer.from(body, 'utf8')), - size: Buffer.byteLength(body), + hash: hashBytes(bytes), + size: bytes.length, mtime: 0 })) } async readNote(p: string): Promise<{ path: string; body: string }> { - return { path: p, body: this.files.get(p) ?? '' } + return { path: p, body: (this.files.get(p) ?? Buffer.alloc(0)).toString('utf8') } } async writeNote(p: string, body: string): Promise<unknown> { - this.files.set(p, body) + this.files.set(p, Buffer.from(body, 'utf8')) return {} } + async readSyncFile(p: string): Promise<Buffer> { + return this.files.get(p) ?? Buffer.alloc(0) + } + async writeSyncFile(p: string, bytes: Buffer): Promise<void> { + this.files.set(p, Buffer.from(bytes)) + } async deleteNote(p: string): Promise<void> { this.files.delete(p) } @@ -55,6 +61,10 @@ async function readLocal(root: string, rel: string): Promise<string | null> { return null } } +function serverText(server: FakeServer, p: string): string | undefined { + const b = server.files.get(p) + return b === undefined ? undefined : b.toString('utf8') +} async function localNotePaths(root: string): Promise<string[]> { const out: string[] = [] const walk = async (dir: string): Promise<void> => { @@ -82,14 +92,14 @@ describe('SyncEngine — end to end (fake server, real local fs)', () => { it('first sync = union merge of local + server', async () => { const server = new FakeServer() - server.files.set('inbox/FromServer.md', 'server note') + server.files.set('inbox/FromServer.md', Buffer.from('server note')) await writeLocal(root, 'inbox/FromLocal.md', 'local note') await makeEngine(root, server).runOnce() // Local gained the server note; server gained the local note. expect(await readLocal(root, 'inbox/FromServer.md')).toBe('server note') - expect(server.files.get('inbox/FromLocal.md')).toBe('local note') + expect(serverText(server, 'inbox/FromLocal.md')).toBe('local note') expect(await localNotePaths(root)).toEqual(['inbox/FromLocal.md', 'inbox/FromServer.md']) }) @@ -97,24 +107,24 @@ describe('SyncEngine — end to end (fake server, real local fs)', () => { const server = new FakeServer() const engine = makeEngine(root, server) await engine.runOnce() // establish base (empty) - server.files.set('inbox/New.md', 'remote-only') + server.files.set('inbox/New.md', Buffer.from('remote-only')) await engine.runOnce() expect(await readLocal(root, 'inbox/New.md')).toBe('remote-only') }) it('pushes a local edit to the server', async () => { const server = new FakeServer() - server.files.set('inbox/A.md', 'v1') + server.files.set('inbox/A.md', Buffer.from('v1')) const engine = makeEngine(root, server) await engine.runOnce() // pulls A=v1 to local, base set await writeLocal(root, 'inbox/A.md', 'v2') await engine.runOnce() - expect(server.files.get('inbox/A.md')).toBe('v2') + expect(serverText(server, 'inbox/A.md')).toBe('v2') }) it('propagates a local delete to the server', async () => { const server = new FakeServer() - server.files.set('inbox/A.md', 'v1') + server.files.set('inbox/A.md', Buffer.from('v1')) const engine = makeEngine(root, server) await engine.runOnce() // local now has A await fs.rm(path.join(root, 'inbox/A.md')) @@ -124,12 +134,12 @@ describe('SyncEngine — end to end (fake server, real local fs)', () => { it('keeps both on a concurrent conflicting edit (never loses data)', async () => { const server = new FakeServer() - server.files.set('inbox/A.md', 'base') + server.files.set('inbox/A.md', Buffer.from('base')) const engine = makeEngine(root, server) await engine.runOnce() // synced: local A = base // Edit differently on both sides before the next sync. await writeLocal(root, 'inbox/A.md', 'local edit') - server.files.set('inbox/A.md', 'server edit') + server.files.set('inbox/A.md', Buffer.from('server edit')) await engine.runOnce() // Local content is canonical at the path; the server edit is preserved as a copy. @@ -139,17 +149,55 @@ describe('SyncEngine — end to end (fake server, real local fs)', () => { expect(conflictCopy).toBeTruthy() expect(await readLocal(root, conflictCopy as string)).toBe('server edit') // Both files exist on the server too (converged). - expect(server.files.get('inbox/A.md')).toBe('local edit') - expect(server.files.get(conflictCopy as string)).toBe('server edit') + expect(serverText(server, 'inbox/A.md')).toBe('local edit') + expect(serverText(server, conflictCopy as string)).toBe('server edit') }) it('is idempotent: a second sync with no changes does nothing', async () => { const server = new FakeServer() - server.files.set('inbox/A.md', 'x') + server.files.set('inbox/A.md', Buffer.from('x')) const engine = makeEngine(root, server) await engine.runOnce() const before = new Map(server.files) await engine.runOnce() expect([...server.files]).toEqual([...before]) }) + + it('syncs binary assets byte-for-byte (push and pull)', async () => { + const server = new FakeServer() + const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xff, 0x00]) + server.files.set('assets/remote.png', png) + await fs.mkdir(path.join(root, 'assets'), { recursive: true }) + const localBin = Buffer.from([0x00, 0x01, 0x02, 0xfe, 0xff]) + await fs.writeFile(path.join(root, 'assets/local.bin'), localBin) + + await makeEngine(root, server).runOnce() + + // Pulled binary lands locally with identical bytes; local binary pushed up. + expect(await fs.readFile(path.join(root, 'assets/remote.png'))).toEqual(png) + expect(server.files.get('assets/local.bin')).toEqual(localBin) + }) + + it('syncs databases, comments, and settings (text under .base/.zennotes)', async () => { + const server = new FakeServer() + server.files.set('.zennotes/vault.json', Buffer.from('{"primaryNotesLocation":"inbox"}')) + server.files.set('inbox/Books.base/data.csv', Buffer.from('id,Title\nr1,Dune\n')) + server.files.set('.zennotes/comments/inbox/A.md.comments.json', Buffer.from('[{"id":"c1"}]')) + + await makeEngine(root, server).runOnce() + + expect(await readLocal(root, '.zennotes/vault.json')).toContain('primaryNotesLocation') + expect(await readLocal(root, 'inbox/Books.base/data.csv')).toContain('Dune') + expect(await readLocal(root, '.zennotes/comments/inbox/A.md.comments.json')).toContain('c1') + }) + + it('never syncs sync-state.json or the meta cache', async () => { + const server = new FakeServer() + await fs.writeFile(path.join(root, '.zennotes', 'note-meta-cache-v1.json'), '{"version":2}') + await fs.writeFile(path.join(root, 'inbox', 'Real.md'), 'real') + await makeEngine(root, server).runOnce() + expect(server.files.has('.zennotes/sync-state.json')).toBe(false) + expect(server.files.has('.zennotes/note-meta-cache-v1.json')).toBe(false) + expect(server.files.has('inbox/Real.md')).toBe(true) + }) }) diff --git a/apps/desktop/src/main/sync/engine.ts b/apps/desktop/src/main/sync/engine.ts index 55c39b66..aad0b567 100644 --- a/apps/desktop/src/main/sync/engine.ts +++ b/apps/desktop/src/main/sync/engine.ts @@ -11,7 +11,17 @@ import { } from './reconcile' import { loadSyncState, saveSyncState, type SyncState } from './sync-state' import { conflictCopyPath, type ConflictPolicy } from './conflict' -import { hashBytes, readPrimaryNotesAtRoot, scanLocalVault } from './scan-local' +import { hashBytes, scanLocalVault } from './scan-local' + +// Text files transfer as UTF-8 strings via writeNote/readNote; everything else +// is treated as binary and transferred byte-for-byte via the sync-write endpoint. +const TEXT_EXTS = new Set(['.md', '.excalidraw', '.json', '.csv', '.txt', '.markdown']) +function isTextPath(p: string): boolean { + const dot = p.lastIndexOf('.') + const slash = p.lastIndexOf('/') + const ext = dot > slash ? p.slice(dot).toLowerCase() : '' + return TEXT_EXTS.has(ext) +} export interface SyncEngineOptions { root: string @@ -142,9 +152,8 @@ export class SyncEngine { private async reconcileOnce(): Promise<void> { const state = this.state if (!state) return - const primaryAtRoot = await readPrimaryNotesAtRoot(this.opts.root) const [localMap, remoteList] = await Promise.all([ - scanLocalVault(this.opts.root, primaryAtRoot), + scanLocalVault(this.opts.root), this.opts.client.getSyncManifest() ]) const remoteMap = new Map<string, FileState>( @@ -187,14 +196,21 @@ export class SyncEngine { const root = this.opts.root switch (action.kind) { case 'push': { - const body = await readLocal(root, action.path) - await client.writeNote(action.path, body) + if (isTextPath(action.path)) { + await client.writeNote(action.path, await readLocal(root, action.path)) + } else { + await client.writeSyncFile(action.path, await readLocalBinary(root, action.path)) + } setBase(state, action.path, localMap.get(action.path)) break } case 'pull': { - const content = await client.readNote(action.path) - await writeLocalAtomic(root, action.path, content.body) + if (isTextPath(action.path)) { + const content = await client.readNote(action.path) + await writeLocalAtomic(root, action.path, content.body) + } else { + await writeLocalBinaryAtomic(root, action.path, await client.readSyncFile(action.path)) + } setBase(state, action.path, remoteMap.get(action.path)) break } @@ -233,31 +249,39 @@ export class SyncEngine { ): Promise<void> { const client = this.opts.client const root = this.opts.root + const text = isTextPath(p) // delete-vs-edit: resurrect from the server (the edit wins over the delete). if (reason === 'delete-vs-edit') { - const content = await client.readNote(p) - await writeLocalAtomic(root, p, content.body) + if (text) await writeLocalAtomic(root, p, (await client.readNote(p)).body) + else await writeLocalBinaryAtomic(root, p, await client.readSyncFile(p)) setBase(state, p, remoteMap.get(p)) conflicts.push(p) return } // edit-vs-delete: resurrect to the server (the edit wins over the delete). if (reason === 'edit-vs-delete') { - const body = await readLocal(root, p) - await client.writeNote(p, body) + if (text) await client.writeNote(p, await readLocal(root, p)) + else await client.writeSyncFile(p, await readLocalBinary(root, p)) setBase(state, p, localMap.get(p)) conflicts.push(p) return } // both-edited / first-sync-clash: keep both. Local stays canonical; the // server's differing version is written next to it as a conflict copy. - const remoteContent = await client.readNote(p) const copyPath = conflictCopyPath(p, state.deviceId, new Date()) - const remoteBytes = Buffer.from(remoteContent.body, 'utf8') - await writeLocalAtomic(root, copyPath, remoteContent.body) - await client.writeNote(copyPath, remoteContent.body) - const localBody = await readLocal(root, p) - await client.writeNote(p, localBody) // server's path now matches local (canonical) + let remoteBytes: Buffer + if (text) { + const remote = await client.readNote(p) + remoteBytes = Buffer.from(remote.body, 'utf8') + await writeLocalAtomic(root, copyPath, remote.body) + await client.writeNote(copyPath, remote.body) + await client.writeNote(p, await readLocal(root, p)) // server path = local (canonical) + } else { + remoteBytes = await client.readSyncFile(p) + await writeLocalBinaryAtomic(root, copyPath, remoteBytes) + await client.writeSyncFile(copyPath, remoteBytes) + await client.writeSyncFile(p, await readLocalBinary(root, p)) + } setBase(state, p, localMap.get(p)) state.entries[copyPath] = { contentHash: hashBytes(remoteBytes), size: remoteBytes.length } conflicts.push(copyPath) @@ -303,6 +327,18 @@ async function writeLocalAtomic(root: string, rel: string, body: string): Promis await fs.rename(tmp, abs) } +async function readLocalBinary(root: string, rel: string): Promise<Buffer> { + return fs.readFile(safeAbs(root, rel)) +} + +async function writeLocalBinaryAtomic(root: string, rel: string, bytes: Buffer): Promise<void> { + const abs = safeAbs(root, rel) + await fs.mkdir(path.dirname(abs), { recursive: true }) + const tmp = `${abs}.${process.pid}.${Date.now()}.synctmp` + await fs.writeFile(tmp, bytes) + await fs.rename(tmp, abs) +} + async function removeLocal(root: string, rel: string): Promise<void> { await fs.rm(safeAbs(root, rel), { force: true }) } diff --git a/apps/desktop/src/main/sync/scan-local.ts b/apps/desktop/src/main/sync/scan-local.ts index 116bdd36..7ee64ef5 100644 --- a/apps/desktop/src/main/sync/scan-local.ts +++ b/apps/desktop/src/main/sync/scan-local.ts @@ -9,51 +9,30 @@ export function hashBytes(body: Buffer): string { return 'sha256:' + createHash('sha256').update(body).digest('hex') } -// Top-level names hidden from the vault-root walk in root mode (mirrors the Go -// shouldHidePrimaryRootName) so they aren't double-listed. -const HIDDEN_PRIMARY_ROOT_NAMES = new Set([ - 'quick', - 'archive', - 'trash', - 'assets', - 'attachements', - '_assets', - '.zennotes' -]) -const TOP_FOLDERS = ['quick', 'archive', 'trash'] +const INTERNAL_DIR = '.zennotes' -function isNoteFile(name: string): boolean { - const lower = name.toLowerCase() - return lower.endsWith('.md') || lower.endsWith('.excalidraw') -} - -function isFormDir(name: string): boolean { - return name.toLowerCase().endsWith('.base') -} - -async function walkFolder( +async function walkAll( vaultRoot: string, - folderRoot: string, - primaryRoot: boolean, + dir: string, out: Map<string, FileState> ): Promise<void> { let entries: Dirent[] try { - entries = await fs.readdir(folderRoot, { withFileTypes: true }) + entries = await fs.readdir(dir, { withFileTypes: true }) } catch { - return // folder may not exist (e.g. no quick/ yet) + return } for (const entry of entries) { const name = entry.name - if (name.startsWith('.')) continue // hidden, incl. .zennotes - if (primaryRoot && HIDDEN_PRIMARY_ROOT_NAMES.has(name.toLowerCase())) continue - const full = path.join(folderRoot, name) + const full = path.join(dir, name) if (entry.isDirectory()) { - if (isFormDir(name)) continue // database folder — synced as a unit later - await walkFolder(vaultRoot, full, false, out) + // Walk into `.zennotes` (for vault.json + comments); skip other dot-dirs. + if (name !== INTERNAL_DIR && name.startsWith('.')) continue + await walkAll(vaultRoot, full, out) continue } - if (!isNoteFile(name)) continue + if (name.startsWith('.')) continue // dotfiles (.DS_Store, etc.) + if (name.endsWith('.tmp') || name.endsWith('.synctmp')) continue const rel = path.relative(vaultRoot, full).split(path.sep).join('/') if (isNeverSync(rel)) continue try { @@ -66,31 +45,13 @@ async function walkFolder( } /** - * Walk every synced note/.excalidraw file in a vault and hash it, mirroring the - * server's collectNoteFiles: the primary notes area (root in root mode, else - * `inbox/`) plus `quick`/`archive`/`trash`, excluding databases, hidden files, - * and `.zennotes/`. + * Walk and hash every syncable file in a vault — notes, drawings, databases, + * assets, comments, and vault settings — mirroring the server's collectSyncFiles + * (the whole tree, excluding the per-device sync-state, the meta cache, dotfiles, + * unrelated dot-dirs, and temp files). */ -export async function scanLocalVault( - root: string, - primaryNotesAtRoot: boolean -): Promise<Map<string, FileState>> { +export async function scanLocalVault(root: string): Promise<Map<string, FileState>> { const out = new Map<string, FileState>() - const inboxRoot = primaryNotesAtRoot ? root : path.join(root, 'inbox') - await walkFolder(root, inboxRoot, primaryNotesAtRoot, out) - for (const folder of TOP_FOLDERS) { - await walkFolder(root, path.join(root, folder), false, out) - } + await walkAll(root, root, out) return out } - -/** Read primaryNotesLocation from the local vault settings; defaults to inbox. */ -export async function readPrimaryNotesAtRoot(root: string): Promise<boolean> { - try { - const raw = await fs.readFile(path.join(root, '.zennotes', 'vault.json'), 'utf8') - const parsed = JSON.parse(raw) as { primaryNotesLocation?: string } - return parsed.primaryNotesLocation === 'root' - } catch { - return false - } -} diff --git a/apps/server/internal/httpserver/server.go b/apps/server/internal/httpserver/server.go index 2c36c1e4..92e477f7 100644 --- a/apps/server/internal/httpserver/server.go +++ b/apps/server/internal/httpserver/server.go @@ -171,6 +171,7 @@ func (s *Server) registerProtectedRoutes(r chi.Router) { r.Get("/notes", s.listNotes) r.Get("/sync/manifest", s.syncManifest) + r.Post("/sync/write", s.syncWrite) r.Get("/folders", s.listFolders) r.Get("/assets", s.listAssets) r.Get("/assets/exists", s.assetsExists) @@ -506,6 +507,37 @@ func (s *Server) syncManifest(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, manifest) } +// syncWrite writes raw bytes to an exact vault path (binary file sync). The path +// is a form field; the bytes are the `file` part of a multipart upload. +func (s *Server) syncWrite(w http.ResponseWriter, r *http.Request) { + cfg := s.currentConfig() + r.Body = http.MaxBytesReader(w, r.Body, cfg.MaxAssetBytes+multipartOverheadBytes) + if err := r.ParseMultipartForm(8 << 20); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + relPath := r.FormValue("path") + if strings.TrimSpace(relPath) == "" { + http.Error(w, "missing path", http.StatusBadRequest) + return + } + file, _, err := r.FormFile("file") + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + defer file.Close() + if err := s.currentVault().WriteSyncFile(relPath, file); err != nil { + if errors.Is(err, vault.ErrAssetTooLarge) { + http.Error(w, "file too large", http.StatusRequestEntityTooLarge) + return + } + writeError(w, err) + return + } + writeJSON(w, http.StatusOK, map[string]bool{"ok": true}) +} + func (s *Server) listFolders(w http.ResponseWriter, _ *http.Request) { folders, err := s.currentVault().ListFolders() if err != nil { diff --git a/apps/server/internal/vault/vault.go b/apps/server/internal/vault/vault.go index f7246fad..66889e48 100644 --- a/apps/server/internal/vault/vault.go +++ b/apps/server/internal/vault/vault.go @@ -918,12 +918,63 @@ func (v *Vault) ListNotes() ([]NoteMeta, error) { // its base snapshot to decide pushes/pulls without downloading bodies. Hashes // are cached in the note-meta-cache (keyed by mtime+size), so an unchanged vault // answers from stat() calls alone. +// collectSyncFiles walks every syncable file in the vault — notes, drawings, +// databases, assets (binary), comments, and vault settings — excluding only the +// per-device sync-state, the meta cache, OS junk, temp files, and unrelated +// dot-directories. `.zennotes` itself is walked (for vault.json + comments). +// Callers hold v.mu (at least RLock). +func (v *Vault) collectSyncFiles() ([]string, error) { + out := []string{} + err := filepath.WalkDir(v.root, func(p string, d os.DirEntry, walkErr error) error { + if walkErr != nil { + if isSkippableWalkErr(walkErr) { + return nil + } + return walkErr + } + if p == v.root { + return nil + } + name := d.Name() + if d.IsDir() { + if name == internalVaultDir { + return nil // walk into .zennotes for vault.json + comments + } + if strings.HasPrefix(name, ".") { + return filepath.SkipDir // unrelated dot-dirs (.git, …) + } + return nil + } + if strings.HasPrefix(name, ".") { + return nil // dotfiles (.DS_Store, etc.) + } + if strings.HasSuffix(name, ".tmp") || strings.HasSuffix(name, ".synctmp") { + return nil + } + rel, err := filepath.Rel(v.root, p) + if err != nil { + return nil + } + relPosix := filepath.ToSlash(rel) + if relPosix == internalVaultDir+"/sync-state.json" || + relPosix == internalVaultDir+"/"+noteMetaCacheFile { + return nil + } + out = append(out, p) + return nil + }) + if err != nil { + return nil, err + } + return out, nil +} + func (v *Vault) Manifest() ([]ManifestEntry, error) { v.mu.RLock() defer v.mu.RUnlock() v.hydratePersistedNoteMetaCache() - files, err := v.collectNoteFiles() + files, err := v.collectSyncFiles() if err != nil { return nil, err } @@ -939,17 +990,17 @@ func (v *Vault) Manifest() ([]ManifestEntry, error) { } sem := make(chan struct{}, limit) var wg sync.WaitGroup - for index, file := range files { + for index, filePath := range files { wg.Add(1) - go func(index int, file noteFile) { + go func(index int, filePath string) { defer wg.Done() sem <- struct{}{} defer func() { <-sem }() - info, err := os.Stat(file.path) + info, err := os.Stat(filePath) if err != nil { return } - h := v.cachedContentHash(file.path, info) + h := v.cachedContentHash(filePath, info) if h == "" { return } @@ -957,16 +1008,16 @@ func (v *Vault) Manifest() ([]ManifestEntry, error) { sizes[index] = info.Size() mtimes[index] = info.ModTime().UnixMilli() okFlags[index] = true - }(index, file) + }(index, filePath) } wg.Wait() out := make([]ManifestEntry, 0, len(files)) - for index, file := range files { + for index, filePath := range files { if !okFlags[index] { continue } - rel, err := filepath.Rel(v.root, file.path) + rel, err := filepath.Rel(v.root, filePath) if err != nil { continue } @@ -2266,6 +2317,46 @@ func (v *Vault) AssetAbsPath(rel string) (string, error) { return SafeJoin(v.root, rel) } +// WriteSyncFile writes raw bytes to an exact vault-relative path (used by the +// sync client to push binary files). Creates parent dirs, caps at the asset +// size limit, and commits atomically via temp + rename. +func (v *Vault) WriteSyncFile(rel string, body io.Reader) error { + v.mu.Lock() + defer v.mu.Unlock() + abs, err := SafeJoin(v.root, rel) + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(abs), v.dirMode); err != nil { + return err + } + tmp := fmt.Sprintf("%s.%d.%d.synctmp", abs, os.Getpid(), time.Now().UnixNano()) + f, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, v.fileMode) + if err != nil { + return err + } + written, copyErr := io.Copy(f, io.LimitReader(body, v.maxAssetBytes+1)) + closeErr := f.Close() + if copyErr != nil { + _ = os.Remove(tmp) + return copyErr + } + if closeErr != nil { + _ = os.Remove(tmp) + return closeErr + } + if written > v.maxAssetBytes { + _ = os.Remove(tmp) + return ErrAssetTooLarge + } + if err := os.Rename(tmp, abs); err != nil { + _ = os.Remove(tmp) + return err + } + v.invalidateTextSearchCache() + return nil +} + func makeAssetMarkdown(relPath, kind, name string) string { dest := "<" + strings.ReplaceAll(relPath, ">", "%3E") + ">" switch kind { diff --git a/apps/server/internal/vault/vault_test.go b/apps/server/internal/vault/vault_test.go index ba2fa53b..2f0d38c7 100644 --- a/apps/server/internal/vault/vault_test.go +++ b/apps/server/internal/vault/vault_test.go @@ -829,29 +829,57 @@ func TestManifestHashesStableAndChange(t *testing.T) { } } -func TestManifestExcludesDatabasesAndInternal(t *testing.T) { +func TestManifestIncludesSyncablesExcludesInternal(t *testing.T) { root := t.TempDir() v, err := New(root, Options{}) if err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(root, "inbox", "Keep.md"), []byte("# Keep"), 0o600); err != nil { - t.Fatal(err) - } - base := filepath.Join(root, "inbox", "DB.base") - if err := os.MkdirAll(base, 0o700); err != nil { - t.Fatal(err) + // Syncable files of various kinds. + writes := map[string]string{ + "inbox/Keep.md": "# Keep", + "inbox/DB.base/data.csv": "id,Title\n", + "inbox/DB.base/schema.json": `{"version":1}`, + "assets/pic.png": "PNGDATA", + ".zennotes/vault.json": `{"primaryNotesLocation":"inbox"}`, + ".zennotes/comments/inbox/Keep.md.comments.json": "[]", + // These must NEVER appear in the manifest. + ".zennotes/sync-state.json": `{"version":1}`, + ".zennotes/note-meta-cache-v1.json": `{"version":2}`, + ".DS_Store": "junk", } - if err := os.WriteFile(filepath.Join(base, "Rec.md"), []byte("# Rec"), 0o600); err != nil { - t.Fatal(err) + for rel, body := range writes { + abs := filepath.Join(root, filepath.FromSlash(rel)) + if err := os.MkdirAll(filepath.Dir(abs), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(abs, []byte(body), 0o600); err != nil { + t.Fatal(err) + } } m, err := v.Manifest() if err != nil { t.Fatal(err) } + present := map[string]bool{} for _, e := range m { - if strings.Contains(e.Path, ".base") || strings.HasPrefix(e.Path, ".zennotes") { - t.Errorf("manifest leaked excluded path: %s", e.Path) + present[e.Path] = true + } + for _, want := range []string{ + "inbox/Keep.md", + "inbox/DB.base/data.csv", + "inbox/DB.base/schema.json", + "assets/pic.png", + ".zennotes/vault.json", + ".zennotes/comments/inbox/Keep.md.comments.json", + } { + if !present[want] { + t.Errorf("manifest missing syncable file: %s", want) + } + } + for _, banned := range []string{".zennotes/sync-state.json", ".zennotes/note-meta-cache-v1.json", ".DS_Store"} { + if present[banned] { + t.Errorf("manifest leaked excluded path: %s", banned) } } } From a38ce62d09ee649f5cb485c039c62785936a700a Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Wed, 17 Jun 2026 17:11:49 -0500 Subject: [PATCH 57/66] feat(sync): conflict-resolution UI (M7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the sidebar sync indicator shows kept-both conflicts, clicking it opens a resolver listing each conflict copy with its original. Per conflict the user can open either version, adopt the copy's content into the original ("Use the copy's version"), or discard the copy — all via normal vault ops that then sync. The engine gains dismissConflict() + a sync:dismiss-conflict IPC to clear a resolved entry from the status list; the store adds resolveSyncConflict(keepMine|keepTheirs) and an originalFromConflictPath() helper. Completes the synced-vault feature end to end: notes, drawings, databases, comments, settings, and assets sync bidirectionally with keep-both conflicts, a live status indicator, attach/detach, and conflict resolution. Typechecks + tests + web build all green. Ready for live testing. --- apps/desktop/src/main/index.ts | 4 + apps/desktop/src/main/sync/engine.ts | 8 ++ apps/desktop/src/preload/index.ts | 2 + apps/web/src/bridge/http-bridge.ts | 1 + packages/app-core/src/components/Sidebar.tsx | 25 ++++- .../src/components/SyncConflictModal.tsx | 103 ++++++++++++++++++ packages/app-core/src/store.ts | 27 +++++ packages/bridge-contract/src/bridge.ts | 2 + packages/bridge-contract/src/ipc.ts | 1 + 9 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 packages/app-core/src/components/SyncConflictModal.tsx diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 86b1acbd..4f2054b1 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -2048,6 +2048,10 @@ function registerIpc(): void { activeSyncEngine?.syncNow() }) + handle(IPC.SYNC_DISMISS_CONFLICT, async (_e, copyPath: string) => { + activeSyncEngine?.dismissConflict(copyPath) + }) + // Attach the currently open local vault to a saved server and switch it to // synced mode (bidirectional backup/sync). handle(IPC.SYNC_ATTACH_SERVER, async (_e, profileId: string) => { diff --git a/apps/desktop/src/main/sync/engine.ts b/apps/desktop/src/main/sync/engine.ts index aad0b567..d5f42921 100644 --- a/apps/desktop/src/main/sync/engine.ts +++ b/apps/desktop/src/main/sync/engine.ts @@ -115,6 +115,14 @@ export class SyncEngine { this.schedule(true) } + /** Drop a conflict-copy path from the status list once the user has dealt + * with it (the file ops happen through the normal vault IPC). */ + dismissConflict(copyPath: string): void { + if (this.status.conflicts.includes(copyPath)) { + this.setStatus({ conflicts: this.status.conflicts.filter((c) => c !== copyPath) }) + } + } + private schedule(immediate = false): void { if (!this.running) return if (this.debounceTimer) clearTimeout(this.debounceTimer) diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index 87a0e987..0c268e0c 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -231,6 +231,8 @@ const api: ZenBridge = { getSyncStatus: (): Promise<SyncStatus | null> => ipcRenderer.invoke(IPC.SYNC_GET_STATUS), syncNow: (): Promise<void> => ipcRenderer.invoke(IPC.SYNC_NOW), + dismissSyncConflict: (copyPath: string): Promise<void> => + ipcRenderer.invoke(IPC.SYNC_DISMISS_CONFLICT, copyPath), attachSyncServer: async (profileId: string): Promise<VaultInfo> => { const vault = await ipcRenderer.invoke(IPC.SYNC_ATTACH_SERVER, profileId) await refreshRemoteWorkspaceInfo() diff --git a/apps/web/src/bridge/http-bridge.ts b/apps/web/src/bridge/http-bridge.ts index 1ac98228..6199bd53 100644 --- a/apps/web/src/bridge/http-bridge.ts +++ b/apps/web/src/bridge/http-bridge.ts @@ -1128,6 +1128,7 @@ export const httpBridge: ZenBridge = { // copy, so sync controls are inert here. getSyncStatus: () => Promise.resolve(null), syncNow: () => Promise.resolve(), + dismissSyncConflict: (_copyPath: string) => Promise.resolve(), attachSyncServer: (_profileId: string) => Promise.reject(new Error('Syncing is a desktop feature.')), detachSyncServer: () => Promise.resolve(null), diff --git a/packages/app-core/src/components/Sidebar.tsx b/packages/app-core/src/components/Sidebar.tsx index 03786b68..f9423507 100644 --- a/packages/app-core/src/components/Sidebar.tsx +++ b/packages/app-core/src/components/Sidebar.tsx @@ -44,6 +44,7 @@ import { TrashIcon, } from "./icons"; import { ContextMenu, type ContextMenuItem } from "./ContextMenu"; +import { SyncConflictModal } from "./SyncConflictModal"; import { ResizeHandle } from "./ResizeHandle"; import { VaultBadge } from "./VaultBadge"; import { confirmApp } from '../lib/confirm-requests' @@ -458,6 +459,7 @@ export function Sidebar(): JSX.Element { const attachSyncServer = useStore((s) => s.attachSyncServer); const detachSyncServer = useStore((s) => s.detachSyncServer); const syncNow = useStore((s) => s.syncNow); + const [conflictModalOpen, setConflictModalOpen] = useState(false); const openVaultPicker = useStore((s) => s.openVaultPicker); const openLocalVault = useStore((s) => s.openLocalVault); const closeVault = useStore((s) => s.closeVault); @@ -2799,7 +2801,13 @@ export function Sidebar(): JSX.Element { )} <div className="flex items-center gap-0.5"> {syncStatus && ( - <SyncStatusButton status={syncStatus} onSyncNow={() => void syncNow()} /> + <SyncStatusButton + status={syncStatus} + onClick={() => { + if (syncStatus.conflicts.length > 0) setConflictModalOpen(true); + else void syncNow(); + }} + /> )} <IconBtn title="Create… (note, drawing, folder, database)" @@ -3371,6 +3379,9 @@ export function Sidebar(): JSX.Element { onClose={() => setFolderMenu(null)} /> )} + {conflictModalOpen && ( + <SyncConflictModal onClose={() => setConflictModalOpen(false)} /> + )} {rootMenu && ( <ContextMenu x={rootMenu.x} @@ -5183,10 +5194,10 @@ function SidebarFooterAction({ function SyncStatusButton({ status, - onSyncNow, + onClick, }: { status: SyncStatus; - onSyncNow: () => void; + onClick: () => void; }): JSX.Element { const hasConflicts = status.conflicts.length > 0; const view = @@ -5214,9 +5225,13 @@ function SyncStatusButton({ return ( <button type="button" - onClick={onSyncNow} + onClick={onClick} aria-label={view.title} - title={`${view.title} — click to sync now`} + title={ + hasConflicts + ? `${view.title} — click to resolve` + : `${view.title} — click to sync now` + } className={[ "group relative flex h-7 w-7 items-center justify-center rounded-md transition-colors hover:bg-paper-200", view.tint, diff --git a/packages/app-core/src/components/SyncConflictModal.tsx b/packages/app-core/src/components/SyncConflictModal.tsx new file mode 100644 index 00000000..434bdd76 --- /dev/null +++ b/packages/app-core/src/components/SyncConflictModal.tsx @@ -0,0 +1,103 @@ +import { Modal } from './ui/Modal' +import { Button } from './ui/Button' +import { useStore } from '../store' + +function basename(p: string): string { + return p.split('/').pop() ?? p +} + +/** Recover the original path from a conflict-copy path. */ +function originalOf(copyPath: string): string { + return copyPath.replace( + / \(conflict [0-9a-f]{8} \d{4}-\d{2}-\d{2} \d{6}\)(?=(\.[^/]+)?$)/, + '' + ) +} + +function isTextPath(p: string): boolean { + return /\.(md|excalidraw|json|csv|txt|markdown)$/i.test(p) +} + +/** + * Lists kept-both sync conflicts and lets the user resolve each: open either + * version, adopt the copy's content into the original, or discard the copy. + */ +export function SyncConflictModal({ onClose }: { onClose: () => void }): JSX.Element { + const conflicts = useStore((s) => s.syncStatus?.conflicts ?? []) + const resolveSyncConflict = useStore((s) => s.resolveSyncConflict) + const selectNote = useStore((s) => s.selectNote) + + return ( + <Modal size="lg" onClose={onClose} labelledBy="sync-conflicts-title"> + <div className="p-5"> + <h2 id="sync-conflicts-title" className="text-base font-semibold text-ink-900"> + Sync conflicts + </h2> + <p className="mt-1 text-sm text-ink-600"> + These notes were edited in two places before they synced. Nothing was lost — each + conflicting version is kept. Review and pick which to keep. + </p> + {conflicts.length === 0 ? ( + <p className="mt-4 text-sm text-ink-500">No conflicts to resolve.</p> + ) : ( + <ul className="mt-4 max-h-[55vh] space-y-3 overflow-y-auto pr-1"> + {conflicts.map((copyPath) => { + const original = originalOf(copyPath) + const text = isTextPath(copyPath) + return ( + <li + key={copyPath} + className="rounded-lg border border-paper-300/70 bg-paper-50 p-3" + > + <div className="truncate text-sm font-medium text-ink-900"> + {basename(original)} + </div> + <div className="truncate text-xs text-ink-500"> + conflict copy: {basename(copyPath)} + </div> + <div className="mt-2 flex flex-wrap gap-2"> + <Button + size="sm" + variant="secondary" + onClick={() => void selectNote(original)} + > + Open original + </Button> + <Button + size="sm" + variant="secondary" + onClick={() => void selectNote(copyPath)} + > + Open copy + </Button> + {text && ( + <Button + size="sm" + variant="primary" + onClick={() => void resolveSyncConflict(copyPath, 'keepTheirs')} + > + Use the copy's version + </Button> + )} + <Button + size="sm" + variant="danger" + onClick={() => void resolveSyncConflict(copyPath, 'keepMine')} + > + Discard copy + </Button> + </div> + </li> + ) + })} + </ul> + )} + <div className="mt-5 flex justify-end"> + <Button variant="primary" onClick={onClose}> + Done + </Button> + </div> + </div> + </Modal> + ) +} diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index d5584678..86b90a2c 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -2016,6 +2016,9 @@ interface Store { attachSyncServer: (profileId: string) => Promise<void> /** Stop syncing and reopen the folder as a plain local vault. */ detachSyncServer: () => Promise<void> + /** Resolve a kept-both conflict copy: keep the local original, or adopt the + * copy's version into the original. Either way the copy is removed. */ + resolveSyncConflict: (copyPath: string, resolution: 'keepMine' | 'keepTheirs') => Promise<void> persistWorkspace: () => void flushDirtyNotes: () => Promise<void> refreshWorkspaceContext: () => Promise<RemoteWorkspaceInfo | null> @@ -2125,6 +2128,15 @@ function workspaceModeFrom(info: RemoteWorkspaceInfo | null): WorkspaceMode { return info?.mode === 'remote' || info?.mode === 'synced' ? info.mode : 'local' } +/** Strip the " (conflict <dev> <date> <time>)" suffix a sync conflict copy adds, + * recovering the original note's path. */ +function originalFromConflictPath(copyPath: string): string { + return copyPath.replace( + / \(conflict [0-9a-f]{8} \d{4}-\d{2}-\d{2} \d{6}\)(?=(\.[^/]+)?$)/, + '' + ) +} + async function ensureWebServerSession( capabilities?: ServerCapabilities | null ): Promise<boolean> { @@ -5635,6 +5647,21 @@ export const useStore = create<Store>((set, get) => { await get().refreshWorkspaceContext() await get().refreshNotes() }, + resolveSyncConflict: async (copyPath, resolution) => { + const original = originalFromConflictPath(copyPath) + try { + if (resolution === 'keepTheirs' && original !== copyPath) { + const copy = await window.zen.readNote(copyPath) + await window.zen.writeNote(original, copy.body) + } + await window.zen.moveToTrash(copyPath) // discard the conflict copy + } catch (err) { + console.error('resolveSyncConflict failed', err) + } + await window.zen.dismissSyncConflict(copyPath) + await get().refreshSyncStatus() + await get().refreshNotes() + }, refreshRemoteWorkspaceProfiles: async () => { if (!window.zen.getCapabilities().supportsRemoteWorkspace) { diff --git a/packages/bridge-contract/src/bridge.ts b/packages/bridge-contract/src/bridge.ts index bee5da77..262fbf28 100644 --- a/packages/bridge-contract/src/bridge.ts +++ b/packages/bridge-contract/src/bridge.ts @@ -106,6 +106,8 @@ export interface ZenBridge { getSyncStatus(): Promise<SyncStatus | null> /** Trigger an immediate reconcile. */ syncNow(): Promise<void> + /** Remove a resolved conflict copy from the status list. */ + dismissSyncConflict(copyPath: string): Promise<void> /** Attach the open local vault to a saved server and switch it to synced mode. */ attachSyncServer(profileId: string): Promise<VaultInfo> /** Stop syncing and reopen the folder as a plain local vault. */ diff --git a/packages/bridge-contract/src/ipc.ts b/packages/bridge-contract/src/ipc.ts index 218cab6f..2963ba71 100644 --- a/packages/bridge-contract/src/ipc.ts +++ b/packages/bridge-contract/src/ipc.ts @@ -15,6 +15,7 @@ export const IPC = { SYNC_ATTACH_SERVER: 'sync:attach-server', SYNC_DETACH_SERVER: 'sync:detach-server', SYNC_OPEN_VAULT: 'sync:open-vault', + SYNC_DISMISS_CONFLICT: 'sync:dismiss-conflict', VAULT_LIST_LOCAL: 'vault:list-local', VAULT_OPEN_LOCAL: 'vault:open-local', VAULT_CLOSE: 'vault:close', From 46376ea7be36fc194c8bc7a2447eeebf468cc47e Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Thu, 18 Jun 2026 09:02:57 -0500 Subject: [PATCH 58/66] =?UTF-8?q?Revert=20the=20synced-vaults=20work=20(M1?= =?UTF-8?q?=E2=80=93M7)=20=E2=80=94=20will=20revisit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the five feat(sync) commits (731abb7, 757098a, aa22df5, 4fd805b, a38ce62): the server sync manifest + binary write endpoint, the desktop sync engine, app activation/IPC/status UI, full-vault coverage, and the conflict UI. The bidirectional-sync direction needs more design/validation before it ships, so it's parked. The work remains in history and can be restored later by reverting this commit (or cherry-picking those SHAs). --- apps/desktop/src/main/index.ts | 153 +------- apps/desktop/src/main/remote/server-client.ts | 32 -- apps/desktop/src/main/sync/conflict.ts | 41 -- apps/desktop/src/main/sync/engine.test.ts | 203 ---------- apps/desktop/src/main/sync/engine.ts | 352 ------------------ apps/desktop/src/main/sync/reconcile.test.ts | 105 ------ apps/desktop/src/main/sync/reconcile.ts | 115 ------ apps/desktop/src/main/sync/scan-local.ts | 57 --- apps/desktop/src/main/sync/sync-state.ts | 87 ----- apps/desktop/src/main/vault.ts | 55 +-- apps/desktop/src/main/window-vaults.ts | 40 +- apps/desktop/src/preload/index.ts | 26 -- .../internal/httpserver/excalidraw_test.go | 47 --- apps/server/internal/httpserver/server.go | 43 --- apps/server/internal/vault/types.go | 10 - apps/server/internal/vault/vault.go | 274 ++------------ apps/server/internal/vault/vault_test.go | 121 ------ apps/web/src/bridge/http-bridge.ts | 13 - packages/app-core/src/components/Sidebar.tsx | 109 +----- .../src/components/SyncConflictModal.tsx | 103 ----- packages/app-core/src/components/icons.tsx | 26 -- packages/app-core/src/store.ts | 73 +--- packages/bridge-contract/src/bridge.ts | 16 - packages/bridge-contract/src/ipc.ts | 39 +- 24 files changed, 43 insertions(+), 2097 deletions(-) delete mode 100644 apps/desktop/src/main/sync/conflict.ts delete mode 100644 apps/desktop/src/main/sync/engine.test.ts delete mode 100644 apps/desktop/src/main/sync/engine.ts delete mode 100644 apps/desktop/src/main/sync/reconcile.test.ts delete mode 100644 apps/desktop/src/main/sync/reconcile.ts delete mode 100644 apps/desktop/src/main/sync/scan-local.ts delete mode 100644 apps/desktop/src/main/sync/sync-state.ts delete mode 100644 packages/app-core/src/components/SyncConflictModal.tsx diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 4f2054b1..8bf31274 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -33,7 +33,6 @@ import type { RemoteWorkspaceProfile, RemoteWorkspaceProfileInput, ServerCapabilities, - SyncStatus, VaultSettings, VaultChangeEvent, VaultInfo, @@ -87,7 +86,6 @@ import { rootContentHiddenByInboxMode, type PersistedRemoteWorkspaceConfig, type PersistedRemoteWorkspaceProfile, - type PersistedSyncedVault, type PersistedWindowState, rememberLocalVault, updateConfig, @@ -108,7 +106,6 @@ import { getRemoteWorkspaceSecret, setRemoteWorkspaceSecret } from './secret-store' -import { SyncEngine } from './sync/engine' import { scanAllTasks, scanTasksForPath } from './tasks' import { readDatabase, @@ -185,17 +182,13 @@ let mainWindow: BrowserWindow | null = null let mainWindowReadyForAppEvents = false let creatingMainWindow: Promise<BrowserWindow> | null = null let currentVault: VaultInfo | null = null -let currentWorkspaceMode: 'local' | 'remote' | 'synced' = 'local' +let currentWorkspaceMode: 'local' | 'remote' = 'local' let remoteWorkspaceConfig: PersistedRemoteWorkspaceConfig | null = null let currentRemoteWorkspaceProfileId: string | null = null let remoteWorkspaceClient: RemoteServerClient | null = null let remoteServerCapabilities: ServerCapabilities | null = null let stopRemoteVaultWatch: (() => void) | null = null const ipcWindowContext = new AsyncLocalStorage<BrowserWindow>() -// The active synced vault's background sync engine (one at a time; keyed by -// root so the local-change fan-out only reaches the matching engine). -let activeSyncEngine: SyncEngine | null = null - const windowVaults = new WindowVaultRegistry({ makeWatcher: () => new VaultWatcher(), invalidateVault: (root, ev) => { @@ -206,45 +199,8 @@ const windowVaults = new WindowVaultRegistry({ const win = BrowserWindow.fromId(windowId) if (!win || win.isDestroyed()) return win.webContents.send(IPC.VAULT_ON_CHANGE, ev) - }, - onLocalChange: (root, ev) => { - if (activeSyncEngine && path.resolve(activeSyncEngine.root) === path.resolve(root)) { - activeSyncEngine.onLocalChange(ev) - } } }) - -function broadcastSyncStatus(status: SyncStatus): void { - for (const win of BrowserWindow.getAllWindows()) { - if (!win.isDestroyed()) win.webContents.send(IPC.SYNC_ON_STATUS, status) - } -} - -function stopSyncEngine(): void { - activeSyncEngine?.stop() - activeSyncEngine = null -} - -/** Start (or restart) the background sync engine for a synced vault. */ -async function startSyncEngine(synced: PersistedSyncedVault): Promise<void> { - const cfg = await loadConfig() - const profile = findRemoteProfileById(cfg.remoteWorkspaceProfiles, synced.serverProfileId) - if (!profile) { - throw new Error('The server for this synced vault is no longer configured.') - } - const authToken = await getRemoteWorkspaceSecret(profile.id) - const client = new RemoteServerClient({ baseUrl: profile.baseUrl, authToken }) - stopSyncEngine() - const engine = new SyncEngine({ - root: path.resolve(synced.root), - client, - serverProfileId: synced.serverProfileId, - conflictPolicy: synced.conflictPolicy, - onStatus: broadcastSyncStatus - }) - activeSyncEngine = engine - await engine.start() -} const DEFAULT_WINDOW_WIDTH = 1280 const DEFAULT_WINDOW_HEIGHT = 820 const MIN_WINDOW_WIDTH = 900 @@ -1239,7 +1195,6 @@ async function setVaultForWindow( windowVaults.setLocalVault(win.id, vault) currentVault = vault currentWorkspaceMode = 'local' - stopSyncEngine() if (!windowVaults.hasRemoteWindows()) { remoteWorkspaceClient = null remoteWorkspaceConfig = null @@ -1259,38 +1214,6 @@ async function setVaultForWindow( return vault } -/** Open a vault in `synced` mode (disk-backed like local, plus the engine). */ -async function setSyncedVaultForWindow( - win: BrowserWindow, - synced: PersistedSyncedVault, - options: { persist?: boolean } = {} -): Promise<VaultInfo> { - const root = path.resolve(synced.root) - await ensureVaultLayout(root) - const vault = vaultInfo(root) - windowVaults.setSyncedVault(win.id, vault) - currentVault = vault - currentWorkspaceMode = 'synced' - if (!windowVaults.hasRemoteWindows()) { - remoteWorkspaceClient = null - remoteWorkspaceConfig = null - currentRemoteWorkspaceProfileId = null - remoteServerCapabilities = null - stopRemoteWatch() - } - await startSyncEngine(synced) - if (options.persist !== false) { - await updateConfig((cfg) => ({ - ...cfg, - workspaceMode: 'synced', - vaultRoot: root, - localVaults: rememberLocalVault(cfg.localVaults, vault), - remoteWorkspaceProfileId: null - })) - } - return vault -} - async function setVault(root: string): Promise<VaultInfo> { const win = currentIpcWindow() ?? mainWindow if (win && !win.isDestroyed()) return await setVaultForWindow(win, root) @@ -1299,7 +1222,6 @@ async function setVault(root: string): Promise<VaultInfo> { const vault = vaultInfo(path.resolve(root)) currentVault = vault currentWorkspaceMode = 'local' - stopSyncEngine() remoteWorkspaceClient = null remoteWorkspaceConfig = null currentRemoteWorkspaceProfileId = null @@ -1400,7 +1322,6 @@ async function setRemoteWorkspace( const win = currentIpcWindow() ?? mainWindow currentWorkspaceMode = 'remote' - stopSyncEngine() currentVault = vault if (win && !win.isDestroyed()) { windowVaults.setRemoteVault(win.id, vault) @@ -1770,19 +1691,6 @@ async function loadCurrentVaultFromConfig(): Promise<VaultInfo | null> { return null } } - if (cfg.workspaceMode === 'synced' && cfg.vaultRoot) { - const synced = cfg.syncedVaults.find( - (s) => path.resolve(s.root) === path.resolve(cfg.vaultRoot!) - ) - if (synced && win && !win.isDestroyed()) { - try { - return await setSyncedVaultForWindow(win, synced, { persist: false }) - } catch { - // Fall through to opening the folder as a plain local vault if the - // engine can't start (e.g. server profile gone) — never block startup. - } - } - } if (cfg.vaultRoot) { try { if (win && !win.isDestroyed()) { @@ -2042,65 +1950,6 @@ function registerIpc(): void { return await connectRemoteWorkspaceProfile(id) }) - handle(IPC.SYNC_GET_STATUS, async () => activeSyncEngine?.getStatus() ?? null) - - handle(IPC.SYNC_NOW, async () => { - activeSyncEngine?.syncNow() - }) - - handle(IPC.SYNC_DISMISS_CONFLICT, async (_e, copyPath: string) => { - activeSyncEngine?.dismissConflict(copyPath) - }) - - // Attach the currently open local vault to a saved server and switch it to - // synced mode (bidirectional backup/sync). - handle(IPC.SYNC_ATTACH_SERVER, async (_e, profileId: string) => { - const win = currentIpcWindow() ?? mainWindow - if (!win || win.isDestroyed()) throw new Error('No window is associated with this call.') - const vault = windowVaults.vaultForWindow(win.id) ?? currentVault - if (!vault) throw new Error('No vault is open to sync.') - const root = path.resolve(vault.root) - const cfg = await loadConfig() - const profile = findRemoteProfileById(cfg.remoteWorkspaceProfiles, profileId) - if (!profile) throw new Error('Add and save a server connection first.') - const synced: PersistedSyncedVault = { - root, - serverProfileId: profileId, - conflictPolicy: 'keep-both', - assetMode: 'lazy', - lastSyncAt: null - } - await updateConfig((c) => ({ - ...c, - syncedVaults: [...c.syncedVaults.filter((s) => path.resolve(s.root) !== root), synced] - })) - return await setSyncedVaultForWindow(win, synced) - }) - - // Detach: stop syncing and reopen the folder as a plain local vault. - handle(IPC.SYNC_DETACH_SERVER, async () => { - const win = currentIpcWindow() ?? mainWindow - const vault = win && !win.isDestroyed() ? windowVaults.vaultForWindow(win.id) : currentVault - if (!vault) return null - const root = path.resolve(vault.root) - await updateConfig((c) => ({ - ...c, - syncedVaults: c.syncedVaults.filter((s) => path.resolve(s.root) !== root) - })) - if (win && !win.isDestroyed()) return await setVaultForWindow(win, root) - return await setVault(root) - }) - - // Open an already-configured synced vault by its root. - handle(IPC.SYNC_OPEN_VAULT, async (_e, root: string) => { - const win = currentIpcWindow() ?? mainWindow - if (!win || win.isDestroyed()) throw new Error('No window is associated with this call.') - const cfg = await loadConfig() - const synced = cfg.syncedVaults.find((s) => path.resolve(s.root) === path.resolve(root)) - if (!synced) throw new Error('That synced vault is not configured.') - return await setSyncedVaultForWindow(win, synced) - }) - handle(IPC.VAULT_GET_CURRENT, async () => { return await loadCurrentVaultFromConfig() }) diff --git a/apps/desktop/src/main/remote/server-client.ts b/apps/desktop/src/main/remote/server-client.ts index 815c8e2d..cacfd990 100644 --- a/apps/desktop/src/main/remote/server-client.ts +++ b/apps/desktop/src/main/remote/server-client.ts @@ -9,7 +9,6 @@ import type { NoteFolder, NoteMeta, ServerCapabilities, - SyncManifestEntry, VaultChangeEvent, VaultDemoTourResult, VaultInfo, @@ -73,37 +72,6 @@ export class RemoteServerClient { return this.jsonRequest<NoteMeta[]>('/api/notes') } - async getSyncManifest(): Promise<SyncManifestEntry[]> { - return this.jsonRequest<SyncManifestEntry[]>('/api/sync/manifest') - } - - /** Push raw bytes to an exact vault path (binary file sync). */ - async writeSyncFile(relPath: string, bytes: Buffer): Promise<void> { - const form = new FormData() - form.set('path', relPath) - form.set('file', new Blob([bytes])) - const headers: Record<string, string> = {} - if (this.authToken) headers['Authorization'] = `Bearer ${this.authToken}` - const response = await fetch(`${this.baseUrl}/api/sync/write`, { - method: 'POST', - headers, - body: form - }) - if (!response.ok) { - const text = await response.text().catch(() => '') - throw new Error( - `Sync write failed (${response.status} ${response.statusText}) for ${relPath}${text ? `: ${text}` : ''}` - ) - } - } - - /** Read raw bytes for an exact vault path (binary file sync). */ - async readSyncFile(relPath: string): Promise<Buffer> { - const response = await this.fetchAssetResponse(relPath) - const buf = await response.arrayBuffer() - return Buffer.from(buf) - } - async listFolders(): Promise<FolderEntry[]> { return this.jsonRequest<FolderEntry[]>('/api/folders') } diff --git a/apps/desktop/src/main/sync/conflict.ts b/apps/desktop/src/main/sync/conflict.ts deleted file mode 100644 index 67e59654..00000000 --- a/apps/desktop/src/main/sync/conflict.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Conflict resolution policy. The default `keep-both` never overwrites: the - * losing version is written next to the original as a conflict copy. The - * `last-write-wins` policy is scaffolded for a future opt-in. - */ - -export type ConflictPolicy = 'keep-both' | 'last-write-wins' - -/** Two-digit zero-padded. */ -function pad(n: number): string { - return n < 10 ? `0${n}` : String(n) -} - -/** `YYYY-MM-DD HHmmss` in local time, for conflict-copy filenames. */ -export function formatConflictTimestamp(now: Date): string { - return ( - `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ` + - `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}` - ) -} - -/** - * Build the conflict-copy path for `originalPath`, preserving its folder and - * extension: `inbox/Note.md` → `inbox/Note (conflict 1a2b3c4d 2026-06-17 150405).md`. - */ -export function conflictCopyPath(originalPath: string, deviceId: string, now: Date): string { - const slash = originalPath.lastIndexOf('/') - const dot = originalPath.lastIndexOf('.') - const hasExt = dot > slash - const ext = hasExt ? originalPath.slice(dot) : '' - const stem = hasExt ? originalPath.slice(0, dot) : originalPath - const device = deviceId.slice(0, 8) - return `${stem} (conflict ${device} ${formatConflictTimestamp(now)})${ext}` -} - -/** A conflict copy this engine produced (so it never conflicts with itself). */ -const CONFLICT_MARKER = /\(conflict [0-9a-f]{8} \d{4}-\d{2}-\d{2} \d{6}\)/ - -export function isConflictCopyPath(path: string): boolean { - return CONFLICT_MARKER.test(path) -} diff --git a/apps/desktop/src/main/sync/engine.test.ts b/apps/desktop/src/main/sync/engine.test.ts deleted file mode 100644 index 0b0435d6..00000000 --- a/apps/desktop/src/main/sync/engine.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import { promises as fs } from 'node:fs' -import os from 'node:os' -import path from 'node:path' -import type { RemoteServerClient } from '../remote/server-client' -import { SyncEngine } from './engine' -import { hashBytes } from './scan-local' - -/** Minimal in-memory stand-in for the Go server (stores raw bytes). */ -class FakeServer { - files = new Map<string, Buffer>() - baseUrl = 'http://fake' - - async getSyncManifest(): Promise<{ path: string; hash: string; size: number; mtime: number }[]> { - return [...this.files].map(([p, bytes]) => ({ - path: p, - hash: hashBytes(bytes), - size: bytes.length, - mtime: 0 - })) - } - async readNote(p: string): Promise<{ path: string; body: string }> { - return { path: p, body: (this.files.get(p) ?? Buffer.alloc(0)).toString('utf8') } - } - async writeNote(p: string, body: string): Promise<unknown> { - this.files.set(p, Buffer.from(body, 'utf8')) - return {} - } - async readSyncFile(p: string): Promise<Buffer> { - return this.files.get(p) ?? Buffer.alloc(0) - } - async writeSyncFile(p: string, bytes: Buffer): Promise<void> { - this.files.set(p, Buffer.from(bytes)) - } - async deleteNote(p: string): Promise<void> { - this.files.delete(p) - } - watchVaultChanges(): () => void { - return () => {} - } -} - -function makeEngine(root: string, server: FakeServer): SyncEngine { - return new SyncEngine({ - root, - client: server as unknown as RemoteServerClient, - serverProfileId: 'p1', - conflictPolicy: 'keep-both' - }) -} - -async function writeLocal(root: string, rel: string, body: string): Promise<void> { - const abs = path.join(root, rel) - await fs.mkdir(path.dirname(abs), { recursive: true }) - await fs.writeFile(abs, body, 'utf8') -} -async function readLocal(root: string, rel: string): Promise<string | null> { - try { - return await fs.readFile(path.join(root, rel), 'utf8') - } catch { - return null - } -} -function serverText(server: FakeServer, p: string): string | undefined { - const b = server.files.get(p) - return b === undefined ? undefined : b.toString('utf8') -} -async function localNotePaths(root: string): Promise<string[]> { - const out: string[] = [] - const walk = async (dir: string): Promise<void> => { - for (const e of await fs.readdir(dir, { withFileTypes: true })) { - if (e.name.startsWith('.')) continue - const full = path.join(dir, e.name) - if (e.isDirectory()) await walk(full) - else if (e.name.endsWith('.md')) out.push(path.relative(root, full).split(path.sep).join('/')) - } - } - await walk(root) - return out.sort() -} - -describe('SyncEngine — end to end (fake server, real local fs)', () => { - let root: string - beforeEach(async () => { - root = await fs.mkdtemp(path.join(os.tmpdir(), 'zen-sync-')) - await fs.mkdir(path.join(root, '.zennotes'), { recursive: true }) - await fs.mkdir(path.join(root, 'inbox'), { recursive: true }) - }) - afterEach(async () => { - await fs.rm(root, { recursive: true, force: true }) - }) - - it('first sync = union merge of local + server', async () => { - const server = new FakeServer() - server.files.set('inbox/FromServer.md', Buffer.from('server note')) - await writeLocal(root, 'inbox/FromLocal.md', 'local note') - - await makeEngine(root, server).runOnce() - - // Local gained the server note; server gained the local note. - expect(await readLocal(root, 'inbox/FromServer.md')).toBe('server note') - expect(serverText(server, 'inbox/FromLocal.md')).toBe('local note') - expect(await localNotePaths(root)).toEqual(['inbox/FromLocal.md', 'inbox/FromServer.md']) - }) - - it('pulls a note added on the server after first sync', async () => { - const server = new FakeServer() - const engine = makeEngine(root, server) - await engine.runOnce() // establish base (empty) - server.files.set('inbox/New.md', Buffer.from('remote-only')) - await engine.runOnce() - expect(await readLocal(root, 'inbox/New.md')).toBe('remote-only') - }) - - it('pushes a local edit to the server', async () => { - const server = new FakeServer() - server.files.set('inbox/A.md', Buffer.from('v1')) - const engine = makeEngine(root, server) - await engine.runOnce() // pulls A=v1 to local, base set - await writeLocal(root, 'inbox/A.md', 'v2') - await engine.runOnce() - expect(serverText(server, 'inbox/A.md')).toBe('v2') - }) - - it('propagates a local delete to the server', async () => { - const server = new FakeServer() - server.files.set('inbox/A.md', Buffer.from('v1')) - const engine = makeEngine(root, server) - await engine.runOnce() // local now has A - await fs.rm(path.join(root, 'inbox/A.md')) - await engine.runOnce() - expect(server.files.has('inbox/A.md')).toBe(false) - }) - - it('keeps both on a concurrent conflicting edit (never loses data)', async () => { - const server = new FakeServer() - server.files.set('inbox/A.md', Buffer.from('base')) - const engine = makeEngine(root, server) - await engine.runOnce() // synced: local A = base - // Edit differently on both sides before the next sync. - await writeLocal(root, 'inbox/A.md', 'local edit') - server.files.set('inbox/A.md', Buffer.from('server edit')) - await engine.runOnce() - - // Local content is canonical at the path; the server edit is preserved as a copy. - expect(await readLocal(root, 'inbox/A.md')).toBe('local edit') - const paths = await localNotePaths(root) - const conflictCopy = paths.find((p) => p.includes('(conflict')) - expect(conflictCopy).toBeTruthy() - expect(await readLocal(root, conflictCopy as string)).toBe('server edit') - // Both files exist on the server too (converged). - expect(serverText(server, 'inbox/A.md')).toBe('local edit') - expect(serverText(server, conflictCopy as string)).toBe('server edit') - }) - - it('is idempotent: a second sync with no changes does nothing', async () => { - const server = new FakeServer() - server.files.set('inbox/A.md', Buffer.from('x')) - const engine = makeEngine(root, server) - await engine.runOnce() - const before = new Map(server.files) - await engine.runOnce() - expect([...server.files]).toEqual([...before]) - }) - - it('syncs binary assets byte-for-byte (push and pull)', async () => { - const server = new FakeServer() - const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xff, 0x00]) - server.files.set('assets/remote.png', png) - await fs.mkdir(path.join(root, 'assets'), { recursive: true }) - const localBin = Buffer.from([0x00, 0x01, 0x02, 0xfe, 0xff]) - await fs.writeFile(path.join(root, 'assets/local.bin'), localBin) - - await makeEngine(root, server).runOnce() - - // Pulled binary lands locally with identical bytes; local binary pushed up. - expect(await fs.readFile(path.join(root, 'assets/remote.png'))).toEqual(png) - expect(server.files.get('assets/local.bin')).toEqual(localBin) - }) - - it('syncs databases, comments, and settings (text under .base/.zennotes)', async () => { - const server = new FakeServer() - server.files.set('.zennotes/vault.json', Buffer.from('{"primaryNotesLocation":"inbox"}')) - server.files.set('inbox/Books.base/data.csv', Buffer.from('id,Title\nr1,Dune\n')) - server.files.set('.zennotes/comments/inbox/A.md.comments.json', Buffer.from('[{"id":"c1"}]')) - - await makeEngine(root, server).runOnce() - - expect(await readLocal(root, '.zennotes/vault.json')).toContain('primaryNotesLocation') - expect(await readLocal(root, 'inbox/Books.base/data.csv')).toContain('Dune') - expect(await readLocal(root, '.zennotes/comments/inbox/A.md.comments.json')).toContain('c1') - }) - - it('never syncs sync-state.json or the meta cache', async () => { - const server = new FakeServer() - await fs.writeFile(path.join(root, '.zennotes', 'note-meta-cache-v1.json'), '{"version":2}') - await fs.writeFile(path.join(root, 'inbox', 'Real.md'), 'real') - await makeEngine(root, server).runOnce() - expect(server.files.has('.zennotes/sync-state.json')).toBe(false) - expect(server.files.has('.zennotes/note-meta-cache-v1.json')).toBe(false) - expect(server.files.has('inbox/Real.md')).toBe(true) - }) -}) diff --git a/apps/desktop/src/main/sync/engine.ts b/apps/desktop/src/main/sync/engine.ts deleted file mode 100644 index d5f42921..00000000 --- a/apps/desktop/src/main/sync/engine.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { promises as fs } from 'node:fs' -import path from 'node:path' -import type { SyncStatus, VaultChangeEvent } from '@shared/ipc' -import type { RemoteServerClient } from '../remote/server-client' -import { - reconcile, - type BaseEntry, - type ConflictReason, - type FileState, - type SyncAction -} from './reconcile' -import { loadSyncState, saveSyncState, type SyncState } from './sync-state' -import { conflictCopyPath, type ConflictPolicy } from './conflict' -import { hashBytes, scanLocalVault } from './scan-local' - -// Text files transfer as UTF-8 strings via writeNote/readNote; everything else -// is treated as binary and transferred byte-for-byte via the sync-write endpoint. -const TEXT_EXTS = new Set(['.md', '.excalidraw', '.json', '.csv', '.txt', '.markdown']) -function isTextPath(p: string): boolean { - const dot = p.lastIndexOf('.') - const slash = p.lastIndexOf('/') - const ext = dot > slash ? p.slice(dot).toLowerCase() : '' - return TEXT_EXTS.has(ext) -} - -export interface SyncEngineOptions { - root: string - client: RemoteServerClient - serverProfileId: string - conflictPolicy: ConflictPolicy - onStatus?: (status: SyncStatus) => void -} - -const DEBOUNCE_MS = 750 -const PERIODIC_MS = 5 * 60 * 1000 - -/** - * The background sync engine for one synced vault. Triggered by local file - * changes (chokidar fan-out), server `/api/watch` events, and a periodic - * backstop; runs a debounced, single-flight three-way reconcile and replicates - * the resulting actions via the existing RemoteServerClient. Pure decisions live - * in reconcile.ts; this file is the I/O + scheduling shell. - */ -export class SyncEngine { - private state: SyncState | null = null - private status: SyncStatus = { - kind: 'idle', - pendingPush: 0, - pendingPull: 0, - conflicts: [], - lastSyncAt: 0, - lastError: null - } - private running = false - private syncing = false - private rerun = false - private debounceTimer: ReturnType<typeof setTimeout> | null = null - private periodicTimer: ReturnType<typeof setInterval> | null = null - private stopWatch: (() => void) | null = null - - constructor(private readonly opts: SyncEngineOptions) {} - - get root(): string { - return this.opts.root - } - - async start(): Promise<void> { - if (this.running) return - this.running = true - this.state = await loadSyncState( - this.opts.root, - this.opts.serverProfileId, - this.opts.client.baseUrl - ) - this.status = { ...this.status, lastSyncAt: this.state.lastSyncAt } - this.stopWatch = this.opts.client.watchVaultChanges(() => this.schedule()) - this.periodicTimer = setInterval(() => this.schedule(), PERIODIC_MS) - this.schedule(true) - } - - stop(): void { - this.running = false - this.stopWatch?.() - this.stopWatch = null - if (this.debounceTimer) clearTimeout(this.debounceTimer) - if (this.periodicTimer) clearInterval(this.periodicTimer) - this.debounceTimer = null - this.periodicTimer = null - } - - getStatus(): SyncStatus { - return this.status - } - - /** Load state (if needed) and run a single reconcile. Used by syncNow paths - * and tests; the scheduler calls reconcileOnce directly via runSync. */ - async runOnce(): Promise<void> { - if (!this.state) { - this.state = await loadSyncState( - this.opts.root, - this.opts.serverProfileId, - this.opts.client.baseUrl - ) - } - await this.reconcileOnce() - } - - /** Fan-out target for local disk changes under this vault root. */ - onLocalChange(_ev: VaultChangeEvent): void { - this.schedule() - } - - /** Force an immediate reconcile (manual "Sync now"). */ - syncNow(): void { - this.schedule(true) - } - - /** Drop a conflict-copy path from the status list once the user has dealt - * with it (the file ops happen through the normal vault IPC). */ - dismissConflict(copyPath: string): void { - if (this.status.conflicts.includes(copyPath)) { - this.setStatus({ conflicts: this.status.conflicts.filter((c) => c !== copyPath) }) - } - } - - private schedule(immediate = false): void { - if (!this.running) return - if (this.debounceTimer) clearTimeout(this.debounceTimer) - this.debounceTimer = setTimeout(() => void this.runSync(), immediate ? 0 : DEBOUNCE_MS) - } - - private setStatus(patch: Partial<SyncStatus>): void { - this.status = { ...this.status, ...patch } - this.opts.onStatus?.(this.status) - } - - private async runSync(): Promise<void> { - if (!this.running) return - if (this.syncing) { - this.rerun = true - return - } - this.syncing = true - this.setStatus({ kind: 'syncing', lastError: null }) - try { - await this.reconcileOnce() - this.setStatus({ kind: 'idle', lastSyncAt: Date.now(), pendingPush: 0, pendingPull: 0 }) - } catch (err) { - const message = err instanceof Error ? err.message : String(err) - this.setStatus({ kind: isOffline(message) ? 'offline' : 'error', lastError: message }) - } finally { - this.syncing = false - if (this.rerun) { - this.rerun = false - this.schedule() - } - } - } - - private async reconcileOnce(): Promise<void> { - const state = this.state - if (!state) return - const [localMap, remoteList] = await Promise.all([ - scanLocalVault(this.opts.root), - this.opts.client.getSyncManifest() - ]) - const remoteMap = new Map<string, FileState>( - remoteList.map((e) => [e.path, { hash: e.hash, size: e.size }]) - ) - const baseMap = new Map<string, BaseEntry>( - Object.entries(state.entries).map(([p, e]) => [p, { contentHash: e.contentHash, size: e.size }]) - ) - - const actions = reconcile(localMap, remoteMap, baseMap) - let pushes = 0 - let pulls = 0 - const conflicts: string[] = [] - for (const action of actions) { - try { - await this.execute(action, localMap, remoteMap, state, conflicts) - if (action.kind === 'push') pushes++ - if (action.kind === 'pull') pulls++ - } catch (err) { - // One file failing (e.g. oversize, gone) must not abort the whole sync. - this.setStatus({ lastError: `${action.kind} ${action.path}: ${(err as Error).message}` }) - } - } - state.lastSyncAt = Date.now() - await saveSyncState(this.opts.root, state) - if (conflicts.length) { - this.setStatus({ conflicts: dedupe([...this.status.conflicts, ...conflicts]) }) - } - this.status = { ...this.status, pendingPush: pushes, pendingPull: pulls } - } - - private async execute( - action: SyncAction, - localMap: Map<string, FileState>, - remoteMap: Map<string, FileState>, - state: SyncState, - conflicts: string[] - ): Promise<void> { - const client = this.opts.client - const root = this.opts.root - switch (action.kind) { - case 'push': { - if (isTextPath(action.path)) { - await client.writeNote(action.path, await readLocal(root, action.path)) - } else { - await client.writeSyncFile(action.path, await readLocalBinary(root, action.path)) - } - setBase(state, action.path, localMap.get(action.path)) - break - } - case 'pull': { - if (isTextPath(action.path)) { - const content = await client.readNote(action.path) - await writeLocalAtomic(root, action.path, content.body) - } else { - await writeLocalBinaryAtomic(root, action.path, await client.readSyncFile(action.path)) - } - setBase(state, action.path, remoteMap.get(action.path)) - break - } - case 'deleteRemote': { - await client.deleteNote(action.path) - delete state.entries[action.path] - break - } - case 'deleteLocal': { - await removeLocal(root, action.path) - delete state.entries[action.path] - break - } - case 'converge': { - setBase(state, action.path, localMap.get(action.path) ?? remoteMap.get(action.path)) - break - } - case 'forget': { - delete state.entries[action.path] - break - } - case 'conflict': { - await this.resolveConflict(action.path, action.reason, localMap, remoteMap, state, conflicts) - break - } - } - } - - private async resolveConflict( - p: string, - reason: ConflictReason, - localMap: Map<string, FileState>, - remoteMap: Map<string, FileState>, - state: SyncState, - conflicts: string[] - ): Promise<void> { - const client = this.opts.client - const root = this.opts.root - const text = isTextPath(p) - // delete-vs-edit: resurrect from the server (the edit wins over the delete). - if (reason === 'delete-vs-edit') { - if (text) await writeLocalAtomic(root, p, (await client.readNote(p)).body) - else await writeLocalBinaryAtomic(root, p, await client.readSyncFile(p)) - setBase(state, p, remoteMap.get(p)) - conflicts.push(p) - return - } - // edit-vs-delete: resurrect to the server (the edit wins over the delete). - if (reason === 'edit-vs-delete') { - if (text) await client.writeNote(p, await readLocal(root, p)) - else await client.writeSyncFile(p, await readLocalBinary(root, p)) - setBase(state, p, localMap.get(p)) - conflicts.push(p) - return - } - // both-edited / first-sync-clash: keep both. Local stays canonical; the - // server's differing version is written next to it as a conflict copy. - const copyPath = conflictCopyPath(p, state.deviceId, new Date()) - let remoteBytes: Buffer - if (text) { - const remote = await client.readNote(p) - remoteBytes = Buffer.from(remote.body, 'utf8') - await writeLocalAtomic(root, copyPath, remote.body) - await client.writeNote(copyPath, remote.body) - await client.writeNote(p, await readLocal(root, p)) // server path = local (canonical) - } else { - remoteBytes = await client.readSyncFile(p) - await writeLocalBinaryAtomic(root, copyPath, remoteBytes) - await client.writeSyncFile(copyPath, remoteBytes) - await client.writeSyncFile(p, await readLocalBinary(root, p)) - } - setBase(state, p, localMap.get(p)) - state.entries[copyPath] = { contentHash: hashBytes(remoteBytes), size: remoteBytes.length } - conflicts.push(copyPath) - } -} - -function setBase(state: SyncState, p: string, entry: FileState | undefined): void { - if (!entry) { - delete state.entries[p] - return - } - state.entries[p] = { contentHash: entry.hash, size: entry.size } -} - -function dedupe(items: string[]): string[] { - return [...new Set(items)] -} - -function isOffline(message: string): boolean { - return /Could not connect|ECONN|fetch failed|ENOTFOUND|ETIMEDOUT|network/i.test(message) -} - -// --- local file helpers (path-safe, atomic writes) --- - -function safeAbs(root: string, rel: string): string { - const abs = path.resolve(root, rel) - const base = path.resolve(root) - if (abs !== base && !abs.startsWith(base + path.sep)) { - throw new Error(`refusing path outside vault: ${rel}`) - } - return abs -} - -async function readLocal(root: string, rel: string): Promise<string> { - return fs.readFile(safeAbs(root, rel), 'utf8') -} - -async function writeLocalAtomic(root: string, rel: string, body: string): Promise<void> { - const abs = safeAbs(root, rel) - await fs.mkdir(path.dirname(abs), { recursive: true }) - const tmp = `${abs}.${process.pid}.${Date.now()}.synctmp` - await fs.writeFile(tmp, body, 'utf8') - await fs.rename(tmp, abs) -} - -async function readLocalBinary(root: string, rel: string): Promise<Buffer> { - return fs.readFile(safeAbs(root, rel)) -} - -async function writeLocalBinaryAtomic(root: string, rel: string, bytes: Buffer): Promise<void> { - const abs = safeAbs(root, rel) - await fs.mkdir(path.dirname(abs), { recursive: true }) - const tmp = `${abs}.${process.pid}.${Date.now()}.synctmp` - await fs.writeFile(tmp, bytes) - await fs.rename(tmp, abs) -} - -async function removeLocal(root: string, rel: string): Promise<void> { - await fs.rm(safeAbs(root, rel), { force: true }) -} diff --git a/apps/desktop/src/main/sync/reconcile.test.ts b/apps/desktop/src/main/sync/reconcile.test.ts deleted file mode 100644 index 13c9f6a1..00000000 --- a/apps/desktop/src/main/sync/reconcile.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { reconcile, isNeverSync, type FileState, type BaseEntry, type SyncAction } from './reconcile' - -function L(entries: Record<string, string>): Map<string, FileState> { - return new Map(Object.entries(entries).map(([p, h]) => [p, { hash: h, size: h.length }])) -} -function B(entries: Record<string, string>): Map<string, BaseEntry> { - return new Map(Object.entries(entries).map(([p, h]) => [p, { contentHash: h, size: h.length }])) -} -/** Run reconcile on a single path and return the one action (or undefined). */ -function one( - local: Record<string, string>, - remote: Record<string, string>, - base: Record<string, string> -): SyncAction | undefined { - const actions = reconcile(L(local), L(remote), B(base)) - expect(actions.length).toBeLessThanOrEqual(1) - return actions[0] -} - -describe('reconcile — decision table', () => { - it('unchanged on both sides → no action', () => { - expect(one({ 'a.md': 'x' }, { 'a.md': 'x' }, { 'a.md': 'x' })).toBeUndefined() - }) - it('edited locally only → push', () => { - expect(one({ 'a.md': 'x2' }, { 'a.md': 'x' }, { 'a.md': 'x' })).toEqual({ kind: 'push', path: 'a.md' }) - }) - it('edited remotely only → pull', () => { - expect(one({ 'a.md': 'x' }, { 'a.md': 'x2' }, { 'a.md': 'x' })).toEqual({ kind: 'pull', path: 'a.md' }) - }) - it('both edited to identical bytes → converge', () => { - expect(one({ 'a.md': 'y' }, { 'a.md': 'y' }, { 'a.md': 'x' })).toEqual({ kind: 'converge', path: 'a.md' }) - }) - it('both edited differently → conflict(both-edited)', () => { - expect(one({ 'a.md': 'L' }, { 'a.md': 'R' }, { 'a.md': 'x' })).toEqual({ - kind: 'conflict', - path: 'a.md', - reason: 'both-edited' - }) - }) - it('deleted locally, remote unchanged → deleteRemote', () => { - expect(one({}, { 'a.md': 'x' }, { 'a.md': 'x' })).toEqual({ kind: 'deleteRemote', path: 'a.md' }) - }) - it('deleted remotely, local unchanged → deleteLocal', () => { - expect(one({ 'a.md': 'x' }, {}, { 'a.md': 'x' })).toEqual({ kind: 'deleteLocal', path: 'a.md' }) - }) - it('deleted locally but edited remotely → conflict(delete-vs-edit)', () => { - expect(one({}, { 'a.md': 'x2' }, { 'a.md': 'x' })).toEqual({ - kind: 'conflict', - path: 'a.md', - reason: 'delete-vs-edit' - }) - }) - it('edited locally but deleted remotely → conflict(edit-vs-delete)', () => { - expect(one({ 'a.md': 'x2' }, {}, { 'a.md': 'x' })).toEqual({ - kind: 'conflict', - path: 'a.md', - reason: 'edit-vs-delete' - }) - }) - it('new local, no base → push', () => { - expect(one({ 'a.md': 'x' }, {}, {})).toEqual({ kind: 'push', path: 'a.md' }) - }) - it('new remote, no base → pull', () => { - expect(one({}, { 'a.md': 'x' }, {})).toEqual({ kind: 'pull', path: 'a.md' }) - }) - it('first sync, same path identical bytes → converge', () => { - expect(one({ 'a.md': 'x' }, { 'a.md': 'x' }, {})).toEqual({ kind: 'converge', path: 'a.md' }) - }) - it('first sync, same path different bytes → conflict(first-sync-clash)', () => { - expect(one({ 'a.md': 'L' }, { 'a.md': 'R' }, {})).toEqual({ - kind: 'conflict', - path: 'a.md', - reason: 'first-sync-clash' - }) - }) - it('deleted on both sides → forget the base entry', () => { - expect(one({}, {}, { 'a.md': 'x' })).toEqual({ kind: 'forget', path: 'a.md' }) - }) -}) - -describe('reconcile — NEVER_SYNC + union', () => { - it('never syncs sync-state.json / note-meta-cache', () => { - expect(isNeverSync('.zennotes/sync-state.json')).toBe(true) - expect(isNeverSync('.zennotes/note-meta-cache-v1.json')).toBe(true) - expect(isNeverSync('inbox/note.md')).toBe(false) - const actions = reconcile(L({ '.zennotes/sync-state.json': 'a' }), L({}), B({})) - expect(actions).toEqual([]) - }) - it('first-sync union merge across both sides', () => { - const actions = reconcile( - L({ 'inbox/A.md': 'a', 'inbox/Shared.md': 'L' }), - L({ 'inbox/B.md': 'b', 'inbox/Shared.md': 'R' }), - B({}) - ) - const byPath = new Map(actions.map((x) => [x.path, x])) - expect(byPath.get('inbox/A.md')).toEqual({ kind: 'push', path: 'inbox/A.md' }) - expect(byPath.get('inbox/B.md')).toEqual({ kind: 'pull', path: 'inbox/B.md' }) - expect(byPath.get('inbox/Shared.md')).toEqual({ - kind: 'conflict', - path: 'inbox/Shared.md', - reason: 'first-sync-clash' - }) - }) -}) diff --git a/apps/desktop/src/main/sync/reconcile.ts b/apps/desktop/src/main/sync/reconcile.ts deleted file mode 100644 index f99cf44a..00000000 --- a/apps/desktop/src/main/sync/reconcile.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Pure three-way reconcile for synced vaults. - * - * For every path we compare three content hashes: - * L = local hash (null if the file is absent locally) - * R = remote hash (null if absent on the server) - * B = base hash (the bytes at the last successful sync; null if never synced) - * - * Because the server has no content hashes of its own and no tombstones (a delete - * is just a missing file), the base snapshot is what lets us tell "deleted here" - * apart from "new there". Decisions are hash-only — never mtime — so clock skew - * across machines can't corrupt them. - */ - -export interface FileState { - hash: string - size: number -} - -export interface BaseEntry { - contentHash: string - size: number -} - -export type ConflictReason = - | 'both-edited' // edited differently on both sides - | 'first-sync-clash' // same path, different bytes, never synced before - | 'delete-vs-edit' // deleted locally, edited remotely → resurrect from server - | 'edit-vs-delete' // edited locally, deleted remotely → resurrect to server - -export type SyncAction = - | { kind: 'push'; path: string } // local → server - | { kind: 'pull'; path: string } // server → local - | { kind: 'deleteRemote'; path: string } // remove on server - | { kind: 'deleteLocal'; path: string } // remove locally - | { kind: 'conflict'; path: string; reason: ConflictReason } - | { kind: 'converge'; path: string } // L === R but ≠ B: update base, no transfer - | { kind: 'forget'; path: string } // deleted on both sides: drop the base entry - -/** Paths that must never participate in sync (per-device / cache files). */ -const NEVER_SYNC_PREFIXES = ['.zennotes/sync-state.json', '.zennotes/note-meta-cache'] - -export function isNeverSync(path: string): boolean { - return NEVER_SYNC_PREFIXES.some((p) => path === p || path.startsWith(p)) -} - -export function reconcile( - local: Map<string, FileState>, - remote: Map<string, FileState>, - base: Map<string, BaseEntry> -): SyncAction[] { - const paths = new Set<string>([...local.keys(), ...remote.keys(), ...base.keys()]) - const actions: SyncAction[] = [] - - for (const path of paths) { - if (isNeverSync(path)) continue - const L = local.get(path)?.hash ?? null - const R = remote.get(path)?.hash ?? null - const B = base.get(path)?.contentHash ?? null - - if (L !== null && R !== null) { - if (L === B && R === B) continue // unchanged both sides - if (L !== B && R === B) { - actions.push({ kind: 'push', path }) - continue - } - if (L === B && R !== B) { - actions.push({ kind: 'pull', path }) - continue - } - // both changed (or never synced) - if (L === R) { - actions.push({ kind: 'converge', path }) - continue - } - actions.push({ - kind: 'conflict', - path, - reason: B === null ? 'first-sync-clash' : 'both-edited' - }) - continue - } - - if (L !== null && R === null) { - if (B === null) { - actions.push({ kind: 'push', path }) // new local - continue - } - if (L === B) { - actions.push({ kind: 'deleteLocal', path }) // deleted remotely, local unchanged - continue - } - actions.push({ kind: 'conflict', path, reason: 'edit-vs-delete' }) - continue - } - - if (L === null && R !== null) { - if (B === null) { - actions.push({ kind: 'pull', path }) // new remote - continue - } - if (R === B) { - actions.push({ kind: 'deleteRemote', path }) // deleted locally, remote unchanged - continue - } - actions.push({ kind: 'conflict', path, reason: 'delete-vs-edit' }) - continue - } - - // L === null && R === null - if (B !== null) actions.push({ kind: 'forget', path }) // deleted on both sides - } - - return actions -} diff --git a/apps/desktop/src/main/sync/scan-local.ts b/apps/desktop/src/main/sync/scan-local.ts deleted file mode 100644 index 7ee64ef5..00000000 --- a/apps/desktop/src/main/sync/scan-local.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { promises as fs } from 'node:fs' -import type { Dirent } from 'node:fs' -import path from 'node:path' -import { createHash } from 'node:crypto' -import { isNeverSync, type FileState } from './reconcile' - -/** sha256 content hash matching the server's hashContent(). */ -export function hashBytes(body: Buffer): string { - return 'sha256:' + createHash('sha256').update(body).digest('hex') -} - -const INTERNAL_DIR = '.zennotes' - -async function walkAll( - vaultRoot: string, - dir: string, - out: Map<string, FileState> -): Promise<void> { - let entries: Dirent[] - try { - entries = await fs.readdir(dir, { withFileTypes: true }) - } catch { - return - } - for (const entry of entries) { - const name = entry.name - const full = path.join(dir, name) - if (entry.isDirectory()) { - // Walk into `.zennotes` (for vault.json + comments); skip other dot-dirs. - if (name !== INTERNAL_DIR && name.startsWith('.')) continue - await walkAll(vaultRoot, full, out) - continue - } - if (name.startsWith('.')) continue // dotfiles (.DS_Store, etc.) - if (name.endsWith('.tmp') || name.endsWith('.synctmp')) continue - const rel = path.relative(vaultRoot, full).split(path.sep).join('/') - if (isNeverSync(rel)) continue - try { - const body = await fs.readFile(full) - out.set(rel, { hash: hashBytes(body), size: body.length }) - } catch { - // skip unreadable file - } - } -} - -/** - * Walk and hash every syncable file in a vault — notes, drawings, databases, - * assets, comments, and vault settings — mirroring the server's collectSyncFiles - * (the whole tree, excluding the per-device sync-state, the meta cache, dotfiles, - * unrelated dot-dirs, and temp files). - */ -export async function scanLocalVault(root: string): Promise<Map<string, FileState>> { - const out = new Map<string, FileState>() - await walkAll(root, root, out) - return out -} diff --git a/apps/desktop/src/main/sync/sync-state.ts b/apps/desktop/src/main/sync/sync-state.ts deleted file mode 100644 index 6011e315..00000000 --- a/apps/desktop/src/main/sync/sync-state.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { promises as fs } from 'node:fs' -import path from 'node:path' -import { randomUUID } from 'node:crypto' - -/** Per-path base: the content + server state recorded at the last successful sync. */ -export interface SyncEntry { - /** sha256 of the bytes that were in sync (local == remote at that moment). */ - contentHash: string - size: number -} - -/** - * The base snapshot for a synced vault, persisted at - * `<root>/.zennotes/sync-state.json`. It is per-device and never itself synced. - * `pendingPull`/`pendingPush` form an intent log: paths whose transfer was - * started but not confirmed, re-derived from the manifest on the next run so a - * crash mid-sync can never be mistaken for a user edit. - */ -export interface SyncState { - version: number - deviceId: string - serverProfileId: string - baseUrl: string - lastSyncAt: number - entries: Record<string, SyncEntry> - pendingPull: string[] - pendingPush: string[] -} - -export const SYNC_STATE_VERSION = 1 - -export function syncStatePath(root: string): string { - return path.join(root, '.zennotes', 'sync-state.json') -} - -export function emptySyncState(serverProfileId: string, baseUrl: string): SyncState { - return { - version: SYNC_STATE_VERSION, - deviceId: randomUUID(), - serverProfileId, - baseUrl, - lastSyncAt: 0, - entries: {}, - pendingPull: [], - pendingPush: [] - } -} - -export async function loadSyncState( - root: string, - serverProfileId: string, - baseUrl: string -): Promise<SyncState> { - try { - const raw = await fs.readFile(syncStatePath(root), 'utf8') - const parsed = JSON.parse(raw) as Partial<SyncState> - if ( - parsed.version === SYNC_STATE_VERSION && - typeof parsed.deviceId === 'string' && - parsed.entries && - typeof parsed.entries === 'object' - ) { - return { - version: SYNC_STATE_VERSION, - deviceId: parsed.deviceId, - serverProfileId: parsed.serverProfileId ?? serverProfileId, - baseUrl: parsed.baseUrl ?? baseUrl, - lastSyncAt: typeof parsed.lastSyncAt === 'number' ? parsed.lastSyncAt : 0, - entries: parsed.entries as Record<string, SyncEntry>, - pendingPull: Array.isArray(parsed.pendingPull) ? parsed.pendingPull : [], - pendingPush: Array.isArray(parsed.pendingPush) ? parsed.pendingPush : [] - } - } - } catch { - // missing or invalid → fresh state (first sync = union merge) - } - return emptySyncState(serverProfileId, baseUrl) -} - -/** Atomically persist the base snapshot (temp file + rename). */ -export async function saveSyncState(root: string, state: SyncState): Promise<void> { - const target = syncStatePath(root) - await fs.mkdir(path.dirname(target), { recursive: true }) - const tmp = `${target}.${process.pid}.${Date.now()}.tmp` - await fs.writeFile(tmp, JSON.stringify(state), 'utf8') - await fs.rename(tmp, target) -} diff --git a/apps/desktop/src/main/vault.ts b/apps/desktop/src/main/vault.ts index 5e2e74f1..7f21681d 100644 --- a/apps/desktop/src/main/vault.ts +++ b/apps/desktop/src/main/vault.ts @@ -40,8 +40,7 @@ import { VaultTextSearchBackendResolved, VaultTextSearchToolPaths, VaultTextSearchMatch, - VaultInfo, - type WorkspaceMode + VaultInfo } from '@shared/ipc' import { DEMO_TOUR_DIR } from '@shared/demo-tour' import { @@ -256,24 +255,10 @@ export interface PersistedRemoteWorkspaceProfile extends PersistedRemoteWorkspac export type PersistedLocalVault = LocalVaultEntry -export type SyncConflictPolicy = 'keep-both' | 'last-write-wins' -export type SyncAssetMode = 'lazy' | 'full' - -/** A local vault folder bound to a server profile for bidirectional sync. */ -export interface PersistedSyncedVault { - root: string - /** id into remoteWorkspaceProfiles — the server hub this folder syncs with. */ - serverProfileId: string - conflictPolicy: SyncConflictPolicy - assetMode: SyncAssetMode - lastSyncAt: number | null -} - export interface PersistedConfig { - workspaceMode: WorkspaceMode + workspaceMode: 'local' | 'remote' vaultRoot: string | null localVaults: PersistedLocalVault[] - syncedVaults: PersistedSyncedVault[] remoteWorkspace: PersistedRemoteWorkspaceConfig | null remoteWorkspaceProfileId: string | null remoteWorkspaceProfiles: PersistedRemoteWorkspaceProfile[] @@ -293,7 +278,6 @@ const DEFAULT_CONFIG: PersistedConfig = { workspaceMode: 'local', vaultRoot: null, localVaults: [], - syncedVaults: [], remoteWorkspace: null, remoteWorkspaceProfileId: null, remoteWorkspaceProfiles: [], @@ -415,50 +399,17 @@ function normalizePersistedConfig(value: unknown): PersistedConfig { localVaultsByRoot.set(entry.root, entry) } } - const normalizeSyncedVault = (raw: unknown): PersistedSyncedVault | null => { - if (!raw || typeof raw !== 'object') return null - const value = raw as Record<string, unknown> - const rawRoot = typeof value.root === 'string' ? value.root.trim() : '' - const serverProfileId = - typeof value.serverProfileId === 'string' ? value.serverProfileId.trim() : '' - if (!rawRoot || !serverProfileId) return null - // Drop bindings whose server profile no longer exists. - if (!remoteWorkspaceProfiles.some((p) => p.id === serverProfileId)) return null - return { - root: path.resolve(rawRoot), - serverProfileId, - conflictPolicy: value.conflictPolicy === 'last-write-wins' ? 'last-write-wins' : 'keep-both', - assetMode: value.assetMode === 'full' ? 'full' : 'lazy', - lastSyncAt: - typeof value.lastSyncAt === 'number' && Number.isFinite(value.lastSyncAt) - ? value.lastSyncAt - : null - } - } - const syncedVaultsByRoot = new Map<string, PersistedSyncedVault>() - const rawSyncedVaults = Array.isArray(candidate.syncedVaults) ? candidate.syncedVaults : [] - for (const raw of rawSyncedVaults) { - const entry = normalizeSyncedVault(raw) - if (entry) syncedVaultsByRoot.set(entry.root, entry) - } const quickCaptureHotkey = typeof candidate.quickCaptureHotkey === 'string' ? candidate.quickCaptureHotkey.trim() : DEFAULT_QUICK_CAPTURE_HOTKEY const quickCapturePinned = candidate.quickCapturePinned === true - const workspaceMode: WorkspaceMode = - candidate.workspaceMode === 'remote' - ? 'remote' - : candidate.workspaceMode === 'synced' - ? 'synced' - : 'local' return { - workspaceMode, + workspaceMode: candidate.workspaceMode === 'remote' ? 'remote' : 'local', vaultRoot: typeof candidate.vaultRoot === 'string' ? candidate.vaultRoot : null, localVaults: [...localVaultsByRoot.values()].sort( (a, b) => b.lastOpenedAt - a.lastOpenedAt || a.name.localeCompare(b.name) ), - syncedVaults: [...syncedVaultsByRoot.values()], remoteWorkspace: legacyRemoteWorkspace, remoteWorkspaceProfileId: typeof candidate.remoteWorkspaceProfileId === 'string' && diff --git a/apps/desktop/src/main/window-vaults.ts b/apps/desktop/src/main/window-vaults.ts index c7279659..7f6a65e2 100644 --- a/apps/desktop/src/main/window-vaults.ts +++ b/apps/desktop/src/main/window-vaults.ts @@ -1,13 +1,7 @@ import path from 'node:path' import type { VaultChangeEvent, VaultInfo } from '@shared/ipc' -export type WindowWorkspaceMode = 'local' | 'remote' | 'synced' - -/** Both `local` and `synced` are backed by real files on disk and share the - * per-root file watcher; `remote` is a thin client with no local files. */ -function isDiskBacked(mode: WindowWorkspaceMode | undefined): boolean { - return mode === 'local' || mode === 'synced' -} +export type WindowWorkspaceMode = 'local' | 'remote' export interface VaultWatcherLike { start(root: string, onEvent: (ev: VaultChangeEvent) => void): void @@ -29,8 +23,6 @@ export interface WindowVaultRegistryOptions { makeWatcher: () => VaultWatcherLike invalidateVault: (root: string, ev: VaultChangeEvent) => void sendVaultChange: (windowId: number, ev: VaultChangeEvent) => void - /** Fan-out of every disk change under a watched root, for the sync engine. */ - onLocalChange?: (root: string, ev: VaultChangeEvent) => void } export class WindowVaultRegistry { @@ -40,30 +32,15 @@ export class WindowVaultRegistry { constructor(private readonly options: WindowVaultRegistryOptions) {} setLocalVault(windowId: number, vault: VaultInfo): void { - this.attachDiskVault(windowId, vault, 'local') - } - - /** A synced vault is disk-backed exactly like a local vault (same watcher); - * the only difference is the mode tag and an attached background sync engine - * (wired by the caller in index.ts). */ - setSyncedVault(windowId: number, vault: VaultInfo): void { - this.attachDiskVault(windowId, vault, 'synced') - } - - private attachDiskVault( - windowId: number, - vault: VaultInfo, - mode: 'local' | 'synced' - ): void { const root = normalizeRoot(vault.root) const previous = this.sessions.get(windowId) - if (previous && isDiskBacked(previous.mode) && previous.vault) { + if (previous?.mode === 'local' && previous.vault) { const previousRoot = normalizeRoot(previous.vault.root) if (previousRoot !== root) this.detachLocalWindow(windowId, previousRoot) } this.sessions.set(windowId, { - mode, + mode: 'local', vault: { ...vault, root } }) @@ -77,7 +54,6 @@ export class WindowVaultRegistry { } watcher.start(root, (ev) => { this.options.invalidateVault(root, ev) - this.options.onLocalChange?.(root, ev) this.sendLocalVaultChange(root, ev) }) this.localVaultWatches.set(root, registration) @@ -87,7 +63,7 @@ export class WindowVaultRegistry { setRemoteVault(windowId: number, vault: VaultInfo | null): void { const previous = this.sessions.get(windowId) - if (previous && isDiskBacked(previous.mode) && previous.vault) { + if (previous?.mode === 'local' && previous.vault) { this.detachLocalWindow(windowId, normalizeRoot(previous.vault.root)) } this.sessions.set(windowId, { @@ -98,7 +74,7 @@ export class WindowVaultRegistry { clearWindow(windowId: number): void { const previous = this.sessions.get(windowId) - if (previous && isDiskBacked(previous.mode) && previous.vault) { + if (previous?.mode === 'local' && previous.vault) { this.detachLocalWindow(windowId, normalizeRoot(previous.vault.root)) } this.sessions.delete(windowId) @@ -128,10 +104,6 @@ export class WindowVaultRegistry { return this.sessions.get(windowId)?.mode === 'remote' } - isSyncedWindow(windowId: number): boolean { - return this.sessions.get(windowId)?.mode === 'synced' - } - hasRemoteWindows(): boolean { for (const session of this.sessions.values()) { if (session.mode === 'remote') return true @@ -149,7 +121,7 @@ export class WindowVaultRegistry { isPathInsideWindowVault(windowId: number, absPath: string): boolean { const session = this.sessions.get(windowId) - if (!isDiskBacked(session?.mode) || !session?.vault) return false + if (session?.mode !== 'local' || !session.vault) return false return isPathInsideRoot(path.resolve(absPath), normalizeRoot(session.vault.root)) } diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index 0c268e0c..2c30c834 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -36,7 +36,6 @@ import type { RemoteWorkspaceProfileInput, ServerCapabilities, ServerSessionStatus, - SyncStatus, VaultChangeEvent, VaultDemoTourResult, VaultInfo, @@ -229,31 +228,6 @@ const api: ZenBridge = { return result }, - getSyncStatus: (): Promise<SyncStatus | null> => ipcRenderer.invoke(IPC.SYNC_GET_STATUS), - syncNow: (): Promise<void> => ipcRenderer.invoke(IPC.SYNC_NOW), - dismissSyncConflict: (copyPath: string): Promise<void> => - ipcRenderer.invoke(IPC.SYNC_DISMISS_CONFLICT, copyPath), - attachSyncServer: async (profileId: string): Promise<VaultInfo> => { - const vault = await ipcRenderer.invoke(IPC.SYNC_ATTACH_SERVER, profileId) - await refreshRemoteWorkspaceInfo() - return vault - }, - detachSyncServer: async (): Promise<VaultInfo | null> => { - const vault = await ipcRenderer.invoke(IPC.SYNC_DETACH_SERVER) - await refreshRemoteWorkspaceInfo() - return vault - }, - openSyncedVault: async (root: string): Promise<VaultInfo> => { - const vault = await ipcRenderer.invoke(IPC.SYNC_OPEN_VAULT, root) - await refreshRemoteWorkspaceInfo() - return vault - }, - onSyncStatus: (cb: (status: SyncStatus) => void): (() => void) => { - const listener = (_: unknown, status: SyncStatus): void => cb(status) - ipcRenderer.on(IPC.SYNC_ON_STATUS, listener) - return () => ipcRenderer.removeListener(IPC.SYNC_ON_STATUS, listener) - }, - getCurrentVault: async (): Promise<VaultInfo | null> => { const vault = await ipcRenderer.invoke(IPC.VAULT_GET_CURRENT) await refreshRemoteWorkspaceInfo() diff --git a/apps/server/internal/httpserver/excalidraw_test.go b/apps/server/internal/httpserver/excalidraw_test.go index 1235d8d9..cf2d8ca9 100644 --- a/apps/server/internal/httpserver/excalidraw_test.go +++ b/apps/server/internal/httpserver/excalidraw_test.go @@ -69,50 +69,3 @@ func TestCreateExcalidrawEndpoint(t *testing.T) { t.Errorf("created drawing %q not returned by /api/notes", created.Path) } } - -func TestSyncManifestEndpoint(t *testing.T) { - root := t.TempDir() - server, _ := newTestServer(t, config.Config{ - VaultPath: root, - DefaultVaultPath: root, - Bind: "127.0.0.1:7878", - AuthToken: "secret-token", - BrowseRoots: []string{root}, - }) - - // Requires auth. - unauth, err := http.Get(server.URL + "/api/sync/manifest") - if err != nil { - t.Fatal(err) - } - unauth.Body.Close() - if unauth.StatusCode != http.StatusUnauthorized { - t.Fatalf("manifest without auth = %d, want 401", unauth.StatusCode) - } - - jar := loginAndJar(t, server, "secret-token") - client := &http.Client{Jar: jar} - resp, err := client.Get(server.URL + "/api/sync/manifest") - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Fatalf("manifest status = %d", resp.StatusCode) - } - var manifest []struct { - Path string `json:"path"` - Hash string `json:"hash"` - } - if err := json.NewDecoder(resp.Body).Decode(&manifest); err != nil { - t.Fatalf("decode manifest: %v", err) - } - if len(manifest) == 0 { - t.Fatal("manifest empty (expected at least the seeded welcome note)") - } - for _, e := range manifest { - if !strings.HasPrefix(e.Hash, "sha256:") { - t.Errorf("entry %s missing sha256 hash: %q", e.Path, e.Hash) - } - } -} diff --git a/apps/server/internal/httpserver/server.go b/apps/server/internal/httpserver/server.go index 92e477f7..bd822a0c 100644 --- a/apps/server/internal/httpserver/server.go +++ b/apps/server/internal/httpserver/server.go @@ -170,8 +170,6 @@ func (s *Server) registerProtectedRoutes(r chi.Router) { r.Get("/fs/browse", s.browseDirectories) r.Get("/notes", s.listNotes) - r.Get("/sync/manifest", s.syncManifest) - r.Post("/sync/write", s.syncWrite) r.Get("/folders", s.listFolders) r.Get("/assets", s.listAssets) r.Get("/assets/exists", s.assetsExists) @@ -299,7 +297,6 @@ func (s *Server) capabilities(w http.ResponseWriter, _ *http.Request) { "supportsVaultSelection": true, "supportsDirectoryBrowsing": true, "supportsWatch": true, - "supportsSyncManifest": true, }) } @@ -498,46 +495,6 @@ func (s *Server) listNotes(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, notes) } -func (s *Server) syncManifest(w http.ResponseWriter, _ *http.Request) { - manifest, err := s.currentVault().Manifest() - if err != nil { - writeError(w, err) - return - } - writeJSON(w, http.StatusOK, manifest) -} - -// syncWrite writes raw bytes to an exact vault path (binary file sync). The path -// is a form field; the bytes are the `file` part of a multipart upload. -func (s *Server) syncWrite(w http.ResponseWriter, r *http.Request) { - cfg := s.currentConfig() - r.Body = http.MaxBytesReader(w, r.Body, cfg.MaxAssetBytes+multipartOverheadBytes) - if err := r.ParseMultipartForm(8 << 20); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - relPath := r.FormValue("path") - if strings.TrimSpace(relPath) == "" { - http.Error(w, "missing path", http.StatusBadRequest) - return - } - file, _, err := r.FormFile("file") - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - defer file.Close() - if err := s.currentVault().WriteSyncFile(relPath, file); err != nil { - if errors.Is(err, vault.ErrAssetTooLarge) { - http.Error(w, "file too large", http.StatusRequestEntityTooLarge) - return - } - writeError(w, err) - return - } - writeJSON(w, http.StatusOK, map[string]bool{"ok": true}) -} - func (s *Server) listFolders(w http.ResponseWriter, _ *http.Request) { folders, err := s.currentVault().ListFolders() if err != nil { diff --git a/apps/server/internal/vault/types.go b/apps/server/internal/vault/types.go index 0613f30b..b6439af8 100644 --- a/apps/server/internal/vault/types.go +++ b/apps/server/internal/vault/types.go @@ -140,16 +140,6 @@ type AssetMeta struct { UpdatedAt int64 `json:"updatedAt"` } -// ManifestEntry — one file in the sync manifest: vault-relative path, sha256 -// content hash ("sha256:<hex>"), size, and mtime (UnixMilli). Mirrors the -// client's SyncManifestEntry. -type ManifestEntry struct { - Path string `json:"path"` - Hash string `json:"hash"` - Size int64 `json:"size"` - Mtime int64 `json:"mtime"` -} - // ImportedAsset — mirrors shared/ipc.ts ImportedAsset. type ImportedAsset struct { Name string `json:"name"` diff --git a/apps/server/internal/vault/vault.go b/apps/server/internal/vault/vault.go index 66889e48..bb332f20 100644 --- a/apps/server/internal/vault/vault.go +++ b/apps/server/internal/vault/vault.go @@ -2,7 +2,6 @@ package vault import ( "crypto/rand" - "crypto/sha256" "encoding/hex" "encoding/json" "errors" @@ -27,12 +26,10 @@ const ( internalVaultDir = ".zennotes" vaultSettingsFile = "vault.json" noteMetaCacheFile = "note-meta-cache-v1.json" - // Bumped 1->2 when contentHash was added to cache entries; older caches - // (without hashes) are discarded by the version check on load. - noteMetaCacheVersion = 2 - noteCommentsDir = "comments" - noteCommentsSuffix = ".comments.json" - noteMetaReadLimit = 64 + noteMetaCacheVersion = 1 + noteCommentsDir = "comments" + noteCommentsSuffix = ".comments.json" + noteMetaReadLimit = 64 // formDirSuffix marks a database folder (`<Name>.base/`), a self-contained // folder holding data.csv, schema.json, and record-page notes. Databases are // a desktop-only feature; the server hides these folders (it neither serves @@ -186,10 +183,9 @@ type textSearchCache struct { } type noteMetaCacheEntry struct { - mtimeMs float64 - size int64 - meta NoteMeta - contentHash string // "sha256:<hex>" of the raw file bytes; "" if unknown + mtimeMs float64 + size int64 + meta NoteMeta } type persistedNoteMetaCache struct { @@ -198,17 +194,10 @@ type persistedNoteMetaCache struct { } type persistedNoteMetaEntry struct { - Path string `json:"path"` - MtimeMs float64 `json:"mtimeMs"` - Size int64 `json:"size"` - Meta NoteMeta `json:"meta"` - ContentHash string `json:"contentHash,omitempty"` -} - -// hashContent returns the sync content hash of raw file bytes. -func hashContent(body []byte) string { - sum := sha256.Sum256(body) - return "sha256:" + hex.EncodeToString(sum[:]) + Path string `json:"path"` + MtimeMs float64 `json:"mtimeMs"` + Size int64 `json:"size"` + Meta NoteMeta `json:"meta"` } func mtimeMs(info fs.FileInfo) float64 { @@ -699,10 +688,9 @@ func (v *Vault) hydratePersistedNoteMetaCache() { continue } entries[abs] = noteMetaCacheEntry{ - mtimeMs: entry.MtimeMs, - size: entry.Size, - meta: entry.Meta, - contentHash: entry.ContentHash, + mtimeMs: entry.MtimeMs, + size: entry.Size, + meta: entry.Meta, } } if len(entries) == 0 { @@ -749,11 +737,10 @@ func (v *Vault) persistNoteMetaCacheSnapshot(metas []NoteMeta) { metaCopy := cached.meta metaCopy.SiblingOrder = meta.SiblingOrder entries = append(entries, persistedNoteMetaEntry{ - Path: meta.Path, - MtimeMs: cached.mtimeMs, - Size: cached.size, - Meta: metaCopy, - ContentHash: cached.contentHash, + Path: meta.Path, + MtimeMs: cached.mtimeMs, + Size: cached.size, + Meta: metaCopy, }) } v.metaCacheMu.Unlock() @@ -802,16 +789,16 @@ func isSkippableWalkErr(err error) bool { return errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission) } -type noteFile struct { - folder NoteFolder - path string -} +func (v *Vault) ListNotes() ([]NoteMeta, error) { + v.mu.RLock() + defer v.mu.RUnlock() + v.hydratePersistedNoteMetaCache() + + type noteFile struct { + folder NoteFolder + path string + } -// collectNoteFiles walks every note/.excalidraw file across all top-level -// folders, applying the same skip rules as ListNotes (database folders, hidden -// dirs, primary-root hiding). Shared by ListNotes and Manifest. Callers hold -// v.mu (at least RLock). -func (v *Vault) collectNoteFiles() ([]noteFile, error) { files := []noteFile{} for _, folder := range AllFolders { folderRoot, err := v.folderRoot(folder) @@ -861,18 +848,6 @@ func (v *Vault) collectNoteFiles() ([]noteFile, error) { return nil, err } } - return files, nil -} - -func (v *Vault) ListNotes() ([]NoteMeta, error) { - v.mu.RLock() - defer v.mu.RUnlock() - v.hydratePersistedNoteMetaCache() - - files, err := v.collectNoteFiles() - if err != nil { - return nil, err - } results := make([]NoteMeta, len(files)) ok := make([]bool, len(files)) @@ -913,154 +888,6 @@ func (v *Vault) ListNotes() ([]NoteMeta, error) { return out, nil } -// Manifest returns a content-addressed listing of every note/.excalidraw file: -// path + sha256 content hash + size + mtime. The sync client diffs this against -// its base snapshot to decide pushes/pulls without downloading bodies. Hashes -// are cached in the note-meta-cache (keyed by mtime+size), so an unchanged vault -// answers from stat() calls alone. -// collectSyncFiles walks every syncable file in the vault — notes, drawings, -// databases, assets (binary), comments, and vault settings — excluding only the -// per-device sync-state, the meta cache, OS junk, temp files, and unrelated -// dot-directories. `.zennotes` itself is walked (for vault.json + comments). -// Callers hold v.mu (at least RLock). -func (v *Vault) collectSyncFiles() ([]string, error) { - out := []string{} - err := filepath.WalkDir(v.root, func(p string, d os.DirEntry, walkErr error) error { - if walkErr != nil { - if isSkippableWalkErr(walkErr) { - return nil - } - return walkErr - } - if p == v.root { - return nil - } - name := d.Name() - if d.IsDir() { - if name == internalVaultDir { - return nil // walk into .zennotes for vault.json + comments - } - if strings.HasPrefix(name, ".") { - return filepath.SkipDir // unrelated dot-dirs (.git, …) - } - return nil - } - if strings.HasPrefix(name, ".") { - return nil // dotfiles (.DS_Store, etc.) - } - if strings.HasSuffix(name, ".tmp") || strings.HasSuffix(name, ".synctmp") { - return nil - } - rel, err := filepath.Rel(v.root, p) - if err != nil { - return nil - } - relPosix := filepath.ToSlash(rel) - if relPosix == internalVaultDir+"/sync-state.json" || - relPosix == internalVaultDir+"/"+noteMetaCacheFile { - return nil - } - out = append(out, p) - return nil - }) - if err != nil { - return nil, err - } - return out, nil -} - -func (v *Vault) Manifest() ([]ManifestEntry, error) { - v.mu.RLock() - defer v.mu.RUnlock() - v.hydratePersistedNoteMetaCache() - - files, err := v.collectSyncFiles() - if err != nil { - return nil, err - } - - hashes := make([]string, len(files)) - sizes := make([]int64, len(files)) - mtimes := make([]int64, len(files)) - okFlags := make([]bool, len(files)) - - limit := noteMetaReadLimit - if len(files) < limit { - limit = len(files) - } - sem := make(chan struct{}, limit) - var wg sync.WaitGroup - for index, filePath := range files { - wg.Add(1) - go func(index int, filePath string) { - defer wg.Done() - sem <- struct{}{} - defer func() { <-sem }() - info, err := os.Stat(filePath) - if err != nil { - return - } - h := v.cachedContentHash(filePath, info) - if h == "" { - return - } - hashes[index] = h - sizes[index] = info.Size() - mtimes[index] = info.ModTime().UnixMilli() - okFlags[index] = true - }(index, filePath) - } - wg.Wait() - - out := make([]ManifestEntry, 0, len(files)) - for index, filePath := range files { - if !okFlags[index] { - continue - } - rel, err := filepath.Rel(v.root, filePath) - if err != nil { - continue - } - out = append(out, ManifestEntry{ - Path: filepath.ToSlash(rel), - Hash: hashes[index], - Size: sizes[index], - Mtime: mtimes[index], - }) - } - return out, nil -} - -// cachedContentHash returns the sha256 content hash of a file, reusing the -// note-meta-cache when the file's mtime+size are unchanged. On a miss it reads -// and hashes the file once and stores a hash-only cache entry (zero meta, so -// readMeta still recomputes full metadata for a genuinely changed file). -func (v *Vault) cachedContentHash(abs string, info fs.FileInfo) string { - statMtimeMs := mtimeMs(info) - v.metaCacheMu.Lock() - cached, ok := v.metaCache[abs] - if ok && sameMtimeMs(cached.mtimeMs, statMtimeMs) && cached.size == info.Size() && cached.contentHash != "" { - h := cached.contentHash - v.metaCacheMu.Unlock() - return h - } - v.metaCacheMu.Unlock() - - body, err := os.ReadFile(abs) - if err != nil { - return "" - } - h := hashContent(body) - v.metaCacheMu.Lock() - v.metaCache[abs] = noteMetaCacheEntry{ - mtimeMs: statMtimeMs, - size: info.Size(), - contentHash: h, - } - v.metaCacheMu.Unlock() - return h -} - func assignSiblingOrder[T any](list []T, key func(T) string, set func(*T, int)) { counts := map[string]int{} for i := range list { @@ -1288,10 +1115,9 @@ func (v *Vault) readMeta(folder NoteFolder, abs string) (NoteMeta, error) { meta := buildNoteMeta(relPosix, title, folder, info, bodyStr) v.metaCacheMu.Lock() v.metaCache[abs] = noteMetaCacheEntry{ - mtimeMs: statMtimeMs, - size: info.Size(), - meta: meta, - contentHash: hashContent(body), + mtimeMs: statMtimeMs, + size: info.Size(), + meta: meta, } v.metaCacheMu.Unlock() return meta, nil @@ -2317,46 +2143,6 @@ func (v *Vault) AssetAbsPath(rel string) (string, error) { return SafeJoin(v.root, rel) } -// WriteSyncFile writes raw bytes to an exact vault-relative path (used by the -// sync client to push binary files). Creates parent dirs, caps at the asset -// size limit, and commits atomically via temp + rename. -func (v *Vault) WriteSyncFile(rel string, body io.Reader) error { - v.mu.Lock() - defer v.mu.Unlock() - abs, err := SafeJoin(v.root, rel) - if err != nil { - return err - } - if err := os.MkdirAll(filepath.Dir(abs), v.dirMode); err != nil { - return err - } - tmp := fmt.Sprintf("%s.%d.%d.synctmp", abs, os.Getpid(), time.Now().UnixNano()) - f, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, v.fileMode) - if err != nil { - return err - } - written, copyErr := io.Copy(f, io.LimitReader(body, v.maxAssetBytes+1)) - closeErr := f.Close() - if copyErr != nil { - _ = os.Remove(tmp) - return copyErr - } - if closeErr != nil { - _ = os.Remove(tmp) - return closeErr - } - if written > v.maxAssetBytes { - _ = os.Remove(tmp) - return ErrAssetTooLarge - } - if err := os.Rename(tmp, abs); err != nil { - _ = os.Remove(tmp) - return err - } - v.invalidateTextSearchCache() - return nil -} - func makeAssetMarkdown(relPath, kind, name string) string { dest := "<" + strings.ReplaceAll(relPath, ">", "%3E") + ">" switch kind { diff --git a/apps/server/internal/vault/vault_test.go b/apps/server/internal/vault/vault_test.go index 2f0d38c7..a168fa1b 100644 --- a/apps/server/internal/vault/vault_test.go +++ b/apps/server/internal/vault/vault_test.go @@ -763,127 +763,6 @@ func hasNotePath(notes []NoteMeta, path string) bool { return false } -func TestManifestHashesStableAndChange(t *testing.T) { - root := t.TempDir() - v, err := New(root, Options{}) - if err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(root, "inbox", "A.md"), []byte("# A"), 0o600); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(root, "inbox", "B.excalidraw"), []byte(`{"type":"excalidraw"}`), 0o600); err != nil { - t.Fatal(err) - } - - m1, err := v.Manifest() - if err != nil { - t.Fatal(err) - } - if len(m1) < 2 { - t.Fatalf("manifest = %d entries, want >= 2: %+v", len(m1), m1) - } - byPath := map[string]ManifestEntry{} - for _, e := range m1 { - if !strings.HasPrefix(e.Hash, "sha256:") { - t.Errorf("entry %s hash missing sha256 prefix: %q", e.Path, e.Hash) - } - if e.Size == 0 { - t.Errorf("entry %s has zero size", e.Path) - } - byPath[e.Path] = e - } - if _, ok := byPath["inbox/A.md"]; !ok { - t.Error("manifest missing inbox/A.md") - } - if _, ok := byPath["inbox/B.excalidraw"]; !ok { - t.Error("manifest missing inbox/B.excalidraw") - } - - // Re-running yields identical hashes (cache hit, no drift). - m2, err := v.Manifest() - if err != nil { - t.Fatal(err) - } - for _, e := range m2 { - if byPath[e.Path].Hash != e.Hash { - t.Errorf("hash for %s changed across runs: %q vs %q", e.Path, byPath[e.Path].Hash, e.Hash) - } - } - - // Editing a note changes only its hash. - if err := os.WriteFile(filepath.Join(root, "inbox", "A.md"), []byte("# A edited"), 0o600); err != nil { - t.Fatal(err) - } - m3, err := v.Manifest() - if err != nil { - t.Fatal(err) - } - for _, e := range m3 { - if e.Path == "inbox/A.md" && e.Hash == byPath["inbox/A.md"].Hash { - t.Error("edited note kept the same hash") - } - if e.Path == "inbox/B.excalidraw" && e.Hash != byPath["inbox/B.excalidraw"].Hash { - t.Error("untouched drawing hash changed") - } - } -} - -func TestManifestIncludesSyncablesExcludesInternal(t *testing.T) { - root := t.TempDir() - v, err := New(root, Options{}) - if err != nil { - t.Fatal(err) - } - // Syncable files of various kinds. - writes := map[string]string{ - "inbox/Keep.md": "# Keep", - "inbox/DB.base/data.csv": "id,Title\n", - "inbox/DB.base/schema.json": `{"version":1}`, - "assets/pic.png": "PNGDATA", - ".zennotes/vault.json": `{"primaryNotesLocation":"inbox"}`, - ".zennotes/comments/inbox/Keep.md.comments.json": "[]", - // These must NEVER appear in the manifest. - ".zennotes/sync-state.json": `{"version":1}`, - ".zennotes/note-meta-cache-v1.json": `{"version":2}`, - ".DS_Store": "junk", - } - for rel, body := range writes { - abs := filepath.Join(root, filepath.FromSlash(rel)) - if err := os.MkdirAll(filepath.Dir(abs), 0o700); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(abs, []byte(body), 0o600); err != nil { - t.Fatal(err) - } - } - m, err := v.Manifest() - if err != nil { - t.Fatal(err) - } - present := map[string]bool{} - for _, e := range m { - present[e.Path] = true - } - for _, want := range []string{ - "inbox/Keep.md", - "inbox/DB.base/data.csv", - "inbox/DB.base/schema.json", - "assets/pic.png", - ".zennotes/vault.json", - ".zennotes/comments/inbox/Keep.md.comments.json", - } { - if !present[want] { - t.Errorf("manifest missing syncable file: %s", want) - } - } - for _, banned := range []string{".zennotes/sync-state.json", ".zennotes/note-meta-cache-v1.json", ".DS_Store"} { - if present[banned] { - t.Errorf("manifest leaked excluded path: %s", banned) - } - } -} - func TestFavoritesRoundTripAndDedupe(t *testing.T) { root := t.TempDir() v, err := New(root, Options{}) diff --git a/apps/web/src/bridge/http-bridge.ts b/apps/web/src/bridge/http-bridge.ts index 6199bd53..33706a0a 100644 --- a/apps/web/src/bridge/http-bridge.ts +++ b/apps/web/src/bridge/http-bridge.ts @@ -47,7 +47,6 @@ import type { RemoteWorkspaceProfile, RemoteWorkspaceProfileInput, ServerCapabilities, - SyncStatus, ServerSessionStatus, VaultSettings, TikzRenderResponse, @@ -1124,18 +1123,6 @@ export const httpBridge: ZenBridge = { deleteRemoteWorkspaceProfile: (_id: string) => deleteRemoteWorkspaceProfile(), connectRemoteWorkspaceProfile: (_id: string) => connectRemoteWorkspaceProfile(), - // The web client is itself the server's thin client — it never syncs a local - // copy, so sync controls are inert here. - getSyncStatus: () => Promise.resolve(null), - syncNow: () => Promise.resolve(), - dismissSyncConflict: (_copyPath: string) => Promise.resolve(), - attachSyncServer: (_profileId: string) => - Promise.reject(new Error('Syncing is a desktop feature.')), - detachSyncServer: () => Promise.resolve(null), - openSyncedVault: (_root: string) => - Promise.reject(new Error('Syncing is a desktop feature.')), - onSyncStatus: (_cb: (status: SyncStatus) => void) => () => {}, - getCurrentVault, listLocalVaults, openLocalVault, diff --git a/packages/app-core/src/components/Sidebar.tsx b/packages/app-core/src/components/Sidebar.tsx index f9423507..3d324ed9 100644 --- a/packages/app-core/src/components/Sidebar.tsx +++ b/packages/app-core/src/components/Sidebar.tsx @@ -13,7 +13,7 @@ import { Button } from "./ui/Button"; import { confirmMoveToTrash } from "../lib/confirm-trash"; import { buildMoveNotePrompt, parseMoveNoteTarget } from "../lib/move-note"; import { extractTags } from "../lib/tags"; -import type { AssetMeta, FolderColorId, FolderEntry, FolderIconId, NoteFolder, NoteMeta, SyncStatus } from "@shared/ipc"; +import type { AssetMeta, FolderColorId, FolderEntry, FolderIconId, NoteFolder, NoteMeta } from "@shared/ipc"; import type { NoteSortOrder } from "../store"; import { isArchiveTabPath } from "@shared/archive"; import { isTrashTabPath } from "@shared/trash"; @@ -25,9 +25,6 @@ import { ChevronRightIcon, CheckSquareIcon, CloseIcon, - CloudIcon, - CloudCheckIcon, - CloudOffIcon, DatabaseIcon, DocumentIcon, ExcalidrawIcon, @@ -44,7 +41,6 @@ import { TrashIcon, } from "./icons"; import { ContextMenu, type ContextMenuItem } from "./ContextMenu"; -import { SyncConflictModal } from "./SyncConflictModal"; import { ResizeHandle } from "./ResizeHandle"; import { VaultBadge } from "./VaultBadge"; import { confirmApp } from '../lib/confirm-requests' @@ -455,11 +451,6 @@ export function Sidebar(): JSX.Element { const remoteWorkspaceInfo = useStore((s) => s.remoteWorkspaceInfo); const localVaults = useStore((s) => s.localVaults); const remoteWorkspaceProfiles = useStore((s) => s.remoteWorkspaceProfiles); - const syncStatus = useStore((s) => s.syncStatus); - const attachSyncServer = useStore((s) => s.attachSyncServer); - const detachSyncServer = useStore((s) => s.detachSyncServer); - const syncNow = useStore((s) => s.syncNow); - const [conflictModalOpen, setConflictModalOpen] = useState(false); const openVaultPicker = useStore((s) => s.openVaultPicker); const openLocalVault = useStore((s) => s.openLocalVault); const closeVault = useStore((s) => s.closeVault); @@ -2490,34 +2481,6 @@ export function Sidebar(): JSX.Element { }); } - // Sync controls: a synced vault can stop syncing; a plain local vault can be - // attached to any saved server for bidirectional backup/sync. - if (syncStatus) { - items.push({ kind: "separator" }); - items.push({ - label: "Stop syncing (keep local copy)", - icon: <CloudIcon className="h-4 w-4" />, - onSelect: async () => { - await detachSyncServer(); - }, - }); - } else if (workspaceMode === "local" && remoteWorkspaceProfiles.length > 0) { - items.push({ kind: "separator" }); - for (const profile of remoteWorkspaceProfiles) { - items.push({ - label: `Back up & sync to ${profile.name}`, - icon: <CloudIcon className="h-4 w-4" />, - onSelect: async () => { - try { - await attachSyncServer(profile.id); - } catch (err) { - window.alert((err as Error).message); - } - }, - }); - } - } - return items; }, [ canCloseCurrentVault, @@ -2530,11 +2493,6 @@ export function Sidebar(): JSX.Element { openVaultPicker, refreshLocalVaults, vaultSwitcherEntries, - syncStatus, - workspaceMode, - remoteWorkspaceProfiles, - attachSyncServer, - detachSyncServer, ]); // A folder only shows the strong "selected" accent highlight when @@ -2800,15 +2758,6 @@ export function Sidebar(): JSX.Element { </div> )} <div className="flex items-center gap-0.5"> - {syncStatus && ( - <SyncStatusButton - status={syncStatus} - onClick={() => { - if (syncStatus.conflicts.length > 0) setConflictModalOpen(true); - else void syncNow(); - }} - /> - )} <IconBtn title="Create… (note, drawing, folder, database)" onClick={(e) => { @@ -3379,9 +3328,6 @@ export function Sidebar(): JSX.Element { onClose={() => setFolderMenu(null)} /> )} - {conflictModalOpen && ( - <SyncConflictModal onClose={() => setConflictModalOpen(false)} /> - )} {rootMenu && ( <ContextMenu x={rootMenu.x} @@ -5192,59 +5138,6 @@ function SidebarFooterAction({ ); } -function SyncStatusButton({ - status, - onClick, -}: { - status: SyncStatus; - onClick: () => void; -}): JSX.Element { - const hasConflicts = status.conflicts.length > 0; - const view = - status.kind === "syncing" - ? { icon: <CloudIcon className="h-4 w-4 animate-pulse" />, title: "Syncing…", tint: "text-accent" } - : status.kind === "offline" - ? { - icon: <CloudOffIcon className="h-4 w-4" />, - title: "Offline — changes will sync when reconnected", - tint: "text-ink-400", - } - : status.kind === "error" - ? { - icon: <CloudOffIcon className="h-4 w-4" />, - title: status.lastError ?? "Sync error", - tint: "text-red-500", - } - : { - icon: <CloudCheckIcon className="h-4 w-4" />, - title: hasConflicts - ? `Synced • ${status.conflicts.length} conflict cop${status.conflicts.length === 1 ? "y" : "ies"} kept` - : "Synced", - tint: hasConflicts ? "text-amber-500" : "text-emerald-500", - }; - return ( - <button - type="button" - onClick={onClick} - aria-label={view.title} - title={ - hasConflicts - ? `${view.title} — click to resolve` - : `${view.title} — click to sync now` - } - className={[ - "group relative flex h-7 w-7 items-center justify-center rounded-md transition-colors hover:bg-paper-200", - view.tint, - ].join(" ")} - > - {view.icon} - {hasConflicts && status.kind !== "syncing" && ( - <span className="absolute right-0.5 top-0.5 h-1.5 w-1.5 rounded-full bg-amber-500" /> - )} - </button> - ); -} - function IconBtn({ children, onClick, diff --git a/packages/app-core/src/components/SyncConflictModal.tsx b/packages/app-core/src/components/SyncConflictModal.tsx deleted file mode 100644 index 434bdd76..00000000 --- a/packages/app-core/src/components/SyncConflictModal.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { Modal } from './ui/Modal' -import { Button } from './ui/Button' -import { useStore } from '../store' - -function basename(p: string): string { - return p.split('/').pop() ?? p -} - -/** Recover the original path from a conflict-copy path. */ -function originalOf(copyPath: string): string { - return copyPath.replace( - / \(conflict [0-9a-f]{8} \d{4}-\d{2}-\d{2} \d{6}\)(?=(\.[^/]+)?$)/, - '' - ) -} - -function isTextPath(p: string): boolean { - return /\.(md|excalidraw|json|csv|txt|markdown)$/i.test(p) -} - -/** - * Lists kept-both sync conflicts and lets the user resolve each: open either - * version, adopt the copy's content into the original, or discard the copy. - */ -export function SyncConflictModal({ onClose }: { onClose: () => void }): JSX.Element { - const conflicts = useStore((s) => s.syncStatus?.conflicts ?? []) - const resolveSyncConflict = useStore((s) => s.resolveSyncConflict) - const selectNote = useStore((s) => s.selectNote) - - return ( - <Modal size="lg" onClose={onClose} labelledBy="sync-conflicts-title"> - <div className="p-5"> - <h2 id="sync-conflicts-title" className="text-base font-semibold text-ink-900"> - Sync conflicts - </h2> - <p className="mt-1 text-sm text-ink-600"> - These notes were edited in two places before they synced. Nothing was lost — each - conflicting version is kept. Review and pick which to keep. - </p> - {conflicts.length === 0 ? ( - <p className="mt-4 text-sm text-ink-500">No conflicts to resolve.</p> - ) : ( - <ul className="mt-4 max-h-[55vh] space-y-3 overflow-y-auto pr-1"> - {conflicts.map((copyPath) => { - const original = originalOf(copyPath) - const text = isTextPath(copyPath) - return ( - <li - key={copyPath} - className="rounded-lg border border-paper-300/70 bg-paper-50 p-3" - > - <div className="truncate text-sm font-medium text-ink-900"> - {basename(original)} - </div> - <div className="truncate text-xs text-ink-500"> - conflict copy: {basename(copyPath)} - </div> - <div className="mt-2 flex flex-wrap gap-2"> - <Button - size="sm" - variant="secondary" - onClick={() => void selectNote(original)} - > - Open original - </Button> - <Button - size="sm" - variant="secondary" - onClick={() => void selectNote(copyPath)} - > - Open copy - </Button> - {text && ( - <Button - size="sm" - variant="primary" - onClick={() => void resolveSyncConflict(copyPath, 'keepTheirs')} - > - Use the copy's version - </Button> - )} - <Button - size="sm" - variant="danger" - onClick={() => void resolveSyncConflict(copyPath, 'keepMine')} - > - Discard copy - </Button> - </div> - </li> - ) - })} - </ul> - )} - <div className="mt-5 flex justify-end"> - <Button variant="primary" onClick={onClose}> - Done - </Button> - </div> - </div> - </Modal> - ) -} diff --git a/packages/app-core/src/components/icons.tsx b/packages/app-core/src/components/icons.tsx index 50c0d802..0f58c1c1 100644 --- a/packages/app-core/src/components/icons.tsx +++ b/packages/app-core/src/components/icons.tsx @@ -29,32 +29,6 @@ export const SearchIcon = (p: IconProps): JSX.Element => ( </I> ) -const CLOUD_PATH = 'M17.5 19a4.5 4.5 0 0 0 .5-8.97A6 6 0 0 0 6.34 9.2 4 4 0 0 0 7 17h10.5z' - -/** Plain cloud — the sync affordance. */ -export const CloudIcon = (p: IconProps): JSX.Element => ( - <I {...p}> - <path d={CLOUD_PATH} /> - </I> -) - -/** Cloud with a check — "synced / up to date". */ -export const CloudCheckIcon = (p: IconProps): JSX.Element => ( - <I {...p}> - <path d="M17.5 17a4.5 4.5 0 0 0 .5-8.97A6 6 0 0 0 6.34 7.2 4 4 0 0 0 6.5 15" /> - <path d="m9 16 2 2 4-4" /> - </I> -) - -/** Cloud with a slash — "offline". */ -export const CloudOffIcon = (p: IconProps): JSX.Element => ( - <I {...p}> - <path d="M6.34 7.2A4 4 0 0 0 7 15h8" /> - <path d="M18 14.5a4.5 4.5 0 0 0-2.9-7.4" /> - <path d="m3 3 18 18" /> - </I> -) - export const TableIcon = (p: IconProps): JSX.Element => ( <I {...p}> <rect x="3" y="3" width="18" height="18" rx="2" /> diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index 86b90a2c..2b03d7e2 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -16,7 +16,6 @@ import type { RemoteWorkspaceProfile, RemoteWorkspaceProfileInput, ServerCapabilities, - SyncStatus, VaultSettings, VaultTextSearchBackendPreference, VaultChangeEvent, @@ -1468,8 +1467,6 @@ interface Store { workspaceMode: WorkspaceMode remoteWorkspaceInfo: RemoteWorkspaceInfo | null remoteWorkspaceProfiles: RemoteWorkspaceProfile[] - /** Live sync status for a synced vault (null when not syncing). */ - syncStatus: SyncStatus | null localVaults: LocalVaultEntry[] workspaceSetupError: string | null vaultSettings: VaultSettings @@ -2008,17 +2005,6 @@ interface Store { deleteRemoteWorkspaceProfile: (id: string) => Promise<void> refreshRemoteWorkspaceProfiles: () => Promise<RemoteWorkspaceProfile[]> refreshLocalVaults: () => Promise<LocalVaultEntry[]> - /** Pull the current sync status for the active synced vault. */ - refreshSyncStatus: () => Promise<void> - /** Trigger an immediate reconcile of the synced vault. */ - syncNow: () => Promise<void> - /** Attach the open local vault to a saved server (switch to synced mode). */ - attachSyncServer: (profileId: string) => Promise<void> - /** Stop syncing and reopen the folder as a plain local vault. */ - detachSyncServer: () => Promise<void> - /** Resolve a kept-both conflict copy: keep the local original, or adopt the - * copy's version into the original. Either way the copy is removed. */ - resolveSyncConflict: (copyPath: string, resolution: 'keepMine' | 'keepTheirs') => Promise<void> persistWorkspace: () => void flushDirtyNotes: () => Promise<void> refreshWorkspaceContext: () => Promise<RemoteWorkspaceInfo | null> @@ -2125,16 +2111,7 @@ function findMatchingRemoteProfile( } function workspaceModeFrom(info: RemoteWorkspaceInfo | null): WorkspaceMode { - return info?.mode === 'remote' || info?.mode === 'synced' ? info.mode : 'local' -} - -/** Strip the " (conflict <dev> <date> <time>)" suffix a sync conflict copy adds, - * recovering the original note's path. */ -function originalFromConflictPath(copyPath: string): string { - return copyPath.replace( - / \(conflict [0-9a-f]{8} \d{4}-\d{2}-\d{2} \d{6}\)(?=(\.[^/]+)?$)/, - '' - ) + return info?.mode === 'remote' ? 'remote' : 'local' } async function ensureWebServerSession( @@ -2824,7 +2801,6 @@ export const useStore = create<Store>((set, get) => { vault: null, workspaceMode: 'local', remoteWorkspaceInfo: null, - syncStatus: null, remoteWorkspaceProfiles: [], localVaults: [], workspaceSetupError: null, @@ -5620,49 +5596,6 @@ export const useStore = create<Store>((set, get) => { } }, - refreshSyncStatus: async () => { - try { - const status = await window.zen.getSyncStatus() - set({ syncStatus: status }) - } catch { - set({ syncStatus: null }) - } - }, - syncNow: async () => { - try { - await window.zen.syncNow() - } catch (err) { - console.error('syncNow failed', err) - } - }, - attachSyncServer: async (profileId) => { - await window.zen.attachSyncServer(profileId) - await get().refreshSyncStatus() - await get().refreshWorkspaceContext() - await get().refreshNotes() - }, - detachSyncServer: async () => { - await window.zen.detachSyncServer() - set({ syncStatus: null }) - await get().refreshWorkspaceContext() - await get().refreshNotes() - }, - resolveSyncConflict: async (copyPath, resolution) => { - const original = originalFromConflictPath(copyPath) - try { - if (resolution === 'keepTheirs' && original !== copyPath) { - const copy = await window.zen.readNote(copyPath) - await window.zen.writeNote(original, copy.body) - } - await window.zen.moveToTrash(copyPath) // discard the conflict copy - } catch (err) { - console.error('resolveSyncConflict failed', err) - } - await window.zen.dismissSyncConflict(copyPath) - await get().refreshSyncStatus() - await get().refreshNotes() - }, - refreshRemoteWorkspaceProfiles: async () => { if (!window.zen.getCapabilities().supportsRemoteWorkspace) { set({ remoteWorkspaceProfiles: [] }) @@ -5786,10 +5719,6 @@ export const useStore = create<Store>((set, get) => { window.zen.onVaultChange((ev) => { void get().applyChange(ev) }) - window.zen.onSyncStatus((status) => { - set({ syncStatus: status }) - }) - void get().refreshSyncStatus() }, openVaultPicker: async () => { diff --git a/packages/bridge-contract/src/bridge.ts b/packages/bridge-contract/src/bridge.ts index 262fbf28..b881cdab 100644 --- a/packages/bridge-contract/src/bridge.ts +++ b/packages/bridge-contract/src/bridge.ts @@ -23,7 +23,6 @@ import type { RemoteWorkspaceProfileInput, ServerCapabilities, ServerSessionStatus, - SyncStatus, VaultSettings, TikzRenderResponse, VaultChangeEvent, @@ -102,21 +101,6 @@ export interface ZenBridge { id: string ): Promise<{ vault: VaultInfo | null; capabilities: ServerCapabilities }> - /** Current sync status for the active synced vault, or null if not synced. */ - getSyncStatus(): Promise<SyncStatus | null> - /** Trigger an immediate reconcile. */ - syncNow(): Promise<void> - /** Remove a resolved conflict copy from the status list. */ - dismissSyncConflict(copyPath: string): Promise<void> - /** Attach the open local vault to a saved server and switch it to synced mode. */ - attachSyncServer(profileId: string): Promise<VaultInfo> - /** Stop syncing and reopen the folder as a plain local vault. */ - detachSyncServer(): Promise<VaultInfo | null> - /** Open an already-configured synced vault by its root. */ - openSyncedVault(root: string): Promise<VaultInfo> - /** Subscribe to sync-status pushes; returns an unsubscribe function. */ - onSyncStatus(cb: (status: SyncStatus) => void): () => void - getCurrentVault(): Promise<VaultInfo | null> listLocalVaults(): Promise<LocalVaultEntry[]> openLocalVault(root: string): Promise<VaultInfo | null> diff --git a/packages/bridge-contract/src/ipc.ts b/packages/bridge-contract/src/ipc.ts index 2963ba71..236da613 100644 --- a/packages/bridge-contract/src/ipc.ts +++ b/packages/bridge-contract/src/ipc.ts @@ -9,13 +9,6 @@ export const IPC = { WORKSPACE_SAVE_REMOTE_PROFILE: 'workspace:save-remote-profile', WORKSPACE_DELETE_REMOTE_PROFILE: 'workspace:delete-remote-profile', WORKSPACE_CONNECT_REMOTE_PROFILE: 'workspace:connect-remote-profile', - SYNC_GET_STATUS: 'sync:get-status', - SYNC_NOW: 'sync:now', - SYNC_ON_STATUS: 'sync:on-status', - SYNC_ATTACH_SERVER: 'sync:attach-server', - SYNC_DETACH_SERVER: 'sync:detach-server', - SYNC_OPEN_VAULT: 'sync:open-vault', - SYNC_DISMISS_CONFLICT: 'sync:dismiss-conflict', VAULT_LIST_LOCAL: 'vault:list-local', VAULT_OPEN_LOCAL: 'vault:open-local', VAULT_CLOSE: 'vault:close', @@ -532,31 +525,6 @@ export interface ServerCapabilities { supportsVaultSelection: boolean supportsDirectoryBrowsing: boolean supportsWatch: boolean - /** Server exposes GET /api/sync/manifest (content hashes) for synced vaults. */ - supportsSyncManifest?: boolean -} - -/** One file in the server sync manifest. Mirrors the Go ManifestEntry. */ -export interface SyncManifestEntry { - path: string - hash: string - size: number - mtime: number -} - -export type SyncStatusKind = 'idle' | 'syncing' | 'offline' | 'error' - -/** Live sync status for a synced vault, pushed to the renderer. */ -export interface SyncStatus { - kind: SyncStatusKind - /** Files changed locally still to push (best-effort, from the last run). */ - pendingPush: number - /** Files changed remotely still to pull (best-effort, from the last run). */ - pendingPull: number - /** Conflict-copy paths created (kept-both). */ - conflicts: string[] - lastSyncAt: number - lastError: string | null } export interface ServerSessionStatus { @@ -565,12 +533,7 @@ export interface ServerSessionStatus { supportsSessionLogin: boolean } -/** - * `local` = files on disk, no server. `remote` = thin HTTP client, no local - * copy. `synced` = a local vault that bidirectionally syncs with a server hub - * (behaves like `local` for all file ops; a background engine replicates it). - */ -export type WorkspaceMode = 'local' | 'remote' | 'synced' +export type WorkspaceMode = 'local' | 'remote' export interface RemoteWorkspaceInfo { mode: WorkspaceMode From 132cc6c720a4e301b63b5de22d7fcfc9754e91f9 Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Thu, 18 Jun 2026 09:32:19 -0500 Subject: [PATCH 59/66] feat(web): databases on the web client (desktop/web parity) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Databases now work in the web client, reusing the exact same shared TS logic the desktop uses so the on-disk format is identical across platforms. - Server (apps/server/internal/vault/vault.go): ListFolders now lists `.base` database folders (then stops descending), mirroring the desktop — that one divergence was why databases never reached the web sidebar. ListNotes and ListAssets still hide the internals. Test updated. - Web bridge (apps/web/src/bridge/http-bridge.ts): implemented the 8 database methods (open/writeRows/writeSchema/create/createRecordPage/rename/list) over HTTP, reusing @shared/database-csv + @shared/databases (parseCsv, parseRows, serializeRows, inferFields, buildDefaultViews, path helpers). Reads/writes use the existing generic /notes/read|write endpoints; `.base` folders are created/ renamed via the folder endpoints — no new server endpoints, no Go re-implementation, so CSV/schema parity is guaranteed. Drawings already worked end-to-end on web (no change). Verified: web typecheck + build, app-core tests (shared CSV unchanged), Go vault/httpserver tests. --- apps/server/internal/vault/vault.go | 8 +- apps/server/internal/vault/vault_test.go | 21 +- apps/web/src/bridge/http-bridge.ts | 330 +++++++++++++++++++++-- 3 files changed, 335 insertions(+), 24 deletions(-) diff --git a/apps/server/internal/vault/vault.go b/apps/server/internal/vault/vault.go index bb332f20..f6fea56f 100644 --- a/apps/server/internal/vault/vault.go +++ b/apps/server/internal/vault/vault.go @@ -924,9 +924,6 @@ func (v *Vault) ListFolders() ([]FolderEntry, error) { if strings.HasPrefix(d.Name(), ".") { return filepath.SkipDir } - if isFormDirName(d.Name()) { - return filepath.SkipDir // database folder — not a user folder - } if isPrimaryRoot { parent := filepath.Dir(path) if filepath.Clean(parent) == filepath.Clean(folderRoot) { @@ -943,6 +940,11 @@ func (v *Vault) ListFolders() ([]FolderEntry, error) { Folder: folder, Subpath: filepath.ToSlash(rel), }) + // A `<Name>.base` database folder is listed (the renderer shows it as + // a database) but its internals are not exposed as folders. + if isFormDirName(d.Name()) { + return filepath.SkipDir + } return nil }) if err != nil { diff --git a/apps/server/internal/vault/vault_test.go b/apps/server/internal/vault/vault_test.go index a168fa1b..d710028c 100644 --- a/apps/server/internal/vault/vault_test.go +++ b/apps/server/internal/vault/vault_test.go @@ -685,15 +685,16 @@ func TestListSkipsUnreadableEntries(t *testing.T) { } } -func TestDatabaseBaseFolderIsHidden(t *testing.T) { +func TestDatabaseBaseFolderListedButInternalsHidden(t *testing.T) { root := t.TempDir() v, err := New(root, Options{}) if err != nil { t.Fatal(err) } - // A database folder with its internals + a record-page note. + // A database folder with its internals, a record-page note, and a nested dir + // (its internals + nested dirs must NOT surface as folders). baseDir := filepath.Join(root, "inbox", "Books.base") - if err := os.MkdirAll(baseDir, 0o700); err != nil { + if err := os.MkdirAll(filepath.Join(baseDir, "pages"), 0o700); err != nil { t.Fatal(err) } for name, body := range map[string]string{ @@ -731,14 +732,24 @@ func TestDatabaseBaseFolderIsHidden(t *testing.T) { t.Fatal(err) } sawReal := false + sawBase := false for _, f := range folders { - if strings.Contains(f.Subpath, ".base") { - t.Errorf("ListFolders leaked a database folder: %s", f.Subpath) + // The database folder itself lists (renderer renders it as a database)... + if f.Subpath == "Books.base" { + sawBase = true + continue + } + // ...but nothing INSIDE it (e.g. Books.base/pages) is exposed as a folder. + if strings.Contains(f.Subpath, ".base/") { + t.Errorf("ListFolders leaked a database-internal folder: %s", f.Subpath) } if f.Subpath == "RealFolder" { sawReal = true } } + if !sawBase { + t.Error("ListFolders should list the .base database folder itself") + } if !sawReal { t.Error("ListFolders dropped a regular folder") } diff --git a/apps/web/src/bridge/http-bridge.ts b/apps/web/src/bridge/http-bridge.ts index 33706a0a..2fbd5138 100644 --- a/apps/web/src/bridge/http-bridge.ts +++ b/apps/web/src/bridge/http-bridge.ts @@ -59,7 +59,27 @@ import type { VaultTextSearchToolPaths } from '@shared/ipc' import type { VaultTask } from '@shared/tasks' -import type { DatabaseDoc, DatabaseSummary } from '@shared/databases' +import { + csvPathForFormDir, + databaseSchemaPathFor, + formDirFromCsvPath, + formTitleFromCsvPath, + FORM_DIR_SUFFIX, + isFormDirName, + type DatabaseDoc, + type DatabaseSidecar, + type DatabaseSummary, + type DbField, + type DbRow, + type DbView +} from '@shared/databases' +import { + buildDefaultViews, + inferFields, + parseCsv, + parseRows, + serializeRows +} from '@shared/database-csv' import type { McpClientId, McpClientStatus, @@ -579,27 +599,305 @@ function scanTasksForPath(relPath: string): Promise<VaultTask[]> { } // Databases are desktop-only for now (no server-side CSV endpoints yet). -const DATABASES_WEB_MSG = 'Databases are only available in the desktop app right now.' -function openDatabase(): Promise<DatabaseDoc> { - return Promise.reject(new Error(DATABASES_WEB_MSG)) +// -------------------------------------------------------------------- +// Databases — reuse the shared CSV/schema logic (@shared/database-csv + +// @shared/databases) over HTTP, mirroring apps/desktop/src/main/databases.ts so +// the on-disk format is byte-identical to the desktop. Reads/writes use the +// generic /notes/read|write endpoints (which accept any vault path, including +// `.base/` internals); the `.base` folder is created/renamed via the folder +// endpoints. The server now lists `.base` folders so the sidebar renders them. +// -------------------------------------------------------------------- + +const SCHEMA_SAMPLE_ROWS = 50 +const DB_TITLE_BAD = /[\\/:*?"<>|]/g + +function dbGenId(): string { + return crypto.randomUUID() } -function writeDatabaseRows(): Promise<DatabaseDoc> { - return Promise.reject(new Error(DATABASES_WEB_MSG)) +function dbToPosix(p: string): string { + return p.replace(/\\/g, '/') } -function writeDatabaseSchema(): Promise<DatabaseDoc> { - return Promise.reject(new Error(DATABASES_WEB_MSG)) +function joinSub(...parts: string[]): string { + return parts + .map((p) => p.replace(/^\/+|\/+$/g, '')) + .filter(Boolean) + .join('/') } -function createDatabase(): Promise<DatabaseDoc> { - return Promise.reject(new Error(DATABASES_WEB_MSG)) + +/** Title of a database from its data.csv path. */ +function dbTitleFromPath(csvPath: string): string { + const posix = dbToPosix(csvPath) + if (formDirFromCsvPath(posix)) return formTitleFromCsvPath(posix) + const base = posix.split('/').filter(Boolean).pop() ?? csvPath + return base.replace(/\.csv$/i, '') } -function renameDatabase(): Promise<string> { - return Promise.reject(new Error(DATABASES_WEB_MSG)) + +/** Read a vault file's text, or null when missing/unreadable (matches the + * desktop's optional-file reads, which treat ENOENT/parse errors as absent). */ +async function readFileTextOrNull(relPath: string): Promise<string | null> { + try { + return (await readNote(relPath)).body + } catch { + return null + } } -function createRecordPage(): Promise<string> { - return Promise.reject(new Error(DATABASES_WEB_MSG)) + +/** True if a note has body beyond frontmatter + a single title heading. */ +function dbNoteHasBody(text: string): boolean { + let body = text + const fm = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/.exec(body) + if (fm) body = body.slice(fm[0].length) + body = body.replace(/^\s*#[^\n]*\r?\n?/, '') + return body.trim().length > 0 +} + +/** Defensive sidecar parse — mirror of databases.ts normalizeSidecar. */ +function dbNormalizeSidecar(raw: unknown): DatabaseSidecar | null { + if (!raw || typeof raw !== 'object') return null + const obj = raw as Record<string, unknown> + const fields = Array.isArray(obj.fields) ? (obj.fields as DbField[]) : null + if (!fields || fields.length === 0) return null + if (!fields.every((f) => f && typeof f.id === 'string' && typeof f.name === 'string')) return null + const fieldIds = new Set(fields.map((f) => f.id)) + const idFieldId = + typeof obj.idFieldId === 'string' && fieldIds.has(obj.idFieldId) ? obj.idFieldId : fields[0].id + let views = Array.isArray(obj.views) ? (obj.views as DbView[]) : [] + views = views.filter( + (v) => v && typeof v.id === 'string' && (v.type === 'table' || v.type === 'board') + ) + if (views.length === 0) views = buildDefaultViews(fields).views + const activeViewId = + typeof obj.activeViewId === 'string' && views.some((v) => v.id === obj.activeViewId) + ? obj.activeViewId + : views[0].id + const pages = + obj.pages && typeof obj.pages === 'object' + ? (Object.fromEntries( + Object.entries(obj.pages as Record<string, unknown>).filter( + ([, v]) => typeof v === 'string' + ) + ) as Record<string, string>) + : undefined + return { version: 1, idFieldId, fields, views, activeViewId, ...(pages ? { pages } : {}) } +} + +function dbPagesToFull(csvRel: string, pages: Record<string, string>): Record<string, string> { + const formDir = formDirFromCsvPath(dbToPosix(csvRel)) + if (!formDir) return pages + const prefix = `${formDir}/` + return Object.fromEntries( + Object.entries(pages).map(([id, p]) => [id, p.startsWith(prefix) ? p : `${prefix}${p}`]) + ) } -function listDatabases(): Promise<DatabaseSummary[]> { - return Promise.resolve([]) +function dbPagesToRelative(csvRel: string, pages: Record<string, string>): Record<string, string> { + const formDir = formDirFromCsvPath(dbToPosix(csvRel)) + if (!formDir) return pages + const prefix = `${formDir}/` + return Object.fromEntries( + Object.entries(pages).map(([id, p]) => [id, p.startsWith(prefix) ? p.slice(prefix.length) : p]) + ) +} + +function dbHydrate( + csvPath: string, + sidecar: DatabaseSidecar, + rows: DbRow[], + pageHasContent?: Record<string, boolean> +): DatabaseDoc { + return { + ...sidecar, + path: dbToPosix(csvPath), + title: dbTitleFromPath(csvPath), + rows, + ...(pageHasContent ? { pageHasContent } : {}) + } +} + +async function dbReadSidecar(csvPath: string): Promise<DatabaseSidecar | null> { + const schemaPath = databaseSchemaPathFor(dbToPosix(csvPath)) + if (!schemaPath) return null + const raw = await readFileTextOrNull(schemaPath) + if (raw === null) return null + let sidecar: DatabaseSidecar | null + try { + sidecar = dbNormalizeSidecar(JSON.parse(raw)) + } catch { + return null + } + if (sidecar?.pages) sidecar.pages = dbPagesToFull(dbToPosix(csvPath), sidecar.pages) + return sidecar +} + +async function dbPersistSidecar(csvPath: string, sidecar: DatabaseSidecar): Promise<void> { + const schemaPath = databaseSchemaPathFor(dbToPosix(csvPath)) + if (!schemaPath) throw new Error(`Not a database folder: ${csvPath}`) + const onDisk: DatabaseSidecar = sidecar.pages + ? { ...sidecar, pages: dbPagesToRelative(dbToPosix(csvPath), sidecar.pages) } + : sidecar + await writeNote(schemaPath, `${JSON.stringify(onDisk, null, 2)}\n`) +} + +async function dbReadPageFlags( + pages?: Record<string, string> +): Promise<Record<string, boolean> | undefined> { + if (!pages || Object.keys(pages).length === 0) return undefined + const flags: Record<string, boolean> = {} + await Promise.all( + Object.entries(pages).map(async ([rowId, notePath]) => { + const text = await readFileTextOrNull(notePath) + if (text !== null) flags[rowId] = dbNoteHasBody(text) + }) + ) + return flags +} + +async function primaryNotesAtRoot(): Promise<boolean> { + try { + return (await getVaultSettings()).primaryNotesLocation === 'root' + } catch { + return false + } +} + +/** Vault-relative directory for a (folder, subpath) — mirrors the server folderRoot. */ +function vaultRelDir(folder: NoteFolder, subpath: string, atRoot: boolean): string { + const sub = (subpath ?? '').replace(/^\/+|\/+$/g, '') + if (folder === 'inbox') return atRoot ? sub : joinSub('inbox', sub) + return joinSub(folder, sub) +} + +/** Split a vault-relative folder path into (folder, subpath) — inverse of vaultRelDir. */ +function splitVaultPath(rel: string, atRoot: boolean): { folder: NoteFolder; subpath: string } { + const parts = dbToPosix(rel).split('/').filter(Boolean) + const top = parts[0] + if (top === 'quick' || top === 'archive' || top === 'trash') { + return { folder: top as NoteFolder, subpath: parts.slice(1).join('/') } + } + if (top === 'inbox' && !atRoot) { + return { folder: 'inbox', subpath: parts.slice(1).join('/') } + } + return { folder: 'inbox', subpath: parts.join('/') } +} + +async function openDatabase(csvPath: string): Promise<DatabaseDoc> { + const rel = dbToPosix(csvPath) + const csvText = await readFileTextOrNull(rel) + if (csvText === null) throw new Error(`Database not found: ${rel}`) + + const existing = await dbReadSidecar(rel) + if (existing) { + const rows = parseRows(csvText, existing.fields, existing.idFieldId, dbGenId) + const pageHasContent = await dbReadPageFlags(existing.pages) + return dbHydrate(rel, existing, rows, pageHasContent) + } + + // Adopt a plain CSV: infer the schema + materialize (matches desktop readDatabase). + const grid = parseCsv(csvText) + const headers = grid[0] ?? [] + const { fields, idFieldId } = inferFields(headers, grid.slice(1, 1 + SCHEMA_SAMPLE_ROWS), dbGenId) + const { views, activeViewId } = buildDefaultViews(fields, dbGenId) + const sidecar: DatabaseSidecar = { version: 1, idFieldId, fields, views, activeViewId } + const rows = parseRows(csvText, fields, idFieldId, dbGenId) + await dbPersistSidecar(rel, sidecar) + await writeNote(rel, serializeRows(rows, fields)) // canonicalize + persist ids + return dbHydrate(rel, sidecar, rows) +} + +async function writeDatabaseRows(csvPath: string, rows: DbRow[]): Promise<DatabaseDoc> { + const rel = dbToPosix(csvPath) + const sidecar = await dbReadSidecar(rel) + if (!sidecar) throw new Error(`Database sidecar missing: ${rel}`) + await writeNote(rel, serializeRows(rows, sidecar.fields)) + return dbHydrate(rel, sidecar, rows.map((r) => ({ ...r }))) +} + +async function writeDatabaseSchema( + csvPath: string, + sidecar: DatabaseSidecar, + rows: DbRow[] +): Promise<DatabaseDoc> { + const rel = dbToPosix(csvPath) + const normalized = dbNormalizeSidecar(sidecar) + if (!normalized) throw new Error(`Invalid database schema: ${rel}`) + await dbPersistSidecar(rel, normalized) + await writeNote(rel, serializeRows(rows, normalized.fields)) + return dbHydrate(rel, normalized, rows.map((r) => ({ ...r }))) +} + +async function createDatabase( + folder: NoteFolder, + subpath: string, + title?: string +): Promise<DatabaseDoc> { + const atRoot = await primaryNotesAtRoot() + const baseTitle = (title ?? 'Untitled Database').trim() || 'Untitled Database' + const baseName = baseTitle.replace(DB_TITLE_BAD, '-') + const dirRel = vaultRelDir(folder, subpath, atRoot) + const csvFor = (name: string): string => + csvPathForFormDir(joinSub(dirRel, `${name}${FORM_DIR_SUFFIX}`)) + // Resolve a non-colliding <Name>.base under the directory. + let name = baseName + let n = 2 + while ((await readFileTextOrNull(csvFor(name))) !== null) name = `${baseName} ${n++}` + const csvPath = csvFor(name) + const folderSub = joinSub(subpath, `${name}${FORM_DIR_SUFFIX}`) + + const idField: DbField = { id: dbGenId(), name: 'id', type: 'text', hidden: true } + const nameField: DbField = { id: dbGenId(), name: 'Name', type: 'text' } + const fields = [idField, nameField] + const { views, activeViewId } = buildDefaultViews(fields, dbGenId) + const sidecar: DatabaseSidecar = { version: 1, idFieldId: idField.id, fields, views, activeViewId } + + await createFolder(folder, folderSub) + await dbPersistSidecar(csvPath, sidecar) + await writeNote(csvPath, serializeRows([], fields)) + return dbHydrate(csvPath, sidecar, []) +} + +async function createRecordPage(csvPath: string, title: string, body: string): Promise<string> { + const formDir = formDirFromCsvPath(dbToPosix(csvPath)) + if (!formDir) throw new Error(`Not a database folder: ${csvPath}`) + const safe = (title.trim() || 'Untitled').replace(/[\\/]/g, '-') + let finalTitle = safe + let n = 2 + while ((await readFileTextOrNull(`${formDir}/${finalTitle}.md`)) !== null) { + finalTitle = `${safe} ${n++}` + } + const noteRel = `${formDir}/${finalTitle}.md` + await writeNote(noteRel, body) + return noteRel +} + +async function renameDatabase(csvPath: string, newTitle: string): Promise<string> { + const oldFormDir = formDirFromCsvPath(dbToPosix(csvPath)) + if (!oldFormDir) throw new Error(`Not a database folder: ${csvPath}`) + const parentRel = oldFormDir.includes('/') ? oldFormDir.slice(0, oldFormDir.lastIndexOf('/')) : '' + const safeName = (newTitle.trim() || 'Untitled Database').replace(DB_TITLE_BAD, '-') + const makeFormDir = (name: string): string => + parentRel ? `${parentRel}/${name}${FORM_DIR_SUFFIX}` : `${name}${FORM_DIR_SUFFIX}` + let targetFormDir = makeFormDir(safeName) + if (targetFormDir === oldFormDir) return csvPath + let n = 2 + while ((await readFileTextOrNull(csvPathForFormDir(targetFormDir))) !== null) { + targetFormDir = makeFormDir(`${safeName} ${n++}`) + } + const atRoot = await primaryNotesAtRoot() + const { folder, subpath: oldSub } = splitVaultPath(oldFormDir, atRoot) + const { subpath: newSub } = splitVaultPath(targetFormDir, atRoot) + await renameFolder(folder, oldSub, newSub) + return csvPathForFormDir(targetFormDir) +} + +async function listDatabases(): Promise<DatabaseSummary[]> { + const [folders, atRoot] = await Promise.all([listFolders(), primaryNotesAtRoot()]) + const out: DatabaseSummary[] = [] + for (const f of folders) { + if (!isFormDirName(f.subpath)) continue + // The folder subpath is folder-relative; reconstruct the vault-relative path. + const csv = csvPathForFormDir(vaultRelDir(f.folder, f.subpath, atRoot)) + out.push({ path: csv, title: dbTitleFromPath(csv) }) + } + return out.sort((a, b) => a.title.localeCompare(b.title)) } // -------------------------------------------------------------------- From 4e6ea388eedd61ebd970f8b6d7102c9f5218fdfb Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Thu, 18 Jun 2026 17:02:31 -0500 Subject: [PATCH 60/66] feat: daily-note task calendar, vim calendar control, and Notion-style sidebar Tasks calendar - Daily-note tasks get an implicit due date (the note's date) so they surface on the right day without writing due: tokens; gated by the new "Tasks are due on the note's date" setting. - Add, move, reschedule, and delete tasks directly from the calendar, creating the daily note on demand when needed. - Drag a task to another day (pointer-based) with a move-vs-set-due choice. - Optional "Roll over unfinished tasks to today" (Obsidian-style). - Week agenda under the month grid in the sidebar calendar panel. Vim keyboard - Full keyboard control of the big Tasks calendar and the sidebar calendar panel: h/j/k/l day nav, Tab to pick a task, x/e/dd/m/</>/T/a actions, gg/G, [/] month, gt today. Sidebar - Notion-style restyle (soft active highlight, roomier rows, muted icons). - Per-folder/note/database icon + color customization with inline reset. - Folder hover-chevron and aligned rows. Editor - "Task" slash command; calendar hidden on quick notes. Settings + docs - New daily-notes task settings (server round-trips them as *bool). - Help docs for the tasks calendar and daily-note settings. --- apps/desktop/src/main/tasklists.test.ts | 92 +++ apps/desktop/src/main/tasks-domain.test.ts | 42 ++ apps/server/internal/vault/types.go | 5 + apps/server/internal/vault/vault.go | 28 +- apps/server/internal/vault/vault_test.go | 64 ++ .../app-core/src/components/CalendarPanel.tsx | 687 +++++++++++++++++- .../app-core/src/components/EditorPane.tsx | 9 +- .../app-core/src/components/FolderColors.tsx | 6 + .../src/components/FolderIconPickerModal.tsx | 9 +- .../app-core/src/components/SettingsModal.tsx | 24 + packages/app-core/src/components/Sidebar.tsx | 382 +++++----- .../app-core/src/components/TasksCalendar.tsx | 525 +++++++++++-- .../app-core/src/components/TasksView.tsx | 28 +- .../app-core/src/lib/cm-slash-commands.ts | 22 +- packages/app-core/src/lib/commands.ts | 10 + packages/app-core/src/lib/help.ts | 7 +- packages/app-core/src/lib/vault-layout.ts | 27 +- packages/app-core/src/store.ts | 355 ++++++++- packages/app-core/src/styles/index.css | 2 +- packages/bridge-contract/src/ipc.ts | 14 +- packages/shared-domain/src/tasklists.ts | 134 ++++ packages/shared-domain/src/tasks.ts | 28 + 22 files changed, 2214 insertions(+), 286 deletions(-) diff --git a/apps/desktop/src/main/tasklists.test.ts b/apps/desktop/src/main/tasklists.test.ts index a5ae07b3..634c200e 100644 --- a/apps/desktop/src/main/tasklists.test.ts +++ b/apps/desktop/src/main/tasklists.test.ts @@ -1,8 +1,12 @@ import { describe, expect, it } from 'vitest' import { + extractUncheckedTaskBlocks, + removeTaskAtIndex, + takeTaskLineAtIndex, setTaskCheckedAtIndex, setTaskDueAtIndex, setTaskPriorityAtIndex, + setTaskTextAtIndex, setTaskWaitingAtIndex, toggleTaskAtIndex } from '@shared/tasklists' @@ -119,3 +123,91 @@ describe('setTaskDueAtIndex', () => { ) }) }) + +describe('extractUncheckedTaskBlocks', () => { + it('pulls out only unchecked tasks, leaving checked + prose behind', () => { + const md = ['# 2026-06-16', '', '- [x] shipped', '- [ ] follow up', 'a note line'].join('\n') + const { moved, rest } = extractUncheckedTaskBlocks(md) + expect(moved).toEqual(['- [ ] follow up']) + expect(rest).toBe(['# 2026-06-16', '', '- [x] shipped', 'a note line'].join('\n')) + }) + + it('moves tokens verbatim (future due dates are preserved)', () => { + const md = ['- [ ] plan Q3 due:2026-09-01 !high', '- [x] done'].join('\n') + const { moved } = extractUncheckedTaskBlocks(md) + expect(moved).toEqual(['- [ ] plan Q3 due:2026-09-01 !high']) + }) + + it('carries indented child lines along with their task', () => { + const md = ['- [ ] parent', ' - [ ] child', ' notes', '- [ ] sibling'].join('\n') + const { moved, rest } = extractUncheckedTaskBlocks(md) + expect(moved).toEqual(['- [ ] parent', ' - [ ] child', ' notes', '- [ ] sibling']) + expect(rest).toBe('') + }) + + it('ignores checkboxes inside fenced code blocks', () => { + const md = ['- [ ] real', '```', '- [ ] fenced', '```'].join('\n') + const { moved, rest } = extractUncheckedTaskBlocks(md) + expect(moved).toEqual(['- [ ] real']) + expect(rest).toBe(['```', '- [ ] fenced', '```'].join('\n')) + }) + + it('returns nothing to move when all tasks are checked', () => { + const md = ['- [x] a', '- [x] b'].join('\n') + const { moved, rest } = extractUncheckedTaskBlocks(md) + expect(moved).toEqual([]) + expect(rest).toBe(md) + }) +}) + +describe('setTaskTextAtIndex', () => { + it('replaces the text after the checkbox, preserving the marker + check state', () => { + expect(setTaskTextAtIndex('- [x] old text', 0, 'new text')).toBe('- [x] new text') + expect(setTaskTextAtIndex(' - [ ] a\n - [ ] b', 1, 'edited')).toBe(' - [ ] a\n - [ ] edited') + }) + + it('writes tokens verbatim when included in the new text', () => { + expect(setTaskTextAtIndex('- [ ] a', 0, 'do thing due:2026-07-01 !high')).toBe( + '- [ ] do thing due:2026-07-01 !high' + ) + }) + + it('leaves an empty checkbox when text is blank', () => { + expect(setTaskTextAtIndex('- [ ] something', 0, ' ')).toBe('- [ ]') + }) +}) + +describe('removeTaskAtIndex', () => { + it('deletes the task line at the given index', () => { + expect(removeTaskAtIndex('- [ ] a\n- [ ] b\n- [ ] c', 1)).toBe('- [ ] a\n- [ ] c') + }) + + it('keeps surrounding prose intact', () => { + const md = ['# Day', '', '- [ ] keep', '- [ ] drop', 'a line'].join('\n') + expect(removeTaskAtIndex(md, 1)).toBe(['# Day', '', '- [ ] keep', 'a line'].join('\n')) + }) + + it('does not count checkboxes inside fenced code', () => { + const md = ['```', '- [ ] fenced', '```', '- [ ] real'].join('\n') + expect(removeTaskAtIndex(md, 0)).toBe(['```', '- [ ] fenced', '```'].join('\n')) + }) + + it('returns markdown unchanged for an out-of-range index', () => { + expect(removeTaskAtIndex('- [ ] only', 5)).toBe('- [ ] only') + }) +}) + +describe('takeTaskLineAtIndex', () => { + it('returns the removed line plus the remaining body (for moving a task)', () => { + const md = ['- [ ] a', '- [ ] b due:2026-07-01 !high', '- [ ] c'].join('\n') + const { line, body } = takeTaskLineAtIndex(md, 1) + expect(line).toBe('- [ ] b due:2026-07-01 !high') + expect(body).toBe('- [ ] a\n- [ ] c') + }) + + it('returns a null line and the unchanged body when out of range', () => { + const { line, body } = takeTaskLineAtIndex('- [ ] only', 9) + expect(line).toBeNull() + expect(body).toBe('- [ ] only') + }) +}) diff --git a/apps/desktop/src/main/tasks-domain.test.ts b/apps/desktop/src/main/tasks-domain.test.ts index 4138bb60..4bf50c89 100644 --- a/apps/desktop/src/main/tasks-domain.test.ts +++ b/apps/desktop/src/main/tasks-domain.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import { bucketTasksByDueDate, + inferDailyTaskDueDates, parseTasksFromBody, tasksDueOn, toIsoDateLocal, @@ -67,3 +68,44 @@ describe('bucketTasksByDueDate', () => { expect(buckets.get('2026-04-30')?.length).toBe(1) }) }) + +describe('inferDailyTaskDueDates', () => { + const dailyA = parseTasksFromBody('- [ ] a\n- [ ] b due:2026-07-01', { + path: 'inbox/Daily Notes/2026-06-17.md', + title: '2026-06-17', + folder: 'inbox' + }) + const dueByPath = new Map([['inbox/Daily Notes/2026-06-17.md', '2026-06-17']]) + + it('gives undated daily-note tasks the note date, flagged inferred', () => { + const out = inferDailyTaskDueDates(dailyA, dueByPath) + expect(out[0].due).toBe('2026-06-17') + expect(out[0].dueInferred).toBe(true) + }) + + it('never overrides an explicit due token', () => { + const out = inferDailyTaskDueDates(dailyA, dueByPath) + expect(out[1].due).toBe('2026-07-01') + expect(out[1].dueInferred).toBeUndefined() + }) + + it('leaves tasks outside any daily note untouched', () => { + const other = parseTasksFromBody('- [ ] loose', { + path: 'inbox/Loose.md', + title: 'Loose', + folder: 'inbox' + }) + const out = inferDailyTaskDueDates(other, dueByPath) + expect(out[0].due).toBeUndefined() + }) + + it('returns the same array instance when nothing changes', () => { + const out = inferDailyTaskDueDates(dailyA, new Map()) + expect(out).toBe(dailyA) + }) + + it('makes inferred tasks show up in their day bucket', () => { + const out = inferDailyTaskDueDates(dailyA, dueByPath) + expect(tasksDueOn(out, '2026-06-17').length).toBe(1) + }) +}) diff --git a/apps/server/internal/vault/types.go b/apps/server/internal/vault/types.go index b6439af8..6b660c7a 100644 --- a/apps/server/internal/vault/types.go +++ b/apps/server/internal/vault/types.go @@ -68,6 +68,11 @@ type DailyNotesSettings struct { Locale string `json:"locale,omitempty"` LegacyPatterns []DateNotePatternSettings `json:"legacyPatterns,omitempty"` TemplateID string `json:"templateId,omitempty"` + // Pointers so an absent field round-trips as "unset" (the TS client applies + // the real default — true for TasksDueOnNoteDate, false for rollover). These + // drive purely client-side behavior; the server only persists them. + TasksDueOnNoteDate *bool `json:"tasksDueOnNoteDate,omitempty"` + RolloverUnfinishedTasks *bool `json:"rolloverUnfinishedTasks,omitempty"` } type WeeklyNotesSettings struct { diff --git a/apps/server/internal/vault/vault.go b/apps/server/internal/vault/vault.go index f6fea56f..769f32fc 100644 --- a/apps/server/internal/vault/vault.go +++ b/apps/server/internal/vault/vault.go @@ -279,12 +279,14 @@ func cloneSettings(settings VaultSettings) VaultSettings { return VaultSettings{ PrimaryNotesLocation: settings.PrimaryNotesLocation, DailyNotes: DailyNotesSettings{ - Enabled: settings.DailyNotes.Enabled, - Directory: settings.DailyNotes.Directory, - TitlePattern: settings.DailyNotes.TitlePattern, - Locale: settings.DailyNotes.Locale, - LegacyPatterns: dailyLegacyPatterns, - TemplateID: settings.DailyNotes.TemplateID, + Enabled: settings.DailyNotes.Enabled, + Directory: settings.DailyNotes.Directory, + TitlePattern: settings.DailyNotes.TitlePattern, + Locale: settings.DailyNotes.Locale, + LegacyPatterns: dailyLegacyPatterns, + TemplateID: settings.DailyNotes.TemplateID, + TasksDueOnNoteDate: settings.DailyNotes.TasksDueOnNoteDate, + RolloverUnfinishedTasks: settings.DailyNotes.RolloverUnfinishedTasks, }, WeeklyNotes: WeeklyNotesSettings{ Enabled: settings.WeeklyNotes.Enabled, @@ -411,12 +413,14 @@ func normalizeVaultSettings(value VaultSettings, fallbackPrimary PrimaryNotesLoc return value.PrimaryNotesLocation }()), DailyNotes: DailyNotesSettings{ - Enabled: value.DailyNotes.Enabled, - Directory: normalizeDailyNotesDirectory(value.DailyNotes.Directory), - TitlePattern: normalizeDailyNoteTitlePattern(value.DailyNotes.TitlePattern), - Locale: normalizeDailyNoteLocale(value.DailyNotes.Locale), - LegacyPatterns: normalizeDailyNoteLegacyPatterns(value.DailyNotes.LegacyPatterns), - TemplateID: value.DailyNotes.TemplateID, + Enabled: value.DailyNotes.Enabled, + Directory: normalizeDailyNotesDirectory(value.DailyNotes.Directory), + TitlePattern: normalizeDailyNoteTitlePattern(value.DailyNotes.TitlePattern), + Locale: normalizeDailyNoteLocale(value.DailyNotes.Locale), + LegacyPatterns: normalizeDailyNoteLegacyPatterns(value.DailyNotes.LegacyPatterns), + TemplateID: value.DailyNotes.TemplateID, + TasksDueOnNoteDate: value.DailyNotes.TasksDueOnNoteDate, + RolloverUnfinishedTasks: value.DailyNotes.RolloverUnfinishedTasks, }, WeeklyNotes: WeeklyNotesSettings{ Enabled: value.WeeklyNotes.Enabled, diff --git a/apps/server/internal/vault/vault_test.go b/apps/server/internal/vault/vault_test.go index d710028c..ff1a1b34 100644 --- a/apps/server/internal/vault/vault_test.go +++ b/apps/server/internal/vault/vault_test.go @@ -622,6 +622,70 @@ func TestVaultSettingsWeeklyNotesRoundTrip(t *testing.T) { } } +// The web client drives the implicit-due and task-rollover behavior off two +// daily-notes booleans. They are pointers so "absent" round-trips as unset +// (the TS client applies the real default); an explicit value must survive a +// SetSettings -> GetSettings round-trip and reach vault.json, or the web +// toggles would silently revert like #117. +func TestVaultSettingsDailyTaskFlagsRoundTrip(t *testing.T) { + root := t.TempDir() + v, err := New(root, Options{}) + if err != nil { + t.Fatal(err) + } + + yes := true + no := false + if _, err := v.SetSettings(VaultSettings{ + PrimaryNotesLocation: PrimaryNotesInbox, + DailyNotes: DailyNotesSettings{ + Enabled: true, + Directory: "Daily", + TasksDueOnNoteDate: &no, // explicitly turn the default (true) OFF + RolloverUnfinishedTasks: &yes, + }, + }); err != nil { + t.Fatal(err) + } + + got, err := v.GetSettings() + if err != nil { + t.Fatal(err) + } + if got.DailyNotes.TasksDueOnNoteDate == nil || *got.DailyNotes.TasksDueOnNoteDate != false { + t.Errorf("tasksDueOnNoteDate = %v, want explicit false", got.DailyNotes.TasksDueOnNoteDate) + } + if got.DailyNotes.RolloverUnfinishedTasks == nil || *got.DailyNotes.RolloverUnfinishedTasks != true { + t.Errorf("rolloverUnfinishedTasks = %v, want explicit true", got.DailyNotes.RolloverUnfinishedTasks) + } + + raw, err := os.ReadFile(v.settingsPath()) + if err != nil { + t.Fatal(err) + } + if !bytes.Contains(raw, []byte("tasksDueOnNoteDate")) { + t.Errorf("vault.json missing tasksDueOnNoteDate key:\n%s", raw) + } + if !bytes.Contains(raw, []byte("rolloverUnfinishedTasks")) { + t.Errorf("vault.json missing rolloverUnfinishedTasks key:\n%s", raw) + } + + // Absent pointers must stay nil (omitted) so the client default wins. + if _, err := v.SetSettings(VaultSettings{ + PrimaryNotesLocation: PrimaryNotesInbox, + DailyNotes: DailyNotesSettings{Enabled: true, Directory: "Daily"}, + }); err != nil { + t.Fatal(err) + } + got, err = v.GetSettings() + if err != nil { + t.Fatal(err) + } + if got.DailyNotes.TasksDueOnNoteDate != nil { + t.Errorf("absent tasksDueOnNoteDate = %v, want nil", *got.DailyNotes.TasksDueOnNoteDate) + } +} + // A file or directory the server can't read must be skipped, not abort the whole // vault scan — otherwise one root-owned entry hides the entire vault. (#159) func TestListSkipsUnreadableEntries(t *testing.T) { diff --git a/packages/app-core/src/components/CalendarPanel.tsx b/packages/app-core/src/components/CalendarPanel.tsx index 217cd081..2173a998 100644 --- a/packages/app-core/src/components/CalendarPanel.tsx +++ b/packages/app-core/src/components/CalendarPanel.tsx @@ -14,9 +14,15 @@ */ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import type { NoteContent, NoteMeta } from '@shared/ipc' -import { parseTasksFromBody } from '@shared/tasks' +import { + inferDailyTaskDueDates, + parseTasksFromBody, + tasksDueOn, + type VaultTask, +} from '@shared/tasks' import { useStore } from '../store' import { + buildDailyNoteDateByPath, buildDateNoteIndexes, classifyDateNote, normalizeVaultSettings, @@ -80,6 +86,21 @@ function isoDateStr(d: Date): string { ).padStart(2, '0')}` } +/** Parse a `YYYY-MM-DD` string back to a local-midnight Date. */ +function parseIsoDate(iso: string): Date { + const [y, m, d] = iso.split('-').map((s) => Number.parseInt(s, 10)) + return new Date(y, m - 1, d) +} + +const TASK_PREFIX_RE = /^\s*(?:>\s*)*(?:[-+*]|\d+[.)])\s+\[[ xX]\]\s?/ + +/** The raw text after the checkbox (content + any `due:`/`!priority` tokens) — + * what an inline edit field prefills with so editing is lossless. */ +function taskTail(task: VaultTask): string { + const m = task.rawText.match(TASK_PREFIX_RE) + return m ? task.rawText.slice(m[0].length) : task.content +} + function isoWeekStr(d: Date): string { return `${getISOWeekYear(d)}-W${String(getISOWeek(d)).padStart(2, '0')}` } @@ -100,6 +121,15 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { const vaultSettings = useStore((s) => s.vaultSettings) const openDailyNoteForDate = useStore((s) => s.openDailyNoteForDate) const openWeeklyNoteForDate = useStore((s) => s.openWeeklyNoteForDate) + const vaultTasks = useStore((s) => s.vaultTasks) + const tasksLoading = useStore((s) => s.tasksLoading) + const refreshTasks = useStore((s) => s.refreshTasks) + const addTaskForDate = useStore((s) => s.addTaskForDate) + const toggleTaskFromList = useStore((s) => s.toggleTaskFromList) + const openTaskAt = useStore((s) => s.openTaskAt) + const applyTaskMutation = useStore((s) => s.applyTaskMutation) + const moveTaskToDate = useStore((s) => s.moveTaskToDate) + const deleteTaskFromList = useStore((s) => s.deleteTaskFromList) const width = useStore((s) => s.panelWidths.calendar) const setPanelWidth = useStore((s) => s.setPanelWidth) const weekStart = useStore((s) => s.calendarWeekStart) @@ -144,6 +174,91 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { [notes, settings] ) + // The day the user has clicked to inspect (tasks + actions shown below the + // grid). Defaults to the active daily note's date, else today; follows the + // active note as it changes. + const [selectedIso, setSelectedIso] = useState<string>(() => activeDayIso ?? todayIso) + useEffect(() => { + setSelectedIso(activeDayIso ?? todayIso) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [note.path]) + const selectedDate = useMemo(() => parseIsoDate(selectedIso), [selectedIso]) + + // Tasks scheduled for the selected day. Tasks written in a daily note inherit + // that note's date (implicit due) just like the full Tasks calendar, so we run + // the same inference over the vault-wide index. Load it lazily the first time. + const dueByPath = useMemo( + () => buildDailyNoteDateByPath(notes, vaultSettings), + [notes, vaultSettings] + ) + const inferredTasks = useMemo( + () => inferDailyTaskDueDates(vaultTasks, dueByPath), + [vaultTasks, dueByPath] + ) + // The 7 days of the selected day's week (respecting the week-start setting), + // shown as an agenda under the calendar. + const weekDays = useMemo(() => { + const offset = (selectedDate.getDay() - firstDay + 7) % 7 + const start = new Date( + selectedDate.getFullYear(), + selectedDate.getMonth(), + selectedDate.getDate() - offset + ) + return Array.from( + { length: 7 }, + (_, i) => new Date(start.getFullYear(), start.getMonth(), start.getDate() + i) + ) + }, [selectedDate, firstDay]) + const weekRangeLabel = useMemo(() => { + const start = weekDays[0] + const end = weekDays[6] + const startStr = start.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + const endStr = + end.getMonth() === start.getMonth() + ? end.toLocaleDateString(undefined, { day: 'numeric' }) + : end.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + return `${startStr} – ${endStr}` + }, [weekDays]) + useEffect(() => { + if (dailyEnabled && vaultTasks.length === 0 && !tasksLoading) void refreshTasks() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dailyEnabled]) + + const [addValue, setAddValue] = useState('') + // Drag-to-reschedule: dragged task held in a ref (drop handlers read it + // synchronously); the hovered day is highlighted while dragging. + const dragTaskRef = useRef<VaultTask | null>(null) + const [dragOverIso, setDragOverIso] = useState<string | null>(null) + // Inline task editing (right-click → Edit). + const [editingTaskId, setEditingTaskId] = useState<string | null>(null) + const [editValue, setEditValue] = useState('') + // Set just before blurring on Escape so the blur handler cancels instead of commits. + const cancelEditRef = useRef(false) + // Keyboard control (active only while the panel has focus, so it never steals + // keys from the editor). Mirrors the big calendar. + const panelRef = useRef<HTMLElement>(null) + const addInputRef = useRef<HTMLInputElement>(null) + const focusedTaskRef = useRef<HTMLDivElement | null>(null) + const [activeTaskIndex, setActiveTaskIndex] = useState(0) + const [grabbedTask, setGrabbedTask] = useState<VaultTask | null>(null) + const gPending = useRef(0) + const gTimer = useRef<ReturnType<typeof setTimeout>>() + const dPending = useRef(false) + const dTimer = useRef<ReturnType<typeof setTimeout>>() + + // The selected day's tasks drive the keyboard "active task" cursor. + const selectedDayTasks = useMemo( + () => tasksDueOn(inferredTasks, selectedIso), + [inferredTasks, selectedIso] + ) + const activeIdx = Math.min(activeTaskIndex, Math.max(0, selectedDayTasks.length - 1)) + useEffect(() => { + setActiveTaskIndex(0) + }, [selectedIso]) + useEffect(() => { + focusedTaskRef.current?.scrollIntoView({ block: 'nearest' }) + }, [selectedIso, activeIdx]) + const grid = useMemo(() => buildGrid(anchor, firstDay), [anchor, firstDay]) const rows = useMemo(() => { const out: { days: Date[]; monday: Date }[] = [] @@ -210,7 +325,10 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { } }, [visibleNotes]) - const handleDayClick = useCallback( + // Open the daily note for a day, creating it (with a prompt) if missing. + // Single-click now just *selects* a day (see the grid); this runs on + // double-click, the "Open/Create note" action, and the context menu. + const openOrCreateDay = useCallback( async (day: Date, iso: string) => { if (!dailyEnabled) return if (dailyByDate.has(iso)) { @@ -228,6 +346,12 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { [dailyEnabled, dailyByDate, openDailyNoteForDate] ) + const addDaysIso = useCallback( + (n: number) => + isoDateStr(new Date(today.getFullYear(), today.getMonth(), today.getDate() + n)), + [today] + ) + const handleWeekClick = useCallback( async (monday: Date, weekIso: string) => { if (!weeklyEnabled) return @@ -281,10 +405,10 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { { kind: 'separator' }, { label: 'Move to Trash', danger: true, onSelect: () => void trashNote(meta) }, ] - : [{ label: `Create ${iso}`, onSelect: () => void handleDayClick(day, iso) }] + : [{ label: `Create ${iso}`, onSelect: () => void openOrCreateDay(day, iso) }] setMenu({ x: e.clientX, y: e.clientY, items }) }, - [dailyEnabled, dailyByDate, openDailyNoteForDate, handleDayClick, trashNote, clearHover] + [dailyEnabled, dailyByDate, openDailyNoteForDate, openOrCreateDay, trashNote, clearHover] ) const openWeekMenu = useCallback( (e: React.MouseEvent, monday: Date, weekIso: string) => { @@ -309,6 +433,310 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { [weeklyEnabled, weeklyByWeek, openWeeklyNoteForDate, handleWeekClick, trashNote, clearHover] ) + // Right-click a task in the detail panel: reschedule presets, inline edit, or + // delete. Arbitrary dates are assigned by dragging the task onto a day. + const openTaskMenu = useCallback( + (e: React.MouseEvent, task: VaultTask) => { + e.preventDefault() + e.stopPropagation() + clearHover() + const reschedule = (iso: string | null): void => + void applyTaskMutation(task, { kind: 'set-due', due: iso }) + const items: ContextMenuItem[] = [ + { label: 'Due today', hint: addDaysIso(0), onSelect: () => reschedule(addDaysIso(0)) }, + { + label: 'Due tomorrow', + hint: addDaysIso(1), + onSelect: () => reschedule(addDaysIso(1)), + }, + { + label: 'Due next week', + hint: addDaysIso(7), + onSelect: () => reschedule(addDaysIso(7)), + }, + ] + if (task.due && !task.dueInferred) { + items.push({ label: 'Clear due date', onSelect: () => reschedule(null) }) + } + items.push( + { kind: 'separator' }, + { + label: 'Edit', + onSelect: () => { + setEditingTaskId(task.id) + setEditValue(taskTail(task)) + }, + }, + { kind: 'separator' }, + { label: 'Delete', danger: true, onSelect: () => void deleteTaskFromList(task) } + ) + setMenu({ x: e.clientX, y: e.clientY, items }) + }, + [clearHover, applyTaskMutation, deleteTaskFromList, addDaysIso] + ) + + // After dropping a task on a day (calendar cell or week-agenda row), offer to + // move it into that day's note or just set its due date. + const openDropMenu = useCallback( + (clientX: number, clientY: number, task: VaultTask, iso: string, day: Date) => { + const dropLabel = day.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + setMenu({ + x: clientX, + y: clientY, + items: [ + { + label: `Move into ${dropLabel}'s note`, + onSelect: () => void moveTaskToDate(task, iso), + }, + { + label: `Just set due: ${dropLabel}`, + onSelect: () => void applyTaskMutation(task, { kind: 'set-due', due: iso }), + }, + ], + }) + }, + [moveTaskToDate, applyTaskMutation] + ) + + const moveSelectedDay = useCallback( + (deltaDays: number): void => { + const next = new Date( + selectedDate.getFullYear(), + selectedDate.getMonth(), + selectedDate.getDate() + deltaDays + ) + setSelectedIso(isoDateStr(next)) + if (next.getMonth() !== anchor.getMonth() || next.getFullYear() !== anchor.getFullYear()) { + setAnchor(new Date(next.getFullYear(), next.getMonth(), 1)) + } + }, + [selectedDate, anchor] + ) + + // Vim keyboard control — active only while focus is inside the panel, so it + // never intercepts keys meant for the editor. Mirrors the big calendar. + useEffect(() => { + if (!dailyEnabled) return + const handler = (e: KeyboardEvent): void => { + const panel = panelRef.current + const active = document.activeElement as HTMLElement | null + if (!panel || !active || !panel.contains(active)) return + if (document.querySelector('[data-vim-hint-overlay]')) return + const tag = active.tagName + if (tag === 'INPUT' || tag === 'TEXTAREA' || active.isContentEditable) return + + const consume = (): void => { + e.preventDefault() + e.stopImmediatePropagation() + } + const tasks = selectedDayTasks + const cur = tasks[Math.min(activeTaskIndex, Math.max(0, tasks.length - 1))] + + if (e.metaKey || e.ctrlKey || e.altKey) return + + if (grabbedTask) { + if (e.key === 'Escape') { + consume() + setGrabbedTask(null) + return + } + if (e.key === 'Enter') { + consume() + const cell = document.querySelector(`[data-cal-day="${selectedIso}"]`) + const rect = cell?.getBoundingClientRect() + openDropMenu( + rect ? rect.left + rect.width / 2 : window.innerWidth / 2, + rect ? rect.bottom : window.innerHeight / 2, + grabbedTask, + selectedIso, + selectedDate + ) + setGrabbedTask(null) + return + } + switch (e.key) { + case 'h': + case 'ArrowLeft': + consume() + moveSelectedDay(-1) + return + case 'l': + case 'ArrowRight': + consume() + moveSelectedDay(1) + return + case 'j': + case 'ArrowDown': + consume() + moveSelectedDay(7) + return + case 'k': + case 'ArrowUp': + consume() + moveSelectedDay(-7) + return + case '[': + consume() + setAnchor((a) => addMonths(a, -1)) + return + case ']': + consume() + setAnchor((a) => addMonths(a, 1)) + return + default: + consume() + return + } + } + + if (e.key === 'a') { + consume() + requestAnimationFrame(() => addInputRef.current?.focus()) + return + } + if (e.key === 't' && gPending.current > 0) { + consume() + gPending.current = 0 + if (gTimer.current) clearTimeout(gTimer.current) + setSelectedIso(todayIso) + setAnchor(new Date(today.getFullYear(), today.getMonth(), 1)) + return + } + if (e.key === 'g') { + consume() + if (gPending.current > 0) { + gPending.current = 0 + if (gTimer.current) clearTimeout(gTimer.current) + setSelectedIso(isoDateStr(grid[0])) + return + } + gPending.current = 1 + if (gTimer.current) clearTimeout(gTimer.current) + gTimer.current = setTimeout(() => (gPending.current = 0), 600) + return + } + if (e.key === 'G') { + consume() + setSelectedIso(isoDateStr(grid[grid.length - 1])) + return + } + + if (tasks.length > 0) { + if (e.key === 'Tab') { + consume() + const n = tasks.length + const dir = e.shiftKey ? -1 : 1 + setActiveTaskIndex((i) => (((Math.min(i, n - 1) + dir) % n) + n) % n) + return + } + if (cur) { + if (e.key === '>' || e.key === '<') { + consume() + const d = new Date( + selectedDate.getFullYear(), + selectedDate.getMonth(), + selectedDate.getDate() + (e.key === '>' ? 1 : -1) + ) + void applyTaskMutation(cur, { kind: 'set-due', due: isoDateStr(d) }) + return + } + if (e.key === 'T') { + consume() + void applyTaskMutation(cur, { kind: 'set-due', due: todayIso }) + return + } + if (e.key === 'x' || e.key === ' ') { + consume() + void toggleTaskFromList(cur) + return + } + if (e.key === 'e') { + consume() + setEditingTaskId(cur.id) + setEditValue(taskTail(cur)) + return + } + if (e.key === 'm') { + consume() + setGrabbedTask(cur) + return + } + if (e.key === 'd') { + consume() + if (dPending.current) { + dPending.current = false + if (dTimer.current) clearTimeout(dTimer.current) + void deleteTaskFromList(cur) + } else { + dPending.current = true + if (dTimer.current) clearTimeout(dTimer.current) + dTimer.current = setTimeout(() => (dPending.current = false), 600) + } + return + } + } + } + + switch (e.key) { + case 'h': + case 'ArrowLeft': + consume() + moveSelectedDay(-1) + return + case 'l': + case 'ArrowRight': + consume() + moveSelectedDay(1) + return + case 'j': + case 'ArrowDown': + consume() + moveSelectedDay(7) + return + case 'k': + case 'ArrowUp': + consume() + moveSelectedDay(-7) + return + case '[': + consume() + setAnchor((a) => addMonths(a, -1)) + return + case ']': + consume() + setAnchor((a) => addMonths(a, 1)) + return + case 'Enter': + consume() + if (cur) void openTaskAt(cur) + else void openOrCreateDay(selectedDate, selectedIso) + return + default: + return + } + } + window.addEventListener('keydown', handler, true) + return () => window.removeEventListener('keydown', handler, true) + }, [ + dailyEnabled, + selectedDayTasks, + activeTaskIndex, + grabbedTask, + selectedIso, + selectedDate, + anchor, + grid, + todayIso, + today, + moveSelectedDay, + openDropMenu, + applyTaskMutation, + toggleTaskFromList, + deleteTaskFromList, + openTaskAt, + openOrCreateDay + ]) + const atRefMonth = anchor.getFullYear() === refDate.getFullYear() && anchor.getMonth() === refDate.getMonth() const atTodayMonth = @@ -330,11 +758,98 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { </span> ) + // One task row in the week agenda: draggable to a calendar day (move / set + // due), click to open, right-click for actions, inline-editable. + const renderTaskRow = ( + task: VaultTask, + dayIso: string, + opts?: { isActive?: boolean; rowRef?: React.RefObject<HTMLDivElement> | null } + ): JSX.Element => + editingTaskId === task.id ? ( + <div key={task.id} className="flex items-center gap-2 px-1.5 py-1"> + <span className="h-3.5 w-3.5 shrink-0 rounded-sm border border-paper-400/70" /> + <input + autoFocus + value={editValue} + onChange={(e) => setEditValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + e.currentTarget.blur() + } else if (e.key === 'Escape') { + e.stopPropagation() + cancelEditRef.current = true + e.currentTarget.blur() + } + }} + onBlur={() => { + if (cancelEditRef.current) { + cancelEditRef.current = false + } else { + const text = editValue.trim() + if (text && text !== taskTail(task)) { + void applyTaskMutation(task, { kind: 'set-text', text }) + } + } + setEditingTaskId(null) + }} + className="min-w-0 flex-1 rounded border border-accent/60 bg-paper-100 px-1 py-0.5 text-xs outline-none" + spellCheck={false} + autoComplete="off" + /> + </div> + ) : ( + // Draggable <div> (not <button>) — Chromium/Electron won't fire dragstart + // on a native button. + <div + key={task.id} + ref={opts?.rowRef ?? undefined} + role="button" + tabIndex={0} + draggable + onDragStart={(e) => { + e.dataTransfer.effectAllowed = 'move' + e.dataTransfer.setData('text/plain', task.id) + dragTaskRef.current = task + }} + onDragEnd={() => { + dragTaskRef.current = null + setDragOverIso(null) + }} + onClick={() => void openTaskAt(task)} + onContextMenu={(e) => openTaskMenu(e, task)} + title={`${task.content} — drag to a day to reschedule, right-click for more`} + className={[ + 'flex w-full cursor-grab items-center gap-2 rounded px-1.5 py-1 text-left text-xs text-ink-700 transition-colors active:cursor-grabbing', + opts?.isActive + ? 'bg-accent/10 ring-1 ring-inset ring-accent/40' + : 'hover:bg-paper-200', + grabbedTask?.id === task.id ? 'opacity-50' : '' + ].join(' ')} + > + <span + role="checkbox" + aria-checked={false} + onClick={(e) => { + e.stopPropagation() + void toggleTaskFromList(task) + }} + className="flex h-3.5 w-3.5 shrink-0 cursor-pointer items-center justify-center rounded-sm border border-paper-400/70 transition-colors hover:bg-paper-300/60" + /> + <span className="min-w-0 flex-1 truncate">{task.content || '(empty task)'}</span> + {task.sourcePath !== (dailyByDate.get(dayIso)?.path ?? '') && ( + <span className="shrink-0 truncate text-2xs text-ink-400">{task.noteTitle}</span> + )} + </div> + ) + return ( <section + ref={panelRef} aria-label="Calendar" + tabIndex={0} style={{ width }} - className="relative flex shrink-0 flex-col border-l border-paper-300/70 bg-paper-50/18" + className="relative flex shrink-0 flex-col border-l border-paper-300/70 bg-paper-50/18 outline-none" > <PanelResizeHandle onStart={startResize} /> @@ -388,7 +903,7 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { )} </div> - <div className="min-h-0 flex-1 overflow-y-auto px-3 py-3"> + <div className="shrink-0 px-3 py-3"> <div className={`grid ${gridCols} gap-y-1`}> {showWeekNumbers && ( <div className="flex items-center justify-center text-2xs font-medium uppercase text-ink-400"> @@ -456,7 +971,9 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { const iso = isoDateStr(day) const inMonth = day.getMonth() === anchorMonth const isActiveDay = iso === activeDayIso + const isSelected = iso === selectedIso const isToday = iso === todayIso + const isDragOver = dragOverIso === iso const dayMeta = dailyByDate.get(iso) const dayStats = dayMeta ? stats.get(dayMeta.path) : undefined const dots = dotsFor(dayStats) @@ -466,25 +983,47 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { <button key={iso} type="button" - onClick={() => void handleDayClick(day, iso)} + data-cal-day={iso} + onClick={() => dailyEnabled && setSelectedIso(iso)} + onDoubleClick={() => void openOrCreateDay(day, iso)} onContextMenu={(e) => openDayMenu(e, day, iso)} onMouseEnter={(e) => armHover(e.currentTarget, dayMeta)} onMouseLeave={clearHover} + onDragOver={(e) => { + if (!dragTaskRef.current || !dailyEnabled) return + e.preventDefault() + e.dataTransfer.dropEffect = 'move' + if (dragOverIso !== iso) setDragOverIso(iso) + }} + onDragLeave={() => { + if (dragOverIso === iso) setDragOverIso(null) + }} + onDrop={(e) => { + e.preventDefault() + const dragged = dragTaskRef.current + dragTaskRef.current = null + setDragOverIso(null) + if (dragged) openDropMenu(e.clientX, e.clientY, dragged, iso, day) + }} disabled={!dailyEnabled} - title={iso} + title={`${iso} — click to view, double-click to open`} className={[ 'relative flex flex-col items-center rounded py-1 text-xs leading-tight transition-colors', !dailyEnabled ? inMonth ? 'cursor-default text-ink-600' : 'cursor-default text-ink-400' - : isActiveDay - ? 'bg-accent font-semibold text-white' - : isToday - ? 'font-semibold text-accent ring-1 ring-inset ring-accent/50' - : inMonth - ? 'text-ink-700 hover:bg-paper-200' - : 'text-ink-400 hover:bg-paper-200', + : isDragOver + ? 'bg-accent/25 font-semibold text-ink-900 ring-2 ring-inset ring-accent/80' + : isActiveDay + ? 'bg-accent font-semibold text-white' + : isSelected + ? 'bg-paper-200 font-semibold text-ink-900 ring-1 ring-inset ring-accent/60' + : isToday + ? 'font-semibold text-accent ring-1 ring-inset ring-accent/50' + : inMonth + ? 'text-ink-700 hover:bg-paper-200' + : 'text-ink-400 hover:bg-paper-200', ].join(' ')} > {openTasks > 0 && ( @@ -511,6 +1050,124 @@ export function CalendarPanel({ note }: { note: NoteContent }): JSX.Element { </div> </div> + {dailyEnabled && ( + <div className="flex min-h-0 flex-1 flex-col border-t border-paper-300/60 px-3 py-3"> + {grabbedTask && ( + <div className="mb-2 rounded bg-accent/10 px-2 py-1 text-2xs text-accent"> + Moving “{grabbedTask.content || 'task'}” — h/j/k/l pick a day · Enter place · Esc cancel + </div> + )} + <div className="mb-2 flex items-center justify-between gap-2"> + <div className="min-w-0 truncate text-xs font-semibold text-ink-800"> + Week of {weekRangeLabel} + </div> + <button + type="button" + onClick={() => void openOrCreateDay(selectedDate, selectedIso)} + className="shrink-0 rounded px-1.5 py-0.5 text-2xs text-ink-500 transition-colors hover:bg-paper-200 hover:text-accent" + > + {dailyByDate.has(selectedIso) ? 'Open note →' : 'Create note'} + </button> + </div> + + <form + className="mb-2" + onSubmit={(e) => { + e.preventDefault() + const value = addValue.trim() + if (!value) return + void addTaskForDate(selectedIso, value) + setAddValue('') + }} + > + <input + ref={addInputRef} + type="text" + value={addValue} + onChange={(e) => setAddValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Escape') { + e.stopPropagation() + if (addValue) setAddValue('') + else e.currentTarget.blur() + } + }} + placeholder={`Add a task for ${selectedDate.toLocaleDateString(undefined, { + weekday: 'short', + day: 'numeric', + })}…`} + className="w-full rounded-md border border-paper-300/60 bg-paper-200/40 px-2 py-1 text-xs outline-none placeholder:text-ink-400 focus:border-accent/60" + spellCheck={false} + autoComplete="off" + /> + </form> + + <div className="min-h-0 flex-1 space-y-2 overflow-y-auto"> + {weekDays.map((day) => { + const iso = isoDateStr(day) + const dayTasks = tasksDueOn(inferredTasks, iso) + const isSel = iso === selectedIso + const isDragOver = dragOverIso === iso + return ( + <div + key={iso} + onDragOver={(e) => { + if (!dragTaskRef.current) return + e.preventDefault() + e.dataTransfer.dropEffect = 'move' + if (dragOverIso !== iso) setDragOverIso(iso) + }} + onDragLeave={() => { + if (dragOverIso === iso) setDragOverIso(null) + }} + onDrop={(e) => { + e.preventDefault() + const dragged = dragTaskRef.current + dragTaskRef.current = null + setDragOverIso(null) + if (dragged) openDropMenu(e.clientX, e.clientY, dragged, iso, day) + }} + className={[ + 'rounded', + isDragOver ? 'bg-accent/5 ring-1 ring-inset ring-accent/50' : '', + ].join(' ')} + > + <button + type="button" + onClick={() => setSelectedIso(iso)} + className={[ + 'flex w-full items-center justify-between rounded px-1.5 py-0.5 text-left text-2xs font-semibold uppercase tracking-wide transition-colors', + isSel ? 'text-ink-900' : 'text-ink-400 hover:text-ink-700', + ].join(' ')} + > + <span> + {day.toLocaleDateString(undefined, { weekday: 'short', day: 'numeric' })} + {iso === todayIso && ( + <span className="ml-1.5 rounded bg-accent/15 px-1 py-px text-2xs font-medium normal-case text-accent"> + Today + </span> + )} + </span> + {dayTasks.length > 0 && <span className="text-ink-400">{dayTasks.length}</span>} + </button> + {dayTasks.length > 0 && ( + <div className="mt-0.5 space-y-0.5"> + {dayTasks.map((task, ti) => { + const isActive = iso === selectedIso && ti === activeIdx + return renderTaskRow(task, iso, { + isActive, + rowRef: isActive ? focusedTaskRef : null + }) + })} + </div> + )} + </div> + ) + })} + </div> + </div> + )} + {hover && ( <div className="fixed z-50 max-w-[260px] rounded-lg border border-paper-300/75 bg-paper-50 p-3 shadow-[0_12px_28px_-18px_rgb(var(--z-shadow)/0.8)]" diff --git a/packages/app-core/src/components/EditorPane.tsx b/packages/app-core/src/components/EditorPane.tsx index 2c6aa5fc..d7172bd5 100644 --- a/packages/app-core/src/components/EditorPane.tsx +++ b/packages/app-core/src/components/EditorPane.tsx @@ -687,8 +687,13 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { ) const calendarAvailable = useMemo(() => { const s = normalizeVaultSettings(vaultSettings) - return s.dailyNotes.enabled || s.weeklyNotes.enabled - }, [vaultSettings]) + if (!(s.dailyNotes.enabled || s.weeklyNotes.enabled)) return false + // The calendar navigates daily/weekly notes — it's meaningless in the Quick + // Notes scratchpad, and showing it there makes a quick note look like a + // calendar-linked daily note (a real source of confusion). Hide it there. + if (content?.folder === 'quick') return false + return true + }, [vaultSettings, content?.folder]) const [commentDraft, setCommentDraft] = useState<CommentDraft | null>(null) const [selectionCommentAction, setSelectionCommentAction] = useState<SelectionCommentAction>(null) diff --git a/packages/app-core/src/components/FolderColors.tsx b/packages/app-core/src/components/FolderColors.tsx index baf32396..c7a82235 100644 --- a/packages/app-core/src/components/FolderColors.tsx +++ b/packages/app-core/src/components/FolderColors.tsx @@ -28,6 +28,12 @@ export const FOLDER_COLOR_OPTIONS: readonly FolderColorOption[] = [ const FOLDER_COLOR_LOOKUP = new Map(FOLDER_COLOR_OPTIONS.map((option) => [option.id, option])) +/** The glyph text-color class for a color id (any keyed entry — folder, note, + * database…), or null when there's no color. */ +export function colorGlyphClassById(id: FolderColorId | null | undefined): string | null { + return id ? FOLDER_COLOR_LOOKUP.get(id)?.glyphClass ?? null : null +} + /** The chosen color for a folder, or null when none is set (default tint). */ export function resolveFolderColorId( folder: NoteFolder, diff --git a/packages/app-core/src/components/FolderIconPickerModal.tsx b/packages/app-core/src/components/FolderIconPickerModal.tsx index 4786f90f..db6fa50f 100644 --- a/packages/app-core/src/components/FolderIconPickerModal.tsx +++ b/packages/app-core/src/components/FolderIconPickerModal.tsx @@ -7,11 +7,13 @@ export function FolderIconPickerModal({ targetLabel, currentIconId, onSelect, + onReset, onCancel }: { targetLabel: string - currentIconId: FolderIconId + currentIconId: FolderIconId | null onSelect: (iconId: FolderIconId) => void + onReset: () => void onCancel: () => void }): JSX.Element { return ( @@ -50,6 +52,11 @@ export function FolderIconPickerModal({ <Button variant="secondary" onClick={onCancel}> Cancel </Button> + {currentIconId && ( + <Button variant="ghost" onClick={onReset}> + Reset icon + </Button> + )} </Modal.Footer> </Modal> ) diff --git a/packages/app-core/src/components/SettingsModal.tsx b/packages/app-core/src/components/SettingsModal.tsx index 91200b2a..d20b765b 100644 --- a/packages/app-core/src/components/SettingsModal.tsx +++ b/packages/app-core/src/components/SettingsModal.tsx @@ -1821,6 +1821,30 @@ export function SettingsModal(): JSX.Element { }) } /> + <ToggleRow + label="Tasks are due on the note's date" + description="A task written inside a daily note appears on the calendar for that day automatically — no need to type a due date. An explicit `due:YYYY-MM-DD` still wins." + value={vaultSettings.dailyNotes.tasksDueOnNoteDate !== false} + settingId="daily-notes-tasks-due-on-date" + onChange={(on) => + void persistVaultSettings({ + ...vaultSettings, + dailyNotes: { ...vaultSettings.dailyNotes, tasksDueOnNoteDate: on } + }) + } + /> + <ToggleRow + label="Roll over unfinished tasks to today" + description="When today's daily note opens, move every unchecked task from previous daily notes into it. Checked tasks stay where they are." + value={vaultSettings.dailyNotes.rolloverUnfinishedTasks === true} + settingId="daily-notes-rollover" + onChange={(on) => + void persistVaultSettings({ + ...vaultSettings, + dailyNotes: { ...vaultSettings.dailyNotes, rolloverUnfinishedTasks: on } + }) + } + /> <div className="flex items-center justify-between gap-4 px-5 py-4" {...settingsSearchTargetProps('open-todays-daily-note')} diff --git a/packages/app-core/src/components/Sidebar.tsx b/packages/app-core/src/components/Sidebar.tsx index 3d324ed9..97d8e69f 100644 --- a/packages/app-core/src/components/Sidebar.tsx +++ b/packages/app-core/src/components/Sidebar.tsx @@ -80,11 +80,11 @@ import { import { isExcalidrawPath } from "@shared/excalidraw"; import { FolderGlyphIcon, - resolveFolderIconId, + iconOptionById, resolveFolderIconOption, } from "./FolderIcons"; import { FolderIconPickerModal } from "./FolderIconPickerModal"; -import { resolveFolderColorId, resolveFolderColorGlyphClass } from "./FolderColors"; +import { colorGlyphClassById, resolveFolderColorGlyphClass } from "./FolderColors"; import { FolderColorPickerModal } from "./FolderColorPickerModal"; import { getSidebarEdgePrefetchPaths, @@ -177,10 +177,10 @@ function SidebarGlyph({ colorClass ? colorClass : active - ? "text-white" + ? "text-ink-900" : rowActive ? "text-accent" - : "text-ink-500 group-hover:text-ink-800", + : "text-ink-400 group-hover:text-ink-700", ].join(" ")} > {children} @@ -875,14 +875,16 @@ export function Sidebar(): JSX.Element { y: number; path: string; } | null>(null); - const [folderIconPicker, setFolderIconPicker] = useState<{ - folder: NoteFolder; - subpath: string; + // Icon/color customization targets — keyed by an arbitrary string so it works + // for folders (`folder:subpath`), notes/databases (vault-relative path), and + // anything else. Folder keys contain ':'; note paths never do, so they coexist + // in the same folderIcons/folderColors maps without colliding. + const [iconPicker, setIconPicker] = useState<{ + key: string; label: string; } | null>(null); - const [folderColorPicker, setFolderColorPicker] = useState<{ - folder: NoteFolder; - subpath: string; + const [colorPicker, setColorPicker] = useState<{ + key: string; label: string; } | null>(null); const [sortMenu, setSortMenu] = useState<{ x: number; y: number } | null>( @@ -919,34 +921,30 @@ export function Sidebar(): JSX.Element { ], ); - const openFolderIconPicker = useCallback( - (folder: NoteFolder, subpath: string, label: string) => { - setFolderMenu(null); - setFolderIconPicker({ folder, subpath, label }); - }, - [], - ); + // The context menu closes itself on select (ContextMenu calls onClose first), + // so these just open the picker. + const openIconPicker = useCallback((key: string, label: string) => { + setIconPicker({ key, label }); + }, []); - const saveFolderIcon = useCallback( - async (folder: NoteFolder, subpath: string, iconId: FolderIconId) => { - const key = folderIconKey(folder, subpath); + const saveIcon = useCallback( + async (key: string, iconId: FolderIconId) => { const nextSettings = normalizeVaultSettings({ ...vaultSettings, - folderIcons: { - ...vaultSettings.folderIcons, - [key]: iconId, - }, + folderIcons: { ...vaultSettings.folderIcons, [key]: iconId }, }); await setVaultSettings(nextSettings); - setFolderIconPicker(null); + setIconPicker(null); }, [setVaultSettings, vaultSettings], ); - const resetFolderIcon = useCallback( - async (folder: NoteFolder, subpath: string) => { - const key = folderIconKey(folder, subpath); - if (!(key in vaultSettings.folderIcons)) return; + const resetIcon = useCallback( + async (key: string) => { + if (!(key in vaultSettings.folderIcons)) { + setIconPicker(null); + return; + } const nextIcons = { ...vaultSettings.folderIcons }; delete nextIcons[key]; const nextSettings = normalizeVaultSettings({ @@ -954,39 +952,33 @@ export function Sidebar(): JSX.Element { folderIcons: nextIcons, }); await setVaultSettings(nextSettings); - setFolderIconPicker(null); + setIconPicker(null); }, [setVaultSettings, vaultSettings], ); - const openFolderColorPicker = useCallback( - (folder: NoteFolder, subpath: string, label: string) => { - setFolderMenu(null); - setFolderColorPicker({ folder, subpath, label }); - }, - [], - ); + const openColorPicker = useCallback((key: string, label: string) => { + setColorPicker({ key, label }); + }, []); - const saveFolderColor = useCallback( - async (folder: NoteFolder, subpath: string, colorId: FolderColorId) => { - const key = folderIconKey(folder, subpath); + const saveColor = useCallback( + async (key: string, colorId: FolderColorId) => { const nextSettings = normalizeVaultSettings({ ...vaultSettings, - folderColors: { - ...vaultSettings.folderColors, - [key]: colorId, - }, + folderColors: { ...vaultSettings.folderColors, [key]: colorId }, }); await setVaultSettings(nextSettings); - setFolderColorPicker(null); + setColorPicker(null); }, [setVaultSettings, vaultSettings], ); - const resetFolderColor = useCallback( - async (folder: NoteFolder, subpath: string) => { - const key = folderIconKey(folder, subpath); - if (!(key in vaultSettings.folderColors)) return; + const resetColor = useCallback( + async (key: string) => { + if (!(key in vaultSettings.folderColors)) { + setColorPicker(null); + return; + } const nextColors = { ...vaultSettings.folderColors }; delete nextColors[key]; const nextSettings = normalizeVaultSettings({ @@ -994,7 +986,7 @@ export function Sidebar(): JSX.Element { folderColors: nextColors, }); await setVaultSettings(nextSettings); - setFolderColorPicker(null); + setColorPicker(null); }, [setVaultSettings, vaultSettings], ); @@ -1669,39 +1661,18 @@ export function Sidebar(): JSX.Element { const label = isTop ? folderLabels[folder] : subpath.split("/").slice(-1)[0]; const trashCount = notes.filter((note) => note.folder === "trash").length; const iconKey = folderIconKey(folder, subpath); - const hasCustomIcon = Object.prototype.hasOwnProperty.call( - vaultSettings.folderIcons, - iconKey, - ); - const hasCustomColor = Object.prototype.hasOwnProperty.call( - vaultSettings.folderColors, - iconKey, - ); + // Reset lives inside each picker now (shown when a custom value is set). const iconItems: ContextMenuItem[] = [ { label: "Change icon…", onSelect: async () => { - openFolderIconPicker(folder, subpath, label); - }, - }, - { - label: "Reset icon", - disabled: !hasCustomIcon, - onSelect: async () => { - await resetFolderIcon(folder, subpath); + openIconPicker(iconKey, label); }, }, { label: "Change color…", onSelect: async () => { - openFolderColorPicker(folder, subpath, label); - }, - }, - { - label: "Reset color", - disabled: !hasCustomColor, - onSelect: async () => { - await resetFolderColor(folder, subpath); + openColorPicker(iconKey, label); }, }, ]; @@ -1964,10 +1935,8 @@ export function Sidebar(): JSX.Element { vaultSettings.folderIcons, vaultSettings, primaryNotesAtRoot, - openFolderIconPicker, - resetFolderIcon, - openFolderColorPicker, - resetFolderColor, + openIconPicker, + openColorPicker, toggleFavorite, bulkSelectionMenuItems, selectedSidebarKeys, @@ -2089,6 +2058,19 @@ export function Sidebar(): JSX.Element { await selectNote(meta.path); }, }); + items.push({ kind: "separator" }); + items.push({ + label: "Change icon…", + onSelect: async () => { + openIconPicker(n.path, n.title); + }, + }); + items.push({ + label: "Change color…", + onSelect: async () => { + openColorPicker(n.path, n.title); + }, + }); } items.push({ label: "Copy as Wikilink", @@ -2217,6 +2199,8 @@ export function Sidebar(): JSX.Element { tabsEnabled, openNoteInTab, toggleFavorite, + openIconPicker, + openColorPicker, vaultSettings.favorites, bulkSelectionMenuItems, selectedSidebarKeys, @@ -3183,10 +3167,10 @@ export function Sidebar(): JSX.Element { "rounded-full px-2.5 py-1 text-xs transition-colors", active ? isVimHighlight - ? "vim-cursor-on-active bg-accent text-white" + ? "vim-cursor-on-active bg-paper-300/70 text-ink-900 font-medium" : isSidebarFocused ? "text-accent" - : "bg-accent text-white" + : "bg-paper-300/70 text-ink-900 font-medium" : isVimHighlight ? "vim-cursor" : "bg-paper-200 text-ink-800 hover:bg-paper-300", @@ -3200,7 +3184,7 @@ export function Sidebar(): JSX.Element { className={[ "ml-1 text-2xs", active && !isSidebarFocused - ? "text-white/80" + ? "text-ink-700" : "text-ink-500", ].join(" ")} > @@ -3352,46 +3336,22 @@ export function Sidebar(): JSX.Element { onClose={() => setAssetMenu(null)} /> )} - {folderIconPicker && ( + {iconPicker && ( <FolderIconPickerModal - targetLabel={folderIconPicker.label} - currentIconId={resolveFolderIconId( - folderIconPicker.folder, - folderIconPicker.subpath, - vaultSettings.folderIcons, - )} - onSelect={(iconId) => - void saveFolderIcon( - folderIconPicker.folder, - folderIconPicker.subpath, - iconId, - ) - } - onCancel={() => setFolderIconPicker(null)} + targetLabel={iconPicker.label} + currentIconId={vaultSettings.folderIcons[iconPicker.key] ?? null} + onSelect={(iconId) => void saveIcon(iconPicker.key, iconId)} + onReset={() => void resetIcon(iconPicker.key)} + onCancel={() => setIconPicker(null)} /> )} - {folderColorPicker && ( + {colorPicker && ( <FolderColorPickerModal - targetLabel={folderColorPicker.label} - currentColorId={resolveFolderColorId( - folderColorPicker.folder, - folderColorPicker.subpath, - vaultSettings.folderColors, - )} - onSelect={(colorId) => - void saveFolderColor( - folderColorPicker.folder, - folderColorPicker.subpath, - colorId, - ) - } - onReset={() => - void resetFolderColor( - folderColorPicker.folder, - folderColorPicker.subpath, - ) - } - onCancel={() => setFolderColorPicker(null)} + targetLabel={colorPicker.label} + currentColorId={vaultSettings.folderColors[colorPicker.key] ?? null} + onSelect={(colorId) => void saveColor(colorPicker.key, colorId)} + onReset={() => void resetColor(colorPicker.key)} + onCancel={() => setColorPicker(null)} /> )} {sortMenu && ( @@ -4144,7 +4104,9 @@ function SubTree({ <div className="flex flex-col"> <TreeRow icon={ - isDatabase ? ( + // A database shows its DB glyph unless a custom icon was chosen for it + // (iconOption.id !== "folder" means the user picked one). + isDatabase && iconOption.id === "folder" ? ( <DatabaseIcon /> ) : iconOption.id === "folder" ? ( <FolderGlyphIcon open={!isCollapsed && hasChildren} /> @@ -4307,7 +4269,6 @@ interface NoteLeafProps { const NoteLeaf = memo(function NoteLeaf({ note, depth, - showSidebarChevrons, active, selected, sidebarFocused, @@ -4345,6 +4306,12 @@ const NoteLeaf = memo(function NoteLeaf({ }, [dragPayloadForItem, note.path], ); + // Custom icon / color set via the note's right-click menu (keyed by path). + // Read directly from the store so the row updates when they change — the + // selector returns a primitive, so it only re-renders for this note. + const customIconId = useStore((s) => s.vaultSettings.folderIcons[note.path]); + const customColorId = useStore((s) => s.vaultSettings.folderColors[note.path]); + const colorClass = colorGlyphClassById(customColorId); return ( <button @@ -4354,13 +4321,15 @@ const NoteLeaf = memo(function NoteLeaf({ draggable onDragStart={handleDragStart} className={[ - "group flex h-8 w-full items-center gap-1.5 rounded-lg px-1 text-left text-sm outline-none transition-colors focus:outline-none", + "group flex h-9 w-full items-center gap-1.5 rounded-lg px-1 text-left text-sm outline-none transition-colors focus:outline-none", active - ? vimHighlight - ? "vim-cursor-on-active bg-accent text-white" - : sidebarFocused - ? "text-accent" - : "bg-accent text-white" + ? colorClass + ? `bg-accent/20 ring-1 ring-inset ring-accent/60${vimHighlight ? " vim-cursor-on-active" : ""}` + : vimHighlight + ? "vim-cursor-on-active bg-paper-300/70 text-ink-900 font-medium" + : sidebarFocused + ? "text-accent" + : "bg-paper-300/70 text-ink-900 font-medium" : selected ? "bg-accent/[0.09] text-ink-900" : vimHighlight @@ -4377,9 +4346,14 @@ const NoteLeaf = memo(function NoteLeaf({ } : {})} > - {showSidebarChevrons && <span className="h-5 w-5 shrink-0" />} - <SidebarGlyph active={strongActive} rowActive={active || selected}> - {isExcalidrawPath(note.path) ? ( + <SidebarGlyph + active={strongActive} + rowActive={active || selected} + colorClass={colorClass ?? undefined} + > + {customIconId ? ( + iconOptionById(customIconId).icon + ) : isExcalidrawPath(note.path) ? ( <ExcalidrawIcon width={14} height={14} /> ) : ( <svg @@ -4397,7 +4371,9 @@ const NoteLeaf = memo(function NoteLeaf({ </svg> )} </SidebarGlyph> - <span className="flex-1 truncate">{note.title}</span> + <span className={["flex-1 truncate", colorClass].filter(Boolean).join(" ")}> + {note.title} + </span> {note.isSymlink && ( <span aria-label="Symlinked note" @@ -4407,7 +4383,7 @@ const NoteLeaf = memo(function NoteLeaf({ active ? sidebarFocused && !vimHighlight ? "text-accent/70" - : "text-white/70" + : "text-ink-600" : selected ? "text-accent/75" : "text-ink-500", @@ -4437,7 +4413,7 @@ const NoteLeaf = memo(function NoteLeaf({ active ? sidebarFocused && !vimHighlight ? "text-accent/70" - : "text-white/70" + : "text-ink-600" : selected ? "text-accent/75" : "text-ink-500", @@ -4488,7 +4464,6 @@ function AssetLeaf({ asset, vaultRoot, depth, - showSidebarChevrons, onOpen, onContextMenu, sidebarFocused, @@ -4524,7 +4499,7 @@ function AssetLeaf({ draggable onDragStart={handleDragStart} className={[ - "group flex h-8 w-full items-center gap-1.5 rounded-lg px-1 text-left text-sm outline-none transition-colors focus:outline-none", + "group flex h-9 w-full items-center gap-1.5 rounded-lg px-1 text-left text-sm outline-none transition-colors focus:outline-none", vimHighlight ? "vim-cursor" : "text-ink-700 hover:bg-paper-200/70", ].join(" ")} style={{ paddingLeft: 4 + depth * 14 }} @@ -4536,7 +4511,6 @@ function AssetLeaf({ } : {})} > - {showSidebarChevrons && <span className="h-5 w-5 shrink-0" />} <SidebarGlyph active={false} rowActive={false}> <svg width="14" @@ -4594,7 +4568,6 @@ function TreeRow({ selectionKey, trailing, isSymlink = false, - reserveLeadingSlot = true, showExpandChevron = true, glyphColorClass, }: { @@ -4624,9 +4597,10 @@ function TreeRow({ trailing?: JSX.Element; /** Show a symlink indicator when this row's directory entry is a link. */ isSymlink?: boolean; - /** Keep a blank chevron column for non-expandable rows when alignment matters. */ + /** Obsolete (no leading chevron gutter anymore); accepted for compatibility. */ reserveLeadingSlot?: boolean; - /** Hide the chevron affordance while keeping row-click toggle behavior. */ + /** Reveal the hover disclosure chevron on the folder icon. When false the + * folder shows only its icon and toggles via row-click. */ showExpandChevron?: boolean; /** Custom resting tint for the leading glyph (folder color). */ glyphColorClass?: string; @@ -4651,7 +4625,7 @@ function TreeRow({ onDragLeave={onDragLeave} onDrop={onDrop} className={[ - "group flex h-8 w-full items-center gap-1.5 rounded-lg px-1 text-left text-sm outline-none transition-colors focus:outline-none", + "group flex h-9 w-full items-center gap-1.5 rounded-lg px-1 text-left text-sm outline-none transition-colors focus:outline-none", active ? glyphColorClass ? // Colored folder: a saturated accent fill would put same-hue text on @@ -4659,10 +4633,10 @@ function TreeRow({ // stays readable while still reading as the active row. `bg-accent/20 ring-1 ring-inset ring-accent/60${vimHighlight ? " vim-cursor-on-active" : ""}` : vimHighlight - ? "vim-cursor-on-active bg-accent text-white" + ? "vim-cursor-on-active bg-paper-300/70 text-ink-900 font-medium" : sidebarFocused ? "text-accent" - : "bg-accent text-white" + : "bg-paper-300/70 text-ink-900 font-medium" : dropTarget ? "bg-accent/20 text-ink-900 ring-1 ring-accent/60" : selected @@ -4686,44 +4660,56 @@ function TreeRow({ style={{ paddingLeft: 4 + depth * 14 }} > {expandable && showExpandChevron ? ( - <button - onClick={(e) => { - e.stopPropagation(); - onToggle(); - }} + // Notion-style disclosure: the folder icon turns into a chevron while the + // row is hovered — click it to expand/collapse. No separate chevron gutter. + <span + className="relative flex h-5 w-5 shrink-0 items-center justify-center" data-vim-hint-ignore - className={[ - "flex h-5 w-5 shrink-0 items-center justify-center rounded transition-colors", - strongActive - ? "text-white/80 hover:bg-white/15" - : "text-ink-500 hover:bg-paper-300/60", - ].join(" ")} - aria-label={collapsed ? "Expand" : "Collapse"} > - <svg - width="10" - height="10" - viewBox="0 0 24 24" - fill="none" - stroke="currentColor" - strokeWidth="3" - strokeLinecap="round" - strokeLinejoin="round" - className={`transition-transform ${collapsed ? "" : "rotate-90"}`} + <span className="flex h-5 w-5 items-center justify-center group-hover:hidden"> + <SidebarGlyph + active={strongActive} + rowActive={active || selected} + colorClass={glyphColorClass} + > + {icon} + </SidebarGlyph> + </span> + <button + onClick={(e) => { + e.stopPropagation(); + onToggle(); + }} + aria-label={collapsed ? "Expand" : "Collapse"} + className={[ + "absolute inset-0 hidden items-center justify-center rounded transition-colors group-hover:flex", + strongActive ? "text-ink-900" : "text-ink-500 hover:text-ink-900", + ].join(" ")} > - <path d="m9 6 6 6-6 6" /> - </svg> - </button> - ) : reserveLeadingSlot ? ( - <span className="h-5 w-5 shrink-0" /> - ) : null} - <SidebarGlyph - active={strongActive} - rowActive={active || selected} - colorClass={glyphColorClass} - > - {icon} - </SidebarGlyph> + <svg + width="11" + height="11" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + strokeWidth="3" + strokeLinecap="round" + strokeLinejoin="round" + className={`transition-transform ${collapsed ? "" : "rotate-90"}`} + > + <path d="m9 6 6 6-6 6" /> + </svg> + </button> + </span> + ) : ( + <SidebarGlyph + active={strongActive} + rowActive={active || selected} + colorClass={glyphColorClass} + > + {icon} + </SidebarGlyph> + )} <span className={["flex-1 truncate", glyphColorClass].filter(Boolean).join(" ")} > @@ -4735,7 +4721,7 @@ function TreeRow({ title="Symlinked into this vault" className={[ "shrink-0", - strongActive ? "text-white/70" : selected ? "text-accent/75" : "text-ink-500", + strongActive ? "text-ink-600" : selected ? "text-accent/75" : "text-ink-500", ].join(" ")} > <svg @@ -4765,7 +4751,7 @@ function TreeRow({ <span className={[ "shrink-0 pr-2 text-xs", - strongActive ? "text-white/80" : selected ? "text-accent/75" : "text-ink-500", + strongActive ? "text-ink-700" : selected ? "text-accent/75" : "text-ink-500", ].join(" ")} > {count} @@ -4803,13 +4789,13 @@ function TaskSidebarRow({ } }} className={[ - "group flex h-8 items-center gap-1.5 rounded-lg px-1 text-left text-sm outline-none transition-colors focus:outline-none", + "group flex h-9 items-center gap-1.5 rounded-lg px-1 text-left text-sm outline-none transition-colors focus:outline-none", active ? vimHighlight - ? "vim-cursor-on-active bg-accent text-white" + ? "vim-cursor-on-active bg-paper-300/70 text-ink-900 font-medium" : sidebarFocused ? "text-accent" - : "bg-accent text-white" + : "bg-paper-300/70 text-ink-900 font-medium" : vimHighlight ? "vim-cursor" : "text-ink-800 hover:bg-paper-200/70", @@ -4868,13 +4854,13 @@ function FavoriteRow({ }} onContextMenu={onContextMenu} className={[ - "group select-none flex h-8 items-center gap-1.5 rounded-lg px-1 text-left text-sm outline-none transition-colors focus:outline-none", + "group select-none flex h-9 items-center gap-1.5 rounded-lg px-1 text-left text-sm outline-none transition-colors focus:outline-none", active ? vimHighlight - ? "vim-cursor-on-active bg-accent text-white" + ? "vim-cursor-on-active bg-paper-300/70 text-ink-900 font-medium" : sidebarFocused ? "text-accent" - : "bg-accent text-white" + : "bg-paper-300/70 text-ink-900 font-medium" : vimHighlight ? "vim-cursor" : "text-ink-800 hover:bg-paper-200/70", @@ -4935,13 +4921,13 @@ function SystemRow({ }} onContextMenu={onContextMenu} className={[ - "group select-none flex h-8 items-center gap-1.5 rounded-lg px-1 text-left text-sm outline-none transition-colors focus:outline-none", + "group select-none flex h-9 items-center gap-1.5 rounded-lg px-1 text-left text-sm outline-none transition-colors focus:outline-none", active ? vimHighlight - ? "vim-cursor-on-active bg-accent text-white" + ? "vim-cursor-on-active bg-paper-300/70 text-ink-900 font-medium" : sidebarFocused ? "text-accent" - : "bg-accent text-white" + : "bg-paper-300/70 text-ink-900 font-medium" : vimHighlight ? "vim-cursor" : "text-ink-800 hover:bg-paper-200/70", @@ -4962,7 +4948,7 @@ function SystemRow({ <span className={[ "shrink-0 pr-2 text-xs", - strongActive ? "text-white/80" : "text-ink-500", + strongActive ? "text-ink-700" : "text-ink-500", ].join(" ")} > {count} @@ -5001,13 +4987,13 @@ function SidebarRow({ <button onClick={onClick} className={[ - "group flex h-8 items-center gap-2 rounded-lg px-2 text-sm outline-none transition-colors focus:outline-none", + "group flex h-9 items-center gap-2 rounded-lg px-2 text-sm outline-none transition-colors focus:outline-none", active ? vimHighlight - ? "vim-cursor-on-active bg-accent text-white" + ? "vim-cursor-on-active bg-paper-300/70 text-ink-900 font-medium" : sidebarFocused ? "text-accent" - : "bg-accent text-white" + : "bg-paper-300/70 text-ink-900 font-medium" : vimHighlight ? "vim-cursor" : "text-ink-800 hover:bg-paper-200/70", @@ -5021,7 +5007,7 @@ function SidebarRow({ > <span className={ - strongActive ? "text-white" : "text-ink-500 group-hover:text-ink-800" + strongActive ? "text-ink-900" : "text-ink-500 group-hover:text-ink-800" } > {icon} @@ -5038,7 +5024,7 @@ function SidebarRow({ <span className={[ "text-xs", - strongActive ? "text-white/80" : "text-ink-500", + strongActive ? "text-ink-700" : "text-ink-500", ].join(" ")} > {count} @@ -5089,10 +5075,10 @@ function SidebarFooterAction({ "inline-flex h-8 items-center gap-1.5 rounded-lg px-2.5 text-xs font-medium leading-none transition-colors whitespace-nowrap", active ? vimHighlight - ? "vim-cursor-on-active bg-accent text-white" + ? "vim-cursor-on-active bg-paper-300/70 text-ink-900 font-medium" : sidebarFocused ? "text-accent" - : "bg-accent text-white" + : "bg-paper-300/70 text-ink-900 font-medium" : vimHighlight ? "vim-cursor" : "text-ink-500 hover:bg-paper-200/70 hover:text-ink-900", @@ -5105,7 +5091,7 @@ function SidebarFooterAction({ : {})} > <span - className={["shrink-0", strongActive ? "text-white" : ""].join(" ")} + className={["shrink-0", strongActive ? "text-ink-900" : ""].join(" ")} > {icon} </span> @@ -5115,7 +5101,7 @@ function SidebarFooterAction({ className={[ "rounded-full px-1.5 py-0.5 text-2xs", strongActive - ? "bg-white/12 text-white/80" + ? "bg-ink-900/10 text-ink-700" : "bg-paper-200/80 text-ink-500", ].join(" ")} > @@ -5127,7 +5113,7 @@ function SidebarFooterAction({ className={[ "rounded-full px-1.5 py-0.5 text-2xs font-semibold", strongActive - ? "bg-white/16 text-white" + ? "bg-accent/20 text-accent" : "bg-accent/12 text-accent", ].join(" ")} > @@ -5404,7 +5390,7 @@ function RowKeyHint({ className={[ "pointer-events-none shrink-0 rounded-md border px-1.5 py-0.5 text-2xs leading-none", active - ? "border-white/25 bg-white/12 text-white/80" + ? "border-ink-900/20 bg-ink-900/10 text-ink-700" : "border-paper-300/70 bg-paper-100/75 text-ink-500", ].join(" ")} > diff --git a/packages/app-core/src/components/TasksCalendar.tsx b/packages/app-core/src/components/TasksCalendar.tsx index b379f423..968ab9b3 100644 --- a/packages/app-core/src/components/TasksCalendar.tsx +++ b/packages/app-core/src/components/TasksCalendar.tsx @@ -23,12 +23,22 @@ import { import { useStore } from '../store' import { ChevronLeftIcon, ChevronRightIcon } from './icons' import { InlineMarkdown } from '../lib/inline-markdown' +import { ContextMenu, type ContextMenuItem } from './ContextMenu' interface Props { tasks: VaultTask[] today: Date onOpenTask: (task: VaultTask) => void onToggleTask: (task: VaultTask) => void + /** Set a task's due date (keyboard reschedule + the drop "set due" choice). */ + onRescheduleTask: (task: VaultTask, dueIso: string) => void + /** Physically move a task into the daily note for the given date. */ + onMoveTask: (task: VaultTask, dateIso: string) => void + /** Add a `- [ ] …` task to the daily note for the given date. */ + onAddTask: (dateIso: string, text: string) => void | Promise<void> + /** Whether daily notes are enabled — gates the quick-add box, which writes + * into the selected day's daily note. */ + dailyNotesEnabled: boolean } /** Sunday-first; matches how most US/Western calendars read. The month @@ -55,6 +65,14 @@ function parseIsoLocal(iso: string): Date { return new Date(y, m - 1, dd) } +const TASK_PREFIX_RE = /^\s*(?:>\s*)*(?:[-+*]|\d+[.)])\s+\[[ xX]\]\s?/ + +/** Text after the checkbox (content + any tokens) — prefill for inline edit. */ +function taskTail(task: VaultTask): string { + const m = task.rawText.match(TASK_PREFIX_RE) + return m ? task.rawText.slice(m[0].length) : task.content +} + function buildMonthGrid(anchor: Date): Date[] { const first = firstOfMonth(anchor) // Walk back to the most recent Sunday (could be in the previous month). @@ -66,12 +84,53 @@ function formatMonthLabel(d: Date): string { return d.toLocaleDateString(undefined, { month: 'long', year: 'numeric' }) } -export function TasksCalendar({ tasks, today, onOpenTask, onToggleTask }: Props): JSX.Element { +export function TasksCalendar({ + tasks, + today, + onOpenTask, + onToggleTask, + onRescheduleTask, + onMoveTask, + onAddTask, + dailyNotesEnabled +}: Props): JSX.Element { const monthAnchorIso = useStore((s) => s.tasksCalendarMonthAnchor) const setMonthAnchor = useStore((s) => s.setTasksCalendarMonthAnchor) const selectedDateIso = useStore((s) => s.tasksCalendarSelectedDate) const setSelectedDate = useStore((s) => s.setTasksCalendarSelectedDate) + const applyTaskMutation = useStore((s) => s.applyTaskMutation) + const deleteTaskFromList = useStore((s) => s.deleteTaskFromList) const rootRef = useRef<HTMLDivElement>(null) + // Pointer-based drag-to-reschedule. Native HTML5 DnD is flaky for these rows + // inside the pane, so (like the Kanban) we grab on pointerdown, float a ghost + // while moving, detect the day under the cursor via a `data-cal-day` attribute, + // and offer the move / set-due choice on release. + const pointerDragRef = useRef<{ + task: VaultTask + startX: number + startY: number + dragging: boolean + } | null>(null) + const suppressClickRef = useRef(false) + const [ghost, setGhost] = useState<{ x: number; y: number; label: string } | null>(null) + const [dragOverIso, setDragOverIso] = useState<string | null>(null) + // Shared context menu — the drop "move vs set due" choice and the right-click + // task actions both feed it. + const [menu, setMenu] = useState<{ x: number; y: number; items: ContextMenuItem[] } | null>(null) + // Inline task editing (right-click → Edit). + const [editingTaskId, setEditingTaskId] = useState<string | null>(null) + const [editValue, setEditValue] = useState('') + const cancelEditRef = useRef(false) + // Keyboard reschedule: which task in the selected day's list is "active". + const [activeTaskIndex, setActiveTaskIndex] = useState(0) + // Keyboard "grab & place": the task picked up with `m`. While set, grid + // navigation chooses a target day; Enter places it (move/set-due choice). + const [grabbedTask, setGrabbedTask] = useState<VaultTask | null>(null) + const dPending = useRef(false) + const dTimer = useRef<ReturnType<typeof setTimeout>>() + const addInputRef = useRef<HTMLInputElement>(null) + // Quick-add box for the selected day. + const [addValue, setAddValue] = useState('') const todayIso = useMemo(() => toIsoDateLocal(today), [today]) @@ -98,6 +157,164 @@ export function TasksCalendar({ tasks, today, onOpenTask, onToggleTask }: Props) const unscheduled = buckets.get('unscheduled') ?? [] const selectedIso = toIsoDateLocal(selectedDate) const selectedTasks = buckets.get(selectedIso) ?? [] + const activeIdx = Math.min(activeTaskIndex, Math.max(0, selectedTasks.length - 1)) + const addLabel = selectedDate.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + + const addDaysIso = (n: number): string => + toIsoDateLocal(new Date(today.getFullYear(), today.getMonth(), today.getDate() + n)) + + const startPointerDrag = (task: VaultTask, e: React.PointerEvent): void => { + if (e.button !== 0) return + suppressClickRef.current = false + pointerDragRef.current = { task, startX: e.clientX, startY: e.clientY, dragging: false } + } + + // Keep the latest reschedule/move callbacks in a ref so the window listeners + // can mount once (the parent passes fresh arrows each render). + const dropCbRef = useRef({ onMoveTask, onRescheduleTask }) + dropCbRef.current = { onMoveTask, onRescheduleTask } + + useEffect(() => { + const dayUnder = (x: number, y: number): string | null => { + const el = document.elementFromPoint(x, y) + const dayEl = el?.closest('[data-cal-day]') as HTMLElement | null + return dayEl?.dataset.calDay ?? null + } + const onMove = (e: PointerEvent): void => { + const drag = pointerDragRef.current + if (!drag) return + if (!drag.dragging) { + if (Math.hypot(e.clientX - drag.startX, e.clientY - drag.startY) < 5) return + drag.dragging = true + } + setGhost({ x: e.clientX, y: e.clientY, label: drag.task.content || 'task' }) + setDragOverIso(dayUnder(e.clientX, e.clientY)) + } + const onUp = (e: PointerEvent): void => { + const drag = pointerDragRef.current + if (!drag) return + pointerDragRef.current = null + setGhost(null) + setDragOverIso(null) + if (!drag.dragging) return + // It was a drag, not a click — keep the row's onClick from firing. + suppressClickRef.current = true + const iso = dayUnder(e.clientX, e.clientY) + if (iso) { + const { onMoveTask: move, onRescheduleTask: resched } = dropCbRef.current + setMenu({ + x: e.clientX, + y: e.clientY, + items: dropChoiceItems(drag.task, iso, move, resched) + }) + } + } + window.addEventListener('pointermove', onMove) + window.addEventListener('pointerup', onUp) + return () => { + window.removeEventListener('pointermove', onMove) + window.removeEventListener('pointerup', onUp) + } + }, []) + + // Right-click a task: reschedule presets, move into a day's note, inline edit, + // delete. (Drag still offers the move/set-due choice.) + const openTaskMenu = (e: React.MouseEvent, task: VaultTask): void => { + e.preventDefault() + e.stopPropagation() + const reschedule = (iso: string | null): void => + void applyTaskMutation(task, { kind: 'set-due', due: iso }) + const items: ContextMenuItem[] = [ + { label: 'Due today', hint: addDaysIso(0), onSelect: () => reschedule(addDaysIso(0)) }, + { label: 'Due tomorrow', hint: addDaysIso(1), onSelect: () => reschedule(addDaysIso(1)) }, + { label: 'Due next week', hint: addDaysIso(7), onSelect: () => reschedule(addDaysIso(7)) } + ] + if (task.due && !task.dueInferred) { + items.push({ label: 'Clear due date', onSelect: () => reschedule(null) }) + } + items.push( + { kind: 'separator' }, + { + label: 'Move to its day’s note', + disabled: !task.due, + onSelect: () => { + if (task.due) onMoveTask(task, task.due) + } + }, + { + label: 'Edit', + onSelect: () => { + setEditingTaskId(task.id) + setEditValue(taskTail(task)) + } + }, + { kind: 'separator' }, + { label: 'Delete', danger: true, onSelect: () => void deleteTaskFromList(task) } + ) + setMenu({ x: e.clientX, y: e.clientY, items }) + } + + const renderTask = ( + task: VaultTask, + extra: { isActive?: boolean; buttonRef?: React.RefObject<HTMLDivElement> | null } + ): JSX.Element => + editingTaskId === task.id ? ( + <div key={task.id} className="flex items-center gap-2 rounded-md px-2 py-1"> + <span className="h-4 w-4 shrink-0 rounded border border-paper-400/70" /> + <input + autoFocus + value={editValue} + onChange={(e) => setEditValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + e.currentTarget.blur() + } else if (e.key === 'Escape') { + e.stopPropagation() + cancelEditRef.current = true + e.currentTarget.blur() + } + }} + onBlur={() => { + if (cancelEditRef.current) { + cancelEditRef.current = false + } else { + const text = editValue.trim() + if (text && text !== taskTail(task)) { + void applyTaskMutation(task, { kind: 'set-text', text }) + } + } + setEditingTaskId(null) + }} + className="min-w-0 flex-1 rounded border border-accent/60 bg-paper-100 px-1 py-0.5 text-sm outline-none" + spellCheck={false} + autoComplete="off" + /> + </div> + ) : ( + <CalendarTaskRow + key={task.id} + task={task} + isOverdue={isTaskOverdue(task, today)} + isActive={extra.isActive} + buttonRef={extra.buttonRef ?? null} + onToggle={() => onToggleTask(task)} + onOpen={() => { + if (suppressClickRef.current) { + suppressClickRef.current = false + return + } + onOpenTask(task) + }} + onContextMenu={(e) => openTaskMenu(e, task)} + onPointerDownTask={(e) => startPointerDrag(task, e)} + /> + ) + + // Reset the keyboard "active task" cursor whenever the selected day changes. + useEffect(() => { + setActiveTaskIndex(0) + }, [selectedIso]) // gt sequence (vim "go to today") const gPending = useRef(0) @@ -139,13 +356,76 @@ export function TasksCalendar({ tasks, today, onOpenTask, onToggleTask }: Props) const tag = active.tagName if (tag === 'INPUT' || tag === 'TEXTAREA' || active.isContentEditable) return } - if (e.metaKey || e.ctrlKey || e.altKey) return const consume = (): void => { e.preventDefault() e.stopImmediatePropagation() } + if (e.metaKey || e.ctrlKey || e.altKey) return + + // Grab & place: while a task is picked up, the grid navigation chooses a + // target day; Enter places it (move / set-due choice), Esc cancels. + if (grabbedTask) { + if (e.key === 'Escape') { + consume() + setGrabbedTask(null) + return + } + if (e.key === 'Enter') { + consume() + const cell = document.querySelector(`[data-cal-day="${selectedIso}"]`) + const rect = cell?.getBoundingClientRect() + setMenu({ + x: rect ? rect.left + rect.width / 2 : window.innerWidth / 2, + y: rect ? rect.bottom : window.innerHeight / 2, + items: dropChoiceItems(grabbedTask, selectedIso, onMoveTask, onRescheduleTask) + }) + setGrabbedTask(null) + return + } + switch (e.key) { + case 'h': + case 'ArrowLeft': + consume() + moveSelection(-1) + return + case 'l': + case 'ArrowRight': + consume() + moveSelection(1) + return + case 'j': + case 'ArrowDown': + consume() + moveSelection(7) + return + case 'k': + case 'ArrowUp': + consume() + moveSelection(-7) + return + case '[': + consume() + goToMonth(-1) + return + case ']': + consume() + goToMonth(1) + return + default: + consume() + return + } + } + + // `a` focuses the quick-add box for the selected day. + if (e.key === 'a' && dailyNotesEnabled) { + consume() + requestAnimationFrame(() => addInputRef.current?.focus()) + return + } + // Two-key `gt` — go to today. if (e.key === 't' && gPending.current > 0) { consume() @@ -182,6 +462,60 @@ export function TasksCalendar({ tasks, today, onOpenTask, onToggleTask }: Props) return } + // Task-list actions on the active task (keyboard analogs of the row's + // drag / right-click menu). + if (selectedTasks.length > 0) { + if (e.key === 'Tab') { + consume() + const n = selectedTasks.length + const dir = e.shiftKey ? -1 : 1 + setActiveTaskIndex((i) => (((Math.min(i, n - 1) + dir) % n) + n) % n) + return + } + const active = selectedTasks[Math.min(activeTaskIndex, selectedTasks.length - 1)] + if (active) { + if (e.key === '>' || e.key === '<') { + consume() + onRescheduleTask(active, toIsoDateLocal(addDays(selectedDate, e.key === '>' ? 1 : -1))) + return + } + if (e.key === 'T') { + consume() + onRescheduleTask(active, todayIso) + return + } + if (e.key === 'x' || e.key === ' ') { + consume() + onToggleTask(active) + return + } + if (e.key === 'e') { + consume() + setEditingTaskId(active.id) + setEditValue(taskTail(active)) + return + } + if (e.key === 'm') { + consume() + setGrabbedTask(active) + return + } + if (e.key === 'd') { + consume() + if (dPending.current) { + dPending.current = false + if (dTimer.current) clearTimeout(dTimer.current) + void deleteTaskFromList(active) + } else { + dPending.current = true + if (dTimer.current) clearTimeout(dTimer.current) + dTimer.current = setTimeout(() => (dPending.current = false), 600) + } + return + } + } + } + switch (e.key) { case 'h': case 'ArrowLeft': @@ -213,7 +547,9 @@ export function TasksCalendar({ tasks, today, onOpenTask, onToggleTask }: Props) return case 'Enter': consume() - if (selectedTasks.length > 0) onOpenTask(selectedTasks[0]) + if (selectedTasks.length > 0) { + onOpenTask(selectedTasks[Math.min(activeTaskIndex, selectedTasks.length - 1)]) + } return default: return @@ -223,12 +559,27 @@ export function TasksCalendar({ tasks, today, onOpenTask, onToggleTask }: Props) return () => window.removeEventListener('keydown', handler, true) // We deliberately re-bind on every relevant change so the closure // sees the latest selection / month / cells. - }, [cells, monthAnchor, selectedDate, selectedTasks, onOpenTask]) + }, [ + cells, + monthAnchor, + selectedDate, + selectedIso, + selectedTasks, + activeTaskIndex, + grabbedTask, + todayIso, + dailyNotesEnabled, + onOpenTask, + onToggleTask, + onRescheduleTask, + onMoveTask, + deleteTaskFromList + ]) - const focusedTaskRef = useRef<HTMLButtonElement | null>(null) + const focusedTaskRef = useRef<HTMLDivElement | null>(null) useEffect(() => { focusedTaskRef.current?.scrollIntoView({ block: 'nearest' }) - }, [selectedIso]) + }, [selectedIso, activeIdx]) return ( <div ref={rootRef} className="flex min-h-0 flex-1 flex-col"> @@ -263,7 +614,9 @@ export function TasksCalendar({ tasks, today, onOpenTask, onToggleTask }: Props) {formatMonthLabel(monthAnchor)} </div> <div className="text-xs text-current/40"> - h/j/k/l move · [ ] month · gt today · Enter open + {grabbedTask + ? `Moving “${grabbedTask.content || 'task'}” — h/j/k/l pick a day · Enter place · Esc cancel` + : 'h/j/k/l day · Tab pick · x toggle · e edit · dd del · m move · < > / T due · a add'} </div> </div> @@ -282,11 +635,13 @@ export function TasksCalendar({ tasks, today, onOpenTask, onToggleTask }: Props) const isOtherMonth = cell.getMonth() !== monthAnchor.getMonth() const isToday = cellIso === todayIso const isSelected = cellIso === selectedIso + const isDragOver = dragOverIso === cellIso const overdueCount = cellTasks.filter((t) => isTaskOverdue(t, today)).length return ( <button type="button" key={cellIso} + data-cal-day={cellIso} onClick={() => { setSelectedDate(cellIso) if (isOtherMonth) setMonthAnchor(toIsoDateLocal(firstOfMonth(cell))) @@ -294,11 +649,13 @@ export function TasksCalendar({ tasks, today, onOpenTask, onToggleTask }: Props) className={[ 'flex h-16 flex-col items-stretch gap-1 px-1.5 py-1 text-left text-xs transition-colors', isOtherMonth ? 'bg-paper-100/40 text-current/35' : 'bg-paper-100/85 text-current/80', - isSelected - ? 'ring-2 ring-inset ring-accent/60' - : isToday - ? 'ring-1 ring-inset ring-accent/40' - : 'hover:bg-paper-200/60' + isDragOver + ? 'bg-accent/10 ring-2 ring-inset ring-accent/80' + : isSelected + ? 'ring-2 ring-inset ring-accent/60' + : isToday + ? 'ring-1 ring-inset ring-accent/40' + : 'hover:bg-paper-200/60' ].join(' ')} > <div className="flex items-center justify-between"> @@ -355,24 +712,63 @@ export function TasksCalendar({ tasks, today, onOpenTask, onToggleTask }: Props) {selectedTasks.length} task{selectedTasks.length === 1 ? '' : 's'} </span> </div> + {dailyNotesEnabled && ( + <form + className="mb-2 flex items-center gap-2" + onSubmit={(e) => { + e.preventDefault() + const value = addValue.trim() + if (!value) return + void onAddTask(selectedIso, value) + setAddValue('') + }} + > + <input + ref={addInputRef} + type="text" + value={addValue} + onChange={(e) => setAddValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Escape') { + e.stopPropagation() + if (addValue) setAddValue('') + else e.currentTarget.blur() + } + }} + placeholder={`Add a task for ${selectedIso === todayIso ? 'today' : addLabel}…`} + className="min-w-0 flex-1 rounded-md border border-paper-300/60 bg-paper-200/50 px-2 py-1 text-sm outline-none placeholder:text-current/40 focus:border-accent/60" + spellCheck={false} + autoComplete="off" + /> + <button + type="submit" + disabled={!addValue.trim()} + className="shrink-0 rounded-md bg-accent/90 px-2.5 py-1 text-xs font-medium text-white transition-opacity hover:bg-accent disabled:opacity-40" + > + Add + </button> + </form> + )} {selectedTasks.length === 0 ? ( <div className="rounded-md border border-dashed border-paper-300/60 px-3 py-4 text-center text-xs text-current/50"> - Nothing scheduled. Add{' '} - <code className="rounded bg-paper-300/60 px-1">due:{selectedIso}</code> to a task to see - it here. + {dailyNotesEnabled ? ( + 'Nothing scheduled for this day yet.' + ) : ( + <> + Nothing scheduled. Add{' '} + <code className="rounded bg-paper-300/60 px-1">due:{selectedIso}</code> to a task to + see it here. + </> + )} </div> ) : ( <div className="space-y-1"> - {selectedTasks.map((task, i) => ( - <CalendarTaskRow - key={task.id} - task={task} - isOverdue={isTaskOverdue(task, today)} - buttonRef={i === 0 ? focusedTaskRef : null} - onToggle={() => onToggleTask(task)} - onOpen={() => onOpenTask(task)} - /> - ))} + {selectedTasks.map((task, i) => + renderTask(task, { + isActive: i === activeIdx, + buttonRef: i === activeIdx ? focusedTaskRef : null + }) + )} </div> )} @@ -382,41 +778,88 @@ export function TasksCalendar({ tasks, today, onOpenTask, onToggleTask }: Props) {unscheduled.length} task{unscheduled.length === 1 ? '' : 's'} without a due date </summary> <div className="mt-2 space-y-1"> - {unscheduled.map((task) => ( - <CalendarTaskRow - key={task.id} - task={task} - isOverdue={false} - onToggle={() => onToggleTask(task)} - onOpen={() => onOpenTask(task)} - /> - ))} + {unscheduled.map((task) => renderTask(task, {}))} </div> </details> )} </div> + + {ghost && ( + <div + className="pointer-events-none fixed z-[70] max-w-[220px] truncate rounded-md border border-accent/50 bg-paper-100 px-2 py-1 text-sm text-ink-900 shadow-float" + style={{ left: ghost.x + 12, top: ghost.y + 12 }} + > + {ghost.label} + </div> + )} + + {menu && ( + <ContextMenu x={menu.x} y={menu.y} items={menu.items} onClose={() => setMenu(null)} /> + )} </div> ) } +/** "Move into that day's note" vs "just set the due date" — the menu shown after + * dropping a task on a calendar day. */ +function dropChoiceItems( + task: VaultTask, + dateIso: string, + onMoveTask: (task: VaultTask, dateIso: string) => void, + onRescheduleTask: (task: VaultTask, dueIso: string) => void +): ContextMenuItem[] { + const label = parseIsoLocal(dateIso).toLocaleDateString(undefined, { + month: 'short', + day: 'numeric' + }) + return [ + { label: `Move into ${label}'s note`, onSelect: () => onMoveTask(task, dateIso) }, + { label: `Just set due: ${label}`, onSelect: () => onRescheduleTask(task, dateIso) } + ] +} + interface RowProps { task: VaultTask isOverdue: boolean - buttonRef?: React.RefObject<HTMLButtonElement> | null + isActive?: boolean + buttonRef?: React.RefObject<HTMLDivElement> | null onToggle: () => void onOpen: () => void + onContextMenu?: (e: React.MouseEvent) => void + onPointerDownTask?: (e: React.PointerEvent) => void } -function CalendarTaskRow({ task, isOverdue, buttonRef, onToggle, onOpen }: RowProps): JSX.Element { +function CalendarTaskRow({ + task, + isOverdue, + isActive, + buttonRef, + onToggle, + onOpen, + onContextMenu, + onPointerDownTask +}: RowProps): JSX.Element { return ( - <button - type="button" + // Pointer-based drag (see TasksCalendar) instead of native HTML5 DnD. + // role/tabIndex/onKeyDown keep it keyboard-accessible. + <div + role="button" + tabIndex={0} ref={buttonRef ?? undefined} onClick={onOpen} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onOpen() + } + }} + onPointerDown={onPointerDownTask} + onContextMenu={onContextMenu} + title="Drag to a day to reschedule · right-click for actions" className={[ - 'flex w-full items-center gap-2 rounded-md border-l-2 px-2 py-1 text-left text-sm', + 'flex w-full cursor-grab select-none items-center gap-2 rounded-md border-l-2 px-2 py-1 text-left text-sm active:cursor-grabbing', isOverdue ? 'border-rose-500/70' : 'border-transparent', - 'hover:bg-paper-200/60' + isActive ? 'bg-accent/10 ring-1 ring-inset ring-accent/40' : 'hover:bg-paper-200/60' ].join(' ')} > <span @@ -475,6 +918,6 @@ function CalendarTaskRow({ task, isOverdue, buttonRef, onToggle, onOpen }: RowPr !{task.priority} </span> )} - </button> + </div> ) } diff --git a/packages/app-core/src/components/TasksView.tsx b/packages/app-core/src/components/TasksView.tsx index 32d07ce7..cc0a0142 100644 --- a/packages/app-core/src/components/TasksView.tsx +++ b/packages/app-core/src/components/TasksView.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { isTasksViewActive, useStore, type TasksViewMode } from '../store' -import type { VaultTask } from '@shared/tasks' +import { inferDailyTaskDueDates, type VaultTask } from '@shared/tasks' +import { buildDailyNoteDateByPath } from '../lib/vault-layout' import { computeTasksRender, isOverdue } from '../lib/tasks-filter' import { TasksRow } from './TasksRow' import { TasksCalendar } from './TasksCalendar' @@ -31,7 +32,9 @@ const VIEW_BUTTONS: Array<{ ] export function TasksView(): JSX.Element { - const tasks = useStore((s) => s.vaultTasks) + const rawTasks = useStore((s) => s.vaultTasks) + const notes = useStore((s) => s.notes) + const vaultSettings = useStore((s) => s.vaultSettings) const loading = useStore((s) => s.tasksLoading) const filter = useStore((s) => s.tasksFilter) const cursorIndex = useStore((s) => s.taskCursorIndex) @@ -40,7 +43,20 @@ export function TasksView(): JSX.Element { const refreshTasks = useStore((s) => s.refreshTasks) const openTaskAt = useStore((s) => s.openTaskAt) const toggleTaskFromList = useStore((s) => s.toggleTaskFromList) + const applyTaskMutation = useStore((s) => s.applyTaskMutation) + const moveTaskToDate = useStore((s) => s.moveTaskToDate) + const addTaskForDate = useStore((s) => s.addTaskForDate) const closeTasksView = useStore((s) => s.closeTasksView) + + // Tasks written inside a daily note inherit that note's date as an implicit + // due date (a clean line, no `due:` token) so they appear on the calendar. + // Done at the display layer so it works on desktop + web identically and + // re-derives whenever notes/settings change. Explicit `due:` still wins. + const dueByPath = useMemo( + () => buildDailyNoteDateByPath(notes, vaultSettings), + [notes, vaultSettings] + ) + const tasks = useMemo(() => inferDailyTaskDueDates(rawTasks, dueByPath), [rawTasks, dueByPath]) const keymapOverrides = useStore((s) => s.keymapOverrides) const vimMode = useStore((s) => s.vimMode) const viewMode = useStore((s) => s.tasksViewMode) @@ -468,6 +484,12 @@ export function TasksView(): JSX.Element { today={today} onOpenTask={(task) => void openTaskAt(task)} onToggleTask={(task) => void toggleTaskFromList(task)} + onRescheduleTask={(task, dueIso) => + void applyTaskMutation(task, { kind: 'set-due', due: dueIso }) + } + onMoveTask={(task, dateIso) => void moveTaskToDate(task, dateIso)} + onAddTask={(dateIso, text) => addTaskForDate(dateIso, text)} + dailyNotesEnabled={vaultSettings.dailyNotes.enabled} /> )} @@ -518,7 +540,7 @@ export function TasksView(): JSX.Element { {viewMode === 'list' ? 'j/k move · Enter/o open · Space/x toggle · / filter · 1/2/3 view · : command · :q close' : viewMode === 'calendar' - ? 'h/j/k/l day · [ ] month · gt today · Enter open · 1/2/3 view · : command · :q close' + ? 'h/j/k/l day · [ ] month · gt today · Tab pick · < > reschedule · drag to move · Enter open · :q' : 'h/l column · j/k card · Space toggle · Enter open · 1/2/3 view · : command · :q close'} </div> )} diff --git a/packages/app-core/src/lib/cm-slash-commands.ts b/packages/app-core/src/lib/cm-slash-commands.ts index 38eb79c1..f940ba14 100644 --- a/packages/app-core/src/lib/cm-slash-commands.ts +++ b/packages/app-core/src/lib/cm-slash-commands.ts @@ -9,6 +9,11 @@ interface SlashCmd { insert: string /** Cursor offset from end of inserted text. Negative = move back. */ cursorOffset?: number + /** Ranking nudge for the unfiltered list — higher floats to the top. */ + boost?: number + /** Extra space-separated terms that should also match this command (e.g. + * `/todo` finding "Task"). Folded into the match label, hidden from display. */ + keywords?: string } type DecoratedCompletion = Completion & { @@ -22,9 +27,16 @@ const COMMANDS: SlashCmd[] = [ { label: 'Heading 2', detail: '##', icon: 'H2', insert: '## ' }, { label: 'Heading 3', detail: '###', icon: 'H3', insert: '### ' }, { label: 'Heading 4', detail: '####', icon: 'H4', insert: '#### ' }, + { + label: 'Task', + detail: '[ ]', + icon: '☐', + insert: '- [ ] ', + boost: 99, + keywords: 'todo to-do checkbox check task list' + }, { label: 'Bulleted list', detail: '-', icon: '•', insert: '- ' }, { label: 'Numbered list', detail: '1.', icon: '1.', insert: '1. ' }, - { label: 'To-do list', detail: '[ ]', icon: '☐', insert: '- [ ] ' }, { label: 'Quote', detail: '>', icon: '❝', insert: '> ' }, { label: 'Code block', detail: '```', icon: '</>', insert: '```\n\n```', cursorOffset: -4 }, { label: 'Divider', detail: '---', icon: '—', insert: '---\n' }, @@ -69,7 +81,7 @@ function renderCompletion(completion: Completion): HTMLElement { const label = document.createElement('span') label.className = 'slash-cmd-label' - label.textContent = completion.label + label.textContent = completion.displayLabel ?? completion.label const detail = document.createElement('span') detail.className = 'slash-cmd-detail' @@ -101,8 +113,12 @@ export function slashCommandSource(context: CompletionContext): CompletionResult from: filterFrom, options: COMMANDS.map( (cmd): Completion => ({ - label: cmd.label, + // Fold alias keywords into the matched label so `/todo`, `/checkbox`, + // etc. surface the command; `displayLabel` keeps the menu text clean. + label: cmd.keywords ? `${cmd.label} ${cmd.keywords}` : cmd.label, + displayLabel: cmd.label, detail: cmd.detail, + boost: cmd.boost, _kind: 'slash', // Store icon for the custom renderer _icon: cmd.icon, diff --git a/packages/app-core/src/lib/commands.ts b/packages/app-core/src/lib/commands.ts index 2021cdd0..abf94645 100644 --- a/packages/app-core/src/lib/commands.ts +++ b/packages/app-core/src/lib/commands.ts @@ -118,6 +118,16 @@ export function buildCommands(options?: { includeUnavailable?: boolean }): Comma when: () => getState().vaultSettings.dailyNotes.enabled, run: () => getState().openTodayDailyNote() }, + { + id: 'note.daily.rollover', + title: 'Roll Over Unfinished Tasks to Today', + category: 'Note', + keywords: 'daily tasks rollover roll over migrate unfinished carry forward today', + when: () => getState().vaultSettings.dailyNotes.enabled, + run: () => { + void getState().rolloverUnfinishedTasksIntoToday({ force: true, open: true }) + } + }, { id: 'note.weekly.thisWeek', title: "Open This Week's Note", diff --git a/packages/app-core/src/lib/help.ts b/packages/app-core/src/lib/help.ts index 5c184649..8a361aef 100644 --- a/packages/app-core/src/lib/help.ts +++ b/packages/app-core/src/lib/help.ts @@ -199,6 +199,11 @@ export const HELP_CORE_CONCEPTS: HelpCard[] = [ body: 'Tasks scans every note for checkboxes, Tags lets you browse notes matching any selected tag, Archive gives you a dedicated list of cold-storage notes, and Trash gives you a recovery surface for deleted notes without turning the left rail into a second browser.' }, + { + title: 'The Tasks calendar schedules and reschedules', + body: + "Switch Tasks to Calendar (button or `2`) to see tasks laid out by due date. A task written inside a daily note automatically shows on that day — no `due:` needed — so the day you wrote it on is the day it lands. Type in the box under the grid to add a task to the selected day (it’s created in that day’s daily note, offering to create the note first for a day that has none). Reschedule by dragging a task onto another day, or from the keyboard: `Tab` picks a task in the day list, `<` / `>` shifts it a day earlier/later, and `T` moves it to today." + }, { title: 'Moving notes is path-first', body: @@ -720,7 +725,7 @@ export const HELP_SETTINGS: HelpSettingsSection[] = [ items: [ { label: 'Vault location', detail: 'Reveal or change the root folder ZenNotes treats as the active vault.' }, { label: 'Primary notes location', detail: 'Treat `inbox/` as the main notes area, or use the vault root directly for an Obsidian-style flat vault.' }, - { label: 'Daily notes', detail: "Enable a daily-notes workflow, choose a directory pattern, naming pattern, locale, and template so each day’s note starts in the right place. Supported tokens are `yyyy`, `yy`, `M`, `MM`, `MMM`, `MMMM`, `d`, `dd`, `EEE`, `EEEE`, `w`, and `ww`; quote literal words like `'Daily Notes'/yyyy/MM-MMM`. Open today’s note with `Space d`, `:daily`, or the command palette." }, + { label: 'Daily notes', detail: "Enable a daily-notes workflow, choose a directory pattern, naming pattern, locale, and template so each day’s note starts in the right place. Supported tokens are `yyyy`, `yy`, `M`, `MM`, `MMM`, `MMMM`, `d`, `dd`, `EEE`, `EEEE`, `w`, and `ww`; quote literal words like `'Daily Notes'/yyyy/MM-MMM`. Open today’s note with `Space d`, `:daily`, or the command palette. Two task options live here too: “Tasks are due on the note’s date” makes tasks in a daily note show on the calendar for that day (on by default), and “Roll over unfinished tasks to today” moves every unchecked task from past daily notes into today when you open it (off by default; also runnable from the command palette)." }, { label: 'Weekly notes', detail: "Enable weekly notes with a directory pattern, naming pattern, locale, and template. Weekly patterns support the same tokens as daily notes plus ISO week `w` and `ww`; the default title pattern is `yyyy-'W'ww`. Open this week’s note with `Space w`, `:weekly`, or the command palette." }, { label: 'System folder labels', detail: 'Rename how Inbox, Quick Notes, Archive, and Trash appear in the UI without renaming the real folders on disk.' } ] diff --git a/packages/app-core/src/lib/vault-layout.ts b/packages/app-core/src/lib/vault-layout.ts index 70c6b070..f70dec81 100644 --- a/packages/app-core/src/lib/vault-layout.ts +++ b/packages/app-core/src/lib/vault-layout.ts @@ -510,7 +510,11 @@ export function normalizeVaultSettings( settings?.dailyNotes?.legacyPatterns, primaryNotesLocation ), - templateId: normalizeTemplateId(settings?.dailyNotes?.templateId) + templateId: normalizeTemplateId(settings?.dailyNotes?.templateId), + // Defaults: derive due dates ON, roll tasks over OFF. Absent (undefined) + // means "use the default", so only an explicit `false`/`true` overrides. + tasksDueOnNoteDate: settings?.dailyNotes?.tasksDueOnNoteDate !== false, + rolloverUnfinishedTasks: settings?.dailyNotes?.rolloverUnfinishedTasks === true }, weeklyNotes: { enabled: !!settings?.weeklyNotes?.enabled, @@ -1099,6 +1103,27 @@ export function buildDateNoteIndexes( return { dailyByDate, weeklyByWeek } } +/** + * Map every daily note to its own date as `vault-relative path -> YYYY-MM-DD` + * (the same encoding a `due:` token uses). Returns an empty map when daily + * notes are disabled or `dailyNotes.tasksDueOnNoteDate` is off, so callers can + * feed the result straight to `inferDailyTaskDueDates` without re-checking the + * setting. Weekly notes are intentionally excluded — a week has no single day. + */ +export function buildDailyNoteDateByPath( + notes: readonly NoteMeta[], + settings: VaultSettings | null | undefined +): Map<string, string> { + const out = new Map<string, string>() + const normalized = normalizeVaultSettings(settings) + if (!normalized.dailyNotes.enabled || !normalized.dailyNotes.tasksDueOnNoteDate) return out + for (const note of notes) { + const info = classifyDateNote(note, normalized) + if (info?.kind === 'daily') out.set(note.path, noteTitleForDate(info.date)) + } + return out +} + export function dateNoteFolderMayBelongToDatePattern( subpath: string, settings: VaultSettings | null | undefined diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index 2b03d7e2..2b557eeb 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -24,7 +24,7 @@ import type { } from '@shared/ipc' import type { VaultTask } from '@shared/tasks' import { isExcalidrawPath } from '@shared/excalidraw' -import { TASKS_TAB_PATH, isTasksTabPath } from '@shared/tasks' +import { TASKS_TAB_PATH, isTasksTabPath, parseTasksFromBody } from '@shared/tasks' import type { DatabaseDoc, DatabaseSidecar } from '@shared/databases' import { databaseTabPath, @@ -45,9 +45,13 @@ import { isAssetTabPath, assetPathFromTab, assetTabPath } from './lib/asset-tabs import { FENCE_RE, TASK_LINE_RE, + extractUncheckedTaskBlocks, + removeTaskAtIndex, + takeTaskLineAtIndex, setTaskCheckedAtIndex, setTaskDueAtIndex, setTaskPriorityAtIndex, + setTaskTextAtIndex, setTaskWaitingAtIndex, toggleTaskAtIndex, type TaskPriority as TaskLinePriority @@ -55,6 +59,7 @@ import { import { DEFAULT_THEME_ID, THEMES, type ThemeFamily, type ThemeMode } from './lib/themes' import { formatMarkdown } from './lib/format-markdown' import { confirmMoveToTrash } from './lib/confirm-trash' +import { confirmApp } from './lib/confirm-requests' import { pickServerDirectoryApp } from './lib/server-directory-picker-requests' import { promptApp } from './lib/prompt-requests' import { @@ -75,12 +80,14 @@ import { workspaceRestorePrefetchContentPaths } from './lib/workspace-tabs' import { + classifyDateNote, duplicateFolderColors, duplicateFolderIcons, dailyNoteLocationForDate, folderForVaultRelativePath, findDailyNoteForDate, findWeeklyNoteForDate, + noteTitleForDate, isPrimaryNotesAtRoot, removeFavoritesForFolder, removeFolderColors, @@ -401,6 +408,7 @@ export type TaskMutation = | { kind: 'set-waiting'; waiting: boolean } | { kind: 'set-priority'; priority: TaskLinePriority | null } | { kind: 'set-due'; due: string | null } + | { kind: 'set-text'; text: string } type AssetUndoEntry = { kind: 'delete-asset'; deleted: DeletedAsset; createdAt: number } @@ -932,6 +940,39 @@ function resolveTaskLineNumber(body: string, task: VaultTask): number { return task.lineNumber } +/** Parse a `YYYY-MM-DD` string to a local-midnight Date, or null if malformed. */ +function parseIsoDateLocal(iso: string): Date | null { + const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso) + if (!m) return null + const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])) + return Number.isNaN(d.getTime()) ? null : d +} + +// Per-vault "we already rolled over today" marker, persisted in localStorage so +// opening today's daily note across sessions doesn't re-scan past notes once +// it's done for the day. Keyed by vault root so multiple vaults don't collide. +function rolloverMarkerKey(root: string): string { + return `zen.tasks.rollover.${root || 'default'}` +} +function readRolloverMarker(root: string): string | null { + try { + return typeof localStorage !== 'undefined' + ? localStorage.getItem(rolloverMarkerKey(root)) + : null + } catch { + return null + } +} +function writeRolloverMarker(root: string, iso: string): void { + try { + if (typeof localStorage !== 'undefined') { + localStorage.setItem(rolloverMarkerKey(root), iso) + } + } catch { + // localStorage may be unavailable (private mode); the in-session flow still works. + } +} + function applyTaskMutationsToTask(task: VaultTask, mutations: TaskMutation[]): VaultTask { let next = task for (const m of mutations) { @@ -952,6 +993,11 @@ function applyTaskMutationsToTask(task: VaultTask, mutations: TaskMutation[]): V if (next.due !== due) next = { ...next, due } break } + case 'set-text': { + const content = m.text.trim() + if (next.content !== content) next = { ...next, content } + break + } } } return next @@ -1737,6 +1783,12 @@ interface Store { task: VaultTask, mutation: TaskMutation | TaskMutation[] ) => Promise<void> + /** Delete a task's line from its note (the right-click "Delete" action). */ + deleteTaskFromList: (task: VaultTask) => Promise<void> + /** Physically move a task's line into the daily note for `dateIso`, + * removing it from its current note. Falls back to setting the due date + * when daily notes are disabled or it already lives in that day's note. */ + moveTaskToDate: (task: VaultTask, dateIso: string) => Promise<void> setTasksFilter: (q: string) => void setTasksViewMode: (mode: TasksViewMode) => void setKanbanGroupBy: (group: KanbanGroupBy) => void @@ -1901,6 +1953,20 @@ interface Store { setCalendarShowWeekNumbers: (show: boolean) => void openDailyNoteForDate: (date: Date) => Promise<void> openWeeklyNoteForDate: (date: Date) => Promise<void> + /** Find the daily note for `date`, creating it on disk (template-aware) + * WITHOUT navigating to it. Returns its meta, or null if daily notes are + * disabled or creation failed. */ + ensureDailyNoteForDate: (date: Date) => Promise<NoteMeta | null> + /** Append a `- [ ] …` task to the daily note for `dateIso` (YYYY-MM-DD), + * prompting to create that daily note first if it doesn't exist. */ + addTaskForDate: (dateIso: string, text: string) => Promise<void> + /** Move unfinished tasks from past daily notes into today's note. Returns the + * number of task lines moved. Without `force`, it is gated by the + * `rolloverUnfinishedTasks` setting and a once-per-day marker. */ + rolloverUnfinishedTasksIntoToday: (opts?: { + force?: boolean + open?: boolean + }) => Promise<number> /** Mark the first-run onboarding as complete (or skipped). Persists. */ completeOnboarding: () => void /** Re-open the first-run onboarding wizard. Persists. */ @@ -3450,6 +3516,9 @@ export const useStore = create<Store>((set, get) => { case 'set-due': nextBody = setTaskDueAtIndex(nextBody, task.taskIndex, m.due) break + case 'set-text': + nextBody = setTaskTextAtIndex(nextBody, task.taskIndex, m.text) + break } } if (nextBody === body) { @@ -3470,6 +3539,119 @@ export const useStore = create<Store>((set, get) => { } }, + deleteTaskFromList: async (task) => { + const path = task.sourcePath + const openBuffer = get().noteContents[path] + let body: string + try { + body = openBuffer?.body ?? (await window.zen.readNote(path)).body + } catch (err) { + console.error('deleteTaskFromList readNote failed', err) + return + } + const nextBody = removeTaskAtIndex(body, task.taskIndex) + if (nextBody === body) return + // Optimistically drop it from the index so the row vanishes immediately. + set((s) => ({ + vaultTasks: s.vaultTasks.filter( + (t) => !(t.sourcePath === path && t.taskIndex === task.taskIndex) + ) + })) + if (openBuffer) { + get().updateNoteBody(path, nextBody) + } else { + try { + await window.zen.writeNote(path, nextBody) + await get().rescanTasksForPath(path) + } catch (err) { + console.error('deleteTaskFromList writeNote failed', err) + void get().rescanTasksForPath(path) + } + } + }, + + moveTaskToDate: async (task, dateIso) => { + const parsed = parseIsoDateLocal(dateIso) + if (!parsed) return + const settings = normalizeVaultSettings(get().vaultSettings) + // No daily notes to move into — just set the due date instead. + if (!settings.dailyNotes.enabled) { + await get().applyTaskMutation(task, { kind: 'set-due', due: dateIso }) + return + } + const target = await get().ensureDailyNoteForDate(parsed) + if (!target) return + const inferDue = settings.dailyNotes.tasksDueOnNoteDate + // Already in that day's note — nothing to relocate; just align its due. + if (target.path === task.sourcePath) { + await get().applyTaskMutation(task, { kind: 'set-due', due: inferDue ? null : dateIso }) + return + } + + const srcBuffer = get().noteContents[task.sourcePath] + const tgtBuffer = get().noteContents[target.path] + let srcBody: string + let tgtBody: string + try { + srcBody = srcBuffer?.body ?? (await window.zen.readNote(task.sourcePath)).body + tgtBody = tgtBuffer?.body ?? (await window.zen.readNote(target.path)).body + } catch (err) { + console.error('moveTaskToDate read failed', err) + return + } + const { line, body: strippedSrc } = takeTaskLineAtIndex(srcBody, task.taskIndex) + if (!line) return + // Moving INTO the target day's note: with implicit due on, a bare line + // already reads as that day, so strip any `due:` token; otherwise write the + // explicit date. + const movedLine = setTaskDueAtIndex(line, 0, inferDue ? null : dateIso) + const trimmed = tgtBody.replace(/\s+$/u, '') + const nextTgt = trimmed.length ? `${trimmed}\n${movedLine}\n` : `${movedLine}\n` + + // Persist both notes (open buffers go through the edit pipeline). + if (srcBuffer) get().updateNoteBody(task.sourcePath, strippedSrc) + else { + try { + await window.zen.writeNote(task.sourcePath, strippedSrc) + } catch (err) { + console.error('moveTaskToDate write source failed', err) + return + } + } + if (tgtBuffer) get().updateNoteBody(target.path, nextTgt) + else { + try { + await window.zen.writeNote(target.path, nextTgt) + } catch (err) { + console.error('moveTaskToDate write target failed', err) + return + } + } + + // Rebuild the index for the two affected notes with a client-side parse — + // authoritative (same parser the scanner uses) and independent of the + // single-file IPC rescanner, so the move shows immediately. + const srcTasks = parseTasksFromBody(strippedSrc, { + path: task.sourcePath, + title: task.noteTitle, + folder: task.noteFolder + }) + const tgtTasks = parseTasksFromBody(nextTgt, { + path: target.path, + title: target.title, + folder: target.folder + }) + set((s) => ({ + vaultTasks: [ + ...s.vaultTasks.filter( + (t) => t.sourcePath !== task.sourcePath && t.sourcePath !== target.path + ), + ...srcTasks, + ...tgtTasks + ] + })) + }, + setTasksFilter: (q) => set({ tasksFilter: q, taskCursorIndex: 0 }), setTasksViewMode: (mode) => { set({ tasksViewMode: mode, taskCursorIndex: 0 }) @@ -4683,20 +4865,179 @@ export const useStore = create<Store>((set, get) => { if (existing) { set({ view: { kind: 'folder', folder: 'inbox', subpath } }) await get().selectNote(existing.path) - return + } else { + const template = resolveTemplate(state.customTemplates, settings.dailyNotes.templateId) + if (template) { + await get().createFromTemplate(template, { folder: 'inbox', subpath, title, date }) + } else { + await get().createAndOpen('inbox', subpath, { title }) + } } - const template = resolveTemplate(state.customTemplates, settings.dailyNotes.templateId) - if (template) { - await get().createFromTemplate(template, { folder: 'inbox', subpath, title, date }) - return + // Opening *today's* note rolls unfinished tasks forward from past daily + // notes (Obsidian-style) when enabled. Fire-and-forget so the note shows + // right away; the rollover appends into the now-open buffer. + if (noteTitleForDate(date) === noteTitleForDate(new Date())) { + void get().rolloverUnfinishedTasksIntoToday() } - await get().createAndOpen('inbox', subpath, { title }) }, openTodayDailyNote: async () => { await get().openDailyNoteForDate(new Date()) }, + ensureDailyNoteForDate: async (date) => { + const state = get() + const settings = normalizeVaultSettings(state.vaultSettings) + if (!settings.dailyNotes.enabled) return null + const existing = findDailyNoteForDate(state.notes, settings, date) + if (existing) return existing + const { title, subpath } = dailyNoteLocationForDate(date, settings) + const template = resolveTemplate(state.customTemplates, settings.dailyNotes.templateId) + const body = template ? renderTemplate(template.body, { title, now: date }).body : '' + try { + const meta = await window.zen.createNote('inbox', title, subpath) + if (body) await window.zen.writeNote(meta.path, body) + await get().refreshNotes() + return get().notes.find((n) => n.path === meta.path) ?? meta + } catch (err) { + console.error('ensureDailyNoteForDate failed', err) + return null + } + }, + + addTaskForDate: async (dateIso, text) => { + const content = text.trim() + if (!content) return + const parsed = parseIsoDateLocal(dateIso) + if (!parsed) return + const settings = normalizeVaultSettings(get().vaultSettings) + if (!settings.dailyNotes.enabled) return + let note = findDailyNoteForDate(get().notes, settings, parsed) + if (!note) { + const ok = await confirmApp({ + title: 'Create daily note?', + description: `No daily note exists for ${dateIso} yet. Create it and add this task?`, + confirmLabel: 'Create & add' + }) + if (!ok) return + note = await get().ensureDailyNoteForDate(parsed) + if (!note) return + } + const path = note.path + // Implicit due already covers daily-note tasks; only write an explicit + // `due:` token when inference is off, so the task still lands on this day. + const line = settings.dailyNotes.tasksDueOnNoteDate + ? `- [ ] ${content}` + : `- [ ] ${content} due:${dateIso}` + const openBuffer = get().noteContents[path] + const body = openBuffer?.body ?? (await window.zen.readNote(path)).body + const trimmed = body.replace(/\s+$/u, '') + const nextBody = trimmed.length ? `${trimmed}\n${line}\n` : `${line}\n` + if (openBuffer) { + // Open note: edit through the buffer so unsaved changes aren't stomped; + // its autosave + the watcher rescan the tasks (a disk rescan now would be + // stale). The common add-from-calendar case hits the writeNote branch. + get().updateNoteBody(path, nextBody) + } else { + try { + await window.zen.writeNote(path, nextBody) + await get().rescanTasksForPath(path) + } catch (err) { + console.error('addTaskForDate writeNote failed', err) + } + } + }, + + rolloverUnfinishedTasksIntoToday: async (opts) => { + const force = opts?.force === true + const settings = normalizeVaultSettings(get().vaultSettings) + if (!settings.dailyNotes.enabled) return 0 + const today = new Date() + const todayIso = noteTitleForDate(today) + const vaultRoot = get().vault?.root ?? '' + if (!force) { + if (!settings.dailyNotes.rolloverUnfinishedTasks) return 0 + if (readRolloverMarker(vaultRoot) === todayIso) return 0 + } + const todayNote = await get().ensureDailyNoteForDate(today) + if (!todayNote) return 0 + if (opts?.open) { + const { subpath } = dailyNoteLocationForDate(today, settings) + set({ view: { kind: 'folder', folder: 'inbox', subpath } }) + await get().selectNote(todayNote.path) + } + + // Gather unfinished task blocks from every *past* daily note, oldest first. + const pastNotes: Array<{ note: NoteMeta; iso: string }> = [] + for (const note of get().notes) { + if (note.path === todayNote.path) continue + const info = classifyDateNote(note, settings) + if (info?.kind !== 'daily') continue + const iso = noteTitleForDate(info.date) + if (iso < todayIso) pastNotes.push({ note, iso }) + } + pastNotes.sort((a, b) => (a.iso < b.iso ? -1 : a.iso > b.iso ? 1 : 0)) + + const movedLines: string[] = [] + for (const { note } of pastNotes) { + const buffer = get().noteContents[note.path] + let body: string + try { + body = buffer?.body ?? (await window.zen.readNote(note.path)).body + } catch (err) { + console.error('rollover readNote failed', note.path, err) + continue + } + const { moved, rest } = extractUncheckedTaskBlocks(body) + if (moved.length === 0) continue + movedLines.push(...moved) + if (buffer) { + // Open buffer: route through the normal edit pipeline (marks dirty, + // autosaves, watcher rescans tasks) — same as toggleTaskFromList. A disk + // rescan here would read the not-yet-flushed file and go stale. + get().updateNoteBody(note.path, rest) + } else { + try { + await window.zen.writeNote(note.path, rest) + await get().rescanTasksForPath(note.path) + } catch (err) { + console.error('rollover writeNote (source) failed', note.path, err) + // Don't drop the lines we already pulled — they'll still land in today. + } + } + } + + if (movedLines.length === 0) { + writeRolloverMarker(vaultRoot, todayIso) + return 0 + } + + const todayBuffer = get().noteContents[todayNote.path] + let todayBody: string + try { + todayBody = todayBuffer?.body ?? (await window.zen.readNote(todayNote.path)).body + } catch (err) { + console.error('rollover readNote (today) failed', err) + return 0 + } + const trimmed = todayBody.replace(/\s+$/u, '') + const block = movedLines.join('\n') + const nextBody = trimmed.length ? `${trimmed}\n${block}\n` : `${block}\n` + if (todayBuffer) { + get().updateNoteBody(todayNote.path, nextBody) + } else { + try { + await window.zen.writeNote(todayNote.path, nextBody) + await get().rescanTasksForPath(todayNote.path) + } catch (err) { + console.error('rollover writeNote (today) failed', err) + return 0 + } + } + writeRolloverMarker(vaultRoot, todayIso) + return movedLines.length + }, + openWeeklyNoteForDate: async (date) => { const state = get() const settings = normalizeVaultSettings(state.vaultSettings) diff --git a/packages/app-core/src/styles/index.css b/packages/app-core/src/styles/index.css index 5fe0e9ae..9fe4d408 100644 --- a/packages/app-core/src/styles/index.css +++ b/packages/app-core/src/styles/index.css @@ -3077,7 +3077,7 @@ textarea, color: rgb(var(--z-ink-900)); } .vim-cursor-on-active { - box-shadow: inset 0 0 0 2px rgb(255 255 255 / 0.5); + box-shadow: inset 0 0 0 2px rgb(var(--z-accent) / 0.65); } .vim-cursor-on-selected { box-shadow: inset 0 0 0 2px rgb(var(--z-accent) / 0.65); diff --git a/packages/bridge-contract/src/ipc.ts b/packages/bridge-contract/src/ipc.ts index 236da613..f22f44ef 100644 --- a/packages/bridge-contract/src/ipc.ts +++ b/packages/bridge-contract/src/ipc.ts @@ -282,6 +282,16 @@ export interface DailyNotesSettings { legacyPatterns?: DateNotePatternSettings[] /** Template applied to new daily notes. Empty/undefined = blank note. */ templateId?: string + /** + * Treat a task written inside a daily note as due on that note's date, so it + * shows up on the calendar without typing `due:`. The line is left untouched — + * the due date is derived. An explicit `due:` token still wins. Default `true`. + */ + tasksDueOnNoteDate?: boolean + /** + * When today's daily note opens, move every unfinished task from previous + * daily notes into it (Obsidian-style). Off by default. */ + rolloverUnfinishedTasks?: boolean } export interface WeeklyNotesSettings { @@ -328,7 +338,9 @@ export const DEFAULT_VAULT_SETTINGS: VaultSettings = { enabled: false, directory: DEFAULT_DAILY_NOTES_DIRECTORY, titlePattern: DEFAULT_DAILY_NOTE_TITLE_PATTERN, - locale: DEFAULT_DAILY_NOTE_LOCALE + locale: DEFAULT_DAILY_NOTE_LOCALE, + tasksDueOnNoteDate: true, + rolloverUnfinishedTasks: false }, weeklyNotes: { enabled: false, diff --git a/packages/shared-domain/src/tasklists.ts b/packages/shared-domain/src/tasklists.ts index 021c074a..54c9ca9c 100644 --- a/packages/shared-domain/src/tasklists.ts +++ b/packages/shared-domain/src/tasklists.ts @@ -160,3 +160,137 @@ export function setTaskDueAtIndex( return `${prefix}${checkChar}]${nextTail}` }) } + +/** Replace everything after the checkbox on the task line at `taskIndex` with + * `text` (verbatim — the caller owns any `due:`/`!priority` tokens). Used by + * inline task editing. */ +export function setTaskTextAtIndex( + markdown: string, + taskIndex: number, + text: string +): string { + return editTaskAtIndex(markdown, taskIndex, (match) => { + const prefix = match[1] + const checkChar = match[2] + const tailWithBracket = match[3] + if (!tailWithBracket.startsWith(']')) return null + const trimmed = text.trim() + return `${prefix}${checkChar}]${trimmed ? ` ${trimmed}` : ''}` + }) +} + +/** Remove the task line at `taskIndex` and return both the removed line and the + * remaining body. `line` is null when the index is out of range. Fence-aware, + * counting tasks the same way the parser does so the index stays in lockstep. + * Used to move a task to another note. */ +export function takeTaskLineAtIndex( + markdown: string, + taskIndex: number +): { line: string | null; body: string } { + if (taskIndex < 0) return { line: null, body: markdown } + const lines = markdown.split('\n') + let currentTaskIndex = 0 + let inFence = false + let fenceMarker: string | null = null + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + const fenceMatch = line.match(FENCE_RE) + if (fenceMatch) { + const marker = fenceMatch[2] + if (!inFence) { + inFence = true + fenceMarker = marker + } else if (marker === fenceMarker) { + inFence = false + fenceMarker = null + } + continue + } + if (inFence) continue + if (!TASK_LINE_RE.test(line)) continue + if (currentTaskIndex !== taskIndex) { + currentTaskIndex += 1 + continue + } + const removed = lines[i] + lines.splice(i, 1) + return { line: removed, body: lines.join('\n') } + } + return { line: null, body: markdown } +} + +/** Delete the task line at `taskIndex` entirely. */ +export function removeTaskAtIndex(markdown: string, taskIndex: number): string { + return takeTaskLineAtIndex(markdown, taskIndex).body +} + +function leadingIndentWidth(line: string): number { + return line.match(/^[ \t]*/)?.[0].length ?? 0 +} + +/** + * Pull every UNCHECKED task line — together with its indented continuation / + * child lines — out of `markdown`. Used to roll unfinished tasks forward from + * past daily notes into today's note. + * + * - Lines are moved verbatim, so any `due:`/`!priority`/`#tag` tokens travel + * with the task unchanged. + * - Checked tasks (`- [x]`) stay put — they're history. + * - `- [ ]` inside fenced code blocks is ignored (never a real task). + * - A task's indented children (deeper-indented following lines, up to the + * first blank line, dedent, or fence) move with it so sub-bullets aren't + * orphaned. + * + * Returns the moved raw lines (in document order) and the remaining body. + */ +export function extractUncheckedTaskBlocks(markdown: string): { + moved: string[] + rest: string +} { + const lines = markdown.split('\n') + const consumed = new Array<boolean>(lines.length).fill(false) + const moved: string[] = [] + let inFence = false + let fenceMarker: string | null = null + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + const fenceMatch = line.match(FENCE_RE) + if (fenceMatch) { + const marker = fenceMatch[2] + if (!inFence) { + inFence = true + fenceMarker = marker + } else if (marker === fenceMarker) { + inFence = false + fenceMarker = null + } + continue + } + if (inFence) continue + + const taskMatch = line.match(TASK_LINE_RE) + if (!taskMatch) continue + if (taskMatch[2] !== ' ') continue // only unchecked tasks roll over + + const baseIndent = leadingIndentWidth(line) + moved.push(line) + consumed[i] = true + + // Carry indented continuation/child lines along with the task. + let j = i + 1 + while (j < lines.length) { + const next = lines[j] + if (next.trim() === '') break + if (FENCE_RE.test(next)) break + if (leadingIndentWidth(next) <= baseIndent) break + moved.push(next) + consumed[j] = true + j++ + } + i = j - 1 // skip the consumed block (its children are not new tasks) + } + + const rest = lines.filter((_, idx) => !consumed[idx]).join('\n') + return { moved, rest } +} diff --git a/packages/shared-domain/src/tasks.ts b/packages/shared-domain/src/tasks.ts index da1434b1..8457f751 100644 --- a/packages/shared-domain/src/tasks.ts +++ b/packages/shared-domain/src/tasks.ts @@ -41,6 +41,10 @@ export interface VaultTask { checked: boolean /** ISO YYYY-MM-DD, validated via Date round-trip. */ due?: string + /** True when `due` was *derived* from the containing daily note's date + * rather than written on the line. Lets UIs tell an implicit due apart + * from an explicit `due:` token. See `inferDailyTaskDueDates`. */ + dueInferred?: boolean priority?: TaskPriority /** True if `@waiting` appears anywhere on the line. */ waiting: boolean @@ -356,6 +360,30 @@ export function tasksDueOn(tasks: VaultTask[], iso: string): VaultTask[] { ) } +/** + * Give undated tasks that live in a daily note an *implicit* due date equal to + * that note's own date, using a precomputed `sourcePath -> ISO date` map (built + * in app-core from the daily-note pattern). An explicit `due:` token always + * wins, so only tasks with no `due` are touched; the result is flagged + * `dueInferred` so UIs can distinguish it. Returns the same array instance when + * nothing changed (cheap to call from a memo). + */ +export function inferDailyTaskDueDates( + tasks: VaultTask[], + dueByPath: ReadonlyMap<string, string> +): VaultTask[] { + if (dueByPath.size === 0) return tasks + let changed = false + const out = tasks.map((task) => { + if (task.due) return task + const iso = dueByPath.get(task.sourcePath) + if (!iso) return task + changed = true + return { ...task, due: iso, dueInferred: true } + }) + return changed ? out : tasks +} + /** Bucket tasks by `due` ISO date. Done and waiting tasks are skipped. * Tasks without a due date land in the special `'unscheduled'` key. */ export function bucketTasksByDueDate( From ab3248617ca3387915c14e1a0e74e42861d64528 Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Thu, 18 Jun 2026 17:55:07 -0500 Subject: [PATCH 61/66] fix: don't reorganize loose files in an existing Obsidian vault (#202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pointing ZenNotes at an existing Obsidian vault silently relocated every loose root-level non-md file — images, PDFs, and Obsidian `.canvas` files — into `assets/` on first open (the loose-asset migration), which a user reported as "a number of different files were changed that I made no edits to." A vault with an `.obsidian/` directory is managed by another app, so its loose files are the user's own, not ZenNotes' legacy attachments. Skip the root-level relocation step in that case; the legacy ZenNotes attachment dirs (which never exist in such a vault) stay handled, so the former-ZenNotes upgrade path is unchanged. Adds end-to-end integrity tests that wire the real main-process vault I/O and the real renderer store for the reporter's exact shape (root mode, nested folders, spaces in filenames, symlinked `~/sync` root): notes never cross-wire content, pure navigation writes nothing to disk, edits autosave to only the edited file, and external watcher changes land under the right path. --- .../src/main/vault-content-integrity.test.ts | 189 +++++++++++++++++ apps/desktop/src/main/vault.ts | 32 ++- .../app-core/src/store-note-integrity.test.ts | 193 ++++++++++++++++++ 3 files changed, 406 insertions(+), 8 deletions(-) create mode 100644 apps/desktop/src/main/vault-content-integrity.test.ts create mode 100644 packages/app-core/src/store-note-integrity.test.ts diff --git a/apps/desktop/src/main/vault-content-integrity.test.ts b/apps/desktop/src/main/vault-content-integrity.test.ts new file mode 100644 index 00000000..dd8b5665 --- /dev/null +++ b/apps/desktop/src/main/vault-content-integrity.test.ts @@ -0,0 +1,189 @@ +import { mkdtemp, mkdir, rm, readFile, symlink, writeFile } from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { + getVaultSettings, + listNotes, + migrateLegacyDatabases, + migrateLooseAssets, + readNote, + setVaultSettings, + writeNote +} from './vault' + +// Reproduction harness for #202 ("Notes show the wrong content" → ZenNotes +// overwrote files on disk with another note's content). The reporter pointed +// ZenNotes at an existing Obsidian vault opened in ROOT mode, with nested +// folders and filenames containing SPACES (e.g. `Work/Documentation/Vault CLI +// Cheatsheet.md`), under `~/sync/obsidian` (a likely-symlinked sync dir). +// +// These tests wire the REAL main-process vault I/O end-to-end and assert that a +// note's identity (path) and content never cross-contaminate. + +const tempDirs: string[] = [] + +async function makeTempVault(prefix = 'zennotes-202-'): Promise<string> { + const dir = await mkdtemp(path.join(os.tmpdir(), prefix)) + tempDirs.push(dir) + return dir +} + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))) +}) + +// The reporter's vault shape: root mode, nested folders, spaces in names, and a +// neighbour in the same folder (the prime cross-contamination suspect). +const FILES: Record<string, string> = { + 'Work/Documentation/Vault CLI Cheatsheet.md': + '---\ntags:\n - "#cheatsheet"\ndate: 242024-11-14\n---\n> [!info] Vault CLI Cheatsheet\nCHEATSHEET BODY\n', + 'Work/Documentation/Another Note.md': '# Another Note\nNEIGHBOUR BODY\n', + 'Work/Projects/plan.md': '# Plan\nPLAN BODY\n', + 'index.md': '# Index\nINDEX BODY\n' +} + +async function buildObsidianVault(root: string, files: Record<string, string> = FILES): Promise<void> { + for (const [rel, body] of Object.entries(files)) { + const abs = path.join(root, ...rel.split('/')) + await mkdir(path.dirname(abs), { recursive: true }) + await writeFile(abs, body, 'utf8') + } + // The `.obsidian/` dir is what marks this as an existing Obsidian vault. + await mkdir(path.join(root, '.obsidian'), { recursive: true }) + const base = await getVaultSettings(root) + await setVaultSettings(root, { ...base, primaryNotesLocation: 'root' }) +} + +async function readDiskBody(root: string, rel: string): Promise<string> { + return await readFile(path.join(root, ...rel.split('/')), 'utf8') +} + +describe('#202 — note content/path integrity (root-mode Obsidian vault, spaces in names)', () => { + it('listNotes emits one correct path per file, no dupes or collisions', async () => { + const root = await makeTempVault() + await buildObsidianVault(root) + + const notes = await listNotes(root) + const paths = notes.map((n) => n.path).sort() + + expect(paths).toEqual([ + 'Work/Documentation/Another Note.md', + 'Work/Documentation/Vault CLI Cheatsheet.md', + 'Work/Projects/plan.md', + 'index.md' + ]) + // No two notes share a path (the collision that would cross-wire content). + expect(new Set(paths).size).toBe(paths.length) + }) + + it('readNote returns each note its OWN body — never a neighbour’s', async () => { + const root = await makeTempVault() + await buildObsidianVault(root) + + for (const [rel, expected] of Object.entries(FILES)) { + const content = await readNote(root, rel) + expect(content.body, `readNote(${rel}) returned the wrong body`).toBe(expected) + expect(content.path).toBe(rel) + } + }) + + it('writeNote to ONE note leaves every OTHER file byte-identical', async () => { + const root = await makeTempVault() + await buildObsidianVault(root) + + const target = 'Work/Documentation/Vault CLI Cheatsheet.md' + const newBody = '---\ntags: []\n---\n> [!info] EDITED\nNEW CHEATSHEET BODY\n' + const meta = await writeNote(root, target, newBody) + + // The path round-trips (so the renderer's `n.path === meta.path` match holds). + expect(meta.path).toBe(target) + expect(await readDiskBody(root, target)).toBe(newBody) + + // Every neighbour is untouched on disk. + for (const [rel, original] of Object.entries(FILES)) { + if (rel === target) continue + expect(await readDiskBody(root, rel), `${rel} was clobbered`).toBe(original) + } + }) + + it('a full list→read→write-back round-trip changes nothing on disk', async () => { + const root = await makeTempVault() + await buildObsidianVault(root) + + const notes = await listNotes(root) + // Simulate the renderer loading each note then autosaving its own buffer. + for (const note of notes) { + const loaded = await readNote(root, note.path) + await writeNote(root, note.path, loaded.body) + } + + for (const [rel, original] of Object.entries(FILES)) { + expect(await readDiskBody(root, rel), `${rel} drifted after round-trip`).toBe(original) + } + }) + + it('survives a symlinked vault root (the ~/sync case) without cross-writing', async () => { + const real = await makeTempVault('zennotes-202-real-') + await buildObsidianVault(real) + const linkParent = await makeTempVault('zennotes-202-link-') + const link = path.join(linkParent, 'obsidian') + await symlink(real, link, 'dir') + + // Open the vault THROUGH the symlink, as `~/sync/obsidian` would. + const notes = await listNotes(link) + expect(notes.map((n) => n.path).sort()).toContain('Work/Documentation/Vault CLI Cheatsheet.md') + + const target = 'Work/Documentation/Vault CLI Cheatsheet.md' + const loaded = await readNote(link, target) + expect(loaded.body).toBe(FILES[target]) + + const newBody = '# rewritten through the symlink\n' + await writeNote(link, target, newBody) + // The write landed on the correct REAL file, and nothing else moved. + expect(await readFile(path.join(real, 'Work', 'Documentation', 'Vault CLI Cheatsheet.md'), 'utf8')).toBe(newBody) + expect(await readFile(path.join(real, 'Work', 'Documentation', 'Another Note.md'), 'utf8')).toBe( + FILES['Work/Documentation/Another Note.md'] + ) + }) + + it('on-open migrations never rewrite or clobber note (.md) content', async () => { + const root = await makeTempVault() + await buildObsidianVault(root) + // An Obsidian vault often has loose non-md files at the root. + await writeFile(path.join(root, 'canvas board.canvas'), 'CANVAS DATA', 'utf8') + await writeFile(path.join(root, 'photo.png'), 'PNGDATA', 'utf8') + + await migrateLegacyDatabases(root) + const assetResult = await migrateLooseAssets(root) + + // Every note's body is exactly as authored — migrations must not touch .md. + for (const [rel, original] of Object.entries(FILES)) { + expect(await readDiskBody(root, rel), `${rel} changed during migration`).toBe(original) + } + // The fix for #202: a vault managed by Obsidian (`.obsidian/`) keeps its + // loose files exactly where the user put them — ZenNotes must not relocate + // `board.canvas`/`photo.png` into assets/ behind their back. + expect(assetResult.moved).toEqual([]) + expect(await readDiskBody(root, 'canvas board.canvas')).toBe('CANVAS DATA') + expect(await readDiskBody(root, 'photo.png')).toBe('PNGDATA') + }) + + it('still migrates loose attachments for a non-Obsidian vault (upgrade path intact)', async () => { + const root = await makeTempVault() + // Same shape but NOT an Obsidian vault (no `.obsidian/`): a former ZenNotes + // vault whose pasted image sits loose at the root. + for (const [rel, body] of Object.entries(FILES)) { + const abs = path.join(root, ...rel.split('/')) + await mkdir(path.dirname(abs), { recursive: true }) + await writeFile(abs, body, 'utf8') + } + const base = await getVaultSettings(root) + await setVaultSettings(root, { ...base, primaryNotesLocation: 'root' }) + await writeFile(path.join(root, 'pasted.png'), 'PNGDATA', 'utf8') + + const assetResult = await migrateLooseAssets(root) + expect(assetResult.moved).toContain('assets/pasted.png') + expect(await readDiskBody(root, 'assets/pasted.png')).toBe('PNGDATA') + }) +}) diff --git a/apps/desktop/src/main/vault.ts b/apps/desktop/src/main/vault.ts index 7f21681d..0a018d5a 100644 --- a/apps/desktop/src/main/vault.ts +++ b/apps/desktop/src/main/vault.ts @@ -1198,17 +1198,33 @@ export async function migrateLooseAssets( } // 1. Loose asset files sitting directly at the vault root. - let rootEntries: Dirent[] + // + // Skip this for a vault managed by another app — an `.obsidian/` directory + // marks an Obsidian vault. Those loose root files (`.canvas`, images, PDFs, + // anything) are the user's own, not ZenNotes' legacy attachments, and + // silently relocating them into `assets/` surprised users who pointed + // ZenNotes at an existing Obsidian vault (#202). The legacy ZenNotes + // attachment dirs below never exist in such a vault, so they stay safe. + let isExternalVault = false try { - rootEntries = await fs.readdir(root, { withFileTypes: true }) + await fs.access(path.join(root, '.obsidian')) + isExternalVault = true } catch { - return { moved, skipped } + /* not an Obsidian vault */ } - for (const entry of rootEntries) { - if (entry.name.startsWith('.') || !entry.isFile()) continue - if (entry.name.toLowerCase().endsWith('.md')) continue // a note - if (databaseCsvPathFor(entry.name) || isDatabaseInternalPath(entry.name)) continue // a database - await moveIntoAssets(entry.name) + if (!isExternalVault) { + let rootEntries: Dirent[] = [] + try { + rootEntries = await fs.readdir(root, { withFileTypes: true }) + } catch { + rootEntries = [] + } + for (const entry of rootEntries) { + if (entry.name.startsWith('.') || !entry.isFile()) continue + if (entry.name.toLowerCase().endsWith('.md')) continue // a note + if (databaseCsvPathFor(entry.name) || isDatabaseInternalPath(entry.name)) continue // a database + await moveIntoAssets(entry.name) + } } // 2. Files in the legacy attachment dirs, then drop the dir if it empties. diff --git a/packages/app-core/src/store-note-integrity.test.ts b/packages/app-core/src/store-note-integrity.test.ts new file mode 100644 index 00000000..a50c149b --- /dev/null +++ b/packages/app-core/src/store-note-integrity.test.ts @@ -0,0 +1,193 @@ +// @vitest-environment jsdom + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +// Reproduction harness for #202 ("Notes show the wrong content" → files +// overwritten with another note's body). Drives the REAL store over an +// in-memory vault, simulating an Obsidian vault opened in ROOT mode and the +// user NAVIGATING between notes (the reporter made "no edits"), plus the +// external file-watcher firing (their vault lived under ~/sync). Asserts that a +// note's content never lands under another note's path, and that pure +// navigation never writes to disk. + +interface MemNote { + path: string + body: string +} + +function meta(path: string, body: string) { + const title = path.split('/').pop()!.replace(/\.md$/, '') + return { + path, + title, + folder: 'inbox' as const, + siblingOrder: 0, + createdAt: 0, + updatedAt: 1, + size: body.length, + tags: [], + wikilinks: [], + assetEmbeds: [], + hasAttachments: false, + excerpt: body.slice(0, 40) + } +} + +// The reporter's vault: nested folders, spaces in names, distinct bodies. +const INITIAL: MemNote[] = [ + { path: 'index.md', body: 'INDEX_BODY' }, + { path: 'Work/Documentation/Vault CLI Cheatsheet.md', body: 'CHEATSHEET_BODY' }, + { path: 'Work/Documentation/Another Note.md', body: 'ANOTHER_BODY' }, + { path: 'Work/Projects/plan.md', body: 'PLAN_BODY' } +] + +let vault: Map<string, string> +const writeCalls: Array<{ path: string; body: string }> = [] + +function installZen(): void { + vault = new Map(INITIAL.map((n) => [n.path, n.body])) + writeCalls.length = 0 + Object.defineProperty(window, 'zen', { + configurable: true, + value: { + getCapabilities: vi.fn().mockReturnValue({ + supportsUpdater: false, + supportsNativeMenus: false, + supportsFloatingWindows: false, + supportsLocalFilesystemPickers: true, + supportsRemoteWorkspace: false, + supportsCliInstall: false, + supportsCustomTemplates: false + }), + scanTasks: vi.fn().mockResolvedValue([]), + scanTasksForPath: vi.fn().mockResolvedValue([]), + listNotes: vi.fn(async () => [...vault.entries()].map(([p, b]) => meta(p, b))), + listFolders: vi.fn().mockResolvedValue([]), + listLocalVaults: vi.fn().mockResolvedValue([]), + listAssets: vi.fn().mockResolvedValue([]), + hasAssetsDir: vi.fn().mockResolvedValue(false), + getRemoteWorkspaceInfo: vi.fn().mockResolvedValue(null), + getVaultSettings: vi.fn().mockResolvedValue({}), + closeVault: vi.fn().mockResolvedValue(null), + readNote: vi.fn(async (path: string) => { + if (!vault.has(path)) throw new Error(`ENOENT ${path}`) + const body = vault.get(path)! + return { ...meta(path, body), body } + }), + writeNote: vi.fn(async (path: string, body: string) => { + writeCalls.push({ path, body }) + vault.set(path, body) + return meta(path, body) + }) + } + }) +} + +async function loadStore() { + vi.resetModules() + localStorage.clear() + return import('./store') +} + +async function flush(): Promise<void> { + await new Promise((r) => window.setTimeout(r, 0)) +} + +beforeEach(() => { + vi.restoreAllMocks() + installZen() +}) + +function seedRootVault(useStore: { setState: (s: Record<string, unknown>) => void }): void { + useStore.setState({ + notes: INITIAL.map((n) => meta(n.path, n.body)), + vaultSettings: { + primaryNotesLocation: 'root', + dailyNotes: { enabled: false, directory: 'Daily Notes' }, + weeklyNotes: { enabled: false, directory: 'Weekly Notes' }, + folderIcons: {}, + folderColors: {}, + favorites: [] + } + }) +} + +describe('#202 — store keeps each note its own content during navigation', () => { + it('opening note after note never cross-wires content', async () => { + const { useStore } = await loadStore() + seedRootVault(useStore) + const paneId = useStore.getState().activePaneId + + for (const n of INITIAL) { + await useStore.getState().openNoteInPane(paneId, n.path) + await flush() + } + // Revisit in a different order (tab switching). + for (const n of [...INITIAL].reverse()) { + await useStore.getState().focusTabInPane(paneId, n.path) + await flush() + } + + const contents = useStore.getState().noteContents + for (const n of INITIAL) { + expect(contents[n.path]?.body, `${n.path} holds the wrong body`).toBe(n.body) + } + }) + + it('pure navigation (no edits) writes NOTHING to disk', async () => { + const { useStore } = await loadStore() + seedRootVault(useStore) + const paneId = useStore.getState().activePaneId + + for (const n of INITIAL) { + await useStore.getState().openNoteInPane(paneId, n.path) + await flush() + } + expect(writeCalls, `navigation triggered a write: ${JSON.stringify(writeCalls)}`).toEqual([]) + // And disk is byte-identical to the originals. + for (const n of INITIAL) expect(vault.get(n.path)).toBe(n.body) + }) + + it('editing one note autosaves ONLY that note, never a neighbour', async () => { + const { useStore } = await loadStore() + seedRootVault(useStore) + const paneId = useStore.getState().activePaneId + + const target = 'Work/Documentation/Vault CLI Cheatsheet.md' + await useStore.getState().openNoteInPane(paneId, target) + await flush() + useStore.getState().updateNoteBody(target, 'EDITED_CHEATSHEET') + await useStore.getState().persistNote(target) + await flush() + + expect(vault.get(target)).toBe('EDITED_CHEATSHEET') + for (const n of INITIAL) { + if (n.path === target) continue + expect(vault.get(n.path), `${n.path} was clobbered by an unrelated edit`).toBe(n.body) + } + expect(writeCalls.every((c) => c.path === target)).toBe(true) + }) + + it('an external watcher change (the ~/sync daemon) lands under the right path', async () => { + const { useStore } = await loadStore() + seedRootVault(useStore) + const paneId = useStore.getState().activePaneId + + const a = 'Work/Documentation/Vault CLI Cheatsheet.md' + const b = 'Work/Documentation/Another Note.md' + await useStore.getState().openNoteInPane(paneId, a) + await useStore.getState().openNoteInPane(paneId, b) + await flush() + + // Sync daemon rewrites A on disk while B is the active tab. + vault.set(a, 'SYNC_REWROTE_CHEATSHEET') + await useStore.getState().applyChange({ kind: 'change', path: a, folder: 'inbox', scope: 'content' }) + await flush() + + const contents = useStore.getState().noteContents + expect(contents[a]?.body).toBe('SYNC_REWROTE_CHEATSHEET') + expect(contents[b]?.body).toBe('ANOTHER_BODY') // untouched + // The external change must not have provoked a write-back. + expect(writeCalls).toEqual([]) + }) +}) From aaa0f783fa4f98dd861e674e65ec669da6fc3b8f Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Fri, 19 Jun 2026 11:21:16 -0500 Subject: [PATCH 62/66] fix: make standard Markdown links clickable (#201) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standard `[text](target)` links didn't navigate the way `[[wikilinks]]` do: internal note links opened as broken asset tabs (the asset enhancer tagged every relative link with a zen-asset URL), and external links — especially a scheme-less domain like `[site](google.com)` — did nothing. Now, across the preview pane, the editor live preview, and `gd`: - `[text](Note.md)` and relative paths (`../x.md`, `#heading`) navigate to the note, resolved relative to the current note. - `[site](https://…)` and bare domains (`google.com`, `www.x.com/path`) open in the browser; a scheme-less domain is promoted to https. - In the editor a plain click follows a rendered link (cursor outside it), mirroring wikilinks; clicking a link the cursor is inside edits it. Cmd/Ctrl- click always follows. Markdown links now show the pointer cursor on hover. - `.md` and external links are no longer mis-tagged as vault assets. New pure module `internal-links.ts` (resolveInternalNoteHref / externalLinkUrl / markdownLinkAt / extractLinkAtCursor) shared by all surfaces, with unit tests and a regression test that note/external links aren't tagged as assets. --- packages/app-core/src/components/Editor.tsx | 57 ++--- .../app-core/src/components/EditorPane.tsx | 72 ++++++- packages/app-core/src/components/Preview.tsx | 29 +++ .../app-core/src/lib/internal-links.test.ts | 145 +++++++++++++ packages/app-core/src/lib/internal-links.ts | 199 ++++++++++++++++++ .../src/lib/local-assets-md-link.test.ts | 68 ++++++ packages/app-core/src/lib/local-assets.ts | 5 + packages/app-core/src/styles/index.css | 5 + 8 files changed, 545 insertions(+), 35 deletions(-) create mode 100644 packages/app-core/src/lib/internal-links.test.ts create mode 100644 packages/app-core/src/lib/internal-links.ts create mode 100644 packages/app-core/src/lib/local-assets-md-link.test.ts diff --git a/packages/app-core/src/components/Editor.tsx b/packages/app-core/src/components/Editor.tsx index 7bfa58c6..1a24118e 100644 --- a/packages/app-core/src/components/Editor.tsx +++ b/packages/app-core/src/components/Editor.tsx @@ -25,6 +25,7 @@ import { } from '../lib/wikilinks' import { openWikilinkHeading } from '../lib/wikilink-navigation' import { classifyLocalAssetHref, resolveAssetVaultRelativePath } from '../lib/local-assets' +import { externalLinkUrl, extractLinkAtCursor, resolveInternalNoteHref } from '../lib/internal-links' import { buildMoveNotePrompt, parseMoveNoteTarget, @@ -285,38 +286,8 @@ function syncVimKeymaps(overrides: KeymapOverrides): void { } } -function unwrapMdUrl(url: string): string { - // Markdown wraps URLs with spaces in angle brackets: `[x](<a b.pdf>)`. - const trimmed = url.trim() - if (trimmed.startsWith('<') && trimmed.endsWith('>')) return trimmed.slice(1, -1) - return trimmed -} - -function extractLinkAtCursor(doc: string, pos: number): string | null { - const lineStart = doc.lastIndexOf('\n', pos - 1) + 1 - const lineEnd = doc.indexOf('\n', pos) - const line = doc.slice(lineStart, lineEnd === -1 ? undefined : lineEnd) - const col = pos - lineStart - const wikiRe = /\[\[([^\]|]+?)(?:\|[^\]]+)?\]\]/g - let m: RegExpExecArray | null - while ((m = wikiRe.exec(line)) !== null) { - if (col >= m.index && col < m.index + m[0].length) return m[1] - } - // Angle-bracketed URLs can contain `)` so match them specifically first. - const mdAngleRe = /\[([^\]]*)\]\(<([^>]+)>\)/g - while ((m = mdAngleRe.exec(line)) !== null) { - if (col >= m.index && col < m.index + m[0].length) return m[2] - } - const mdRe = /\[([^\]]*)\]\(([^)]+)\)/g - while ((m = mdRe.exec(line)) !== null) { - if (col >= m.index && col < m.index + m[0].length) return unwrapMdUrl(m[2]) - } - const urlRe = /https?:\/\/[^\s)>\]]+/g - while ((m = urlRe.exec(line)) !== null) { - if (col >= m.index && col < m.index + m[0].length) return m[0] - } - return null -} +// `extractLinkAtCursor` lives in ../lib/internal-links so the editor, the +// preview, and the Cmd/Ctrl-click handler can all share it. /** * Report an ex-command error as a non-blocking, in-editor notification — the @@ -506,8 +477,9 @@ function registerVimCommands(): void { const target = extractLinkAtCursor(doc, pos) if (!target) return - if (/^https?:\/\//i.test(target)) { - window.open(target, '_blank') + const external = externalLinkUrl(target) + if (external) { + window.open(external, '_blank') return } @@ -543,6 +515,23 @@ function registerVimCommands(): void { return } + // A standard Markdown link whose href resolves relative to this note — + // e.g. `[text](../Projects/plan.md)` — that wikilink name matching can't + // reach. (#201) + const internal = resolveInternalNoteHref(state.selectedPath, target, notes) + if (internal) { + const focusEditorSoon = (): void => { + state.setFocusedPanel('editor') + requestAnimationFrame(() => useStore.getState().editorViewRef?.focus()) + } + if (internal.heading) { + void openWikilinkHeading(internal.path, internal.heading).then(focusEditorSoon) + } else { + void state.selectNote(internal.path).then(focusEditorSoon) + } + return + } + void promptApp({ title: `Create note for "${target}"?`, description: diff --git a/packages/app-core/src/components/EditorPane.tsx b/packages/app-core/src/components/EditorPane.tsx index d7172bd5..855f731c 100644 --- a/packages/app-core/src/components/EditorPane.tsx +++ b/packages/app-core/src/components/EditorPane.tsx @@ -78,6 +78,14 @@ import { wikilinkRenderExtension } from '../lib/cm-wikilink-render' import { slashCommandSource, slashCommandRender } from '../lib/cm-slash-commands' import { dateShortcutSource } from '../lib/cm-date-shortcuts' import { wikilinkSource, wikilinkHeadingSource } from '../lib/cm-wikilinks' +import { resolveWikilinkTarget, wikilinkHeadingAnchor } from '../lib/wikilinks' +import { openWikilinkHeading } from '../lib/wikilink-navigation' +import { + externalLinkUrl, + extractLinkAtCursor, + markdownLinkAt, + resolveInternalNoteHref +} from '../lib/internal-links' import { appMarkdownSnippetExtension } from '../lib/markdown-snippets-config' import { LazyDiagramTabView, LazyPreview as Preview } from './LazyPreview' import { ConnectionsPanel } from './ConnectionsPanel' @@ -597,6 +605,39 @@ function getEditorContextMenuPosition(view: EditorView): { x: number; y: number } } +/** + * Follow a link target extracted from the editor (Cmd/Ctrl-click): an external + * URL opens in the browser; a Markdown link to another note or a `[[wikilink]]` + * navigates, scrolling to its `#heading` when present. Returns false when the + * target resolves to nothing (so the click falls through to normal behavior). (#201) + */ +function followEditorLink(target: string): boolean { + const external = externalLinkUrl(target) + if (external) { + window.open(external, '_blank') + return true + } + const state = useStore.getState() + const focusSoon = (): void => { + state.setFocusedPanel('editor') + requestAnimationFrame(() => useStore.getState().editorViewRef?.focus()) + } + const internal = resolveInternalNoteHref(state.selectedPath, target, state.notes) + if (internal) { + if (internal.heading) void openWikilinkHeading(internal.path, internal.heading).then(focusSoon) + else void state.selectNote(internal.path).then(focusSoon) + return true + } + const wikilink = resolveWikilinkTarget(state.notes, target) + if (wikilink) { + const heading = wikilinkHeadingAnchor(target) + if (heading) void openWikilinkHeading(wikilink.path, heading).then(focusSoon) + else void state.selectNote(wikilink.path).then(focusSoon) + return true + } + return false +} + export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { const paneId = pane.id const isActive = useStore((s) => s.activePaneId === paneId) @@ -1418,8 +1459,37 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { completionNavKeymap, editorKeymapCompartment.of(buildEditorKeymap(s0.vimMode)), EditorView.domEventHandlers({ - mousedown: (event) => { + mousedown: (event, view) => { const target = event.target as HTMLElement | null + // Follow a Markdown link in live preview. A plain click follows a + // *rendered* link (the cursor is outside it, so its `(url)` syntax + // is hidden) — mirroring how `[[wikilinks]]` behave; clicking a + // link the cursor is already inside edits it instead. Cmd/Ctrl-click + // always follows (and reaches wikilinks too), gated to the primary + // button so a macOS Ctrl+click right-click doesn't trigger it. (#201) + if (event.button === 0 && !event.altKey && !event.shiftKey) { + const pos = view.posAtCoords({ x: event.clientX, y: event.clientY }) + if (pos != null) { + const doc = view.state.doc.toString() + if (event.metaKey || event.ctrlKey) { + const linkTarget = extractLinkAtCursor(doc, pos) + if (linkTarget && followEditorLink(linkTarget)) { + event.preventDefault() + return true + } + } else { + const link = markdownLinkAt(doc, pos) + if (link) { + const sel = view.state.selection.main + const rendered = sel.to < link.from || sel.from > link.to + if (rendered && followEditorLink(link.href)) { + event.preventDefault() + return true + } + } + } + } + } const marker = target?.closest<HTMLElement>('.cm-comment-marker[data-comment-id]') const commentId = marker?.dataset.commentId if (!commentId) return false diff --git a/packages/app-core/src/components/Preview.tsx b/packages/app-core/src/components/Preview.tsx index 1a311f15..48568baf 100644 --- a/packages/app-core/src/components/Preview.tsx +++ b/packages/app-core/src/components/Preview.tsx @@ -6,6 +6,7 @@ import { useStore } from "../store"; import { resolveAuto, THEMES } from "../lib/themes"; import { resolveWikilinkTarget, wikilinkHeadingAnchor } from "../lib/wikilinks"; import { openWikilinkHeading } from "../lib/wikilink-navigation"; +import { externalLinkUrl, resolveInternalNoteHref } from "../lib/internal-links"; import { toggleTaskAtIndex } from "../lib/tasklists"; import { enhanceLocalAssetNodes, @@ -550,6 +551,34 @@ export const Preview = memo(function Preview({ if (tag) void useStore.getState().openTagView(tag); return; } + // A standard Markdown link to another note — `[text](path/to/Note.md)` — + // navigates like a wikilink, resolved relative to this note. Checked + // before the asset branch: `enhanceLocalAssetNodes` may have tagged a + // relative link and rewritten its href, keeping the original in + // `data-local-asset-href`. (#201) + const linkHref = + anchor.dataset.localAssetHref || anchor.getAttribute("href") || ""; + const internalNote = resolveInternalNoteHref( + notePathRef.current, + linkHref, + notesRef.current, + ); + if (internalNote) { + e.preventDefault(); + if (internalNote.heading) + void openWikilinkHeading(internalNote.path, internalNote.heading); + else void selectNoteRef.current(internalNote.path); + return; + } + // An external web link — `[site](https://…)` or a bare `[site](google.com)` + // a user typed without a scheme — opens in the browser. Checked before the + // asset branch since a scheme-less domain looks like a relative path. (#201) + const external = externalLinkUrl(linkHref); + if (external) { + e.preventDefault(); + window.open(external, "_blank"); + return; + } const localAssetUrl = anchor.dataset.localAssetUrl; if (localAssetUrl) { e.preventDefault(); diff --git a/packages/app-core/src/lib/internal-links.test.ts b/packages/app-core/src/lib/internal-links.test.ts new file mode 100644 index 00000000..cdde4ea0 --- /dev/null +++ b/packages/app-core/src/lib/internal-links.test.ts @@ -0,0 +1,145 @@ +import { describe, expect, it } from 'vitest' +import { + externalLinkUrl, + extractLinkAtCursor, + markdownLinkAt, + resolveInternalNoteHref +} from './internal-links' + +const NOTES = [ + { path: 'Work/Documentation/Vault CLI Cheatsheet.md', folder: 'inbox' }, + { path: 'Work/Documentation/Another Note.md', folder: 'inbox' }, + { path: 'Work/Projects/plan.md', folder: 'inbox' }, + { path: 'index.md', folder: 'inbox' }, + { path: 'Archive/old plan.md', folder: 'trash' } +] + +describe('resolveInternalNoteHref', () => { + const from = 'Work/Documentation/Vault CLI Cheatsheet.md' + + it('resolves a same-folder relative link', () => { + expect(resolveInternalNoteHref(from, 'Another Note.md', NOTES)).toEqual({ + path: 'Work/Documentation/Another Note.md', + heading: null + }) + }) + + it('resolves a `../` relative link', () => { + expect(resolveInternalNoteHref(from, '../Projects/plan.md', NOTES)?.path).toBe( + 'Work/Projects/plan.md' + ) + }) + + it('resolves a vault-absolute (leading slash) link', () => { + expect(resolveInternalNoteHref(from, '/index.md', NOTES)?.path).toBe('index.md') + }) + + it('decodes percent-encoded spaces (Obsidian Markdown-link style)', () => { + expect(resolveInternalNoteHref(from, 'Another%20Note.md', NOTES)?.path).toBe( + 'Work/Documentation/Another Note.md' + ) + }) + + it('carries a #heading anchor', () => { + expect(resolveInternalNoteHref(from, 'Another%20Note.md#My%20Heading', NOTES)).toEqual({ + path: 'Work/Documentation/Another Note.md', + heading: 'My Heading' + }) + }) + + it('tolerates a missing .md extension', () => { + expect(resolveInternalNoteHref(from, 'Another Note', NOTES)?.path).toBe( + 'Work/Documentation/Another Note.md' + ) + }) + + it('falls back to a unique basename match when the path is off', () => { + // `plan.md` doesn't exist in this folder, but there's exactly one elsewhere. + expect(resolveInternalNoteHref(from, 'plan.md', NOTES)?.path).toBe('Work/Projects/plan.md') + }) + + it('returns null for external and in-page links', () => { + for (const href of [ + 'https://example.com', + 'http://x.test/a.md', + 'mailto:a@b.com', + '#heading', + '//cdn.test/x' + ]) { + expect(resolveInternalNoteHref(from, href, NOTES), href).toBeNull() + } + }) + + it('returns null when nothing matches (e.g. an asset or missing note)', () => { + expect(resolveInternalNoteHref(from, 'diagram.png', NOTES)).toBeNull() + expect(resolveInternalNoteHref(from, 'Nope.md', NOTES)).toBeNull() + }) + + it('never resolves to a trashed note', () => { + expect(resolveInternalNoteHref(from, '../../Archive/old plan.md', NOTES)).toBeNull() + }) + + it('returns null when the path escapes the vault', () => { + expect(resolveInternalNoteHref('index.md', '../secrets.md', NOTES)).toBeNull() + }) +}) + +describe('extractLinkAtCursor', () => { + it('pulls a Markdown link url under the cursor', () => { + const doc = 'see [the plan](Work/Projects/plan.md) here' + expect(extractLinkAtCursor(doc, doc.indexOf('plan.md'))).toBe('Work/Projects/plan.md') + }) + + it('pulls a wikilink target under the cursor', () => { + const doc = 'see [[Another Note]] here' + expect(extractLinkAtCursor(doc, doc.indexOf('Another'))).toBe('Another Note') + }) + + it('unwraps an angle-bracketed url with spaces', () => { + const doc = '[x](<a b.md>)' + expect(extractLinkAtCursor(doc, 2)).toBe('a b.md') + }) + + it('returns null when not inside a link', () => { + expect(extractLinkAtCursor('just text', 3)).toBeNull() + }) +}) + +describe('externalLinkUrl', () => { + it('keeps explicit web/scheme URLs as-is', () => { + expect(externalLinkUrl('https://google.com')).toBe('https://google.com') + expect(externalLinkUrl('http://x.test/a')).toBe('http://x.test/a') + expect(externalLinkUrl('mailto:a@b.com')).toBe('mailto:a@b.com') + }) + + it('promotes a bare domain a user typed without a scheme', () => { + expect(externalLinkUrl('google.com')).toBe('https://google.com') + expect(externalLinkUrl('www.example.com/path?q=1')).toBe('https://www.example.com/path?q=1') + }) + + it('is null for note links, relative paths, files and anchors', () => { + for (const href of [ + 'Another Note.md', + 'folder/Note.md', + '../x.md', + '/index.md', + 'image.png', + 'report.pdf', + '#heading' + ]) { + expect(externalLinkUrl(href), href).toBeNull() + } + }) +}) + +describe('markdownLinkAt', () => { + it('returns the href and source range of the link under the cursor', () => { + const doc = 'a [test](google.com) b' + const at = markdownLinkAt(doc, doc.indexOf('test')) + expect(at).toEqual({ href: 'google.com', from: 2, to: 2 + '[test](google.com)'.length }) + }) + + it('returns null when the cursor is outside any link', () => { + expect(markdownLinkAt('a [test](google.com) b', 0)).toBeNull() + }) +}) diff --git a/packages/app-core/src/lib/internal-links.ts b/packages/app-core/src/lib/internal-links.ts new file mode 100644 index 00000000..3027c1db --- /dev/null +++ b/packages/app-core/src/lib/internal-links.ts @@ -0,0 +1,199 @@ +/** + * Standard-Markdown links to other notes — `[text](path/to/Note.md)` — should + * navigate the same way `[[wikilinks]]` do (#201). Unlike a wikilink (resolved + * by global note name), a Markdown link's href is a path resolved RELATIVE to + * the note that contains it, exactly like Markdown / Obsidian's "Markdown + * links" mode. This module holds the pure resolution so the editor (`gd`, + * Cmd/Ctrl-click) and the rendered preview can all share it. + */ + +export interface InternalNoteLink { + /** Vault-relative path of the resolved note. */ + path: string + /** A `#heading` anchor carried by the link, or null. */ + heading: string | null +} + +interface NoteRef { + path: string + folder?: string +} + +function decode(value: string): string { + try { + return decodeURIComponent(value) + } catch { + return value + } +} + +function posixJoin(a: string, b: string): string { + if (!a) return b + if (!b) return a + return a.endsWith('/') ? `${a}${b}` : `${a}/${b}` +} + +function posixNormalize(input: string): string { + const out: string[] = [] + for (const part of input.split('/')) { + if (!part || part === '.') continue + if (part === '..') { + if (out.length === 0 || out[out.length - 1] === '..') out.push('..') + else out.pop() + } else { + out.push(part) + } + } + return out.join('/') +} + +const lc = (s: string): string => s.toLowerCase() + +/** External / non-note targets we must leave to their existing handlers. */ +function isExternalHref(href: string): boolean { + return ( + href.startsWith('#') || // same-note anchor + href.startsWith('//') || // protocol-relative URL + /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(href) // scheme: http:, mailto:, zen-asset:, … + ) +} + +function matchNote(notes: NoteRef[], target: string): string | null { + const visible = notes.filter((n) => n.folder !== 'trash') + // Markdown links usually carry the `.md`; tolerate links that omit it. + const candidates = /\.md$/i.test(target) ? [target] : [`${target}.md`, target] + for (const cand of candidates) { + const exact = visible.find((n) => lc(n.path) === lc(cand)) + if (exact) return exact.path + } + // Basename fallback for a single unambiguous match — tolerates a link + // written before the note moved, mirroring wikilink path-suffix resolution. + const wantBase = lc(candidates[0].split('/').pop() ?? '') + if (!wantBase) return null + const baseMatches = visible.filter((n) => lc(n.path.split('/').pop() ?? '') === wantBase) + return baseMatches.length === 1 ? baseMatches[0].path : null +} + +/** + * Resolve a Markdown link href to an internal note, relative to `notePath`. + * Returns null for external links, in-page anchors, assets, or no match. + */ +export function resolveInternalNoteHref( + notePath: string | null | undefined, + href: string, + notes: NoteRef[] +): InternalNoteLink | null { + if (!notePath) return null + const raw = href.trim() + if (!raw || isExternalHref(raw)) return null + + const hashIdx = raw.indexOf('#') + const rawPath = hashIdx >= 0 ? raw.slice(0, hashIdx) : raw + if (!rawPath) return null // pure "#heading" — same note, handled elsewhere + const heading = hashIdx >= 0 ? decode(raw.slice(hashIdx + 1)).trim() || null : null + + const decoded = decode(rawPath) + const noteDir = notePath.includes('/') ? notePath.slice(0, notePath.lastIndexOf('/')) : '' + let target = decoded.startsWith('/') + ? decoded.replace(/^\/+/, '') + : noteDir + ? posixJoin(noteDir, decoded) + : decoded + target = posixNormalize(target) + if (!target || target === '..' || target.startsWith('../')) return null + + const match = matchNote(notes, target) + return match ? { path: match, heading } : null +} + +function unwrapMdUrl(url: string): string { + // Markdown wraps URLs containing spaces in angle brackets: `[x](<a b.pdf>)`. + const trimmed = url.trim() + if (trimmed.startsWith('<') && trimmed.endsWith('>')) return trimmed.slice(1, -1) + return trimmed +} + +const LOCAL_FILE_EXT_RE = + /\.(md|markdown|txt|png|apng|avif|gif|jpe?g|svg|webp|pdf|mp3|m4a|aac|flac|ogg|wav|mp4|m4v|mov|ogv|webm|canvas|excalidraw)$/i + +/** + * A fully-qualified URL to open in the browser, or null. Handles explicit + * `http(s)://` / `mailto:` / `tel:` links AND bare domains a user typed without + * a scheme — e.g. `[site](google.com)` or `[docs](example.com/path)` — which + * Markdown would otherwise treat as a dead relative link. Returns null for note + * links, relative paths, in-page anchors, and local files. (#201) + */ +export function externalLinkUrl(href: string): string | null { + const h = href.trim() + if (!h) return null + if (/^(https?:|mailto:|tel:)/i.test(h)) return h + if (/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(h)) return null // another scheme — not ours + if (h.startsWith('#') || h.startsWith('/') || h.startsWith('.') || h.startsWith('//')) return null + // Bare domain heuristic: `host.tld` (one or more labels) optionally followed + // by a /path, ?query, or #fragment — but not something that looks like a + // local note/asset file. + const host = h.split(/[/?#]/)[0] ?? '' + if (!/^[a-z0-9-]+(\.[a-z0-9-]+)+$/i.test(host)) return null + if (LOCAL_FILE_EXT_RE.test(host)) return null + return `https://${h}` +} + +/** + * The link target at a document offset — a `[[wikilink]]` name, a Markdown + * link's URL, or a bare URL. Returns null when the offset isn't inside a link. + */ +export function extractLinkAtCursor(doc: string, pos: number): string | null { + const lineStart = doc.lastIndexOf('\n', pos - 1) + 1 + const lineEnd = doc.indexOf('\n', pos) + const line = doc.slice(lineStart, lineEnd === -1 ? undefined : lineEnd) + const col = pos - lineStart + const wikiRe = /\[\[([^\]|]+?)(?:\|[^\]]+)?\]\]/g + let m: RegExpExecArray | null + while ((m = wikiRe.exec(line)) !== null) { + if (col >= m.index && col < m.index + m[0].length) return m[1] + } + // Angle-bracketed URLs can contain `)` so match them specifically first. + const mdAngleRe = /\[([^\]]*)\]\(<([^>]+)>\)/g + while ((m = mdAngleRe.exec(line)) !== null) { + if (col >= m.index && col < m.index + m[0].length) return m[2] + } + const mdRe = /\[([^\]]*)\]\(([^)]+)\)/g + while ((m = mdRe.exec(line)) !== null) { + if (col >= m.index && col < m.index + m[0].length) return unwrapMdUrl(m[2]) + } + const urlRe = /https?:\/\/[^\s)>\]]+/g + while ((m = urlRe.exec(line)) !== null) { + if (col >= m.index && col < m.index + m[0].length) return m[0] + } + return null +} + +/** + * The Markdown link `[label](href)` covering a document offset, with its full + * source range. Used to follow a *rendered* link on a plain click (when the + * selection is outside the range) while still allowing edits when the cursor is + * inside it — mirroring how `[[wikilinks]]` behave in the editor. (#201) + */ +export function markdownLinkAt( + doc: string, + pos: number +): { href: string; from: number; to: number } | null { + const lineStart = doc.lastIndexOf('\n', pos - 1) + 1 + const lineEnd = doc.indexOf('\n', pos) + const line = doc.slice(lineStart, lineEnd === -1 ? undefined : lineEnd) + const col = pos - lineStart + const angleRe = /\[[^\]]*\]\(<([^>]+)>\)/g + let m: RegExpExecArray | null + while ((m = angleRe.exec(line)) !== null) { + if (col >= m.index && col < m.index + m[0].length) { + return { href: m[1], from: lineStart + m.index, to: lineStart + m.index + m[0].length } + } + } + const re = /\[[^\]]*\]\(([^)]+)\)/g + while ((m = re.exec(line)) !== null) { + if (col >= m.index && col < m.index + m[0].length) { + return { href: unwrapMdUrl(m[1]), from: lineStart + m.index, to: lineStart + m.index + m[0].length } + } + } + return null +} diff --git a/packages/app-core/src/lib/local-assets-md-link.test.ts b/packages/app-core/src/lib/local-assets-md-link.test.ts new file mode 100644 index 00000000..c0519338 --- /dev/null +++ b/packages/app-core/src/lib/local-assets-md-link.test.ts @@ -0,0 +1,68 @@ +// @vitest-environment jsdom + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +// Regression guard for #201: a standard-Markdown link to another note — +// `[text](Note.md)` — must NOT be rewritten into a `zen-asset://` URL by the +// asset enhancer (which made the preview open note links as broken asset tabs). +// Real asset links (images/PDFs) still get tagged. + +function installZen(): void { + Object.defineProperty(window, 'zen', { + configurable: true, + value: { + getCapabilities: vi.fn().mockReturnValue({ + supportsUpdater: false, + supportsNativeMenus: false, + supportsFloatingWindows: false, + supportsLocalFilesystemPickers: true, + supportsRemoteWorkspace: false, + supportsCliInstall: false, + supportsCustomTemplates: false + }), + resolveLocalAssetUrl: vi.fn((_root: string, _note: string, href: string) => `zen-asset://v/${href}`), + resolveVaultAssetUrl: vi.fn((_root: string, rel: string) => `zen-asset://v/${rel}`) + } + }) +} + +async function load() { + vi.resetModules() + localStorage.clear() + installZen() + const { useStore } = await import('../store') + const { enhanceLocalAssetNodes } = await import('./local-assets') + return { useStore, enhanceLocalAssetNodes } +} + +beforeEach(() => { + vi.restoreAllMocks() +}) + +describe('#201 — enhanceLocalAssetNodes leaves note links alone', () => { + it('tags a real asset link but not a `.md` note link', async () => { + const { useStore, enhanceLocalAssetNodes } = await load() + useStore.setState({ assetFiles: [{ path: 'Folder/pic.png' }] as never }) + + const root = document.createElement('div') + root.innerHTML = [ + '<a id="note" href="Another Note.md">a note</a>', + '<a id="noteh" href="Another%20Note.md#Heading">with heading</a>', + '<a id="asset" href="pic.png">an image</a>' + ].join('') + + enhanceLocalAssetNodes(root, { vaultRoot: '/v', notePath: 'Folder/b.md' }) + + const note = root.querySelector<HTMLAnchorElement>('#note')! + const noteh = root.querySelector<HTMLAnchorElement>('#noteh')! + const asset = root.querySelector<HTMLAnchorElement>('#asset')! + + // Note links keep their original href and are NOT tagged as assets. + expect(note.dataset.localAssetUrl).toBeUndefined() + expect(note.getAttribute('href')).toBe('Another Note.md') + expect(noteh.dataset.localAssetUrl).toBeUndefined() + + // The real image link still gets resolved + tagged. + expect(asset.dataset.localAssetUrl).toBe('zen-asset://v/Folder/pic.png') + }) +}) diff --git a/packages/app-core/src/lib/local-assets.ts b/packages/app-core/src/lib/local-assets.ts index c44b4dc7..5b99c7f1 100644 --- a/packages/app-core/src/lib/local-assets.ts +++ b/packages/app-core/src/lib/local-assets.ts @@ -1,4 +1,5 @@ import { useStore } from '../store' +import { externalLinkUrl } from './internal-links' const IMAGE_EXTENSIONS = new Set([ '.apng', @@ -398,6 +399,10 @@ export function enhanceLocalAssetNodes( root.querySelectorAll<HTMLAnchorElement>('a[href]').forEach((anchor) => { if (anchor.classList.contains('wikilink') || anchor.classList.contains('hashtag')) return const raw = anchor.getAttribute('href') || '' + // A `.md` link is a note link, and an external web link (`google.com`, + // `https://…`) isn't a vault asset — leave both for the link-navigation + // handlers instead of rewriting them to a zen-asset URL. (#201) + if (/\.md(?:[#?].*)?$/i.test(raw.trim()) || externalLinkUrl(raw)) return const resolved = resolveLocalAssetUrl(vaultRoot, notePath, raw) if (!resolved) return diff --git a/packages/app-core/src/styles/index.css b/packages/app-core/src/styles/index.css index 9fe4d408..f8bd5e4a 100644 --- a/packages/app-core/src/styles/index.css +++ b/packages/app-core/src/styles/index.css @@ -3878,6 +3878,11 @@ textarea, text-decoration: underline; text-decoration-color: rgb(var(--z-accent) / 0.4); text-underline-offset: 2px; + cursor: pointer; +} +.cm-wysiwyg .cm-editor .tok-link:hover, +.cm-wysiwyg .cm-editor .tok-url:hover { + text-decoration-color: rgb(var(--z-accent)); } /* Wikilinks — Obsidian internal-link style: accent underlined text, clickable From d35627ab6dc951cad48288fb4de32350f8a37d49 Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Fri, 19 Jun 2026 12:33:10 -0500 Subject: [PATCH 63/66] feat: live-sync database fields to record-page frontmatter + tidy metadata card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The database table is the source of truth for a record's properties, and a record page's frontmatter is a derived "metadata" mirror. Previously the frontmatter was only re-mirrored when the page was opened, so editing a cell or adding a field in the table left an already-open record page stale. Now table changes (a cell value, an added/renamed/removed field) re-mirror the frontmatter of any open record page live, preserving its body; pages that aren't open are still synced lazily on next open. The existing database write-guards keep this from looping through the file watcher. Also redesign the in-editor frontmatter rendering: instead of small grey text it reads as a compact properties card — a subtle rounded container, the `---` fences hidden into quiet rounded edges, and each key shown as a muted label beside its value. --- .../app-core/src/lib/cm-frontmatter.test.ts | 49 ++++++ packages/app-core/src/lib/cm-frontmatter.ts | 18 ++- .../app-core/src/store-database-sync.test.ts | 140 ++++++++++++++++++ packages/app-core/src/store.ts | 32 ++++ packages/app-core/src/styles/index.css | 39 ++++- 5 files changed, 269 insertions(+), 9 deletions(-) create mode 100644 packages/app-core/src/lib/cm-frontmatter.test.ts create mode 100644 packages/app-core/src/store-database-sync.test.ts diff --git a/packages/app-core/src/lib/cm-frontmatter.test.ts b/packages/app-core/src/lib/cm-frontmatter.test.ts new file mode 100644 index 00000000..e7e4cf78 --- /dev/null +++ b/packages/app-core/src/lib/cm-frontmatter.test.ts @@ -0,0 +1,49 @@ +// @vitest-environment jsdom + +import { EditorState } from '@codemirror/state' +import { EditorView } from '@codemirror/view' +import { afterEach, describe, expect, it } from 'vitest' +import { frontmatterStyle } from './cm-frontmatter' + +const views: EditorView[] = [] +function mount(doc: string): EditorView { + const parent = document.createElement('div') + document.body.append(parent) + const view = new EditorView({ + parent, + state: EditorState.create({ doc, extensions: [frontmatterStyle] }) + }) + views.push(view) + return view +} + +afterEach(() => { + while (views.length) views.pop()!.destroy() +}) + +describe('frontmatterStyle', () => { + it('builds line + key decorations for a record-page frontmatter without throwing', () => { + // The mixed line + mark decorations must be added in sorted order, or the + // RangeSetBuilder throws and breaks the editor for every note with + // frontmatter. Constructing the view exercises that. + const view = mount(['---', 'New field: test', 'New field 2:', '---', '', '# test'].join('\n')) + + // Property lines render as the metadata card body; the fences become caps. + expect(view.dom.querySelector('.cm-frontmatter-top')).not.toBeNull() + expect(view.dom.querySelector('.cm-frontmatter-bottom')).not.toBeNull() + // The key (before the `:`) is marked as a label. + const key = view.dom.querySelector('.cm-frontmatter-key') + expect(key?.textContent).toBe('New field') + }) + + it('no-ops a note without leading frontmatter', () => { + const view = mount('# Just a note\n\nbody') + expect(view.dom.querySelector('.cm-frontmatter-line')).toBeNull() + }) + + it('handles a value that itself contains a colon', () => { + const view = mount(['---', 'time: 12:30', '---', '', 'body'].join('\n')) + // The key stops at the FIRST colon. + expect(view.dom.querySelector('.cm-frontmatter-key')?.textContent).toBe('time') + }) +}) diff --git a/packages/app-core/src/lib/cm-frontmatter.ts b/packages/app-core/src/lib/cm-frontmatter.ts index d2fa3273..9321a1b4 100644 --- a/packages/app-core/src/lib/cm-frontmatter.ts +++ b/packages/app-core/src/lib/cm-frontmatter.ts @@ -15,7 +15,9 @@ import { } from '@codemirror/view' const FRONTMATTER_LINE = Decoration.line({ class: 'cm-frontmatter-line' }) -const FRONTMATTER_FENCE = Decoration.line({ class: 'cm-frontmatter-line cm-frontmatter-fence' }) +const FRONTMATTER_TOP = Decoration.line({ class: 'cm-frontmatter-line cm-frontmatter-top' }) +const FRONTMATTER_BOTTOM = Decoration.line({ class: 'cm-frontmatter-line cm-frontmatter-bottom' }) +const FRONTMATTER_KEY = Decoration.mark({ class: 'cm-frontmatter-key' }) function buildFrontmatterDeco(view: EditorView): DecorationSet { const builder = new RangeSetBuilder<Decoration>() @@ -32,7 +34,19 @@ function buildFrontmatterDeco(view: EditorView): DecorationSet { if (endLine === -1) return builder.finish() for (let i = 1; i <= endLine; i++) { const line = doc.line(i) - builder.add(line.from, line.from, i === 1 || i === endLine ? FRONTMATTER_FENCE : FRONTMATTER_LINE) + // Line decoration first (its start side sorts before any mark at the same + // offset), then the key mark for property lines. + builder.add( + line.from, + line.from, + i === 1 ? FRONTMATTER_TOP : i === endLine ? FRONTMATTER_BOTTOM : FRONTMATTER_LINE + ) + if (i !== 1 && i !== endLine) { + // Mark the key (text before the first `:`) so it reads as a muted label + // next to its value — a metadata panel, not a wall of text. + const colon = line.text.indexOf(':') + if (colon > 0) builder.add(line.from, line.from + colon, FRONTMATTER_KEY) + } } return builder.finish() } diff --git a/packages/app-core/src/store-database-sync.test.ts b/packages/app-core/src/store-database-sync.test.ts new file mode 100644 index 00000000..5604b8bd --- /dev/null +++ b/packages/app-core/src/store-database-sync.test.ts @@ -0,0 +1,140 @@ +// @vitest-environment jsdom + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +// Record pages mirror their database row's fields as frontmatter (the table is +// the source of truth). Editing the table — a cell value or an added field — +// must update an OPEN record page's frontmatter live, preserving its body. + +const CSV = 'Untitled Database.base/data.csv' +const PAGE = 'Untitled Database.base/pages/test.md' + +function baseDoc() { + return { + version: 1, + idFieldId: 'f_id', + activeViewId: 'v1', + fields: [ + { id: 'f_id', name: 'id', type: 'text' }, + { id: 'f_name', name: 'Name', type: 'text' }, + { id: 'f_a', name: 'New field', type: 'text' } + ], + views: [{ id: 'v1', type: 'table', name: 'Table', columnOrder: ['f_name', 'f_a'] }], + rows: [{ id: 'row1', cells: { f_id: 'row1', f_name: 'test', f_a: 'old' } }], + pages: { row1: PAGE } + } +} + +function pageNote(body: string) { + return { + path: PAGE, + title: 'test', + folder: 'inbox' as const, + siblingOrder: 0, + createdAt: 0, + updatedAt: 1, + size: body.length, + tags: [], + wikilinks: [], + assetEmbeds: [], + hasAttachments: false, + excerpt: '', + body + } +} + +function installZen(): void { + Object.defineProperty(window, 'zen', { + configurable: true, + value: { + getCapabilities: vi.fn().mockReturnValue({ + supportsUpdater: false, + supportsNativeMenus: false, + supportsFloatingWindows: false, + supportsLocalFilesystemPickers: true, + supportsRemoteWorkspace: false, + supportsCliInstall: false, + supportsCustomTemplates: false + }), + scanTasks: vi.fn().mockResolvedValue([]), + listNotes: vi.fn().mockResolvedValue([]), + listFolders: vi.fn().mockResolvedValue([]), + listLocalVaults: vi.fn().mockResolvedValue([]), + listAssets: vi.fn().mockResolvedValue([]), + hasAssetsDir: vi.fn().mockResolvedValue(false), + getRemoteWorkspaceInfo: vi.fn().mockResolvedValue(null), + getVaultSettings: vi.fn().mockResolvedValue({}), + writeNote: vi.fn().mockResolvedValue(pageNote('')), + writeDatabaseRows: vi.fn().mockResolvedValue(undefined), + writeDatabaseSchema: vi.fn().mockResolvedValue(undefined) + } + }) +} + +async function loadStore() { + vi.resetModules() + localStorage.clear() + installZen() + return import('./store') +} + +beforeEach(() => { + vi.restoreAllMocks() +}) + +describe('record page ↔ table sync (table is the source)', () => { + it('updates an open record page frontmatter when a table cell changes', async () => { + const { useStore } = await loadStore() + const doc = baseDoc() + useStore.setState({ + databases: { [CSV]: doc as never }, + noteContents: { [PAGE]: pageNote('---\nNew field: old\n---\n# test\n\nbody text\n') as never } + }) + + // Edit the "New field" cell from "old" → "new" in the table. + const next = { + ...doc, + rows: [{ id: 'row1', cells: { f_id: 'row1', f_name: 'test', f_a: 'new' } }] + } + useStore.getState().updateDatabaseRows(CSV, next as never) + + const body = useStore.getState().noteContents[PAGE]!.body + expect(body).toContain('New field: new') + expect(body).not.toContain('New field: old') + expect(body).toContain('# test') // body preserved + expect(body).toContain('body text') + }) + + it('adds a newly created field to an open record page frontmatter', async () => { + const { useStore } = await loadStore() + const doc = baseDoc() + useStore.setState({ + databases: { [CSV]: doc as never }, + noteContents: { [PAGE]: pageNote('---\nNew field: old\n---\n# test\n') as never } + }) + + // Add "New field 2" via a schema change. + const next = { + ...doc, + fields: [...doc.fields, { id: 'f_b', name: 'New field 2', type: 'text' }], + rows: [{ id: 'row1', cells: { f_id: 'row1', f_name: 'test', f_a: 'old', f_b: 'testing' } }] + } + useStore.getState().updateDatabaseSchema(CSV, next as never) + + const body = useStore.getState().noteContents[PAGE]!.body + expect(body).toContain('New field: old') + expect(body).toContain('New field 2: testing') + }) + + it('leaves a record page that is not open untouched (lazily synced later)', async () => { + const { useStore } = await loadStore() + const doc = baseDoc() + // No noteContents entry for PAGE → it isn't open. + useStore.setState({ databases: { [CSV]: doc as never }, noteContents: {} }) + + const next = { ...doc, rows: [{ id: 'row1', cells: { f_id: 'row1', f_name: 'test', f_a: 'new' } }] } + useStore.getState().updateDatabaseRows(CSV, next as never) + + expect(useStore.getState().noteContents[PAGE]).toBeUndefined() + }) +}) diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index 2b557eeb..d9c9c087 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -2137,6 +2137,36 @@ function scheduleDatabaseWrite( ) } +/** + * The database table is the source of truth for a record's properties; the + * record-page note's frontmatter is a derived "metadata" mirror. Whenever the + * table changes (a cell value, an added/renamed/removed field), re-mirror the + * frontmatter of any record page that's currently open so it updates live — + * preserving the page's body. Pages that aren't open are re-mirrored lazily the + * next time they're opened (see `openRecordPage`). + */ +function remirrorOpenRecordPages( + csvPath: string, + get: () => { + databases: Record<string, DatabaseDoc> + noteContents: Record<string, NoteContent> + updateNoteBody: (path: string, body: string) => void + } +): void { + const doc = get().databases[csvPath] + if (!doc?.pages) return + const { noteContents } = get() + for (const [rowId, pagePath] of Object.entries(doc.pages)) { + const current = noteContents[pagePath] + if (!current) continue // not open — re-mirrored on next open + const row = doc.rows.find((r) => r.id === rowId) + if (!row) continue + const { body } = parseFrontmatter(current.body) + const next = composePageBody(doc, row, body) + if (next !== current.body) get().updateNoteBody(pagePath, next) + } +} + function normalizeServerBaseUrl(value: string): string { const trimmed = value.trim() const normalized = /^https?:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}` @@ -3242,10 +3272,12 @@ export const useStore = create<Store>((set, get) => { updateDatabaseRows: (csvPath, next) => { set((s) => ({ databases: { ...s.databases, [csvPath]: next } })) scheduleDatabaseWrite(csvPath, 'rows', () => get().databases[csvPath]) + remirrorOpenRecordPages(csvPath, get) }, updateDatabaseSchema: (csvPath, next) => { set((s) => ({ databases: { ...s.databases, [csvPath]: next } })) scheduleDatabaseWrite(csvPath, 'schema', () => get().databases[csvPath]) + remirrorOpenRecordPages(csvPath, get) }, syncDatabaseFromDisk: async (csvPath) => { if (!get().databases[csvPath]) return diff --git a/packages/app-core/src/styles/index.css b/packages/app-core/src/styles/index.css index f8bd5e4a..177860c3 100644 --- a/packages/app-core/src/styles/index.css +++ b/packages/app-core/src/styles/index.css @@ -1741,19 +1741,44 @@ textarea, color: rgb(var(--z-fg)); } -/* Leading YAML frontmatter — render as compact, muted "properties" (see - * cm-frontmatter.ts) instead of full-size body text. */ +/* Leading YAML frontmatter — render as a compact "properties" metadata card + * (see cm-frontmatter.ts). For database record pages these values mirror the + * database fields, so it should read like a tidy property list, not body text. */ .cm-editor .cm-frontmatter-line { - font-size: 0.8em; - line-height: 1.55; + font-size: 0.84em; + line-height: 1.85; color: rgb(var(--z-grey-1)); - background: rgb(var(--z-bg-1) / 0.35); + background: rgb(var(--z-bg-1) / 0.5); + padding-left: 0.85em; + padding-right: 0.85em; + border-left: 1px solid rgb(var(--z-grey-0) / 0.13); + border-right: 1px solid rgb(var(--z-grey-0) / 0.13); } .cm-editor .cm-frontmatter-line :is(.tok-meta, .tok-string, .tok-keyword, .tok-atom) { color: inherit; } -.cm-editor .cm-frontmatter-fence { - color: rgb(var(--z-grey-0) / 0.5); +/* The key (before the `:`) is a muted label; the value keeps the normal color. */ +.cm-editor .cm-frontmatter-key, +.cm-editor .cm-frontmatter-key :is(.tok-meta, .tok-string, .tok-keyword, .tok-atom) { + color: rgb(var(--z-grey-0) / 0.62); + font-weight: 500; +} +/* The `---` fences become the card's quiet, rounded top/bottom edges: hide the + * dashes (transparent) but keep a thin capped row. */ +.cm-editor .cm-frontmatter-top, +.cm-editor .cm-frontmatter-bottom { + color: transparent; + font-size: 0.5em; +} +.cm-editor .cm-frontmatter-top { + border-top: 1px solid rgb(var(--z-grey-0) / 0.13); + border-top-left-radius: 8px; + border-top-right-radius: 8px; +} +.cm-editor .cm-frontmatter-bottom { + border-bottom: 1px solid rgb(var(--z-grey-0) / 0.13); + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; } .cm-editor .tok-heading, .cm-editor .tok-heading1, From 9e744c01cae5d33e90d5ba7f654ee19532bab915 Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Fri, 19 Jun 2026 14:44:00 -0500 Subject: [PATCH 64/66] feat: Notion-style selection toolbar with formatting + shortcuts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Selecting text in the editor now pops up a floating bubble toolbar: a "Turn into" block menu (Text, Heading 1–3, bulleted/numbered/to-do lists, quote, code), inline formatting (bold, italic, strikethrough, highlight, code, math), a link, and a comment. The footer shows the focused/hovered action's keyboard shortcut on the popup itself. Inline formats also have keyboard shortcuts that work on every platform, in or out of Vim mode — handled in VimNav's window-capture pass so they beat Vim's own Ctrl chords on Linux/Windows, with `Mod` resolving to ⌘ on macOS and Ctrl elsewhere: ⌘B bold, ⌘I italic, ⌘E code, ⌘K link, ⇧⌘S strikethrough, ⇧⌘H highlight, ⇧⌘M math. The toolbar is fully keyboard-navigable: `Mod+/` focuses it, then the arrows — or h/j/k/l in Vim mode — move row-aware (h/l across the format row, j/k between it and "Turn into"); Enter applies, Esc returns to text. `l`/→ on "Turn into" opens its submenu. New pure `cm-format` commands (toggleWrap / wrapLink / setBlockType) with unit tests; new toolbar icons; in-app Help updated. --- .../app-core/src/components/EditorPane.tsx | 88 +++-- .../src/components/EditorSelectionToolbar.tsx | 345 ++++++++++++++++++ packages/app-core/src/components/VimNav.tsx | 45 +++ packages/app-core/src/components/icons.tsx | 51 +++ packages/app-core/src/lib/cm-format.test.ts | 123 +++++++ packages/app-core/src/lib/cm-format.ts | 157 ++++++++ packages/app-core/src/lib/help.ts | 5 + 7 files changed, 767 insertions(+), 47 deletions(-) create mode 100644 packages/app-core/src/components/EditorSelectionToolbar.tsx create mode 100644 packages/app-core/src/lib/cm-format.test.ts create mode 100644 packages/app-core/src/lib/cm-format.ts diff --git a/packages/app-core/src/components/EditorPane.tsx b/packages/app-core/src/components/EditorPane.tsx index 855f731c..2b5aacc7 100644 --- a/packages/app-core/src/components/EditorPane.tsx +++ b/packages/app-core/src/components/EditorPane.tsx @@ -86,6 +86,8 @@ import { markdownLinkAt, resolveInternalNoteHref } from '../lib/internal-links' +import { setBlockType, toggleWrap, wrapLink } from '../lib/cm-format' +import { EditorSelectionToolbar } from './EditorSelectionToolbar' import { appMarkdownSnippetExtension } from '../lib/markdown-snippets-config' import { LazyDiagramTabView, LazyPreview as Preview } from './LazyPreview' import { ConnectionsPanel } from './ConnectionsPanel' @@ -530,8 +532,10 @@ function lineNumberExtension(mode: LineNumberMode): Extension { type TabDropIndicator = { path: string; position: 'before' | 'after' } | null type SelectionCommentAction = { x: number; y: number } | null -const COMMENT_ACTION_WIDTH = 34 -const COMMENT_ACTION_HEIGHT = 34 +// The selection bubble toolbar is centered over the selection (translateX -50%), +// so we only need a rough half-width to keep it on screen. +const SELECTION_TOOLBAR_HALF_WIDTH = 140 +const SELECTION_TOOLBAR_HEIGHT = 112 function clampViewport(value: number, min: number, max: number): number { return Math.max(min, Math.min(max, value)) @@ -553,43 +557,32 @@ function selectionEdgeCoords(view: EditorView): { ) } -function selectionEndCoords(view: EditorView): { - right: number - top: number - bottom: number -} | null { - const sel = view.state.selection.main - return ( - view.coordsAtPos(sel.to, -1) ?? - view.coordsAtPos(sel.to, 1) ?? - selectionEdgeCoords(view) - ) -} - function getSelectionCommentAction(view: EditorView): SelectionCommentAction { const sel = view.state.selection.main const active = document.activeElement - const hasFocus = view.hasFocus || (active instanceof Node && view.dom.contains(active)) + // Keep the toolbar up while the editor holds the selection OR while the user + // has tabbed into the toolbar itself (keyboard navigation). + const inToolbar = active instanceof Element && active.closest('[data-selection-toolbar]') != null + const hasFocus = + view.hasFocus || (active instanceof Node && view.dom.contains(active)) || inToolbar if (sel.empty || !hasFocus) return null - const coords = selectionEndCoords(view) - if (!coords) return null - const editorRect = view.scrollDOM.getBoundingClientRect() - const desiredX = coords.right + 10 - const maxX = Math.min( - editorRect.right - COMMENT_ACTION_WIDTH - 12, - window.innerWidth - COMMENT_ACTION_WIDTH - 8 - ) + const start = view.coordsAtPos(sel.from, 1) + const end = view.coordsAtPos(sel.to, -1) + if (!start || !end) return null + // Center the bubble horizontally over the selection; sit it just above the + // top of the selection, flipping below when there isn't room. + const centerX = (Math.min(start.left, end.left) + Math.max(start.right, end.right)) / 2 + const gap = 8 + const above = Math.min(start.top, end.top) - SELECTION_TOOLBAR_HEIGHT - gap + const below = Math.max(start.bottom, end.bottom) + gap + const y = above < 8 ? below : above return { x: clampViewport( - desiredX, - editorRect.left + 8, - Math.max(editorRect.left + 8, maxX) + centerX, + SELECTION_TOOLBAR_HALF_WIDTH + 8, + window.innerWidth - SELECTION_TOOLBAR_HALF_WIDTH - 8 ), - y: clampViewport( - coords.top + (coords.bottom - coords.top) / 2 - COMMENT_ACTION_HEIGHT / 2, - 8, - window.innerHeight - COMMENT_ACTION_HEIGHT - 8 - ) + y: clampViewport(y, 8, window.innerHeight - SELECTION_TOOLBAR_HEIGHT - 8) } } @@ -3365,23 +3358,24 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { selectionCommentAction && !commentDraft && !zenMode && ( - <button - type="button" - data-selection-comment-action - title="Add comment" - aria-label="Add comment to selected text" - onMouseDown={(event) => { - event.preventDefault() - event.stopPropagation() + <EditorSelectionToolbar + x={selectionCommentAction.x} + y={selectionCommentAction.y} + onWrap={(marker) => { + const view = viewRef.current + if (view) toggleWrap(view, marker) }} - onClick={() => { - captureCommentDraft() + onLink={() => { + const view = viewRef.current + if (view) wrapLink(view) }} - className="fixed z-50 flex h-[34px] w-[34px] items-center justify-center rounded-lg border border-paper-300/75 bg-paper-100/92 text-ink-700 shadow-[0_10px_24px_-18px_rgb(var(--z-shadow)/0.7),0_0_0_1px_rgb(var(--z-bg)/0.55)] backdrop-blur transition-colors hover:border-accent/45 hover:bg-paper-50 hover:text-accent" - style={{ left: selectionCommentAction.x, top: selectionCommentAction.y }} - > - <FeedbackIcon width={16} height={16} /> - </button> + onComment={() => captureCommentDraft()} + onBlockType={(type) => { + const view = viewRef.current + if (view) setBlockType(view, type) + }} + onDismiss={() => viewRef.current?.focus()} + /> )} {tabMenu && ( <ContextMenu diff --git a/packages/app-core/src/components/EditorSelectionToolbar.tsx b/packages/app-core/src/components/EditorSelectionToolbar.tsx new file mode 100644 index 00000000..eff3ba5c --- /dev/null +++ b/packages/app-core/src/components/EditorSelectionToolbar.tsx @@ -0,0 +1,345 @@ +import { useEffect, useRef, useState } from 'react' +import type { JSX, SVGProps } from 'react' +import type { BlockType } from '../lib/cm-format' +import { formatKeyToken } from '../lib/keymaps' +import { useStore } from '../store' +import { + BoldIcon, + CheckSquareIcon, + ChevronRightIcon, + CodeIcon, + FeedbackIcon, + HighlighterIcon, + ItalicIcon, + LinkIcon, + ListIcon, + SigmaIcon, + StrikethroughIcon +} from './icons' + +type IconCmp = (p: SVGProps<SVGSVGElement>) => JSX.Element + +interface Props { + x: number + y: number + /** Toggle a symmetric inline marker (`**`, `*`, `~~`, `` ` ``, `==`, `$`). */ + onWrap: (marker: string) => void + onLink: () => void + onComment: () => void + /** "Turn into" — re-type the selected line(s) as a block. */ + onBlockType: (type: BlockType) => void + /** Return focus to the editor (Escape / after acting via keyboard). */ + onDismiss: () => void +} + +// `binding` is in canonical modifier order so `formatKeyToken` shows it right. +const FORMATS: Array<{ label: string; marker: string; binding: string; Icon: IconCmp }> = [ + { label: 'Bold', marker: '**', binding: 'Mod+B', Icon: BoldIcon }, + { label: 'Italic', marker: '*', binding: 'Mod+I', Icon: ItalicIcon }, + { label: 'Strikethrough', marker: '~~', binding: 'Shift+Mod+S', Icon: StrikethroughIcon }, + { label: 'Highlight', marker: '==', binding: 'Shift+Mod+H', Icon: HighlighterIcon }, + { label: 'Code', marker: '`', binding: 'Mod+E', Icon: CodeIcon }, + { label: 'Math', marker: '$', binding: 'Shift+Mod+M', Icon: SigmaIcon } +] + +const BLOCKS: Array<{ type: BlockType; label: string; Icon?: IconCmp; glyph?: string }> = [ + { type: 'paragraph', label: 'Text', glyph: 'T' }, + { type: 'h1', label: 'Heading 1', glyph: 'H1' }, + { type: 'h2', label: 'Heading 2', glyph: 'H2' }, + { type: 'h3', label: 'Heading 3', glyph: 'H3' }, + { type: 'bullet', label: 'Bulleted list', Icon: ListIcon }, + { type: 'numbered', label: 'Numbered list', glyph: '1.' }, + { type: 'todo', label: 'To-do list', Icon: CheckSquareIcon }, + { type: 'quote', label: 'Quote', glyph: '“' }, + { type: 'code', label: 'Code', Icon: CodeIcon } +] + +const BTN = + 'flex h-8 w-8 items-center justify-center rounded-md text-ink-700 outline-none transition-colors hover:bg-paper-200/80 hover:text-accent focus-visible:bg-paper-200/90 focus-visible:text-accent' + +interface Hint { + label: string + binding?: string +} + +function BlockGlyph({ Icon, glyph }: { Icon?: IconCmp; glyph?: string }): JSX.Element { + if (Icon) return <Icon width={16} height={16} /> + return <span className="text-xs font-semibold tabular-nums">{glyph}</span> +} + +function KeyCap({ binding }: { binding: string }): JSX.Element { + return ( + <kbd className="rounded border border-paper-300 bg-paper-200/70 px-1.5 py-0.5 font-mono text-[11px] leading-none text-ink-600"> + {formatKeyToken(binding)} + </kbd> + ) +} + +/** + * A Notion-style floating "bubble" toolbar over a text selection: a "Turn into" + * block-type menu, quick inline formatting, a link, and a comment. The footer + * shows the focused/hovered action's keyboard shortcut. Fully keyboard-navigable + * (arrows / Enter / Esc) once focused via `Mod+/`. + */ +export function EditorSelectionToolbar({ + x, + y, + onWrap, + onLink, + onComment, + onBlockType, + onDismiss +}: Props): JSX.Element { + const [menuOpen, setMenuOpen] = useState(false) + const [menuIndex, setMenuIndex] = useState(0) + const [hint, setHint] = useState<Hint | null>(null) + // In Vim mode, h/j/k/l navigate the toolbar too (the controls aren't text + // inputs, so the letters are free to act as motions). + const vimMode = useStore((s) => s.vimMode) + // Roving focus over the toolbar's controls in render order: turn-into, + // formats…, link, comment. + const itemRefs = useRef<Array<HTMLButtonElement | null>>([]) + const menuRefs = useRef<Array<HTMLButtonElement | null>>([]) + const TURN_INTO_INDEX = 0 + const FORMAT_START = 1 // first control of the format row + // Remembered format-row column, so going Turn into → row → Turn into returns + // to where you were. + const lastFormatRef = useRef(FORMAT_START) + + useEffect(() => { + if (menuOpen) menuRefs.current[menuIndex]?.focus() + }, [menuOpen, menuIndex]) + + // Keep the editor's selection: never let a toolbar mousedown move focus/caret. + const keepSelection = (e: React.MouseEvent): void => { + e.preventDefault() + e.stopPropagation() + } + + const currentIndex = (): number => + itemRefs.current.findIndex((el) => el === document.activeElement) + + // h/l (← →): move across the format row; on the Turn-into row there's nothing + // to move to horizontally. + const moveHorizontal = (dir: 1 | -1): void => { + const els = itemRefs.current + const last = els.length - 1 + const cur = currentIndex() + if (cur < FORMAT_START) return + const count = last - FORMAT_START + 1 + if (count <= 0) return + const rel = (((cur - FORMAT_START + dir) % count) + count) % count + const next = FORMAT_START + rel + lastFormatRef.current = next + els[next]?.focus() + } + + // j/k (↓ ↑): move between the two rows — Turn into ↔ the format row, + // returning to the column you left. + const moveVertical = (): void => { + const els = itemRefs.current + const cur = currentIndex() + if (cur === TURN_INTO_INDEX) { + const col = Math.min(Math.max(lastFormatRef.current, FORMAT_START), els.length - 1) + els[col]?.focus() + } else { + if (cur >= FORMAT_START) lastFormatRef.current = cur + els[TURN_INTO_INDEX]?.focus() + } + } + + const onToolbarKeyDown = (e: React.KeyboardEvent): void => { + const k = e.key + if (menuOpen) { + const down = k === 'ArrowDown' || (vimMode && k === 'j') + const up = k === 'ArrowUp' || (vimMode && k === 'k') + const back = k === 'Escape' || (vimMode && k === 'h') + if (down) { + e.preventDefault() + setMenuIndex((i) => (i + 1) % BLOCKS.length) + } else if (up) { + e.preventDefault() + setMenuIndex((i) => (i - 1 + BLOCKS.length) % BLOCKS.length) + } else if (k === 'Enter' || k === ' ' || (vimMode && k === 'l')) { + e.preventDefault() + onBlockType(BLOCKS[menuIndex].type) + setMenuOpen(false) + onDismiss() + } else if (back) { + e.preventDefault() + e.stopPropagation() + setMenuOpen(false) + itemRefs.current[TURN_INTO_INDEX]?.focus() + } + return + } + const right = k === 'ArrowRight' || (vimMode && k === 'l') + const left = k === 'ArrowLeft' || (vimMode && k === 'h') + const down = k === 'ArrowDown' || (vimMode && k === 'j') + const up = k === 'ArrowUp' || (vimMode && k === 'k') + if (right) { + e.preventDefault() + // On the Turn-into row the `›` opens its submenu; elsewhere move across. + if (currentIndex() === TURN_INTO_INDEX) { + setMenuIndex(0) + setMenuOpen(true) + } else { + moveHorizontal(1) + } + } else if (left) { + e.preventDefault() + moveHorizontal(-1) + } else if (down || up) { + e.preventDefault() + moveVertical() + } else if (k === 'Escape') { + e.preventDefault() + e.stopPropagation() + onDismiss() + } + } + + // Assigns the next roving-order ref index as each control renders. + let idx = 0 + const ref = (): ((el: HTMLButtonElement | null) => void) => { + const at = idx++ + return (el) => { + itemRefs.current[at] = el + } + } + + return ( + <div + data-selection-toolbar + role="toolbar" + aria-label="Format selection" + onMouseDown={keepSelection} + onKeyDown={onToolbarKeyDown} + className="fixed z-50 flex w-[268px] flex-col rounded-xl bg-paper-100 p-1 text-ink-700 shadow-float ring-1 ring-paper-300" + style={{ left: x, top: y, transform: 'translateX(-50%)' }} + > + {/* Turn into */} + <button + ref={ref()} + data-toolbar-item + type="button" + className="flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-sm text-ink-700 outline-none transition-colors hover:bg-paper-200/80 focus-visible:bg-paper-200/90" + onMouseDown={keepSelection} + onMouseEnter={() => setHint({ label: 'Turn into' })} + onFocus={() => setHint({ label: 'Turn into' })} + onClick={() => (menuOpen ? setMenuOpen(false) : (setMenuIndex(0), setMenuOpen(true)))} + aria-expanded={menuOpen} + aria-haspopup="menu" + > + <ListIcon width={15} height={15} className="shrink-0 text-ink-500" /> + <span className="flex-1">Turn into</span> + <ChevronRightIcon + width={14} + height={14} + className={`shrink-0 text-ink-500 transition-transform ${menuOpen ? 'rotate-90' : ''}`} + /> + </button> + + <div className="my-1 h-px bg-paper-300/70" aria-hidden /> + + {/* Inline formatting */} + <div className="flex items-center gap-0.5"> + {FORMATS.map(({ label, marker, binding, Icon }) => ( + <button + key={marker} + ref={ref()} + data-toolbar-item + type="button" + title={`${label} · ${formatKeyToken(binding)}`} + aria-label={`${label} (${formatKeyToken(binding)})`} + aria-keyshortcuts={binding} + className={BTN} + onMouseDown={keepSelection} + onMouseEnter={() => setHint({ label, binding })} + onFocus={() => setHint({ label, binding })} + onClick={() => onWrap(marker)} + > + <Icon width={16} height={16} /> + </button> + ))} + <button + ref={ref()} + data-toolbar-item + type="button" + title={`Link · ${formatKeyToken('Mod+K')}`} + aria-label={`Insert link (${formatKeyToken('Mod+K')})`} + aria-keyshortcuts="Mod+K" + className={BTN} + onMouseDown={keepSelection} + onMouseEnter={() => setHint({ label: 'Link', binding: 'Mod+K' })} + onFocus={() => setHint({ label: 'Link', binding: 'Mod+K' })} + onClick={onLink} + > + <LinkIcon width={16} height={16} /> + </button> + <span className="mx-0.5 h-5 w-px shrink-0 bg-paper-300/70" aria-hidden /> + <button + ref={ref()} + data-toolbar-item + type="button" + title="Comment" + aria-label="Add comment to selection" + className={BTN} + onMouseDown={keepSelection} + onMouseEnter={() => setHint({ label: 'Comment' })} + onFocus={() => setHint({ label: 'Comment' })} + onClick={onComment} + > + <FeedbackIcon width={16} height={16} /> + </button> + </div> + + {/* Footer: shows the active action's keyboard shortcut on the popup. */} + <div className="mt-1 border-t border-paper-300/70 pt-1"> + <div className="flex h-6 items-center justify-between px-1.5 text-xs text-ink-500"> + <span className="truncate">{hint?.label ?? 'Format selection'}</span> + {hint?.binding ? ( + <KeyCap binding={hint.binding} /> + ) : ( + <span className="font-mono text-[11px] text-ink-400">{formatKeyToken('Mod+/')} nav</span> + )} + </div> + </div> + + {menuOpen && ( + <div + role="menu" + aria-label="Turn into" + className="absolute left-1 top-full z-10 mt-1 w-[200px] overflow-hidden rounded-lg bg-paper-100 p-1 shadow-float ring-1 ring-paper-300" + onMouseDown={keepSelection} + > + {BLOCKS.map(({ type, label, Icon, glyph }, i) => ( + <button + key={type} + ref={(el) => { + menuRefs.current[i] = el + }} + role="menuitem" + type="button" + className={`flex h-8 w-full items-center gap-2.5 rounded-md px-2 text-left text-sm text-ink-700 outline-none transition-colors hover:bg-paper-200/80 hover:text-accent focus-visible:bg-paper-200/90 focus-visible:text-accent ${ + i === menuIndex ? 'bg-paper-200/80' : '' + }`} + onMouseEnter={() => setMenuIndex(i)} + onMouseDown={keepSelection} + onClick={() => { + onBlockType(type) + setMenuOpen(false) + onDismiss() + }} + > + <span className="flex h-5 w-5 shrink-0 items-center justify-center text-ink-500"> + <BlockGlyph Icon={Icon} glyph={glyph} /> + </span> + <span className="flex-1">{label}</span> + </button> + ))} + </div> + )} + </div> + ) +} diff --git a/packages/app-core/src/components/VimNav.tsx b/packages/app-core/src/components/VimNav.tsx index 36b2aeb9..fb176456 100644 --- a/packages/app-core/src/components/VimNav.tsx +++ b/packages/app-core/src/components/VimNav.tsx @@ -20,8 +20,10 @@ import { getKeymapDisplay, getSequenceTokens, matchesSequenceToken, + matchesShortcutBinding, sequenceTokenFromEvent } from '../lib/keymaps' +import { toggleWrap, wrapLink } from '../lib/cm-format' import { ZEN_OPEN_EDITOR_CONTEXT_MENU_EVENT, dispatchKeyboardContextMenu, @@ -310,6 +312,9 @@ export function VimNav(): JSX.Element | null { // Never steal keys from normal text-entry fields such as the // inline note title, prompt inputs, or textarea-based controls. if (tag === 'INPUT' || tag === 'TEXTAREA') return + // The selection format toolbar handles its own keyboard navigation + // (arrows / Enter / Esc) once focused — yield to it entirely. + if (target?.closest('[data-selection-toolbar]')) return // The database/table view runs its own vim-style motion grid; yield to it // so sidebar/note-list navigation doesn't steal j/k/h/l etc. — EXCEPT the // pane prefix (Ctrl+W) and its pending direction key, so the grid can hand @@ -333,6 +338,46 @@ export function VimNav(): JSX.Element | null { const previewEl = getPreviewScrollElement() const hoverPreviewEl = getHoverPreviewScrollElement() + // Inline-format shortcuts (Bold/Italic/Strike/Highlight/Code/Math/Link) + // mirror the selection toolbar. Handled here — in the window capture + // handler — so they work on every platform and beat Vim's own Ctrl + // chords (e.g. <C-b>) in normal/visual mode on Linux/Windows. `Mod` + // resolves to ⌘ on macOS and Ctrl elsewhere. + const fmtView = state.editorViewRef + if (fmtView && isEditorFocused(fmtView)) { + // Focus the selection toolbar (when shown) for keyboard navigation. + if (matchesShortcutBinding(e, 'Mod+/')) { + const firstItem = document.querySelector<HTMLElement>( + '[data-selection-toolbar] [data-toolbar-item]' + ) + if (firstItem) { + e.preventDefault() + e.stopImmediatePropagation() + firstItem.focus() + return + } + } + // Bindings in canonical modifier order (Shift before Mod), matching + // `normalizeShortcutBinding` so `matchesShortcutBinding` compares equal. + const formats: Array<[string, () => void]> = [ + ['Mod+B', () => toggleWrap(fmtView, '**')], + ['Mod+I', () => toggleWrap(fmtView, '*')], + ['Mod+E', () => toggleWrap(fmtView, '`')], + ['Shift+Mod+S', () => toggleWrap(fmtView, '~~')], + ['Shift+Mod+H', () => toggleWrap(fmtView, '==')], + ['Shift+Mod+M', () => toggleWrap(fmtView, '$')], + ['Mod+K', () => wrapLink(fmtView)] + ] + for (const [binding, run] of formats) { + if (matchesShortcutBinding(e, binding)) { + e.preventDefault() + e.stopImmediatePropagation() + run() + return + } + } + } + const wantsJumpBack = matchesSequenceToken(e, overrides, 'vim.historyBack') const wantsJumpForward = matchesSequenceToken(e, overrides, 'vim.historyForward') if ( diff --git a/packages/app-core/src/components/icons.tsx b/packages/app-core/src/components/icons.tsx index 0f58c1c1..e664f1f6 100644 --- a/packages/app-core/src/components/icons.tsx +++ b/packages/app-core/src/components/icons.tsx @@ -361,3 +361,54 @@ export const FileDownIcon = (p: IconProps): JSX.Element => ( <path d="m9 14 3 3 3-3" /> </I> ) + +// --- inline-format toolbar icons (selection bubble) ----------------------- +export const BoldIcon = (p: IconProps): JSX.Element => ( + <I {...p}> + <path d="M6 4h7a4 4 0 0 1 0 8H6z" /> + <path d="M6 12h8a4 4 0 0 1 0 8H6z" /> + </I> +) + +export const ItalicIcon = (p: IconProps): JSX.Element => ( + <I {...p}> + <line x1="19" y1="4" x2="10" y2="4" /> + <line x1="14" y1="20" x2="5" y2="20" /> + <line x1="15" y1="4" x2="9" y2="20" /> + </I> +) + +export const StrikethroughIcon = (p: IconProps): JSX.Element => ( + <I {...p}> + <path d="M16 4H9a3 3 0 0 0-2.83 4" /> + <path d="M14 12a4 4 0 0 1 0 8H6" /> + <line x1="4" y1="12" x2="20" y2="12" /> + </I> +) + +export const HighlighterIcon = (p: IconProps): JSX.Element => ( + <I {...p}> + <path d="m9 11-6 6v3h3l6-6" /> + <path d="m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4" /> + </I> +) + +export const CodeIcon = (p: IconProps): JSX.Element => ( + <I {...p}> + <polyline points="16 18 22 12 16 6" /> + <polyline points="8 6 2 12 8 18" /> + </I> +) + +export const SigmaIcon = (p: IconProps): JSX.Element => ( + <I {...p}> + <path d="M18 7V5a1 1 0 0 0-1-1H7l5 8-5 8h10a1 1 0 0 0 1-1v-2" /> + </I> +) + +export const LinkIcon = (p: IconProps): JSX.Element => ( + <I {...p}> + <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> + <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /> + </I> +) diff --git a/packages/app-core/src/lib/cm-format.test.ts b/packages/app-core/src/lib/cm-format.test.ts new file mode 100644 index 00000000..fcc2e447 --- /dev/null +++ b/packages/app-core/src/lib/cm-format.test.ts @@ -0,0 +1,123 @@ +// @vitest-environment jsdom + +import { EditorSelection, EditorState } from '@codemirror/state' +import { EditorView } from '@codemirror/view' +import { afterEach, describe, expect, it } from 'vitest' +import { setBlockType, toggleWrap, wrapLink } from './cm-format' + +const views: EditorView[] = [] +function mount(doc: string, from: number, to: number): EditorView { + const parent = document.createElement('div') + document.body.append(parent) + const view = new EditorView({ + parent, + state: EditorState.create({ doc, selection: EditorSelection.range(from, to) }) + }) + views.push(view) + return view +} + +afterEach(() => { + while (views.length) views.pop()!.destroy() +}) + +describe('toggleWrap', () => { + it('wraps the selection and keeps it selected', () => { + const view = mount('hello world', 6, 11) // "world" + toggleWrap(view, '**') + expect(view.state.doc.toString()).toBe('hello **world**') + expect(view.state.sliceDoc(view.state.selection.main.from, view.state.selection.main.to)).toBe( + 'world' + ) + }) + + it('unwraps when the markers sit just outside the selection', () => { + const view = mount('hello **world**', 8, 13) // "world" inside the **…** + toggleWrap(view, '**') + expect(view.state.doc.toString()).toBe('hello world') + }) + + it('unwraps when the selection itself includes the markers', () => { + const view = mount('a *italic* b', 2, 10) // "*italic*" + toggleWrap(view, '*') + expect(view.state.doc.toString()).toBe('a italic b') + }) + + it('inserts an empty pair with the cursor between on an empty selection', () => { + const view = mount('x', 1, 1) + toggleWrap(view, '==') + expect(view.state.doc.toString()).toBe('x====') + expect(view.state.selection.main.empty).toBe(true) + expect(view.state.selection.main.head).toBe(3) // between == and == + }) + + it('works for the other markers', () => { + for (const [marker, expected] of [ + ['~~', 'a ~~b~~ c'], + ['`', 'a `b` c'], + ['$', 'a $b$ c'] + ] as const) { + const view = mount('a b c', 2, 3) + toggleWrap(view, marker) + expect(view.state.doc.toString()).toBe(expected) + } + }) +}) + +describe('setBlockType', () => { + it('turns a paragraph into a heading', () => { + const view = mount('hello', 0, 5) + setBlockType(view, 'h1') + expect(view.state.doc.toString()).toBe('# hello') + }) + + it('re-types an existing block, replacing its marker', () => { + const view = mount('## hello', 0, 0) + setBlockType(view, 'h3') + expect(view.state.doc.toString()).toBe('### hello') + }) + + it('turns a list item back into plain text', () => { + const view = mount('- item', 0, 0) + setBlockType(view, 'paragraph') + expect(view.state.doc.toString()).toBe('item') + }) + + it('makes a to-do and a quote', () => { + const todo = mount('task', 0, 4) + setBlockType(todo, 'todo') + expect(todo.state.doc.toString()).toBe('- [ ] task') + + const quote = mount('wisdom', 0, 6) + setBlockType(quote, 'quote') + expect(quote.state.doc.toString()).toBe('> wisdom') + }) + + it('numbers a multi-line selection sequentially', () => { + const view = mount('a\nb\nc', 0, 5) + setBlockType(view, 'numbered') + expect(view.state.doc.toString()).toBe('1. a\n2. b\n3. c') + }) + + it('preserves indentation when re-typing', () => { + const view = mount(' - nested', 0, 0) + setBlockType(view, 'bullet') + expect(view.state.doc.toString()).toBe(' - nested') + }) + + it('wraps the selection in a fenced code block', () => { + const view = mount('print(1)', 0, 8) + setBlockType(view, 'code') + expect(view.state.doc.toString()).toBe('```\nprint(1)\n```') + }) +}) + +describe('wrapLink', () => { + it('wraps the selection as [text]() with the cursor in the parens', () => { + const view = mount('see docs here', 4, 8) // "docs" + wrapLink(view) + expect(view.state.doc.toString()).toBe('see [docs]() here') + // Cursor sits between ( and ): after "see [docs](" = index 11. + expect(view.state.selection.main.head).toBe(11) + }) +}) diff --git a/packages/app-core/src/lib/cm-format.ts b/packages/app-core/src/lib/cm-format.ts new file mode 100644 index 00000000..5b932313 --- /dev/null +++ b/packages/app-core/src/lib/cm-format.ts @@ -0,0 +1,157 @@ +/** + * Inline Markdown formatting commands for the selection bubble toolbar: toggle a + * symmetric marker (`**` bold, `*` italic, `~~` strike, `` ` `` code, `==` + * highlight, `$` math) around the selection, or wrap it as a link. (#201-style + * quick-format affordance.) + */ +import { EditorSelection } from '@codemirror/state' +import type { EditorView } from '@codemirror/view' + +/** + * Toggle a symmetric inline marker around each selection range: wrap when it + * isn't wrapped, unwrap when the markers already sit just outside (or just + * inside) the selection. + */ +export function toggleWrap(view: EditorView, marker: string): boolean { + const m = marker + view.dispatch( + view.state.changeByRange((range) => { + const { from, to } = range + if (from === to) { + // No selection: insert the pair and drop the cursor between them. + return { + changes: { from, insert: m + m }, + range: EditorSelection.cursor(from + m.length) + } + } + const before = view.state.sliceDoc(Math.max(0, from - m.length), from) + const after = view.state.sliceDoc(to, Math.min(view.state.doc.length, to + m.length)) + if (before === m && after === m) { + // Unwrap: drop the markers just outside the selection. + return { + changes: [ + { from: from - m.length, to: from, insert: '' }, + { from: to, to: to + m.length, insert: '' } + ], + range: EditorSelection.range(from - m.length, to - m.length) + } + } + const selected = view.state.sliceDoc(from, to) + if (selected.length >= m.length * 2 && selected.startsWith(m) && selected.endsWith(m)) { + // The selection itself includes the markers — strip them from inside. + return { + changes: { from, to, insert: selected.slice(m.length, selected.length - m.length) }, + range: EditorSelection.range(from, to - m.length * 2) + } + } + // Wrap. + return { + changes: [ + { from, insert: m }, + { from: to, insert: m } + ], + range: EditorSelection.range(from + m.length, to + m.length) + } + }) + ) + view.focus() + return true +} + +/** + * The block types offered by the selection toolbar's "Turn into" menu — a + * lighter version of Notion's block menu. + */ +export type BlockType = + | 'paragraph' + | 'h1' + | 'h2' + | 'h3' + | 'bullet' + | 'numbered' + | 'todo' + | 'quote' + | 'code' + +// Leading block marker (indent captured separately): heading, quote, list +// bullet (optionally a task checkbox), or an ordered-list number. +const LINE_MARKER_RE = /^(\s*)(?:#{1,6}\s+|>\s+|[-*+]\s+\[[ xX]\]\s+|[-*+]\s+|\d+[.)]\s+)?/ + +function blockPrefix(type: BlockType, index: number): string { + switch (type) { + case 'h1': + return '# ' + case 'h2': + return '## ' + case 'h3': + return '### ' + case 'bullet': + return '- ' + case 'numbered': + return `${index + 1}. ` + case 'todo': + return '- [ ] ' + case 'quote': + return '> ' + default: + return '' // paragraph + } +} + +/** + * Turn the line(s) touched by the selection into a block of `type`: re-prefix + * each line (stripping any existing heading/list/quote marker), or wrap them in + * a fenced code block. "paragraph" just removes the marker. + */ +export function setBlockType(view: EditorView, type: BlockType): boolean { + const { state } = view + const sel = state.selection.main + const firstLine = state.doc.lineAt(sel.from) + const lastLine = state.doc.lineAt(sel.to) + + if (type === 'code') { + const text = state.sliceDoc(firstLine.from, lastLine.to) + const insert = '```\n' + text + '\n```' + view.dispatch({ + changes: { from: firstLine.from, to: lastLine.to, insert }, + selection: EditorSelection.range(firstLine.from + 4, firstLine.from + 4 + text.length) + }) + view.focus() + return true + } + + const changes: Array<{ from: number; to: number; insert: string }> = [] + let index = 0 + for (let ln = firstLine.number; ln <= lastLine.number; ln++) { + const line = state.doc.line(ln) + if (line.text.trim() === '') continue + const m = line.text.match(LINE_MARKER_RE) + const indent = m?.[1] ?? '' + const body = line.text.slice(m?.[0].length ?? 0) + const next = indent + blockPrefix(type, index) + body + index++ + if (next !== line.text) changes.push({ from: line.from, to: line.to, insert: next }) + } + if (changes.length > 0) view.dispatch({ changes }) + view.focus() + return true +} + +/** + * Wrap each selection as a Markdown link `[text](url)`, leaving the cursor in + * the empty `()` so the URL can be typed. An empty selection inserts `[]()`. + */ +export function wrapLink(view: EditorView): boolean { + view.dispatch( + view.state.changeByRange((range) => { + const { from, to } = range + const text = view.state.sliceDoc(from, to) + const insert = `[${text}]()` + // Cursor between the parentheses: after `[text](`. + const cursor = from + 1 + text.length + 2 + return { changes: { from, to, insert }, range: EditorSelection.cursor(cursor) } + }) + ) + view.focus() + return true +} diff --git a/packages/app-core/src/lib/help.ts b/packages/app-core/src/lib/help.ts index 8a361aef..d319cfcf 100644 --- a/packages/app-core/src/lib/help.ts +++ b/packages/app-core/src/lib/help.ts @@ -53,6 +53,11 @@ export const HELP_QUICK_START: HelpCard[] = [ body: 'Type `/` to insert headings, lists, callouts, code blocks, tables, links, images, and other markdown structures. Type `@` to insert date shortcuts like Today and Tomorrow as ISO dates.' }, + { + title: 'Format a selection', + body: + 'Select text to pop up a formatting toolbar — bold, italic, strikethrough, highlight, code, math, link, comment, and a “Turn into” menu that re-types the block (Text, Heading 1–3, lists, quote, code). The same actions have keyboard shortcuts that work on every platform, in or out of Vim mode: Mod+B bold, Mod+I italic, Mod+E code, Mod+K link, Shift+Mod+S strikethrough, Shift+Mod+H highlight, Shift+Mod+M math (Mod is ⌘ on macOS, Ctrl on Windows/Linux). Press Mod+/ to focus the toolbar and walk it with the arrow keys; Enter applies, Esc returns to the text.' + }, { title: 'Switch between write and read modes', body: From 5c0d4f3c76b9b4e8ac4cd9b729d784e02d74c85a Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Fri, 19 Jun 2026 16:19:00 -0500 Subject: [PATCH 65/66] =?UTF-8?q?fix:=20recognize=20tags=20in=20any=20scri?= =?UTF-8?q?pt=20=E2=80=94=20Cyrillic,=20CJK,=20etc.=20(#205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hashtags required an ASCII letter to start (`#[a-zA-Z]…`, with `\w` for the rest), so `#тест`, `#ошибка`, `#标签` and other non-Latin tags were never recognized and rendered as plain text. Make the tag character classes Unicode-aware everywhere — first char `\p{L}` (a letter in any script), rest letters/digits/`_`/`-`/`/` — keeping behavior otherwise identical (a heading `# x` still isn't a tag; a hex colour like `#1971c2` still isn't, since the first char must be a letter). Updated in lockstep across the preview, editor pills, inline render, live sidebar extraction, tag rename, the desktop scanner + MCP, shared task tags, and the Go server so desktop and the self-hosted/web client stay byte-for-byte. Adds regression tests: the editor renders Cyrillic/CJK tags as pills, and the Go scanner extracts them. --- apps/desktop/src/main/vault.ts | 2 +- apps/desktop/src/mcp/vault-ops.ts | 4 ++-- apps/server/internal/vault/parse.go | 4 ++-- apps/server/internal/vault/parse_test.go | 15 +++++++++++++++ packages/app-core/src/lib/cm-hashtags.test.ts | 7 +++++++ packages/app-core/src/lib/cm-hashtags.ts | 5 +++-- packages/app-core/src/lib/inline-markdown.tsx | 2 +- packages/app-core/src/lib/markdown.ts | 2 +- packages/app-core/src/lib/tags.ts | 6 +++--- packages/app-core/src/store.ts | 5 +++-- packages/shared-domain/src/tasks.ts | 3 ++- 11 files changed, 40 insertions(+), 15 deletions(-) diff --git a/apps/desktop/src/main/vault.ts b/apps/desktop/src/main/vault.ts index 0a018d5a..7d633ecf 100644 --- a/apps/desktop/src/main/vault.ts +++ b/apps/desktop/src/main/vault.ts @@ -1421,7 +1421,7 @@ function localAssetTargetKind(target: string): ImportedAssetKind | null { function extractTags(body: string): string[] { if (!body.includes('#')) return [] const stripped = stripCodeContent(body) - const matches = stripped.match(/(?:^|\s)#([a-zA-Z][\w\-/]*)/g) || [] + const matches = stripped.match(/(?:^|\s)#(\p{L}[\p{L}\d_/-]*)/gu) || [] const seen = new Set<string>() for (const m of matches) seen.add(m.trim().slice(1)) return [...seen] diff --git a/apps/desktop/src/mcp/vault-ops.ts b/apps/desktop/src/mcp/vault-ops.ts index 627514ca..755ae67a 100644 --- a/apps/desktop/src/mcp/vault-ops.ts +++ b/apps/desktop/src/mcp/vault-ops.ts @@ -357,7 +357,7 @@ function stripCodeContent(body: string): string { function extractTags(body: string): string[] { const stripped = stripCodeContent(body) - const matches = stripped.match(/(?:^|\s)#([a-zA-Z][\w\-/]*)/g) || [] + const matches = stripped.match(/(?:^|\s)#(\p{L}[\p{L}\d_/-]*)/gu) || [] const seen = new Set<string>() for (const m of matches) seen.add(m.trim().slice(1)) return [...seen] @@ -832,7 +832,7 @@ const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?/ const INLINE_DUE_RE = /(?:^|\s)due:(\S+)/i const INLINE_PRIORITY_RE = /(?:^|\s)!(high|med|medium|low|h|m|l)\b/i const INLINE_WAITING_RE = /(?:^|\s)@waiting\b/i -const INLINE_TAG_RE = /(?:^|\s)#([a-z0-9][a-z0-9/_-]*)/gi +const INLINE_TAG_RE = /(?:^|\s)#([\p{L}\d][\p{L}\d/_-]*)/gu function normalizePriority(raw: string | undefined): 'high' | 'med' | 'low' | undefined { if (!raw) return undefined diff --git a/apps/server/internal/vault/parse.go b/apps/server/internal/vault/parse.go index 9310b8b9..60d08b65 100644 --- a/apps/server/internal/vault/parse.go +++ b/apps/server/internal/vault/parse.go @@ -12,7 +12,7 @@ import ( var ( fencedBlockRe = regexp.MustCompile("(?m)(^|\n)```[^\n]*\n[\\s\\S]*?\n```[ \t]*(?:\n|$)") inlineCodeRe = regexp.MustCompile("`[^`\n]*`") - tagRe = regexp.MustCompile(`(?:^|\s)#([A-Za-z][\w\-/]*)`) + tagRe = regexp.MustCompile(`(?:^|\s)#(\p{L}[\p{L}\d_/-]*)`) wikilinkRe = regexp.MustCompile(`(!?)\[\[([^\]|]+?)(?:\|[^\]]+)?\]\]`) linkRe = regexp.MustCompile(`(!?)\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)`) embedRe = regexp.MustCompile(`!\[\[([^\]|]+?)(?:\|[^\]]+)?\]\]`) @@ -199,7 +199,7 @@ var ( inlineDueRe = regexp.MustCompile(`(?i)(?:^|\s)due:(\S+)`) inlinePriority = regexp.MustCompile(`(?i)(?:^|\s)!(high|med|medium|low|h|m|l)\b`) inlineWaitingRe = regexp.MustCompile(`(?i)(?:^|\s)@waiting\b`) - inlineTagRe = regexp.MustCompile(`(?i)(?:^|\s)#([a-z0-9][a-z0-9/_\-]*)`) + inlineTagRe = regexp.MustCompile(`(?:^|\s)#([\p{L}\d][\p{L}\d/_\-]*)`) isoDateRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) ) diff --git a/apps/server/internal/vault/parse_test.go b/apps/server/internal/vault/parse_test.go index e6ae0887..71c4fff0 100644 --- a/apps/server/internal/vault/parse_test.go +++ b/apps/server/internal/vault/parse_test.go @@ -57,3 +57,18 @@ func TestExtractorsStillIgnoreCodeAfterFastPathGuards(t *testing.T) { t.Fatalf("ExtractWikilinks() = %#v, want [Target]", wikilinks) } } + +// #205: tags in non-Latin scripts (Cyrillic, CJK, …) must be recognized. +func TestExtractTagsUnicode(t *testing.T) { + body := "Заметки: #тест #ошибка/баг и 笔记 #标签 plus #ascii-1 done" + got := ExtractTags(body) + want := map[string]bool{"тест": true, "ошибка/баг": true, "标签": true, "ascii-1": true} + if len(got) != len(want) { + t.Fatalf("ExtractTags() = %#v, want keys %#v", got, want) + } + for _, tag := range got { + if !want[tag] { + t.Fatalf("unexpected tag %q in %#v", tag, got) + } + } +} diff --git a/packages/app-core/src/lib/cm-hashtags.test.ts b/packages/app-core/src/lib/cm-hashtags.test.ts index f572df20..2e3e1ae0 100644 --- a/packages/app-core/src/lib/cm-hashtags.test.ts +++ b/packages/app-core/src/lib/cm-hashtags.test.ts @@ -30,6 +30,13 @@ describe('hashtagPlugin', () => { view.destroy() }) + it('recognizes non-Latin tags — Cyrillic, CJK (#205)', () => { + const view = mount('Заметки: #тест #ошибка и 笔记 #标签 done') + const tags = Array.from(view.dom.querySelectorAll('.cm-hashtag')).map((e) => e.textContent) + expect(tags).toEqual(['#тест', '#ошибка', '#标签']) + view.destroy() + }) + it('skips `#` inside code and headings', () => { const view = mount('# Heading #notatag\n\nuse `#include` here\n\n#real') const tags = Array.from(view.dom.querySelectorAll('.cm-hashtag')).map( diff --git a/packages/app-core/src/lib/cm-hashtags.ts b/packages/app-core/src/lib/cm-hashtags.ts index b6dcb226..3d7cc34b 100644 --- a/packages/app-core/src/lib/cm-hashtags.ts +++ b/packages/app-core/src/lib/cm-hashtags.ts @@ -19,8 +19,9 @@ import { import { useStore } from '../store' // Same shape as the Preview regex: a `#`, preceded by start-of-text or -// whitespace, then a letter and word/`-`/`/` characters. -const HASHTAG_RE = /(^|\s)#([a-zA-Z][\w\-/]*)/g +// whitespace, then any Unicode letter (Cyrillic/CJK/… included, #205) followed +// by letters, digits, `_`, `-`, or `/`. +const HASHTAG_RE = /(^|\s)#(\p{L}[\p{L}\d_/-]*)/gu /** True when `pos` sits inside a code span/block or a heading — contexts where * a `#` isn't a tag (`#include`, `# Heading`, …). */ diff --git a/packages/app-core/src/lib/inline-markdown.tsx b/packages/app-core/src/lib/inline-markdown.tsx index 4419b82a..0533fd17 100644 --- a/packages/app-core/src/lib/inline-markdown.tsx +++ b/packages/app-core/src/lib/inline-markdown.tsx @@ -96,7 +96,7 @@ const MATCHERS: Matcher[] = [ // `#tag` tokens are pulled out of plain-text runs only — never from inside // code spans or URLs. Mirrors the boundary rule the note renderer uses. -const TAG_RE = /(^|\s)#([a-zA-Z][\w/-]*)/g +const TAG_RE = /(^|\s)#(\p{L}[\p{L}\d_/-]*)/gu function pushText(tokens: InlineToken[], text: string): void { if (!text) return diff --git a/packages/app-core/src/lib/markdown.ts b/packages/app-core/src/lib/markdown.ts index b026ced2..7affc6dd 100644 --- a/packages/app-core/src/lib/markdown.ts +++ b/packages/app-core/src/lib/markdown.ts @@ -202,7 +202,7 @@ function remarkHashtags() { if (p.type === 'link' || p.type === 'linkReference' || p.type === 'heading') return const value = (node as { value: string }).value if (!value.includes('#')) return - const regex = /(^|\s)#([a-zA-Z][\w\-/]*)/g + const regex = /(^|\s)#(\p{L}[\p{L}\d_/-]*)/gu const next: AnyNode[] = [] let last = 0 let m: RegExpExecArray | null diff --git a/packages/app-core/src/lib/tags.ts b/packages/app-core/src/lib/tags.ts index 1f981061..b1054463 100644 --- a/packages/app-core/src/lib/tags.ts +++ b/packages/app-core/src/lib/tags.ts @@ -7,8 +7,8 @@ * Rules: * - The hash must be preceded by start-of-line or whitespace (so * `me#tag` and `url.com/#x` don't match). - * - The first tag character must be a letter, the rest can be - * letters, digits, `_`, `-`, or `/`. + * - The first tag character must be a letter in any script (Cyrillic, + * CJK, … — #205), the rest can be letters, digits, `_`, `-`, or `/`. * - Fenced code blocks and inline code spans are stripped first. * - Heading markers (`#`, `##`, …) are not a hashtag because the * character after the hash is a space, not a letter. @@ -17,7 +17,7 @@ export function extractTags(body: string): string[] { const stripped = body .replace(/```[\s\S]*?```/g, ' ') .replace(/`[^`\n]*`/g, ' ') - const regex = /(?:^|\s)#([a-zA-Z][\w\-/]*)/g + const regex = /(?:^|\s)#(\p{L}[\p{L}\d_/-]*)/gu const seen = new Set<string>() let m: RegExpExecArray | null while ((m = regex.exec(stripped)) !== null) { diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index d9c9c087..63f70296 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -1076,8 +1076,9 @@ async function rewriteTagAcrossVault( const { notes, activeNote } = get() const escaped = oldTag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Match `#tag` preceded by start/whitespace and followed by a non - // tag-character or end-of-string, keeping the leading separator. - const pattern = new RegExp(`(^|\\s)#${escaped}(?=[^\\w\\-/]|$)`, 'gm') + // tag-character or end-of-string, keeping the leading separator. The + // boundary excludes any Unicode letter so Cyrillic/CJK tags rename too (#205). + const pattern = new RegExp(`(^|\\s)#${escaped}(?=[^\\p{L}\\d_/-]|$)`, 'gmu') const rewriteBody = (src: string): string => { // Preserve code fences and inline code exactly. Split the body diff --git a/packages/shared-domain/src/tasks.ts b/packages/shared-domain/src/tasks.ts index 8457f751..298bd08d 100644 --- a/packages/shared-domain/src/tasks.ts +++ b/packages/shared-domain/src/tasks.ts @@ -140,7 +140,8 @@ const INLINE_DUE_RE = /(?:^|\s)due:(\S+)/i const INLINE_PRIORITY_RE = /(?:^|\s)!(high|med|medium|low|h|m|l)\b/i const INLINE_WAITING_RE = /(?:^|\s)@waiting\b/i // Match #tag-like tokens but only when preceded by start-of-string/whitespace. -const INLINE_TAG_RE = /(?:^|\s)#([a-z0-9][a-z0-9/_-]*)/gi +// Letters in any script (Cyrillic/CJK/…) plus digits, `_`, `-`, `/` (#205). +const INLINE_TAG_RE = /(?:^|\s)#([\p{L}\d][\p{L}\d/_-]*)/gu interface ExtractedTokens { due?: string From 0af25732dd151b00e1d151b373feb3ac9dadde60 Mon Sep 17 00:00:00 2001 From: Adib Hanna <adibhanna@gmail.com> Date: Fri, 19 Jun 2026 16:51:31 -0500 Subject: [PATCH 66/66] =?UTF-8?q?chore(release):=20prep=20v2.4.0=20?= =?UTF-8?q?=E2=80=94=20version=20bump,=20green=20typecheck,=20packaging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump all package versions 2.3.0 → 2.4.0. - Fix the typecheck gate (was red, would fail CI build:prod): - Add RemoteServerClient.createExcalidraw — a real fix: creating a drawing while connected to a self-hosted server called a method that didn't exist. - Add the now-required `assetEmbeds` field to older NoteMeta test fixtures. - release.yml: upload `*.tar.gz` so the AUR source asset is published (was built then dropped by the upload glob). - Flatpak: add the 2.4.0 metainfo release entry and point the AppImage URL at v2.4.0 (sha256 pinned at release once the asset exists). --- .github/workflows/release.yml | 2 +- apps/desktop/package.json | 2 +- apps/desktop/src/main/remote/server-client.ts | 7 +++++++ apps/server/package.json | 2 +- apps/web/package.json | 2 +- package.json | 2 +- packages/app-core/package.json | 2 +- packages/app-core/src/lib/app-core.perf.test.ts | 1 + packages/app-core/src/lib/note-prefetch.test.ts | 1 + packages/app-core/src/lib/note-search.test.ts | 1 + packages/app-core/src/lib/vault-layout.test.ts | 1 + packages/app-core/src/store.test.ts | 1 + packages/bridge-contract/package.json | 2 +- packages/shared-domain/package.json | 2 +- packages/shared-ui/package.json | 2 +- packaging/flatpak/com.adibhanna.zennotes.metainfo.xml | 1 + packaging/flatpak/com.adibhanna.zennotes.yml | 8 +++++--- 17 files changed, 27 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 384634d7..56c06988 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -103,7 +103,7 @@ jobs: files+=("$file") done < <( find dist -maxdepth 1 -type f \ - \( -name '*.dmg' -o -name '*.zip' -o -name '*.exe' -o -name '*.AppImage' -o -name '*.deb' -o -name '*.pacman' -o -name '*.blockmap' -o -name 'latest*.yml' -o -name 'latest*.yaml' \) + \( -name '*.dmg' -o -name '*.zip' -o -name '*.exe' -o -name '*.AppImage' -o -name '*.deb' -o -name '*.pacman' -o -name '*.tar.gz' -o -name '*.blockmap' -o -name 'latest*.yml' -o -name 'latest*.yaml' \) ) if [ "${#files[@]}" -eq 0 ]; then echo "No release artifacts found in dist/" >&2 diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 1e3f769e..65856622 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@zennotes/desktop", "productName": "ZenNotes", - "version": "2.3.0", + "version": "2.4.0", "description": "ZenNotes desktop shell", "private": true, "main": "./out/main/index.js", diff --git a/apps/desktop/src/main/remote/server-client.ts b/apps/desktop/src/main/remote/server-client.ts index cacfd990..c64dc5e2 100644 --- a/apps/desktop/src/main/remote/server-client.ts +++ b/apps/desktop/src/main/remote/server-client.ts @@ -147,6 +147,13 @@ export class RemoteServerClient { }) } + async createExcalidraw(folder: NoteFolder, subpath = '', title?: string): Promise<NoteMeta> { + return this.jsonRequest<NoteMeta>('/api/excalidraw/create', { + method: 'POST', + body: { folder, subpath, title } + }) + } + async renameNote(relPath: string, nextTitle: string): Promise<NoteMeta> { return this.jsonRequest<NoteMeta>('/api/notes/rename', { method: 'POST', diff --git a/apps/server/package.json b/apps/server/package.json index 9ffe9db5..d48a7c38 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,7 +1,7 @@ { "name": "@zennotes/server", "private": true, - "version": "2.3.0", + "version": "2.4.0", "scripts": { "dev": "node ../../tooling/scripts/run-go-server-dev.mjs", "prepare-web": "node ../../tooling/scripts/prepare-server-web-dist.mjs", diff --git a/apps/web/package.json b/apps/web/package.json index 4185784a..786c426c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,7 +1,7 @@ { "name": "@zennotes/web", "private": true, - "version": "2.3.0", + "version": "2.4.0", "type": "module", "description": "ZenNotes web client for self-hosted and hosted deployments", "homepage": "https://zennotes.org", diff --git a/package.json b/package.json index 71032f7a..7b36f508 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "zennotes-monorepo", "private": true, - "version": "2.3.0", + "version": "2.4.0", "description": "ZenNotes monorepo for desktop, web, and self-hosted server builds", "packageManager": "npm@10.9.2", "engines": { diff --git a/packages/app-core/package.json b/packages/app-core/package.json index 0d4d88e3..838b1ba3 100644 --- a/packages/app-core/package.json +++ b/packages/app-core/package.json @@ -1,7 +1,7 @@ { "name": "@zennotes/app-core", "private": true, - "version": "2.3.0", + "version": "2.4.0", "type": "module", "exports": { "./main": "./src/main.tsx" diff --git a/packages/app-core/src/lib/app-core.perf.test.ts b/packages/app-core/src/lib/app-core.perf.test.ts index eaf98232..0b86ad85 100644 --- a/packages/app-core/src/lib/app-core.perf.test.ts +++ b/packages/app-core/src/lib/app-core.perf.test.ts @@ -19,6 +19,7 @@ function makeNote(index: number): NoteMeta { size: 512, tags: ['perf', tag], wikilinks: [`Target ${index % 200}`], + assetEmbeds: [], hasAttachments: index % 31 === 0, excerpt: `Synthetic renderer benchmark note ${index} with phrase needle-${index} and desktop-runtime-benchmark-${id}.` } diff --git a/packages/app-core/src/lib/note-prefetch.test.ts b/packages/app-core/src/lib/note-prefetch.test.ts index 47813f8d..aafadcca 100644 --- a/packages/app-core/src/lib/note-prefetch.test.ts +++ b/packages/app-core/src/lib/note-prefetch.test.ts @@ -13,6 +13,7 @@ function note(folder: NoteFolder, index: number, overrides: Partial<NoteMeta> = size: 10, tags: [], wikilinks: [], + assetEmbeds: [], hasAttachments: false, excerpt: '', ...overrides diff --git a/packages/app-core/src/lib/note-search.test.ts b/packages/app-core/src/lib/note-search.test.ts index b0d50834..e0d15a33 100644 --- a/packages/app-core/src/lib/note-search.test.ts +++ b/packages/app-core/src/lib/note-search.test.ts @@ -21,6 +21,7 @@ function note( size: 10, tags: [], wikilinks: [], + assetEmbeds: [], hasAttachments: false, excerpt: '', ...overrides diff --git a/packages/app-core/src/lib/vault-layout.test.ts b/packages/app-core/src/lib/vault-layout.test.ts index 75cf30b6..caa224f9 100644 --- a/packages/app-core/src/lib/vault-layout.test.ts +++ b/packages/app-core/src/lib/vault-layout.test.ts @@ -31,6 +31,7 @@ function note(path: string, title: string): NoteMeta { size: 0, tags: [], wikilinks: [], + assetEmbeds: [], hasAttachments: false, excerpt: '' } diff --git a/packages/app-core/src/store.test.ts b/packages/app-core/src/store.test.ts index 9eb9c8fd..05e24ff7 100644 --- a/packages/app-core/src/store.test.ts +++ b/packages/app-core/src/store.test.ts @@ -33,6 +33,7 @@ function makeNote(body: string) { size: body.length, tags: [], wikilinks: [], + assetEmbeds: [], hasAttachments: false, excerpt: body, body diff --git a/packages/bridge-contract/package.json b/packages/bridge-contract/package.json index 711e97ec..6fad0e4e 100644 --- a/packages/bridge-contract/package.json +++ b/packages/bridge-contract/package.json @@ -1,7 +1,7 @@ { "name": "@zennotes/bridge-contract", "private": true, - "version": "2.3.0", + "version": "2.4.0", "type": "module", "exports": { "./bridge": "./src/bridge.ts", diff --git a/packages/shared-domain/package.json b/packages/shared-domain/package.json index dfbfd8a8..d6117047 100644 --- a/packages/shared-domain/package.json +++ b/packages/shared-domain/package.json @@ -1,7 +1,7 @@ { "name": "@zennotes/shared-domain", "private": true, - "version": "2.3.0", + "version": "2.4.0", "type": "module", "exports": { "./*": "./src/*.ts" diff --git a/packages/shared-ui/package.json b/packages/shared-ui/package.json index 21052fba..217464f1 100644 --- a/packages/shared-ui/package.json +++ b/packages/shared-ui/package.json @@ -1,7 +1,7 @@ { "name": "@zennotes/shared-ui", "private": true, - "version": "2.3.0", + "version": "2.4.0", "type": "module", "exports": { ".": "./src/index.ts" diff --git a/packaging/flatpak/com.adibhanna.zennotes.metainfo.xml b/packaging/flatpak/com.adibhanna.zennotes.metainfo.xml index bc3f2085..0be35b0e 100644 --- a/packaging/flatpak/com.adibhanna.zennotes.metainfo.xml +++ b/packaging/flatpak/com.adibhanna.zennotes.metainfo.xml @@ -23,6 +23,7 @@ </developer> <content_rating type="oars-1.1"/> <releases> + <release version="2.4.0" date="2026-06-19"/> <release version="2.3.0" date="2026-06-13"/> </releases> </component> diff --git a/packaging/flatpak/com.adibhanna.zennotes.yml b/packaging/flatpak/com.adibhanna.zennotes.yml index dfa2851c..dcde0307 100644 --- a/packaging/flatpak/com.adibhanna.zennotes.yml +++ b/packaging/flatpak/com.adibhanna.zennotes.yml @@ -46,10 +46,12 @@ modules: - install -Dm644 com.adibhanna.zennotes.metainfo.xml /app/share/metainfo/com.adibhanna.zennotes.metainfo.xml sources: - # Bump `url` + `sha256` on each release (see README.md). + # Bump `url` + `sha256` on each release (see README.md). The sha256 below + # is a placeholder — set it to the real hash of the v2.4.0 AppImage once + # the release is published: `shasum -a 256 ZenNotes-2.4.0-linux-x86_64.AppImage`. - type: file - url: https://github.com/ZenNotes/zennotes/releases/download/v2.3.0/ZenNotes-2.3.0-linux-x86_64.AppImage - sha256: 22f1462bb9f72905461139adea1414cbe6d935338291fbb01fc89f978293c71c + url: https://github.com/ZenNotes/zennotes/releases/download/v2.4.0/ZenNotes-2.4.0-linux-x86_64.AppImage + sha256: 0000000000000000000000000000000000000000000000000000000000000000 dest-filename: ZenNotes.AppImage - type: file path: zennotes.sh