From 8739805f997e76fba3b5b52ea2e93770d8478638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Sun, 5 Jul 2026 17:45:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=89=E6=B9=96=E5=BC=80=E6=BA=90=E6=BA=90?= =?UTF-8?q?=E7=A0=81=E5=BF=AB=E7=85=A7=20=C2=B7=20Tolaria=20AGPL=20?= =?UTF-8?q?=E5=88=86=E5=8F=89=E5=9F=BA=E7=BA=BF=20=C2=B7=20=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=E6=9B=B4=E6=96=B0=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .chunk/README.md | 63 + .chunk/config.json | 157 + .chunk/install-playwright-browsers.mjs | 100 + .chunk/run-playwright-shards.sh | 146 + .chunk/run-playwright-smoke.sh | 94 + .chunk/run-rust-gate.sh | 30 + .chunk/run-sidecar-gates-local.sh | 395 + .chunk/run-sidecar-gates.sh | 115 + .chunk/run-sidecar-lane.sh | 136 + .claude/commands/create-adr.md | 133 + .claude/commands/laputa-done.md | 42 + .claude/commands/laputa-next-task.md | 60 + .claude/settings.local.json | 11 + .codacy.yaml | 21 + .codescene-thresholds | 2 + .codesceneignore | 5 + .codescenerc | 9 + .editor-performance-thresholds.json | 83 + .env.example | 18 + .githooks-info | 3 + .github/FUNDING.yml | 3 + .github/HOOKS.md | 64 + .github/SETUP.md | 227 + .github/hooks/install-hooks.sh | 26 + .github/hooks/pre-commit | 81 + .../configure-windows-authenticode.ps1 | 115 + .github/scripts/prefetch-tauri-nsis.ps1 | 137 + .github/workflows/README.md | 133 + .github/workflows/auto-update-prs.yml | 38 + .github/workflows/ci.yml | 296 + .github/workflows/deploy-docs.yml | 104 + .github/workflows/release-build-artifacts.yml | 619 + .github/workflows/release-stable.yml | 300 + .github/workflows/release.yml | 410 + .gitignore | 81 + .husky/pre-commit | 55 + .husky/pre-push | 338 + .semgrepignore | 10 + AGENTS.md | 194 + CLAUDE.md | 8 + CONTRIBUTING.md | 49 + GEMINI.md | 8 + LICENSE | 661 + README.md | 171 + SECURITY.md | 55 + UPSTREAM.md | 14 + biome.json | 38 + components.json | 21 + demo-vault-v2/.fixture-manifest.json | 61 + demo-vault-v2/.gitignore | 5 + demo-vault-v2/24q4-laputa-start.md | 17 + demo-vault-v2/24q4.md | 16 + demo-vault-v2/25q1-laputa-v1.md | 17 + demo-vault-v2/25q1.md | 13 + demo-vault-v2/25q2-laputa-v2.md | 19 + demo-vault-v2/25q2.md | 13 + demo-vault-v2/area-building.md | 12 + .../attachments/laputa-reference.png | Bin 0 -> 705442 bytes demo-vault-v2/event-team-sync-2025-01-13.md | 13 + demo-vault-v2/laputa-qa-reference.md | 16 + demo-vault-v2/measure-close-rate.md | 12 + demo-vault-v2/measure-sponsorship-mrr.md | 12 + demo-vault-v2/note-on-clear-prose.md | 9 + demo-vault-v2/person-luca-rossi.md | 11 + demo-vault-v2/person-matteo-cellini.md | 11 + .../procedure-quarterly-sponsor-outreach.md | 16 + demo-vault-v2/procedure-sponsor-onboarding.md | 17 + demo-vault-v2/refactoring-business-plan.md | 140 + demo-vault-v2/responsibility-sponsorships.md | 21 + demo-vault-v2/rtl-mixed-direction-qa.md | 17 + .../tolaria-sheet-prototype-sample.md | 30 + demo-vault-v2/topic-writing.md | 13 + demo-vault-v2/type/area.md | 11 + demo-vault-v2/type/event.md | 11 + demo-vault-v2/type/measure.md | 11 + demo-vault-v2/type/note.md | 11 + demo-vault-v2/type/person.md | 11 + demo-vault-v2/type/procedure.md | 11 + demo-vault-v2/type/project.md | 11 + demo-vault-v2/type/quarter.md | 11 + demo-vault-v2/type/responsibility.md | 11 + demo-vault-v2/type/topic.md | 11 + demo-vault-v2/views/active-projects.yml | 9 + ...iting-for-clarity-vs-writing-for-credit.md | 11 + demo-vault-v2/writing-weekly-rhythm.md | 11 + design/add-property-inline.pen | 448 + design/ai-agent-panel-ui.pen | 340 + design/ai-agent-wiring.pen | 1 + design/align-property-dropdown.pen | 1 + design/archived-note-indicator.pen | 1 + design/auto-pull-vault.pen | 1 + design/autocomplete-type-color.pen | 1 + design/build-number-status-bar.pen | 96 + design/command-palette-type-aware.pen | 387 + design/date-picker-shadcn.pen | 285 + design/design-full-layouts.pen | 13769 ++++++++ design/drag-drop-images.pen | 78 + design/fix-property-dropdown-narrow.pen | 56 + design/getting-started-vault.pen | 381 + design/git-status-bar.pen | 1 + design/github-oauth-fix.pen | 258 + design/gs-git-init.pen | 1 + design/gs-wikilinks-fix.pen | 17 + design/hide-backlinks-empty.pen | 65 + design/keyboard-first-nav.pen | 156 + design/mcp-autodetect-status-bar.pen | 1 + design/new-note-creation.pen | 185 + design/note-list-title-position.pen | 1 + design/note-subtitle-metadata.pen | 1 + design/note-templates.pen | 1 + design/note-type-labels.pen | 1 + design/notelist-multiselect.pen | 422 + design/notes-type-icon-search.pen | 1 + design/properties-panel-header.pen | 1 + design/property-value-input.pen | 1 + design/raw-editor-mode.pen | 1 + design/relationship-x-cosmetic.pen | 165 + design/search-bundle-qmd.pen | 1 + design/search-results-subtitle.pen | 665 + design/search-subtitle-metadata.pen | 436 + design/smart-property-display.pen | 524 + design/sort-picker-properties.pen | 1 + design/status-color-picker.pen | 619 + design/status-property-dropdown.pen | 1 + design/sync-conflict-resolution.pen | 1 + design/tab-responsive-width.pen | 1 + design/tags-property-type.pen | 1 + design/themes-editable.pen | 1 + design/theming-system.pen | 1 + design/trash-management.pen | 1 + design/trashed-note-editor.pen | 1 + design/ui-design-cleanup.pen | 329 + design/vault-agents-md.pen | 1 + design/vault-management-remove.pen | 281 + design/zoom-shortcuts.pen | 202 + docs/ABSTRACTIONS.md | 1042 + docs/ARCHITECTURE.md | 1176 + docs/GETTING-STARTED.md | 534 + docs/PUBLIC-DOCS-PLAN.md | 47 + docs/VISION.md | 195 + docs/adr/0001-tauri-react-stack.md | 31 + docs/adr/0002-filesystem-source-of-truth.md | 30 + docs/adr/0003-single-note-model.md | 30 + .../adr/0004-vault-vs-app-settings-storage.md | 42 + docs/adr/0005-tauri-ios-for-ipad.md | 38 + docs/adr/0006-flat-vault-structure.md | 30 + docs/adr/0007-title-filename-sync.md | 31 + docs/adr/0008-underscore-system-properties.md | 42 + docs/adr/0009-keyword-only-search.md | 30 + ...dynamic-wikilink-relationship-detection.md | 30 + .../adr/0011-mcp-server-for-ai-integration.md | 30 + docs/adr/0012-claude-cli-for-ai-agent.md | 30 + docs/adr/0013-remove-theming-system.md | 30 + docs/adr/0014-git-based-vault-cache.md | 31 + docs/adr/0015-auto-save-with-debounce.md | 29 + docs/adr/0016-sentry-posthog-telemetry.md | 31 + docs/adr/0018-codescene-code-health-gates.md | 32 + docs/adr/0019-github-device-flow-oauth.md | 30 + docs/adr/0020-keyboard-first-design.md | 32 + docs/adr/0021-push-to-main-workflow.md | 30 + docs/adr/0022-blocknote-rich-text-editor.md | 32 + docs/adr/0023-repair-vault-auto-bootstrap.md | 31 + docs/adr/0024-cache-outside-vault.md | 30 + docs/adr/0025-type-field-canonical.md | 30 + docs/adr/0026-props-down-no-global-state.md | 31 + docs/adr/0027-dual-ai-architecture.md | 32 + docs/adr/0028-cli-agent-only-no-api-key.md | 40 + .../0029-domain-command-builder-pattern.md | 29 + docs/adr/0030-rust-commands-module-split.md | 29 + docs/adr/0031-full-app-for-note-windows.md | 30 + docs/adr/0032-status-bar-for-git-actions.md | 30 + ...0033-subfolder-scanning-and-folder-tree.md | 34 + docs/adr/0034-git-repo-required-for-vault.md | 29 + .../0035-path-suffix-wikilink-resolution.md | 31 + ...-external-rename-detection-via-git-diff.md | 31 + ...demirror-language-markdown-highlighting.md | 31 + docs/adr/0038-frontmatter-backed-favorites.md | 32 + docs/adr/0039-git-history-for-note-dates.md | 40 + .../0040-custom-views-yml-filter-engine.md | 59 + ...041-filekind-all-files-in-vault-scanner.md | 39 + ...-posthog-release-channels-feature-flags.md | 37 + .../adr/0042-trash-auto-purge-safety-model.md | 56 + docs/adr/0043-reactive-vault-state-on-save.md | 63 + docs/adr/0044-h1-as-title-primary-source.md | 46 + docs/adr/0045-permanent-delete-no-trash.md | 37 + .../0046-starter-vault-cloned-from-github.md | 36 + ...7-regex-mode-for-view-filter-conditions.md | 34 + ...lative-date-expressions-in-view-filters.md | 46 + docs/adr/0049-per-note-icon-property.md | 55 + ...-deterministic-shortcut-command-routing.md | 29 + ...-shortcut-manifest-for-testable-routing.md | 31 + ...rtcut-execution-with-native-menu-dedupe.md | 33 + ...evention-for-browser-reserved-shortcuts.md | 30 + .../0054-deterministic-shortcut-qa-matrix.md | 35 + ...055-h1-is-the-only-editor-title-surface.md | 43 + ...6-system-git-cli-auth-no-provider-oauth.md | 50 + ...table-release-channels-and-beta-cohorts.md | 45 + ...laude-code-first-launch-onboarding-gate.md | 36 + ...9-local-only-git-commits-without-remote.md | 33 + ...ork-aware-ui-gating-for-remote-features.md | 28 + docs/adr/0061-ai-prompt-bridge-event-bus.md | 29 + docs/adr/0062-selectable-cli-ai-agents.md | 32 + ...e-block-package-for-editor-highlighting.md | 29 + .../0064-ratcheted-codescene-thresholds.md | 30 + .../0065-root-managed-ai-guidance-files.md | 30 + ...ersioning-for-alpha-and-stable-releases.md | 37 + ...7-autogit-idle-and-inactive-checkpoints.md | 39 + ...face-with-optional-untitled-auto-rename.md | 35 + ...ode-for-note-list-relationship-browsing.md | 33 + ...l-first-with-explicit-remote-connection.md | 33 + ...rnal-vault-refresh-and-clean-tab-reopen.md | 49 + ...onfirmed-vault-paths-gate-startup-state.md | 33 + ...rotocol-registry-across-editor-remounts.md | 30 + ...setup-and-least-privilege-desktop-scope.md | 36 + ...075-crash-safe-note-rename-transactions.md | 36 + ...rgeting-separates-type-and-folder-moves.md | 34 + ...concurrent-safe-vault-cache-replacement.md | 37 + ...ed-fallback-for-app-managed-git-commits.md | 36 + ...0079-linux-window-chrome-and-menu-reuse.md | 37 + ...ease-artifacts-and-portable-vault-names.md | 36 + .../0081-internal-light-dark-theme-runtime.md | 43 + docs/adr/0082-markdown-durable-math-notes.md | 40 + ...al-architecture-macos-release-artifacts.md | 38 + docs/adr/0084-app-localization-foundation.md | 35 + docs/adr/0085-non-git-vault-support.md | 39 + docs/adr/0086-in-app-image-file-preview.md | 32 + ...json-catalogs-and-lara-cli-localization.md | 39 + .../0088-markdown-durable-mermaid-diagrams.md | 40 + .../0089-active-vault-filesystem-watcher.md | 36 + docs/adr/0090-pi-cli-agent-adapter.md | 35 + docs/adr/0091-gemini-cli-external-ai-setup.md | 35 + .../0092-vault-ai-agent-permission-modes.md | 39 + .../0093-shared-cli-agent-runtime-adapters.md | 35 + ...ored-content-visibility-boundary-filter.md | 37 + docs/adr/0095-saved-view-order-field.md | 35 + docs/adr/0096-root-created-type-documents.md | 37 + docs/adr/0097-gemini-cli-agent-adapter.md | 42 + ...0098-in-app-image-and-pdf-file-previews.md | 30 + docs/adr/0099-cumulative-vault-asset-scope.md | 37 + .../0100-synthetic-vault-root-folder-row.md | 35 + ...01-categorical-product-analytics-events.md | 35 + .../0102-low-end-safe-autosave-idle-window.md | 29 + ...dapter-specific-ai-permission-semantics.md | 32 + .../0104-tauri-frontend-readiness-watchdog.md | 48 + ...correctness-and-responsiveness-contract.md | 39 + docs/adr/0106-shared-app-command-manifest.md | 31 + ...107-markdown-durable-tldraw-whiteboards.md | 43 + ...7-pointer-owned-editor-block-reordering.md | 32 + docs/adr/0108-direct-model-ai-targets.md | 30 + ...anitized-rendered-markup-and-safe-regex.md | 25 + ...debounced-worker-derived-editor-indexes.md | 32 + ...0110-in-app-media-and-pdf-file-previews.md | 44 + ...efresh-with-focused-editor-preservation.md | 49 + docs/adr/0112-system-theme-mode.md | 41 + ...-renderer-attachment-path-normalization.md | 43 + .../0114-mounted-workspaces-unified-graph.md | 40 + ...react-context-for-shared-ui-preferences.md | 28 + ...-transition-and-serialization-ownership.md | 44 + ...117-appimage-fcitx-gtk3-frontend-bundle.md | 24 + ...-note-windows-without-vault-index-scans.md | 35 + ...tration-with-mounted-workspace-guidance.md | 35 + ...-server-path-with-opencode-registration.md | 37 + ...l-fallback-for-audio-and-video-previews.md | 34 + ...122-scalar-array-frontmatter-properties.md | 33 + ...-vault-graph-for-secondary-note-windows.md | 32 + ...24-cached-secondary-note-window-startup.md | 27 + docs/adr/0126-renderer-action-history.md | 29 + docs/adr/0127-native-ai-workspace-window.md | 32 + .../0128-lightweight-ai-workspace-window.md | 35 + .../adr/0129-tolaria-vault-item-deep-links.md | 45 + ...30-windows-authenticode-release-signing.md | 36 + ...eusable-release-artifact-build-workflow.md | 29 + docs/adr/0132-alpha-authenticode-soft-gate.md | 25 + .../0133-request-scoped-ai-stream-events.md | 27 + ...134-direct-shiki-language-registrations.md | 27 + ...-nodes-with-plain-text-workbook-storage.md | 49 + ...active-note-refresh-after-external-edit.md | 44 + docs/adr/0136-macos-webview-pdf-export.md | 31 + ...137-shared-rich-editor-input-transforms.md | 58 + ...icode-required-for-all-release-channels.md | 32 + ...emporary-windows-authenticode-soft-gate.md | 26 + ...sion-based-raw-text-syntax-highlighting.md | 35 + ...coped-linux-webkit-rendering-safeguards.md | 33 + ...ditor-prosemirror-decoration-dependency.md | 29 + docs/adr/0143-shared-focus-ownership-guard.md | 32 + .../adr/0144-collections-and-presentations.md | 48 + docs/adr/0145-xdg-backed-app-config-path.md | 44 + ...ndow-startup-with-empty-reload-recovery.md | 30 + .../adr/0147-antigravity-cli-agent-adapter.md | 42 + docs/adr/0148-cancellable-ai-agent-streams.md | 29 + .../0149-shared-app-config-policy-manifest.md | 32 + .../0150-github-copilot-cli-agent-adapter.md | 39 + ...0151-antigravity-add-dir-workspace-flag.md | 36 + docs/adr/0152-wsl2-git-provider.md | 29 + docs/adr/README.md | 211 + ...release-channel-and-local-feature-flags.md | 31 + e2e/ai-chat-screenshot.spec.ts | 57 + e2e/ai-chat.spec.ts | 69 + e2e/app.spec.ts | 27 + e2e/auto-save.spec.ts | 63 + e2e/core-flows.spec.ts | 260 + e2e/create-note.spec.ts | 57 + e2e/create-type.spec.ts | 120 + e2e/debug-content-bug.spec.ts | 38 + e2e/editor-min-width.spec.ts | 33 + e2e/filtering.spec.ts | 82 + e2e/find-selectors.spec.ts | 19 + e2e/image-drag-drop.spec.ts | 182 + e2e/image-upload.spec.ts | 134 + e2e/keyboard-shortcuts.spec.ts | 43 + e2e/quick-open.spec.ts | 88 + e2e/rename-tab.spec.ts | 80 + e2e/screenshot.spec.ts | 94 + e2e/settings-oauth.spec.ts | 51 + e2e/sidebar-collapse.spec.ts | 90 + e2e/vault-picker-local.spec.ts | 39 + e2e/visual-verify.spec.ts | 103 + e2e/zero-shift-closeup.spec.ts | 77 + e2e/zero-shift-detail.spec.ts | 83 + e2e/zero-shift.spec.ts | 98 + eslint.config.js | 32 + index.html | 170 + lara.lock | 1000 + lara.yaml | 38 + mcp-server/agent-instructions.js | 23 + mcp-server/app-config-policy.json | 20 + mcp-server/index.js | 350 + mcp-server/package-lock.json | 1269 + mcp-server/package.json | 25 + mcp-server/test.js | 608 + mcp-server/tool-service.js | 213 + mcp-server/tool-service.test.js | 156 + mcp-server/vault-path.js | 139 + mcp-server/vault.js | 460 + mcp-server/ws-bridge.js | 241 + package.json | 156 + patches/@blocknote__code-block@0.46.2.patch | 86 + patches/@blocknote__core@0.46.2.patch | 1209 + patches/@blocknote__react@0.46.2.patch | 112 + patches/@tiptap__extension-link@3.19.0.patch | 101 + patches/prosemirror-tables@1.8.5.patch | 234 + playwright.config.ts | 32 + playwright.integration.config.ts | 18 + playwright.smoke.config.ts | 40 + pnpm-lock.yaml | 12993 +++++++ pnpm-workspace.yaml | 11 + public/ai-agent-icons/SOURCES.md | 10 + public/ai-agent-icons/claude-code.svg | 3 + public/ai-agent-icons/codex.svg | 3 + public/ai-agent-icons/copilot.svg | 9 + public/ai-agent-icons/gemini.svg | 10 + public/ai-agent-icons/hermes.svg | 6 + public/ai-agent-icons/kiro.svg | 11 + public/ai-agent-icons/opencode.svg | 18 + public/ai-agent-icons/pi.svg | 11 + public/favicon.svg | 1 + release-notes/stable-v2026.5.2.md | 18 + release-notes/v2026-05-04.md | 20 + release-notes/v2026-05-06.md | 18 + release-notes/v2026-05-07.md | 17 + release-notes/v2026-05-12.md | 21 + release-notes/v2026-05-14.md | 17 + release-notes/v2026-05-18.md | 18 + release-notes/v2026-05-21.md | 19 + release-notes/v2026-05-25.md | 19 + release-notes/v2026-05-29.md | 21 + release-notes/v2026-06-01.md | 17 + release-notes/v2026-06-06.md | 18 + release-notes/v2026-06-10.md | 19 + release-notes/v2026-06-14.md | 14 + release-notes/v2026-06-23.md | 21 + release-notes/v2026-06-26.md | 16 + release-notes/v2026-07-01.md | 19 + scripts/appimage-launcher-tools.mjs | 329 + scripts/appimage-launcher-tools.test.mjs | 182 + scripts/build-agent-docs.mjs | 197 + scripts/build-agent-docs.test.mjs | 11 + scripts/build-release-download-page.ts | 39 + scripts/build-release-history-page.ts | 33 + scripts/bundle-mcp-server.mjs | 45 + scripts/editor-performance-benchmark.mjs | 521 + scripts/editor-performance-thresholds.mjs | 113 + scripts/generate_demo_vault.py | 1197 + scripts/playwright-smoke-server.mjs | 38 + scripts/run-vitest-coverage-shards.mjs | 200 + scripts/run-vitest-coverage.mjs | 203 + scripts/serve-demo.mjs | 284 + scripts/validate-locales.mjs | 149 + site/.vitepress/config.ts | 119 + site/.vitepress/theme/LandingHome.vue | 1285 + site/.vitepress/theme/Layout.vue | 212 + .../theme/assets/RefactoringSans.otf | Bin 0 -> 185516 bytes site/.vitepress/theme/index.ts | 12 + site/.vitepress/theme/styles.css | 240 + site/concepts/ai.md | 32 + site/concepts/editor.md | 23 + site/concepts/files-and-media.md | 34 + site/concepts/git.md | 29 + site/concepts/inbox.md | 23 + site/concepts/notes.md | 36 + site/concepts/properties.md | 26 + site/concepts/relationships.md | 32 + site/concepts/spreadsheets.md | 138 + site/concepts/types.md | 51 + site/concepts/vaults.md | 45 + site/guides/build-custom-views.md | 25 + site/guides/capture-a-note.md | 18 + site/guides/commit-and-push.md | 23 + site/guides/configure-ai-models.md | 25 + site/guides/connect-a-git-remote.md | 23 + site/guides/create-types.md | 35 + site/guides/manage-display-preferences.md | 26 + site/guides/organize-inbox.md | 31 + site/guides/use-ai-panel.md | 38 + site/guides/use-command-palette.md | 26 + site/guides/use-media-previews.md | 25 + site/guides/use-spreadsheets.md | 146 + site/guides/use-table-of-contents.md | 23 + site/guides/use-wikilinks.md | 24 + site/index.md | 10 + site/public/CNAME | 1 + site/public/landing/Ai chat 3.png | Bin 0 -> 743480 bytes site/public/landing/Block editor.png | Bin 0 -> 219011 bytes site/public/landing/Luca hello.jpeg | Bin 0 -> 294249 bytes site/public/landing/ananth.webp | Bin 0 -> 13456 bytes site/public/landing/favicon.png | Bin 0 -> 17696 bytes site/public/landing/git-history.png | Bin 0 -> 51710 bytes site/public/landing/gregor.webp | Bin 0 -> 4834 bytes site/public/landing/jordan.webp | Bin 0 -> 1140 bytes site/public/landing/les-saintes.jpg | Bin 0 -> 803626 bytes site/public/landing/pulse.png | Bin 0 -> 239489 bytes site/public/landing/relationships.png | Bin 0 -> 286153 bytes site/public/landing/simply-files.png | Bin 0 -> 293525 bytes site/public/landing/sponsors/SOURCES.md | 12 + .../public/landing/sponsors/circleci-dark.svg | 3 + .../landing/sponsors/circleci-light.svg | 3 + site/public/landing/sponsors/codacy-dark.svg | 16 + site/public/landing/sponsors/codacy-light.svg | 16 + .../landing/sponsors/codescene-dark.svg | 3 + .../landing/sponsors/codescene-light.svg | 3 + .../landing/sponsors/unblocked-dark.svg | 22 + .../landing/sponsors/unblocked-light.svg | 28 + site/public/landing/tolaria-icon.png | Bin 0 -> 13746 bytes .../landing/tolaria-screenshot-dark.png | Bin 0 -> 948858 bytes site/public/landing/tolaria-screenshot.png | Bin 0 -> 947306 bytes .../public/landing/track changes and push.png | Bin 0 -> 410615 bytes site/public/landing/wikilinks.png | Bin 0 -> 160715 bytes site/public/landing/yaml frontmatter.png | Bin 0 -> 183592 bytes site/public/tolaria-icon.svg | 10 + site/reference/contribute.md | 32 + site/reference/docs-maintenance.md | 38 + site/reference/file-layout.md | 43 + site/reference/frontmatter-fields.md | 30 + site/reference/keyboard-shortcuts.md | 24 + site/reference/release-channels.md | 36 + site/reference/spreadsheet-format.md | 162 + site/reference/spreadsheet-functions.md | 186 + site/reference/supported-platforms.md | 23 + site/reference/view-filters.md | 33 + site/start/first-launch.md | 35 + site/start/getting-started-vault.md | 32 + site/start/install.md | 40 + site/start/open-or-create-vault.md | 30 + site/templates/portent.md | 72 + site/troubleshooting/ai-agent-not-found.md | 23 + site/troubleshooting/git-auth.md | 26 + .../model-provider-connection.md | 25 + site/troubleshooting/sync-conflicts.md | 20 + site/troubleshooting/vault-not-loading.md | 25 + src-tauri/.gitignore | 8 + src-tauri/Cargo.lock | 7249 ++++ src-tauri/Cargo.toml | 54 + src-tauri/Info.plist | 8 + src-tauri/build.rs | 11 + src-tauri/capabilities/default.json | 28 + src-tauri/capabilities/mobile.json | 15 + src-tauri/gen/apple/.gitignore | 3 + .../AppIcon.appiconset/AppIcon-20x20@1x.png | Bin 0 -> 1036 bytes .../AppIcon.appiconset/AppIcon-20x20@2x-1.png | Bin 0 -> 2216 bytes .../AppIcon.appiconset/AppIcon-20x20@2x.png | Bin 0 -> 2216 bytes .../AppIcon.appiconset/AppIcon-20x20@3x.png | Bin 0 -> 3512 bytes .../AppIcon.appiconset/AppIcon-29x29@1x.png | Bin 0 -> 1568 bytes .../AppIcon.appiconset/AppIcon-29x29@2x-1.png | Bin 0 -> 3394 bytes .../AppIcon.appiconset/AppIcon-29x29@2x.png | Bin 0 -> 3394 bytes .../AppIcon.appiconset/AppIcon-29x29@3x.png | Bin 0 -> 5228 bytes .../AppIcon.appiconset/AppIcon-40x40@1x.png | Bin 0 -> 2216 bytes .../AppIcon.appiconset/AppIcon-40x40@2x-1.png | Bin 0 -> 4794 bytes .../AppIcon.appiconset/AppIcon-40x40@2x.png | Bin 0 -> 4794 bytes .../AppIcon.appiconset/AppIcon-40x40@3x.png | Bin 0 -> 7288 bytes .../AppIcon.appiconset/AppIcon-512@2x.png | Bin 0 -> 121323 bytes .../AppIcon.appiconset/AppIcon-60x60@2x.png | Bin 0 -> 7288 bytes .../AppIcon.appiconset/AppIcon-60x60@3x.png | Bin 0 -> 11091 bytes .../AppIcon.appiconset/AppIcon-76x76@1x.png | Bin 0 -> 4515 bytes .../AppIcon.appiconset/AppIcon-76x76@2x.png | Bin 0 -> 9257 bytes .../AppIcon-83.5x83.5@2x.png | Bin 0 -> 10219 bytes .../AppIcon.appiconset/Contents.json | 116 + .../gen/apple/Assets.xcassets/Contents.json | 6 + src-tauri/gen/apple/ExportOptions.plist | 8 + src-tauri/gen/apple/LaunchScreen.storyboard | 30 + src-tauri/gen/apple/Podfile | 21 + .../apple/Sources/laputa/bindings/bindings.h | 8 + src-tauri/gen/apple/Sources/laputa/main.mm | 6 + .../gen/apple/assets/mcp-server/index.js | 28192 ++++++++++++++++ .../gen/apple/assets/mcp-server/package.json | 1 + .../gen/apple/assets/mcp-server/ws-bridge.js | 7380 ++++ .../apple/laputa.xcodeproj/project.pbxproj | 566 + .../contents.xcworkspacedata | 7 + .../xcshareddata/WorkspaceSettings.xcsettings | 10 + .../xcschemes/laputa_iOS.xcscheme | 131 + src-tauri/gen/apple/laputa_iOS/Info.plist | 44 + .../apple/laputa_iOS/laputa_iOS.entitlements | 5 + src-tauri/gen/apple/project.yml | 88 + src-tauri/icons/128x128.png | Bin 0 -> 8253 bytes src-tauri/icons/128x128@2x.png | Bin 0 -> 24914 bytes src-tauri/icons/256x256.png | Bin 0 -> 36937 bytes src-tauri/icons/32x32.png | Bin 0 -> 1490 bytes src-tauri/icons/512x512-dark.png | Bin 0 -> 105934 bytes src-tauri/icons/512x512.png | Bin 0 -> 116147 bytes src-tauri/icons/64x64.png | Bin 0 -> 3201 bytes src-tauri/icons/Square107x107Logo.png | Bin 0 -> 6333 bytes src-tauri/icons/Square142x142Logo.png | Bin 0 -> 9806 bytes src-tauri/icons/Square150x150Logo.png | Bin 0 -> 10259 bytes src-tauri/icons/Square284x284Logo.png | Bin 0 -> 30640 bytes src-tauri/icons/Square30x30Logo.png | Bin 0 -> 1437 bytes src-tauri/icons/Square310x310Logo.png | Bin 0 -> 36814 bytes src-tauri/icons/Square44x44Logo.png | Bin 0 -> 2011 bytes src-tauri/icons/Square71x71Logo.png | Bin 0 -> 3868 bytes src-tauri/icons/Square89x89Logo.png | Bin 0 -> 5085 bytes src-tauri/icons/StoreLogo.png | Bin 0 -> 2562 bytes .../android/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../icons/android/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2507 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 11664 bytes .../android/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 2482 bytes .../icons/android/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2380 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 6516 bytes .../android/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2335 bytes .../android/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 5302 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 18097 bytes .../mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 5230 bytes .../android/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 9657 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 40273 bytes .../mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10277 bytes .../android/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 14517 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 77804 bytes .../mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15958 bytes .../android/values/ic_launcher_background.xml | 4 + src-tauri/icons/icon-source-dark.svg | 41 + src-tauri/icons/icon-source.png | Bin 0 -> 533641 bytes src-tauri/icons/icon.icns | Bin 0 -> 832737 bytes src-tauri/icons/icon.ico | Bin 0 -> 34669 bytes src-tauri/icons/icon.png | Bin 0 -> 117907 bytes src-tauri/icons/ios/AppIcon-20x20@1x.png | Bin 0 -> 815 bytes src-tauri/icons/ios/AppIcon-20x20@2x-1.png | Bin 0 -> 1877 bytes src-tauri/icons/ios/AppIcon-20x20@2x.png | Bin 0 -> 1877 bytes src-tauri/icons/ios/AppIcon-20x20@3x.png | Bin 0 -> 2904 bytes src-tauri/icons/ios/AppIcon-29x29@1x.png | Bin 0 -> 1257 bytes src-tauri/icons/ios/AppIcon-29x29@2x-1.png | Bin 0 -> 2687 bytes src-tauri/icons/ios/AppIcon-29x29@2x.png | Bin 0 -> 2687 bytes src-tauri/icons/ios/AppIcon-29x29@3x.png | Bin 0 -> 4343 bytes src-tauri/icons/ios/AppIcon-40x40@1x.png | Bin 0 -> 1877 bytes src-tauri/icons/ios/AppIcon-40x40@2x-1.png | Bin 0 -> 4045 bytes src-tauri/icons/ios/AppIcon-40x40@2x.png | Bin 0 -> 4045 bytes src-tauri/icons/ios/AppIcon-40x40@3x.png | Bin 0 -> 6697 bytes src-tauri/icons/ios/AppIcon-512@2x.png | Bin 0 -> 349971 bytes src-tauri/icons/ios/AppIcon-60x60@2x.png | Bin 0 -> 6697 bytes src-tauri/icons/ios/AppIcon-60x60@3x.png | Bin 0 -> 11850 bytes src-tauri/icons/ios/AppIcon-76x76@1x.png | Bin 0 -> 3628 bytes src-tauri/icons/ios/AppIcon-76x76@2x.png | Bin 0 -> 9337 bytes src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png | Bin 0 -> 10455 bytes src-tauri/resources/agent-docs/AGENTS.md | 15 + src-tauri/resources/agent-docs/all.md | 2254 ++ src-tauri/resources/agent-docs/concepts.md | 554 + src-tauri/resources/agent-docs/guides.md | 597 + src-tauri/resources/agent-docs/home.md | 6 + src-tauri/resources/agent-docs/index.md | 81 + .../resources/agent-docs/pages/concepts/ai.md | 37 + .../agent-docs/pages/concepts/editor.md | 28 + .../pages/concepts/files-and-media.md | 39 + .../agent-docs/pages/concepts/git.md | 34 + .../agent-docs/pages/concepts/inbox.md | 28 + .../agent-docs/pages/concepts/notes.md | 41 + .../agent-docs/pages/concepts/properties.md | 31 + .../pages/concepts/relationships.md | 37 + .../agent-docs/pages/concepts/spreadsheets.md | 143 + .../agent-docs/pages/concepts/types.md | 56 + .../agent-docs/pages/concepts/vaults.md | 50 + .../pages/guides/build-custom-views.md | 30 + .../agent-docs/pages/guides/capture-a-note.md | 23 + .../pages/guides/commit-and-push.md | 28 + .../pages/guides/configure-ai-models.md | 30 + .../pages/guides/connect-a-git-remote.md | 28 + .../agent-docs/pages/guides/create-types.md | 40 + .../guides/manage-display-preferences.md | 31 + .../agent-docs/pages/guides/organize-inbox.md | 36 + .../agent-docs/pages/guides/use-ai-panel.md | 43 + .../pages/guides/use-command-palette.md | 31 + .../pages/guides/use-media-previews.md | 30 + .../pages/guides/use-spreadsheets.md | 151 + .../pages/guides/use-table-of-contents.md | 28 + .../agent-docs/pages/guides/use-wikilinks.md | 29 + src-tauri/resources/agent-docs/pages/index.md | 6 + .../agent-docs/pages/reference/contribute.md | 37 + .../pages/reference/docs-maintenance.md | 43 + .../agent-docs/pages/reference/file-layout.md | 48 + .../pages/reference/frontmatter-fields.md | 35 + .../pages/reference/keyboard-shortcuts.md | 29 + .../pages/reference/release-channels.md | 41 + .../pages/reference/spreadsheet-format.md | 167 + .../pages/reference/spreadsheet-functions.md | 191 + .../pages/reference/supported-platforms.md | 28 + .../pages/reference/view-filters.md | 38 + .../agent-docs/pages/start/first-launch.md | 40 + .../pages/start/getting-started-vault.md | 37 + .../agent-docs/pages/start/install.md | 45 + .../pages/start/open-or-create-vault.md | 35 + .../agent-docs/pages/templates/portent.md | 77 + .../troubleshooting/ai-agent-not-found.md | 27 + .../pages/troubleshooting/git-auth.md | 30 + .../model-provider-connection.md | 30 + .../pages/troubleshooting/sync-conflicts.md | 24 + .../troubleshooting/vault-not-loading.md | 29 + src-tauri/resources/agent-docs/reference.md | 684 + .../resources/agent-docs/search-index.json | 560 + src-tauri/resources/agent-docs/start.md | 166 + src-tauri/resources/agent-docs/templates.md | 77 + .../resources/agent-docs/troubleshooting.md | 152 + src-tauri/src/ai_agent_processes.rs | 140 + src-tauri/src/ai_agents.rs | 448 + src-tauri/src/ai_model_tools.rs | 623 + src-tauri/src/ai_models.rs | 788 + src-tauri/src/antigravity_cli.rs | 195 + src-tauri/src/antigravity_config.rs | 208 + src-tauri/src/antigravity_discovery.rs | 175 + src-tauri/src/app_config.rs | 295 + src-tauri/src/app_icon.rs | 59 + src-tauri/src/app_updater.rs | 426 + src-tauri/src/claude_cli.rs | 1680 + src-tauri/src/claude_invocation.rs | 613 + src-tauri/src/cli_agent_runtime.rs | 679 + .../src/cli_agent_runtime/line_stream.rs | 189 + src-tauri/src/cli_agent_runtime/mcp_config.rs | 21 + src-tauri/src/cli_agent_runtime/shell_env.rs | 313 + .../src/cli_agent_runtime/windows_cmd_shim.rs | 115 + src-tauri/src/codex_cli.rs | 1195 + src-tauri/src/commands/ai.rs | 471 + src-tauri/src/commands/app_icon.rs | 11 + src-tauri/src/commands/clipboard.rs | 207 + src-tauri/src/commands/delete.rs | 44 + src-tauri/src/commands/folders.rs | 50 + src-tauri/src/commands/git.rs | 646 + src-tauri/src/commands/git_clone.rs | 18 + src-tauri/src/commands/git_connect.rs | 36 + src-tauri/src/commands/memory.rs | 167 + src-tauri/src/commands/mod.rs | 102 + src-tauri/src/commands/pdf_export.rs | 141 + src-tauri/src/commands/runtime.rs | 48 + src-tauri/src/commands/sheet.rs | 679 + src-tauri/src/commands/system.rs | 656 + src-tauri/src/commands/vault.rs | 397 + src-tauri/src/commands/vault/boundary.rs | 460 + src-tauri/src/commands/vault/file_cmds.rs | 458 + .../src/commands/vault/frontmatter_cmds.rs | 135 + .../src/commands/vault/lifecycle_cmds.rs | 156 + src-tauri/src/commands/vault/rename_cmds.rs | 452 + src-tauri/src/commands/vault/scan_cmds.rs | 361 + src-tauri/src/commands/vault/view_cmds.rs | 94 + src-tauri/src/commands/version.rs | 121 + src-tauri/src/copilot_cli.rs | 332 + src-tauri/src/copilot_discovery.rs | 116 + src-tauri/src/frontmatter/keys.rs | 184 + src-tauri/src/frontmatter/mod.rs | 254 + src-tauri/src/frontmatter/ops.rs | 267 + src-tauri/src/frontmatter/ops_update_tests.rs | 237 + src-tauri/src/frontmatter/yaml.rs | 253 + src-tauri/src/git/author.rs | 294 + src-tauri/src/git/clone.rs | 266 + src-tauri/src/git/command.rs | 57 + src-tauri/src/git/commit.rs | 274 + src-tauri/src/git/conflict.rs | 414 + src-tauri/src/git/connect.rs | 591 + src-tauri/src/git/credentials.rs | 122 + src-tauri/src/git/dates.rs | 230 + src-tauri/src/git/file_url.rs | 403 + src-tauri/src/git/history.rs | 349 + src-tauri/src/git/mod.rs | 1150 + src-tauri/src/git/provider.rs | 443 + src-tauri/src/git/pulse.rs | 599 + src-tauri/src/git/remote.rs | 594 + src-tauri/src/git/remote_branch_tests.rs | 106 + src-tauri/src/git/remote_config.rs | 88 + src-tauri/src/git/remote_status.rs | 223 + src-tauri/src/git/remote_url.rs | 132 + src-tauri/src/git/status.rs | 708 + src-tauri/src/git/upstream.rs | 77 + src-tauri/src/hermes_cli.rs | 209 + src-tauri/src/hermes_discovery.rs | 87 + src-tauri/src/kiro_cli.rs | 198 + src-tauri/src/kiro_discovery.rs | 59 + src-tauri/src/lib.rs | 801 + src-tauri/src/linux_appimage.rs | 795 + src-tauri/src/main.rs | 6 + src-tauri/src/mcp.rs | 1170 + src-tauri/src/mcp/extraction.rs | 303 + src-tauri/src/mcp/opencode.rs | 278 + src-tauri/src/mcp/paths.rs | 169 + src-tauri/src/mcp/runtime.rs | 512 + src-tauri/src/mcp/subprocess.rs | 78 + src-tauri/src/menu.rs | 683 + src-tauri/src/opencode_cli.rs | 121 + src-tauri/src/opencode_config.rs | 418 + src-tauri/src/opencode_discovery.rs | 262 + src-tauri/src/opencode_events.rs | 185 + src-tauri/src/opencode_events_tests.rs | 237 + src-tauri/src/pi_cli.rs | 159 + src-tauri/src/pi_config.rs | 558 + src-tauri/src/pi_discovery.rs | 403 + src-tauri/src/pi_events.rs | 168 + src-tauri/src/pi_events_tests.rs | 127 + src-tauri/src/search.rs | 446 + src-tauri/src/settings.rs | 965 + src-tauri/src/telemetry.rs | 225 + src-tauri/src/vault/cache.rs | 1476 + src-tauri/src/vault/config_seed.rs | 870 + src-tauri/src/vault/entry.rs | 98 + src-tauri/src/vault/file.rs | 256 + src-tauri/src/vault/filename_rules.rs | 101 + src-tauri/src/vault/folders.rs | 193 + src-tauri/src/vault/frontmatter.rs | 546 + .../src/vault/frontmatter_regression_tests.rs | 188 + src-tauri/src/vault/getting_started.rs | 870 + src-tauri/src/vault/ignored.rs | 370 + src-tauri/src/vault/image.rs | 196 + src-tauri/src/vault/migration.rs | 250 + src-tauri/src/vault/mod.rs | 512 + src-tauri/src/vault/mod_tests.rs | 45 + .../src/vault/mod_tests/archival_metadata.rs | 217 + src-tauri/src/vault/mod_tests/basics.rs | 150 + .../vault/mod_tests/complex_frontmatter.rs | 179 + .../src/vault/mod_tests/display_metadata.rs | 198 + .../vault/mod_tests/folder_and_file_kind.rs | 122 + .../mod_tests/journal_type_visibility.rs | 38 + .../vault/mod_tests/real_vault_consistency.rs | 118 + .../src/vault/mod_tests/relationships.rs | 286 + .../vault/mod_tests/scan_and_file_access.rs | 136 + .../src/vault/mod_tests/type_and_links.rs | 203 + src-tauri/src/vault/modified_dates_tests.rs | 91 + src-tauri/src/vault/parsing.rs | 346 + src-tauri/src/vault/parsing_tests.rs | 559 + src-tauri/src/vault/path_identity.rs | 117 + src-tauri/src/vault/relationship_key_tests.rs | 63 + src-tauri/src/vault/rename.rs | 1503 + src-tauri/src/vault/rename_transaction.rs | 287 + src-tauri/src/vault/system_metadata_tests.rs | 99 + src-tauri/src/vault/title_sync.rs | 179 + src-tauri/src/vault/trash.rs | 111 + src-tauri/src/vault/type_templates.rs | 57 + src-tauri/src/vault/view_date_filters.rs | 107 + src-tauri/src/vault/view_migration.rs | 69 + src-tauri/src/vault/view_relationships.rs | 156 + src-tauri/src/vault/view_tests.rs | 541 + src-tauri/src/vault/view_value_conversions.rs | 29 + src-tauri/src/vault/views.rs | 532 + src-tauri/src/vault_list.rs | 275 + src-tauri/src/vault_watcher.rs | 445 + src-tauri/src/window_state.rs | 587 + src-tauri/tauri.conf.json | 84 + src-tauri/tests/git_connect_no_remote.rs | 61 + src/App.css | 86 + src/App.note-window-properties.test.tsx | 293 + src/App.test.tsx | 1438 + src/App.tsx | 1823 + src/assets/sponsors/circleci-dark.svg | 3 + src/assets/sponsors/circleci-light.svg | 3 + src/assets/sponsors/codacy-dark.svg | 16 + src/assets/sponsors/codacy-light.svg | 16 + src/assets/sponsors/codescene-dark.svg | 3 + src/assets/sponsors/codescene-light.svg | 3 + src/assets/sponsors/unblocked-dark.svg | 22 + src/assets/sponsors/unblocked-light.svg | 28 + src/assets/tolaria-icon.svg | 10 + .../collectionFromSelection.test.ts | 100 + src/collections/collectionFromSelection.ts | 88 + src/collections/collectionTypes.ts | 26 + src/collections/presentationConfig.ts | 44 + .../resolveCollectionEntries.test.ts | 80 + src/collections/resolveCollectionEntries.ts | 57 + src/components/AccentColorPicker.tsx | 76 + src/components/AddPropertyForm.tsx | 222 + src/components/AddRemoteModal.test.tsx | 96 + src/components/AddRemoteModal.tsx | 171 + src/components/AiActionCard.test.tsx | 167 + src/components/AiActionCard.tsx | 262 + src/components/AiAgentIcon.tsx | 57 + .../AiAgentsOnboardingPrompt.test.tsx | 116 + src/components/AiAgentsOnboardingPrompt.tsx | 240 + src/components/AiMessage.test.tsx | 246 + src/components/AiMessage.tsx | 413 + src/components/AiPanel.test.tsx | 556 + src/components/AiPanel.tsx | 315 + .../AiPanelChrome.performance.test.tsx | 55 + src/components/AiPanelChrome.tsx | 573 + src/components/AiProviderSettings.tsx | 357 + src/components/AiWorkspace.test.tsx | 681 + src/components/AiWorkspace.tsx | 1071 + src/components/AiWorkspaceChrome.tsx | 93 + .../AiWorkspaceFloatingButton.test.tsx | 75 + src/components/AiWorkspaceFloatingButton.tsx | 88 + src/components/AiWorkspaceResizeHandles.tsx | 62 + src/components/AiWorkspaceSideHeader.tsx | 381 + src/components/AiWorkspaceSidebar.tsx | 437 + src/components/AiWorkspaceWindowApp.tsx | 324 + src/components/AppAiWorkspaceSurface.tsx | 120 + src/components/ArchivedNoteBanner.test.tsx | 26 + src/components/ArchivedNoteBanner.tsx | 50 + src/components/BreadcrumbBar.test.tsx | 895 + src/components/BreadcrumbBar.tsx | 1277 + .../BreadcrumbBar.visibility.test.tsx | 98 + src/components/BulkActionBar.test.tsx | 93 + src/components/BulkActionBar.tsx | 105 + .../ClaudeCodeOnboardingPrompt.test.tsx | 56 + src/components/ClaudeCodeOnboardingPrompt.tsx | 101 + src/components/CloneVaultModal.test.tsx | 147 + src/components/CloneVaultModal.tsx | 250 + src/components/ColorInput.test.tsx | 101 + src/components/ColorInput.tsx | 173 + src/components/CommandPalette.test.tsx | 614 + src/components/CommandPalette.tsx | 504 + src/components/CommandPaletteAiMode.tsx | 76 + src/components/CommitDialog.test.tsx | 219 + src/components/CommitDialog.tsx | 319 + src/components/ConfirmDeleteDialog.test.tsx | 100 + src/components/ConfirmDeleteDialog.tsx | 197 + src/components/ConflictNoteBanner.test.tsx | 29 + src/components/ConflictNoteBanner.tsx | 70 + .../ConflictResolverModal.extra.test.tsx | 69 + .../ConflictResolverModal.keyboard.test.tsx | 119 + src/components/ConflictResolverModal.test.tsx | 130 + src/components/ConflictResolverModal.tsx | 429 + src/components/CreateNoteDialog.test.tsx | 107 + src/components/CreateNoteDialog.tsx | 124 + src/components/CreateTypeDialog.test.tsx | 91 + src/components/CreateTypeDialog.tsx | 80 + src/components/CreateViewDialog.test.tsx | 107 + src/components/CreateViewDialog.tsx | 178 + src/components/DeleteProgressNotice.test.tsx | 20 + src/components/DeleteProgressNotice.tsx | 31 + src/components/DiffView.test.tsx | 64 + src/components/DiffView.tsx | 42 + ...micPropertiesPanel.systemMetadata.test.tsx | 199 + .../DynamicPropertiesPanel.test.tsx | 980 + src/components/DynamicPropertiesPanel.tsx | 688 + src/components/EditableValue.test.tsx | 318 + src/components/EditableValue.tsx | 280 + src/components/Editor.css | 572 + src/components/Editor.raw-mode-sync.test.ts | 242 + src/components/Editor.test.tsx | 1396 + src/components/Editor.tsx | 794 + src/components/EditorContent.tsx | 7 + src/components/EditorMemoryProbe.tsx | 82 + src/components/EditorRightPanel.test.tsx | 145 + src/components/EditorRightPanel.tsx | 276 + src/components/EditorTheme.css | 744 + src/components/EmojiPicker.tsx | 144 + src/components/FeedbackDialog.test.tsx | 180 + src/components/FeedbackDialog.tsx | 562 + src/components/FilePreview.test.tsx | 278 + src/components/FilePreview.tsx | 547 + src/components/FilterBuilder.test.tsx | 273 + src/components/FilterBuilder.tsx | 291 + src/components/FilterFieldCombobox.tsx | 389 + src/components/FolderTree.test.tsx | 656 + src/components/FolderTree.tsx | 316 + src/components/FormatSelector.test.tsx | 73 + src/components/FormatSelector.tsx | 269 + src/components/FrontendReadyMarker.tsx | 10 + src/components/GitProviderSettingsRows.tsx | 237 + src/components/GitRepositorySelect.tsx | 52 + src/components/GitRequiredModal.test.tsx | 70 + src/components/GitRequiredModal.tsx | 68 + src/components/GitSettingsSection.tsx | 123 + src/components/HiddenEditorMemoryProbe.tsx | 73 + src/components/IconEditableValue.test.tsx | 90 + src/components/IconEditableValue.tsx | 182 + src/components/ImageLightbox.test.tsx | 44 + src/components/ImageLightbox.tsx | 34 + src/components/InlineWikilinkInput.tsx | 614 + src/components/InlineWikilinkParts.tsx | 359 + src/components/Inspector.test.tsx | 786 + src/components/Inspector.tsx | 382 + src/components/InspectorPanels.test.tsx | 895 + src/components/InspectorPanels.tsx | 8 + src/components/LinuxMenuButton.test.tsx | 102 + src/components/LinuxMenuButton.tsx | 180 + src/components/LinuxTitlebar.test.tsx | 90 + src/components/LinuxTitlebar.tsx | 239 + src/components/MarkdownContent.test.tsx | 232 + src/components/MarkdownContent.tsx | 84 + .../MarkdownContent.webkit.test.tsx | 73 + src/components/McpSetupDialog.test.tsx | 170 + src/components/McpSetupDialog.tsx | 266 + src/components/MermaidDiagram.test.tsx | 270 + src/components/MermaidDiagram.tsx | 257 + src/components/NoteAutocomplete.test.tsx | 179 + src/components/NoteAutocomplete.tsx | 342 + src/components/NoteIcon.test.tsx | 72 + src/components/NoteIcon.tsx | 54 + src/components/NoteItem.test.tsx | 536 + src/components/NoteItem.tsx | 594 + src/components/NoteList.behavior.test.tsx | 335 + src/components/NoteList.changes.test.tsx | 297 + src/components/NoteList.contextMenu.test.tsx | 213 + src/components/NoteList.keyboard.test.tsx | 177 + src/components/NoteList.rendering.test.tsx | 1140 + src/components/NoteList.sorting.test.tsx | 402 + src/components/NoteList.tsx | 53 + src/components/NoteSearchList.test.tsx | 261 + src/components/NoteSearchList.tsx | 190 + src/components/NoteTitleIcon.test.tsx | 35 + src/components/NoteTitleIcon.tsx | 70 + src/components/OnboardingShell.tsx | 50 + src/components/PrivacySettingsSection.tsx | 76 + .../PropertyValueCells.extra.test.tsx | 410 + src/components/PropertyValueCells.test.tsx | 395 + src/components/PropertyValueCells.tsx | 679 + src/components/PulseView.test.tsx | 399 + src/components/PulseView.tsx | 519 + src/components/QuickOpenPalette.test.tsx | 261 + src/components/QuickOpenPalette.tsx | 198 + src/components/RawEditorFindBar.test.tsx | 97 + src/components/RawEditorFindBar.tsx | 527 + .../RawEditorView.behavior.test.tsx | 347 + .../RawEditorView.coverage.test.tsx | 276 + src/components/RawEditorView.test.tsx | 135 + src/components/RawEditorView.tsx | 506 + src/components/RenameDetectedBanner.test.tsx | 41 + src/components/RenameDetectedBanner.tsx | 38 + src/components/ResizeHandle.test.tsx | 106 + src/components/ResizeHandle.tsx | 82 + src/components/SafeMarkup.test.tsx | 22 + src/components/SafeMarkup.tsx | 63 + src/components/SearchPanel.test.tsx | 661 + src/components/SearchPanel.tsx | 623 + src/components/SettingsBodyNav.tsx | 48 + src/components/SettingsControls.tsx | 288 + src/components/SettingsFooter.tsx | 31 + src/components/SettingsPanel.test.tsx | 1051 + src/components/SettingsPanel.tsx | 1278 + src/components/SheetContextMenu.tsx | 164 + src/components/SheetEditor.css | 170 + .../SheetEditor.frozenViewport.test.tsx | 46 + .../SheetEditor.serialization.test.tsx | 328 + src/components/SheetEditor.test.tsx | 1229 + src/components/SheetEditor.testUtils.tsx | 698 + src/components/SheetEditor.tsx | 156 + .../SheetFormulaAutocompleteMenu.test.tsx | 41 + .../SheetFormulaAutocompleteMenu.tsx | 55 + src/components/Sidebar.test.ts | 161 + src/components/Sidebar.test.tsx | 1619 + src/components/Sidebar.tsx | 687 + src/components/Sidebar.typeActions.test.tsx | 168 + .../Sidebar.typeVisibilityWorkspaces.test.tsx | 130 + src/components/Sidebar.viewActions.test.tsx | 95 + src/components/SidebarParts.tsx | 749 + src/components/SingleEditorView.test.tsx | 860 + src/components/SingleEditorView.testUtils.tsx | 446 + src/components/SingleEditorView.tsx | 1336 + src/components/SortDropdown.tsx | 462 + src/components/StartupScreen.tsx | 163 + src/components/StatusBar.test.tsx | 932 + src/components/StatusBar.tsx | 275 + src/components/StatusDropdown.extra.test.tsx | 104 + src/components/StatusDropdown.test.tsx | 161 + src/components/StatusDropdown.tsx | 410 + src/components/TableOfContentsPanel.test.tsx | 185 + src/components/TableOfContentsPanel.tsx | 294 + src/components/TagsDropdown.test.tsx | 149 + src/components/TagsDropdown.tsx | 378 + .../TelemetryConsentDialog.test.tsx | 51 + src/components/TelemetryConsentDialog.tsx | 79 + src/components/TldrawWhiteboard.test.tsx | 429 + src/components/TldrawWhiteboard.tsx | 710 + src/components/Toast.tsx | 28 + src/components/TypeCustomizePopover.test.tsx | 243 + src/components/TypeCustomizePopover.tsx | 255 + src/components/TypeSelector.test.tsx | 115 + src/components/TypeSelector.tsx | 496 + src/components/UpdateBanner.test.tsx | 163 + src/components/UpdateBanner.tsx | 184 + .../VaultContentSettingsSection.tsx | 161 + src/components/WelcomeScreen.test.tsx | 224 + src/components/WelcomeScreen.tsx | 602 + src/components/WikilinkChatInput.test.tsx | 786 + src/components/WikilinkChatInput.tsx | 48 + src/components/WikilinkSuggestionMenu.css | 65 + .../WikilinkSuggestionMenu.test.tsx | 66 + src/components/WikilinkSuggestionMenu.tsx | 56 + src/components/WorkspaceInitialsBadge.tsx | 28 + src/components/WorkspaceMoveButtons.tsx | 59 + src/components/WorkspaceSelector.test.tsx | 97 + src/components/WorkspaceSelector.tsx | 395 + src/components/WorkspaceSettingsRows.tsx | 355 + src/components/WorkspaceSettingsSection.tsx | 56 + src/components/aiWorkspaceConversations.ts | 419 + src/components/aiWorkspaceSizing.ts | 131 + src/components/aiWorkspaceTargetGroups.ts | 36 + src/components/anchoredDropdown.test.ts | 71 + src/components/anchoredDropdown.ts | 208 + .../arrowLigaturesExtension.test.ts | 168 + src/components/arrowLigaturesExtension.ts | 142 + src/components/blockNoteCursorTarget.ts | 45 + ...eFormattingToolbarHoverGuard.extra.test.ts | 198 + ...ockNoteFormattingToolbarHoverGuard.test.ts | 296 + .../blockNoteFormattingToolbarHoverGuard.ts | 238 + .../blockNoteRenderRecovery.test.ts | 96 + src/components/blockNoteRenderRecovery.ts | 42 + .../blockNoteRenderRecoveryDocument.test.ts | 79 + .../blockNoteRenderRecoveryDocument.ts | 45 + .../blockNoteSideMenuHoverGuard.test.ts | 100 + src/components/blockNoteSideMenuHoverGuard.ts | 85 + src/components/codeBlockOptions.ts | 179 + src/components/contextMenuPosition.ts | 91 + .../dateDisplayPreferenceFlow.test.tsx | 64 + .../EditorContentLayout.test.tsx | 219 + .../editor-content/EditorContentLayout.tsx | 572 + .../editor-content/editorContentState.test.ts | 193 + .../editor-content/editorContentState.ts | 118 + .../editor-content/useEditorContentModel.ts | 115 + .../editorAttachmentActions.test.ts | 158 + src/components/editorAttachmentActions.ts | 126 + .../editorContentFlushRegistration.test.tsx | 64 + .../editorContentFlushRegistration.ts | 97 + src/components/editorExternalChangeEvents.ts | 40 + src/components/editorImageInsertion.test.ts | 92 + src/components/editorImageInsertion.ts | 23 + src/components/editorMemoryProbeRuntime.ts | 82 + src/components/editorMemoryProbeTypes.ts | 61 + src/components/editorModePosition.test.ts | 232 + src/components/editorModePosition.ts | 398 + .../editorRawModeSync.arrowLigatures.test.ts | 74 + .../editorRawModeSync.images.test.ts | 49 + .../editorRawModeSync.mermaid.test.ts | 67 + src/components/editorRawModeSync.ts | 196 + src/components/editorRichCopy.test.ts | 175 + src/components/editorRichCopy.ts | 258 + .../editorSchema.codeBlockLanguages.test.ts | 28 + .../editorSchema.markdownHighlight.test.ts | 40 + src/components/editorSchema.math.test.tsx | 124 + src/components/editorSchema.media.test.ts | 32 + src/components/editorSchema.mermaid.test.ts | 25 + src/components/editorSchema.tsx | 494 + src/components/editorSchema.webkit.test.ts | 187 + src/components/editorTiptapSelection.test.ts | 106 + src/components/editorTiptapSelection.ts | 141 + .../filter-builder/DateValueInput.test.tsx | 59 + .../filter-builder/DateValueInput.tsx | 114 + .../FilterFieldCombobox.test.tsx | 59 + .../filter-builder/FilterFieldOptionsList.tsx | 69 + .../folder-tree/FolderContextMenu.test.tsx | 50 + .../folder-tree/FolderContextMenu.tsx | 121 + src/components/folder-tree/FolderItemRow.tsx | 123 + .../folder-tree/FolderNameInput.tsx | 57 + src/components/folder-tree/FolderTreeRow.tsx | 196 + .../folder-tree/folderTreeLayout.ts | 17 + src/components/folder-tree/folderTreeUtils.ts | 31 + .../folder-tree/useFolderContextMenu.ts | 71 + .../folder-tree/useFolderRowInteractions.ts | 56 + .../folder-tree/useFolderTreeDisclosure.ts | 115 + src/components/freshListItemPaste.ts | 44 + .../imeCompositionKeyGuardExtension.test.ts | 137 + .../imeCompositionKeyGuardExtension.ts | 43 + src/components/inlineWikilinkBeforeInput.ts | 28 + src/components/inlineWikilinkDom.test.ts | 100 + src/components/inlineWikilinkDom.ts | 208 + src/components/inlineWikilinkDropText.ts | 71 + src/components/inlineWikilinkEdits.ts | 84 + src/components/inlineWikilinkKeydown.ts | 143 + .../inlineWikilinkPasteRecovery.test.ts | 26 + src/components/inlineWikilinkPasteRecovery.ts | 32 + .../inlineWikilinkRemountState.test.ts | 74 + src/components/inlineWikilinkRemountState.ts | 57 + .../inlineWikilinkSuggestions.test.ts | 18 + src/components/inlineWikilinkSuggestions.ts | 78 + src/components/inlineWikilinkText.ts | 149 + src/components/inlineWikilinkTokens.ts | 16 + src/components/inspector/BacklinksPanel.tsx | 65 + src/components/inspector/GitHistoryPanel.tsx | 43 + src/components/inspector/InspectorChrome.tsx | 118 + src/components/inspector/InstancesPanel.tsx | 57 + src/components/inspector/LinkButton.tsx | 58 + .../inspector/NoteInfoPanel.test.tsx | 81 + src/components/inspector/NoteInfoPanel.tsx | 58 + .../inspector/ReferencedByPanel.tsx | 85 + .../inspector/RelationshipsPanel.tsx | 986 + .../relationshipDisplayLabels.test.tsx | 153 + .../relationshipLayoutRegression.test.tsx | 79 + src/components/inspector/shared.test.ts | 38 + src/components/inspector/shared.ts | 49 + .../inspector/useInspectorData.test.ts | 57 + src/components/inspector/useInspectorData.ts | 224 + .../inspector/useInspectorPropertyActions.ts | 82 + .../markdownHighlightInputExtension.test.ts | 223 + .../markdownHighlightInputExtension.ts | 168 + ...markdownHighlightShortcutExtension.test.ts | 138 + .../markdownHighlightShortcutExtension.ts | 67 + src/components/mathInputExtension.test.ts | 378 + src/components/mathInputExtension.ts | 356 + .../note-item/ChangeNoteContent.tsx | 158 + src/components/note-item/PropertyChips.tsx | 161 + .../note-item/propertyChipValues.ts | 219 + src/components/note-item/typeIcon.ts | 37 + src/components/note-list/FilterPills.tsx | 54 + src/components/note-list/InboxFilterPills.tsx | 55 + .../note-list/ListPropertiesPopover.tsx | 415 + .../note-list/NoteListChangesMenu.tsx | 154 + .../note-list/NoteListChangesMenuView.tsx | 106 + .../note-list/NoteListContextMenu.tsx | 179 + .../note-list/NoteListContextMenuView.tsx | 438 + .../note-list/NoteListHeader.expand.test.tsx | 61 + src/components/note-list/NoteListHeader.tsx | 348 + src/components/note-list/NoteListLayout.tsx | 386 + .../note-list/NoteListSearchKeyboard.test.tsx | 145 + src/components/note-list/NoteListViews.tsx | 84 + src/components/note-list/PinnedCard.tsx | 8 + .../RelationshipGroupSection.test.tsx | 55 + .../note-list/RelationshipGroupSection.tsx | 53 + .../note-list/TrashWarningBanner.tsx | 15 + .../note-list/multiSelectionCommands.ts | 5 + .../note-list/noteListFullTextSearch.ts | 150 + .../note-list/noteListHooks.extra.test.tsx | 585 + src/components/note-list/noteListHooks.ts | 1282 + .../note-list/noteListPropertiesEvents.ts | 13 + src/components/note-list/noteListSearch.ts | 80 + .../note-list/noteListUtils.test.ts | 137 + src/components/note-list/noteListUtils.ts | 236 + .../note-list/useMultiSelectKeyboard.ts | 86 + src/components/note-list/useNoteListModel.tsx | 838 + .../note-list/useNoteListSearchState.ts | 75 + .../NoteRetargetingDialogs.tsx | 62 + .../note-retargeting/RetargetNoteDialog.tsx | 175 + src/components/noteIconPropertyEvents.ts | 5 + src/components/propertyChipStyles.test.tsx | 85 + src/components/propertyChipStyles.ts | 9 + src/components/propertyDropdownUtils.test.ts | 21 + src/components/propertyDropdownUtils.ts | 25 + src/components/propertyPanelLayout.test.tsx | 188 + src/components/propertyPanelLayout.ts | 20 + .../propertyTypeIconControl.test.tsx | 123 + .../richEditorBlockNoteOptions.test.ts | 35 + src/components/richEditorBlockNoteOptions.ts | 11 + .../richEditorBlockSelectionClipboard.test.ts | 86 + .../richEditorBlockSelectionClipboard.ts | 99 + .../richEditorBlockSelectionDocument.test.ts | 84 + .../richEditorBlockSelectionDocument.ts | 280 + .../richEditorBlockSelectionExtension.test.ts | 490 + .../richEditorBlockSelectionExtension.ts | 634 + .../richEditorBlockSelectionTypes.ts | 57 + .../richEditorBlockTypeCommands.test.ts | 89 + src/components/richEditorBlockTypeCommands.ts | 94 + .../richEditorDispatchPerformance.test.ts | 56 + .../richEditorDispatchPerformance.ts | 68 + src/components/richEditorInputTransform.ts | 192 + .../richEditorInputTransformExtension.test.ts | 216 + .../richEditorInputTransformExtension.ts | 13 + src/components/richEditorPaste.ts | 51 + .../richEditorRecoveryClassifier.test.ts | 66 + .../richEditorRecoveryClassifier.ts | 202 + .../richEditorTextDirection.test.ts | 18 + src/components/richEditorTextDirection.ts | 69 + ...torTransformErrorRecoveryExtension.test.ts | 463 + ...chEditorTransformErrorRecoveryExtension.ts | 278 + src/components/settingsPreferenceTracking.ts | 86 + src/components/settingsSectionIds.ts | 10 + .../sheetAutocompleteKeyActions.ts | 34 + .../sheetEditorActiveSheetTextInput.ts | 9 + .../sheetEditorAutocompleteState.ts | 54 + .../sheet-editor/sheetEditorFocusOwnership.ts | 36 + .../sheet-editor/sheetEditorFormulaText.ts | 64 + .../sheet-editor/sheetEditorHelpers.ts | 36 + .../sheet-editor/sheetEditorInputDom.ts | 3 + .../sheetEditorInputEvents.test.ts | 19 + .../sheet-editor/sheetEditorInputEvents.ts | 24 + .../sheet-editor/sheetEditorInputTarget.ts | 4 + .../sheet-editor/sheetEditorKeyboard.ts | 17 + .../sheet-editor/sheetEditorKeyboardEvents.ts | 6 + .../sheet-editor/sheetEditorKeyboardFocus.ts | 41 + .../sheet-editor/sheetEditorPlainKeyGuards.ts | 21 + .../sheet-editor/sheetEditorPointer.ts | 27 + .../sheet-editor/sheetEditorSerializeKeys.ts | 11 + .../sheetEditorSpreadsheetKeys.ts | 26 + .../sheet-editor/sheetEditorTargetGuards.ts | 17 + .../sheet-editor/sheetEditorTypes.ts | 17 + .../sheetEditorVisibleFormulaInput.ts | 11 + .../sheetEditorVisibleSheetTextInput.test.ts | 31 + .../sheetEditorVisibleSheetTextInput.ts | 6 + .../sheetEditorWikilinkAutocomplete.ts | 40 + .../sheetInlineAutocompleteTypes.ts | 29 + .../useGuardedWorkbookFocus.test.tsx | 173 + .../sheet-editor/useGuardedWorkbookFocus.ts | 70 + .../sheet-editor/useSheetCellInputCommit.ts | 292 + .../useSheetClipboardActions.test.tsx | 76 + .../sheet-editor/useSheetClipboardActions.ts | 401 + .../useSheetContextMenuActions.ts | 135 + .../useSheetContextMenuCapture.ts | 54 + .../sheet-editor/useSheetEditorController.ts | 526 + .../useSheetExternalFormulaResolution.ts | 372 + .../useSheetFormulaAutocomplete.ts | 115 + .../useSheetInlineAutocompletes.ts | 32 + .../useSheetInputActivityHandlers.ts | 91 + .../useSheetKeyboardFocus.test.tsx | 151 + .../sheet-editor/useSheetKeyboardFocus.ts | 111 + .../sheet-editor/useSheetKeyboardHandlers.ts | 309 + .../useSheetKeyboardReleaseOutside.ts | 28 + .../useSheetPointerCoordinatePatching.ts | 45 + .../sheet-editor/useSheetPointerHandlers.ts | 186 + .../sheet-editor/useSheetSelectionChrome.ts | 106 + .../useSheetWikilinkAutocomplete.ts | 179 + .../useSheetWorkbookController.ts | 570 + src/components/sidebar/FavoritesSection.tsx | 161 + src/components/sidebar/SidebarGroupHeader.tsx | 39 + .../sidebar/SidebarLoadingSections.tsx | 278 + src/components/sidebar/SidebarSections.tsx | 534 + .../sidebar/SidebarTitleBar.test.tsx | 50 + src/components/sidebar/SidebarTopNav.tsx | 66 + src/components/sidebar/SidebarViewActions.tsx | 174 + src/components/sidebar/SidebarViewItem.tsx | 178 + src/components/sidebar/sidebarHooks.ts | 242 + src/components/sidebar/sidebarStyles.ts | 13 + .../sidebar/useSidebarTypeInteractions.ts | 155 + .../sidebar/useSidebarViewItemInteractions.ts | 246 + .../status-bar/AiAgentsBadge.test.tsx | 188 + src/components/status-bar/AiAgentsBadge.tsx | 395 + .../status-bar/StatusBarBadges.extra.test.tsx | 156 + src/components/status-bar/StatusBarBadges.tsx | 921 + .../status-bar/StatusBarSections.tsx | 571 + .../VaultMenu.applyMountedChange.test.ts | 102 + src/components/status-bar/VaultMenu.tsx | 801 + src/components/status-bar/styles.ts | 18 + src/components/status-bar/types.ts | 11 + .../status-bar/useDismissibleLayer.ts | 21 + .../status-bar/vaultMenuMountedChange.ts | 47 + src/components/tableOfContents.worker.ts | 27 + src/components/tableOfContentsModel.ts | 311 + src/components/tableOfContentsWorkerClient.ts | 76 + src/components/tableResizeState.ts | 22 + .../titleHeadingInteractions.test.ts | 107 + src/components/titleHeadingInteractions.ts | 97 + src/components/titleHeadingPasteTarget.ts | 34 + src/components/tldrawBlockProps.test.ts | 109 + src/components/tldrawBlockProps.ts | 96 + .../tldrawTextMeasurementGuard.test.ts | 91 + src/components/tldrawTextMeasurementGuard.ts | 86 + src/components/tolariaBlockNoteDom.ts | 81 + .../tolariaBlockNoteSideMenu.test.tsx | 846 + src/components/tolariaBlockNoteSideMenu.tsx | 445 + src/components/tolariaBlockReorder.ts | 341 + src/components/tolariaCollapsedSections.ts | 853 + .../tolariaEditorFormatting.behavior.test.tsx | 620 + .../tolariaEditorFormatting.test.tsx | 254 + src/components/tolariaEditorFormatting.tsx | 877 + .../tolariaEditorFormattingConfig.ts | 291 + src/components/tolariaSideMenuAlignment.ts | 187 + src/components/tolariaSideMenuBlocks.ts | 53 + .../ui/action-tooltip.recovery.test.tsx | 78 + src/components/ui/action-tooltip.tsx | 83 + src/components/ui/actionTooltipRecovery.ts | 21 + src/components/ui/badge.tsx | 49 + src/components/ui/button.tsx | 65 + src/components/ui/calendar.tsx | 272 + src/components/ui/card.tsx | 92 + src/components/ui/checkbox.tsx | 59 + src/components/ui/dialog.tsx | 171 + src/components/ui/dropdown-menu.tsx | 275 + src/components/ui/input.tsx | 23 + src/components/ui/overlayPresence.test.tsx | 107 + src/components/ui/popover.tsx | 98 + src/components/ui/scroll-area.tsx | 56 + src/components/ui/select.tsx | 202 + src/components/ui/separator.tsx | 28 + src/components/ui/switch.tsx | 46 + src/components/ui/tabs.tsx | 90 + src/components/ui/textControls.test.tsx | 26 + src/components/ui/textarea.tsx | 24 + src/components/ui/tooltip.tsx | 65 + src/components/useAiPanelContextSnapshot.ts | 53 + src/components/useAiPanelController.ts | 220 + src/components/useAiPanelFocus.ts | 70 + src/components/useAiPanelPromptQueue.test.tsx | 87 + src/components/useAiPanelPromptQueue.ts | 91 + src/components/useEditorComposing.ts | 121 + .../useEditorLinkActivation.test.tsx | 233 + src/components/useEditorLinkActivation.ts | 180 + .../useEditorMemoryProbeController.ts | 135 + .../useEditorModePositionSync.test.tsx | 271 + src/components/useEditorModePositionSync.ts | 131 + src/components/useEditorPdfExport.ts | 157 + .../useFilenameAutolinkGuard.extra.test.tsx | 163 + .../useFilenameAutolinkGuard.test.tsx | 174 + src/components/useFilenameAutolinkGuard.ts | 132 + src/components/useInlineWikilinkSelection.ts | 103 + .../useInlineWikilinkSuggestionsState.ts | 137 + src/components/useNativePathDrop.ts | 85 + src/components/useNoteListSort.test.tsx | 164 + src/components/useQueuedAiPrompt.ts | 24 + src/components/useRawModeWithFlush.test.tsx | 71 + src/components/useRawModeWithFlush.ts | 370 + .../useRichEditorSheetTransition.ts | 65 + src/components/useRightPanelExclusion.ts | 100 + src/components/useSettingsPanelFocus.ts | 74 + .../useTurnCurrentBlockIntoCommand.test.tsx | 53 + .../useTurnCurrentBlockIntoCommand.ts | 43 + src/constants/appStorage.test.ts | 42 + src/constants/appStorage.ts | 75 + src/constants/feedback.ts | 12 + src/extensions/frontmatterHighlight.test.ts | 63 + src/extensions/frontmatterHighlight.ts | 80 + src/extensions/markdownHighlight.test.ts | 132 + src/extensions/markdownHighlight.ts | 61 + src/extensions/rawEditorLanguage.ts | 41 + src/extensions/zoomCursorFix.behavior.test.ts | 161 + src/extensions/zoomCursorFix.extra.test.ts | 113 + src/extensions/zoomCursorFix.test.ts | 94 + src/extensions/zoomCursorFix.ts | 156 + src/hooks/appCommandCatalog.test.ts | 33 + src/hooks/appCommandCatalog.ts | 510 + src/hooks/appCommandDispatcher.test.ts | 365 + src/hooks/appCommandDispatcher.ts | 330 + .../appKeyboardShortcuts.editorFind.test.ts | 85 + src/hooks/appKeyboardShortcuts.ts | 114 + src/hooks/commands/aiAgentCommands.test.ts | 67 + src/hooks/commands/aiAgentCommands.ts | 126 + src/hooks/commands/editorFindCommands.ts | 59 + src/hooks/commands/filterCommands.ts | 16 + src/hooks/commands/gitCommands.test.ts | 39 + src/hooks/commands/gitCommands.ts | 87 + src/hooks/commands/index.ts | 10 + src/hooks/commands/localizeCommands.ts | 230 + src/hooks/commands/navigationCommands.ts | 140 + src/hooks/commands/noteCommands.ts | 396 + src/hooks/commands/settingsCommands.test.ts | 204 + src/hooks/commands/settingsCommands.ts | 240 + src/hooks/commands/typeCommands.ts | 47 + src/hooks/commands/types.ts | 17 + src/hooks/commands/viewCommands.ts | 160 + src/hooks/editorBlockRepair.test.ts | 80 + src/hooks/editorBlockRepair.ts | 166 + .../editorBlockResolution.performance.test.ts | 104 + src/hooks/editorBlockResolution.ts | 351 + src/hooks/editorChangeDebounce.ts | 75 + src/hooks/editorContentSwapApply.test.ts | 194 + src/hooks/editorContentSwapApply.ts | 305 + src/hooks/editorDocumentState.ts | 35 + src/hooks/editorDomSelection.ts | 56 + src/hooks/editorFastMarkdownBlocks.test.ts | 84 + src/hooks/editorFastMarkdownBlocks.ts | 620 + src/hooks/editorFastMarkdownBlocks.worker.ts | 5 + src/hooks/editorFocusOwnership.test.tsx | 103 + src/hooks/editorFocusOwnership.ts | 56 + src/hooks/editorFocusUtils.extra.test.ts | 194 + src/hooks/editorFocusUtils.ts | 124 + src/hooks/editorMarkdownParseFallback.test.ts | 44 + src/hooks/editorMarkdownParseFallback.ts | 54 + src/hooks/editorParsedBlockCache.ts | 92 + src/hooks/editorParsedBlockPreload.test.tsx | 181 + src/hooks/editorParsedBlockPreload.ts | 171 + src/hooks/editorParsedBlockSwap.ts | 72 + src/hooks/editorSwapToken.ts | 77 + src/hooks/editorTabContent.test.ts | 223 + src/hooks/editorTabContent.ts | 128 + src/hooks/editorTabSwapLifecycle.ts | 31 + src/hooks/editorTiptapSelection.ts | 31 + src/hooks/editorTitleHeadingText.ts | 24 + src/hooks/editorTitleSelection.ts | 92 + src/hooks/focusOwnershipGuard.ts | 212 + src/hooks/folder-actions/folderActionUtils.ts | 204 + src/hooks/folder-actions/useFolderDelete.ts | 92 + src/hooks/folder-actions/useFolderRename.ts | 86 + .../frontmatterOps.systemMetadata.test.ts | 40 + src/hooks/frontmatterOps.test.ts | 200 + src/hooks/frontmatterOps.ts | 410 + src/hooks/mockFrontmatterHelpers.test.ts | 262 + src/hooks/mockFrontmatterHelpers.ts | 170 + src/hooks/noteContentCache.ts | 493 + src/hooks/noteCreationRequests.ts | 33 + src/hooks/rawEditorEntryState.ts | 62 + src/hooks/useActionHistory.test.ts | 91 + src/hooks/useActionHistory.ts | 139 + src/hooks/useAiActivity.test.ts | 169 + src/hooks/useAiActivity.ts | 184 + src/hooks/useAiAgentPreferences.test.ts | 100 + src/hooks/useAiAgentPreferences.ts | 90 + src/hooks/useAiAgentsOnboarding.ts | 38 + src/hooks/useAiAgentsStatus.test.ts | 167 + src/hooks/useAiAgentsStatus.ts | 114 + src/hooks/useAiWorkspacePublishedContext.ts | 112 + src/hooks/useAiWorkspaceWindowBridgeEvents.ts | 64 + src/hooks/useAppAiWorkspaceBridge.ts | 97 + src/hooks/useAppCommandAiActions.ts | 65 + src/hooks/useAppCommands.ts | 654 + src/hooks/useAppKeyboard.test.ts | 605 + src/hooks/useAppKeyboard.ts | 20 + src/hooks/useAppNavigation.test.ts | 99 + src/hooks/useAppNavigation.ts | 73 + src/hooks/useAppPreferences.ts | 116 + src/hooks/useAppSave.test.ts | 952 + src/hooks/useAppSave.ts | 889 + src/hooks/useAppViewActions.ts | 369 + src/hooks/useAppWindowControls.ts | 155 + src/hooks/useAutoGit.test.ts | 120 + src/hooks/useAutoGit.ts | 177 + src/hooks/useAutoGitWork.ts | 43 + src/hooks/useAutoSync.extra.test.ts | 201 + src/hooks/useAutoSync.test.ts | 740 + src/hooks/useAutoSync.ts | 655 + src/hooks/useBuildNumber.test.ts | 50 + src/hooks/useBuildNumber.ts | 29 + src/hooks/useBulkActions.test.ts | 145 + src/hooks/useBulkActions.ts | 52 + src/hooks/useClaudeCodeOnboarding.test.ts | 54 + src/hooks/useClaudeCodeOnboarding.ts | 33 + src/hooks/useClaudeCodeStatus.test.ts | 63 + src/hooks/useClaudeCodeStatus.ts | 41 + src/hooks/useCliAiAgent.test.ts | 223 + src/hooks/useCliAiAgent.ts | 180 + .../useCodeMirror.arrowLigatures.test.ts | 169 + src/hooks/useCodeMirror.test.ts | 166 + src/hooks/useCodeMirror.ts | 255 + .../useCommandRegistry.editorFind.test.ts | 76 + src/hooks/useCommandRegistry.test.ts | 1146 + src/hooks/useCommandRegistry.ts | 330 + src/hooks/useCommitFlow.test.ts | 562 + src/hooks/useCommitFlow.ts | 942 + src/hooks/useConflictFlow.test.ts | 134 + src/hooks/useConflictFlow.ts | 113 + src/hooks/useConflictResolver.test.ts | 162 + src/hooks/useConflictResolver.ts | 145 + src/hooks/useDeepLinks.test.tsx | 59 + src/hooks/useDeepLinks.ts | 460 + src/hooks/useDeleteActions.test.ts | 227 + src/hooks/useDeleteActions.ts | 219 + src/hooks/useDialogs.ts | 51 + src/hooks/useDiffMode.test.ts | 218 + src/hooks/useDiffMode.ts | 288 + src/hooks/useDocumentThemeMode.test.ts | 28 + src/hooks/useDocumentThemeMode.ts | 33 + src/hooks/useDragRegion.test.tsx | 91 + src/hooks/useDragRegion.ts | 52 + src/hooks/useEditorContentPathSignal.ts | 26 + src/hooks/useEditorFocus.test.ts | 330 + src/hooks/useEditorFocus.ts | 90 + src/hooks/useEditorSave.test.ts | 583 + src/hooks/useEditorSave.ts | 683 + src/hooks/useEditorSaveWithLinks.test.ts | 353 + src/hooks/useEditorSaveWithLinks.ts | 244 + .../useEditorTabSwap.performance.test.ts | 149 + src/hooks/useEditorTabSwap.rename.test.ts | 323 + src/hooks/useEditorTabSwap.selection.test.ts | 116 + src/hooks/useEditorTabSwap.test.ts | 1217 + src/hooks/useEditorTabSwap.ts | 1205 + .../useEntryActions.actionHistory.test.ts | 116 + src/hooks/useEntryActions.auto-create.test.ts | 78 + src/hooks/useEntryActions.history.test.ts | 184 + src/hooks/useEntryActions.test.ts | 568 + src/hooks/useEntryActions.ts | 615 + .../useEntryActions.typeVisibility.test.ts | 51 + src/hooks/useFeatureFlag.test.ts | 70 + src/hooks/useFeatureFlag.ts | 22 + src/hooks/useFileActions.ts | 93 + src/hooks/useFolderActions.test.ts | 149 + src/hooks/useFolderActions.ts | 59 + src/hooks/useGettingStartedClone.test.ts | 94 + src/hooks/useGettingStartedClone.ts | 44 + src/hooks/useGitFileWorkflows.ts | 461 + src/hooks/useGitHistory.test.ts | 132 + src/hooks/useGitHistory.ts | 47 + src/hooks/useGitRepositories.test.ts | 169 + src/hooks/useGitRepositories.ts | 262 + src/hooks/useGitSetupState.test.tsx | 74 + src/hooks/useGitSetupState.ts | 133 + src/hooks/useImageDrop.test.ts | 440 + src/hooks/useImageDrop.ts | 348 + src/hooks/useImageLightbox.test.tsx | 94 + src/hooks/useImageLightbox.ts | 36 + src/hooks/useInboxOrganizeAdvance.test.ts | 111 + src/hooks/useInboxOrganizeAdvance.ts | 86 + src/hooks/useKeyboardNavigation.test.ts | 173 + src/hooks/useKeyboardNavigation.ts | 74 + src/hooks/useLayoutPanels.test.ts | 165 + src/hooks/useLayoutPanels.ts | 107 + .../useMainWindowSizeConstraints.test.ts | 180 + src/hooks/useMainWindowSizeConstraints.ts | 90 + src/hooks/useMcpBridge.ts | 113 + src/hooks/useMcpSetupDialogController.ts | 90 + src/hooks/useMcpStatus.test.ts | 283 + src/hooks/useMcpStatus.ts | 233 + src/hooks/useMenuEvents.editorFind.test.ts | 91 + .../useMenuEvents.noteListSearch.test.ts | 105 + src/hooks/useMenuEvents.test.ts | 439 + src/hooks/useMenuEvents.ts | 206 + src/hooks/useMultiSelect.ts | 76 + src/hooks/useNavigationGestures.ts | 34 + src/hooks/useNavigationHistory.test.ts | 171 + src/hooks/useNavigationHistory.ts | 99 + src/hooks/useNeighborhoodSelection.test.ts | 132 + src/hooks/useNeighborhoodSelection.ts | 147 + src/hooks/useNetworkStatus.test.ts | 43 + src/hooks/useNetworkStatus.ts | 28 + src/hooks/useNoteActions.hook.test.ts | 962 + src/hooks/useNoteActions.missing-path.test.ts | 199 + src/hooks/useNoteActions.persistence.test.ts | 85 + src/hooks/useNoteActions.rename-guard.test.ts | 125 + src/hooks/useNoteActions.ts | 821 + src/hooks/useNoteActions.typeSync.test.tsx | 105 + src/hooks/useNoteCreation.extra.test.ts | 366 + src/hooks/useNoteCreation.test.ts | 929 + src/hooks/useNoteCreation.ts | 1005 + src/hooks/useNoteGitUrls.test.ts | 108 + src/hooks/useNoteGitUrls.ts | 70 + src/hooks/useNoteLayout.test.ts | 51 + src/hooks/useNoteLayout.ts | 33 + src/hooks/useNoteListKeyboard.test.ts | 265 + src/hooks/useNoteListKeyboard.ts | 641 + src/hooks/useNoteRename.test.ts | 533 + src/hooks/useNoteRename.ts | 616 + src/hooks/useNoteRetargeting.test.ts | 132 + src/hooks/useNoteRetargeting.ts | 241 + src/hooks/useNoteRetargetingUi.ts | 222 + src/hooks/useNoteSearch.test.ts | 326 + src/hooks/useNoteSearch.ts | 236 + src/hooks/useNoteWidthMode.ts | 169 + src/hooks/useNoteWindowLifecycle.test.tsx | 104 + src/hooks/useNoteWindowLifecycle.ts | 187 + src/hooks/useOnboarding.test.ts | 400 + src/hooks/useOnboarding.ts | 365 + src/hooks/usePropertyPanelState.extra.test.ts | 191 + src/hooks/usePropertyPanelState.test.ts | 240 + src/hooks/usePropertyPanelState.ts | 293 + src/hooks/useRawMode.test.ts | 154 + src/hooks/useRawMode.ts | 49 + src/hooks/useRenameVaultRefresh.test.ts | 129 + src/hooks/useSaveNote.test.ts | 64 + src/hooks/useSaveNote.ts | 31 + src/hooks/useSavedViewOrdering.test.ts | 99 + src/hooks/useSavedViewOrdering.ts | 108 + src/hooks/useSettings.test.ts | 380 + src/hooks/useSettings.ts | 203 + src/hooks/useSheetWikilinkNavigation.ts | 59 + src/hooks/useStartupScreenState.ts | 156 + src/hooks/useStatusBarAddRemote.test.ts | 173 + src/hooks/useStatusBarAddRemote.ts | 108 + src/hooks/useTabManagement.test.ts | 1189 + src/hooks/useTabManagement.ts | 606 + src/hooks/useTauriDragDropEvent.test.ts | 82 + src/hooks/useTauriDragDropEvent.ts | 66 + src/hooks/useTelemetry.test.ts | 119 + src/hooks/useTelemetry.ts | 75 + src/hooks/useTheme.test.ts | 32 + src/hooks/useTheme.ts | 60 + src/hooks/useThemeMode.test.ts | 113 + src/hooks/useThemeMode.ts | 72 + src/hooks/useUnavailableVaultState.ts | 60 + src/hooks/useUnifiedSearch.ts | 140 + src/hooks/useUpdater.test.ts | 367 + src/hooks/useUpdater.ts | 186 + src/hooks/useVaultAiGuidanceStatus.test.ts | 77 + src/hooks/useVaultAiGuidanceStatus.ts | 73 + src/hooks/useVaultBridge.test.ts | 190 + src/hooks/useVaultBridge.ts | 160 + src/hooks/useVaultConfig.ts | 77 + src/hooks/useVaultLoader.empty-cache.test.ts | 127 + src/hooks/useVaultLoader.extra.test.ts | 326 + src/hooks/useVaultLoader.startup.test.ts | 114 + src/hooks/useVaultLoader.test.ts | 1334 + src/hooks/useVaultLoader.ts | 1279 + src/hooks/useVaultLoader.workspaces.test.ts | 399 + src/hooks/useVaultOpenedTelemetry.ts | 40 + src/hooks/useVaultRenameDetection.ts | 62 + src/hooks/useVaultReorderAction.ts | 26 + src/hooks/useVaultSwitcher.test.ts | 705 + src/hooks/useVaultSwitcher.ts | 1281 + src/hooks/useVaultWatcher.test.ts | 331 + src/hooks/useVaultWatcher.ts | 382 + src/hooks/useViewMode.test.ts | 78 + src/hooks/useViewMode.ts | 40 + src/hooks/useWorkspaceGraphState.ts | 262 + src/hooks/useWorkspaceIdentityActions.ts | 51 + src/hooks/useZoom.test.ts | 148 + src/hooks/useZoom.ts | 89 + src/hooks/vaultLoaderCommands.ts | 285 + src/hooks/vaultLoaderCommands.views.test.ts | 178 + src/hooks/vaultStateReset.ts | 23 + src/hooks/vaultSwitcherSanitization.ts | 29 + src/hooks/vaultWorkspaceEntries.ts | 177 + src/index.css | 626 + src/indexBootDiagnostics.test.ts | 46 + src/lib/agentDocsPath.ts | 20 + src/lib/aiAgentConversation.test.ts | 281 + src/lib/aiAgentConversation.ts | 134 + src/lib/aiAgentFileOperations.test.ts | 86 + src/lib/aiAgentFileOperations.ts | 279 + src/lib/aiAgentMessageState.ts | 64 + src/lib/aiAgentPermissionMode.test.ts | 30 + src/lib/aiAgentPermissionMode.ts | 48 + src/lib/aiAgentSession.test.ts | 434 + src/lib/aiAgentSession.ts | 270 + src/lib/aiAgentStreamCallbacks.test.ts | 448 + src/lib/aiAgentStreamCallbacks.ts | 239 + src/lib/aiAgents.test.ts | 68 + src/lib/aiAgents.ts | 151 + src/lib/aiFeatures.test.ts | 11 + src/lib/aiFeatures.ts | 5 + src/lib/aiTargets.test.ts | 139 + src/lib/aiTargets.ts | 216 + src/lib/aiWorkspaceSessionStore.test.ts | 123 + src/lib/aiWorkspaceSessionStore.ts | 247 + .../aiWorkspaceWindowSharedContext.test.ts | 94 + src/lib/aiWorkspaceWindowSharedContext.ts | 127 + src/lib/appIconTheme.test.ts | 39 + src/lib/appIconTheme.ts | 13 + src/lib/appUpdater.ts | 55 + src/lib/automaticUpdateChecks.ts | 7 + src/lib/blockNoteChecklist.regression.test.ts | 95 + ...ockNoteCodeBlockControl.regression.test.ts | 105 + ...ckNoteCopyCompatibility.regression.test.ts | 58 + src/lib/blockNoteLinkClick.regression.test.ts | 26 + src/lib/blockNotePopover.regression.test.ts | 15 + src/lib/blockNoteSideMenu.regression.test.ts | 72 + ...blockNoteSuggestionMenu.regression.test.ts | 105 + ...kNoteSuggestionWrapper.regression.test.tsx | 60 + .../blockNoteTableHandles.regression.test.ts | 372 + src/lib/crossWindowPersistedStore.test.ts | 115 + src/lib/crossWindowPersistedStore.ts | 91 + src/lib/feedbackDiagnostics.test.ts | 49 + src/lib/feedbackDiagnostics.ts | 157 + src/lib/feedbackDialogOpener.ts | 30 + src/lib/gitSettings.ts | 5 + src/lib/gitignoredVisibility.ts | 7 + src/lib/gitignoredVisibilityEvents.ts | 36 + src/lib/i18n.test.ts | 85 + src/lib/i18n.ts | 359 + src/lib/locales/be-BY.json | 999 + src/lib/locales/be-Latn.json | 999 + src/lib/locales/de-DE.json | 999 + src/lib/locales/en.json | 999 + src/lib/locales/es-419.json | 999 + src/lib/locales/es-ES.json | 999 + src/lib/locales/fr-FR.json | 999 + src/lib/locales/id-ID.json | 999 + src/lib/locales/it-IT.json | 999 + src/lib/locales/ja-JP.json | 999 + src/lib/locales/ko-KR.json | 999 + src/lib/locales/pl-PL.json | 999 + src/lib/locales/pt-BR.json | 999 + src/lib/locales/pt-PT.json | 999 + src/lib/locales/ru-RU.json | 999 + src/lib/locales/sk-SK.json | 999 + src/lib/locales/sv-SE.json | 999 + src/lib/locales/uk-UA.json | 999 + src/lib/locales/vi.json | 999 + src/lib/locales/zh-CN.json | 999 + src/lib/locales/zh-TW.json | 999 + src/lib/localizedStreamError.ts | 65 + src/lib/nativeTextAssistance.ts | 66 + src/lib/productAnalytics.ts | 242 + src/lib/releaseChannel.test.ts | 38 + src/lib/releaseChannel.ts | 13 + src/lib/runtimeStyleNonce.ts | 6 + src/lib/sensitiveTextRedaction.ts | 124 + src/lib/telemetry.test.ts | 286 + src/lib/telemetry.ts | 213 + src/lib/telemetryConfig.test.ts | 146 + src/lib/telemetryConfig.ts | 157 + src/lib/themeMode.test.ts | 89 + src/lib/themeMode.ts | 105 + src/lib/utils.ts | 6 + src/lib/vaultAiGuidance.test.ts | 123 + src/lib/vaultAiGuidance.ts | 131 + src/main.test.ts | 315 + src/main.tsx | 201 + src/mock-tauri/index.test.ts | 41 + src/mock-tauri/index.ts | 46 + src/mock-tauri/mock-content.ts | 698 + src/mock-tauri/mock-entries.ts | 1119 + src/mock-tauri/mock-handlers.coverage.test.ts | 224 + src/mock-tauri/mock-handlers.more.test.ts | 234 + src/mock-tauri/mock-handlers.test.ts | 33 + src/mock-tauri/mock-handlers.ts | 741 + src/mock-tauri/vault-api.test.ts | 118 + src/mock-tauri/vault-api.ts | 217 + src/shared/aiModelProviderCatalog.json | 79 + src/shared/appCommandManifest.json | 590 + src/test-utils/noteListTestUtils.tsx | 165 + src/test/setup.test.ts | 13 + src/test/setup.ts | 124 + src/theme.json | 130 + src/types.ts | 334 + src/types/laputaTestBridge.ts | 24 + src/utils/__tests__/emoji.test.ts | 104 + src/utils/addRemoteEvents.ts | 5 + src/utils/ai-agent.test.ts | 65 + src/utils/ai-agent.ts | 100 + src/utils/ai-chat.test.ts | 228 + src/utils/ai-chat.ts | 216 + src/utils/ai-context.test.ts | 444 + src/utils/ai-context.ts | 351 + src/utils/ai-reference-content.ts | 26 + src/utils/aiConversationTitle.test.ts | 118 + src/utils/aiConversationTitle.ts | 254 + src/utils/aiPromptBridge.ts | 55 + src/utils/aiProviderSecrets.ts | 28 + src/utils/aiStreamEvents.ts | 7 + src/utils/allNotesFileVisibility.ts | 51 + src/utils/appOrchestration.test.ts | 65 + src/utils/appOrchestration.ts | 69 + src/utils/arrowLigatures.test.ts | 126 + src/utils/arrowLigatures.ts | 140 + src/utils/autoGitWork.test.ts | 78 + src/utils/autoGitWork.ts | 58 + src/utils/autoSave.test.ts | 93 + src/utils/autoSave.ts | 28 + src/utils/automaticCommitMessage.test.ts | 26 + src/utils/automaticCommitMessage.ts | 17 + src/utils/blockNoteDirectMarkdown.test.ts | 189 + src/utils/blockNoteDirectMarkdown.ts | 411 + src/utils/calendarVersion.test.ts | 14 + src/utils/calendarVersion.ts | 62 + src/utils/chatWikilinks.ts | 36 + src/utils/clipboardText.test.ts | 66 + src/utils/clipboardText.ts | 38 + src/utils/codeBlockLanguage.test.ts | 81 + src/utils/codeBlockLanguage.ts | 140 + src/utils/codeBlockLanguageCatalog.ts | 70 + src/utils/colorUtils.test.ts | 134 + src/utils/colorUtils.ts | 102 + src/utils/commitEntryAction.test.ts | 32 + src/utils/commitEntryAction.ts | 21 + src/utils/commitMessage.test.ts | 73 + src/utils/commitMessage.ts | 33 + src/utils/commitMessageDraft.test.ts | 176 + src/utils/commitMessageDraft.ts | 200 + src/utils/compact-markdown.test.ts | 147 + src/utils/compact-markdown.ts | 187 + src/utils/configMigration.test.ts | 191 + src/utils/configMigration.ts | 113 + src/utils/cssFunctionalColor.ts | 48 + src/utils/dateDisplay.test.ts | 30 + src/utils/dateDisplay.ts | 80 + src/utils/dateStringParts.ts | 90 + src/utils/deepLinks.test.ts | 123 + src/utils/deepLinks.ts | 244 + src/utils/domScroll.ts | 9 + src/utils/durableMarkdownBlocks.ts | 308 + src/utils/editorDurableMarkdown.test.ts | 73 + src/utils/editorDurableMarkdown.ts | 66 + src/utils/editorFind.test.ts | 69 + src/utils/editorFind.ts | 126 + src/utils/editorFindEvents.ts | 24 + src/utils/editorLinkAutolink.test.ts | 62 + src/utils/editorLinkAutolink.ts | 151 + src/utils/editorPerformanceTrace.ts | 124 + src/utils/emoji.ts | 89 + src/utils/fileAttachmentMarkdown.test.ts | 130 + src/utils/fileAttachmentMarkdown.ts | 394 + src/utils/filePreview.ts | 61 + src/utils/filterDates.test.ts | 43 + src/utils/filterDates.ts | 124 + src/utils/frontendReady.test.ts | 48 + src/utils/frontendReady.ts | 74 + src/utils/frontmatter.test.ts | 175 + src/utils/frontmatter.ts | 217 + src/utils/fuzzyMatch.test.ts | 109 + src/utils/fuzzyMatch.ts | 59 + src/utils/gettingStartedVault.test.ts | 45 + src/utils/gettingStartedVault.ts | 80 + src/utils/gitRepositories.test.ts | 74 + src/utils/gitRepositories.ts | 80 + src/utils/guanghuPortalMarkdown.test.ts | 38 + src/utils/guanghuPortalMarkdown.ts | 93 + src/utils/iconRegistry.test.ts | 27 + src/utils/iconRegistry.ts | 350 + src/utils/imageLightboxTarget.test.ts | 88 + src/utils/imageLightboxTarget.ts | 45 + src/utils/initializeNoteProperties.test.ts | 19 + src/utils/initializeNoteProperties.ts | 13 + src/utils/inverseRelationshipLabels.ts | 35 + src/utils/legacyTypes.ts | 5 + src/utils/markdownFences.test.ts | 47 + src/utils/markdownFences.ts | 92 + src/utils/markdownHighlightMarkdown.test.ts | 99 + src/utils/markdownHighlightMarkdown.ts | 248 + src/utils/mathMarkdown.test.ts | 183 + src/utils/mathMarkdown.ts | 520 + src/utils/mediaPreviewRuntime.ts | 50 + src/utils/mermaidMarkdown.test.ts | 212 + src/utils/mermaidMarkdown.ts | 136 + src/utils/mouseMovement.test.ts | 65 + src/utils/mouseMovement.ts | 44 + src/utils/neighborhoodHistory.test.ts | 133 + src/utils/neighborhoodHistory.ts | 119 + src/utils/noteFormat.test.ts | 54 + src/utils/noteFormat.ts | 59 + src/utils/noteIcon.ts | 36 + src/utils/noteListHelpers.extra.test.ts | 276 + src/utils/noteListHelpers.test.ts | 467 + src/utils/noteListHelpers.ts | 641 + .../noteListHelpers.view-workspaces.test.ts | 65 + src/utils/noteListSearchEvents.ts | 34 + src/utils/noteOpenPerformance.extra.test.ts | 145 + src/utils/noteOpenPerformance.test.ts | 88 + src/utils/noteOpenPerformance.ts | 112 + src/utils/notePathIdentity.test.ts | 38 + src/utils/notePathIdentity.ts | 67 + src/utils/notePdfExport.test.ts | 132 + src/utils/notePdfExport.ts | 204 + src/utils/noteSlug.test.ts | 18 + src/utils/noteSlug.ts | 11 + src/utils/noteTitle.test.ts | 98 + src/utils/noteTitle.ts | 129 + src/utils/noteWidth.test.ts | 33 + src/utils/noteWidth.ts | 26 + src/utils/openAiWorkspaceWindow.test.ts | 266 + src/utils/openAiWorkspaceWindow.ts | 340 + src/utils/openNoteWindow.test.ts | 120 + src/utils/openNoteWindow.ts | 61 + src/utils/organizationWorkflow.test.ts | 36 + src/utils/organizationWorkflow.ts | 29 + src/utils/plainTextPaste.test.ts | 95 + src/utils/plainTextPaste.ts | 197 + src/utils/platform.test.ts | 59 + src/utils/platform.ts | 26 + src/utils/pointerButtons.ts | 3 + src/utils/propertyLabels.test.ts | 18 + src/utils/propertyLabels.ts | 6 + src/utils/propertyTypes.test.ts | 230 + src/utils/propertyTypes.ts | 166 + src/utils/pulledVaultRefresh.test.ts | 173 + src/utils/pulledVaultRefresh.ts | 170 + src/utils/rawEditorLanguage.test.ts | 25 + src/utils/rawEditorLanguage.ts | 49 + src/utils/rawEditorUtils.test.ts | 188 + src/utils/rawEditorUtils.ts | 128 + src/utils/regexCapabilities.ts | 29 + src/utils/releaseDownloadPage.test.ts | 283 + src/utils/releaseDownloadPage.ts | 589 + src/utils/releaseHistoryPage.test.ts | 186 + src/utils/releaseHistoryPage.ts | 914 + src/utils/richEditorBlockTypes.ts | 112 + src/utils/richEditorMarkdown.ts | 80 + src/utils/safeRegex.ts | 21 + src/utils/sheetBrowserScheduling.ts | 34 + src/utils/sheetClipboard.test.ts | 101 + src/utils/sheetClipboard.ts | 303 + src/utils/sheetContextMenuState.test.ts | 37 + src/utils/sheetContextMenuState.ts | 66 + src/utils/sheetCsv.test.ts | 98 + src/utils/sheetCsv.ts | 317 + src/utils/sheetDirtyState.test.ts | 46 + src/utils/sheetDirtyState.ts | 42 + src/utils/sheetExternalFormulaWorker.ts | 162 + src/utils/sheetExternalReferences.test.ts | 54 + src/utils/sheetExternalReferences.ts | 219 + src/utils/sheetFormulaAutocomplete.test.ts | 102 + src/utils/sheetFormulaAutocomplete.ts | 248 + src/utils/sheetFrontmatterProperties.ts | 185 + src/utils/sheetInitialWorkbookReadiness.ts | 27 + src/utils/sheetIronCalcColor.test.ts | 17 + src/utils/sheetIronCalcColor.ts | 79 + src/utils/sheetMarkdownCell.test.ts | 50 + src/utils/sheetMarkdownCell.ts | 68 + src/utils/sheetMetadata.test.ts | 136 + src/utils/sheetMetadata.ts | 519 + src/utils/sheetPointerCoordinates.ts | 139 + src/utils/sheetPointerHitTest.ts | 144 + src/utils/sheetSelection.ts | 118 + src/utils/sheetSelectionChrome.ts | 433 + src/utils/sheetWikilinkModelBridge.ts | 105 + src/utils/sheetWikilinks.test.ts | 74 + src/utils/sheetWikilinks.ts | 64 + src/utils/sheetWorkbook.test.ts | 126 + src/utils/sheetWorkbook.ts | 1300 + src/utils/sidebarSections.ts | 140 + src/utils/statusStyles.test.ts | 81 + src/utils/statusStyles.ts | 97 + src/utils/streamAiAgent.test.ts | 277 + src/utils/streamAiAgent.ts | 174 + src/utils/streamAiModel.test.ts | 114 + src/utils/streamAiModel.ts | 140 + src/utils/suggestionEnrichment.test.ts | 202 + src/utils/suggestionEnrichment.ts | 77 + src/utils/systemMetadata.ts | 81 + src/utils/tabEntrySync.test.ts | 55 + src/utils/tabEntrySync.ts | 12 + src/utils/tableOfContents.test.ts | 113 + src/utils/tableOfContents.ts | 124 + src/utils/tagStyles.test.ts | 92 + src/utils/tagStyles.ts | 69 + src/utils/tauriCsp.test.ts | 51 + src/utils/tauriDragDropConfig.test.ts | 9 + src/utils/tauriEventCleanup.ts | 15 + src/utils/tauriLinuxBundleConfig.test.ts | 11 + .../tauriWindowControlPermissions.test.ts | 34 + src/utils/tldrawMarkdown.test.ts | 110 + src/utils/tldrawMarkdown.ts | 172 + src/utils/typeColors.test.ts | 110 + src/utils/typeColors.ts | 109 + src/utils/typeDefinitions.ts | 74 + src/utils/typeDeletion.test.ts | 85 + src/utils/typeDeletion.ts | 41 + src/utils/typeVisibility.ts | 89 + src/utils/url.test.ts | 132 + src/utils/url.ts | 109 + src/utils/vault-dialog.test.ts | 131 + src/utils/vault-dialog.ts | 126 + src/utils/vaultAttachments.test.ts | 134 + src/utils/vaultAttachments.ts | 250 + src/utils/vaultCollections.test.ts | 32 + src/utils/vaultCollections.ts | 54 + src/utils/vaultConfigStore.test.ts | 60 + src/utils/vaultConfigStore.ts | 60 + src/utils/vaultErrors.ts | 9 + src/utils/vaultImages.test.ts | 387 + src/utils/vaultImages.ts | 495 + src/utils/vaultListStore.ts | 84 + src/utils/vaultMetadataNormalization.test.ts | 40 + src/utils/vaultMetadataNormalization.ts | 263 + src/utils/vaultOrdering.test.ts | 50 + src/utils/vaultOrdering.ts | 54 + src/utils/vaultPathContainment.test.ts | 18 + src/utils/vaultPathContainment.ts | 42 + src/utils/vaultTypes.ts | 71 + src/utils/viewFilename.test.ts | 26 + src/utils/viewFilename.ts | 59 + src/utils/viewFilterArrayFields.ts | 133 + src/utils/viewFilterArrayProperties.ts | 74 + src/utils/viewFilters.arrayProperties.test.ts | 70 + src/utils/viewFilters.relativeDates.test.ts | 95 + src/utils/viewFilters.test.ts | 338 + src/utils/viewFilters.ts | 251 + src/utils/viewIdentity.ts | 20 + src/utils/viewOrdering.test.ts | 81 + src/utils/viewOrdering.ts | 62 + src/utils/viewTargetVault.test.ts | 29 + src/utils/viewTargetVault.ts | 22 + .../whiteboardPlatformPermissionRejection.ts | 40 + src/utils/wikilink.test.ts | 303 + src/utils/wikilink.ts | 294 + src/utils/wikilinkColors.test.ts | 160 + src/utils/wikilinkColors.ts | 46 + src/utils/wikilinkSuggestions.test.ts | 173 + src/utils/wikilinkSuggestions.ts | 60 + src/utils/wikilinks.table.test.ts | 55 + src/utils/wikilinks.test.ts | 737 + src/utils/wikilinks.ts | 552 + src/utils/windowMode.test.ts | 256 + src/utils/windowMode.ts | 203 + src/utils/workspaces.test.ts | 191 + src/utils/workspaces.ts | 204 + .../fixtures/test-vault/event/team-meeting.md | 12 + .../fixtures/test-vault/note/archived-note.md | 8 + tests/fixtures/test-vault/note/note-b.md | 10 + tests/fixtures/test-vault/note/note-c.md | 12 + .../fixtures/test-vault/note/trashed-note.md | 9 + .../test-vault/project/alpha-project.md | 16 + .../test-vault/quarter/spring-2026.md | 10 + tests/fixtures/test-vault/type/event.md | 9 + tests/fixtures/test-vault/type/note.md | 9 + tests/fixtures/test-vault/type/project.md | 9 + tests/helpers/fixtureVault.ts | 582 + tests/helpers/fixtureVaultDesktopBridge.ts | 27 + tests/integration/vault-workflows.spec.ts | 185 + .../add-ds-store-files-to-gitignore.spec.ts | 15 + tests/smoke/add-remote-from-no-remote.spec.ts | 133 + tests/smoke/ai-chat-history.spec.ts | 120 + .../smoke/ai-chat-wikilink-clickable.spec.ts | 85 + tests/smoke/ai-notes-visibility-fix.spec.ts | 113 + tests/smoke/ai-panel-shortcut.spec.ts | 18 + tests/smoke/autogit-checkpoints.spec.ts | 253 + tests/smoke/autosave-low-end-typing.spec.ts | 104 + tests/smoke/better-sync-error-ux.spec.ts | 80 + .../blocknote-serializer-blank-lines.spec.ts | 167 + .../smoke/blocknote-slash-menu-mouse.spec.ts | 91 + .../blocknote-slash-menu-styling.spec.ts | 140 + .../smoke/breadcrumb-filename-rename.spec.ts | 107 + tests/smoke/breadcrumb-icon-size.spec.ts | 47 + .../cache-invalidation-vault-open.spec.ts | 29 + .../changing-type-data-corruption.spec.ts | 67 + .../checklist-pull-refresh-regression.spec.ts | 121 + tests/smoke/collision-create-flows.spec.ts | 156 + ...command-palette-ai-mode-regression.spec.ts | 170 + ...mmand-palette-hides-legacy-journal.spec.ts | 54 + .../command-palette-new-note-dedupe.spec.ts | 45 + tests/smoke/contribute-modal.spec.ts | 102 + tests/smoke/create-empty-vault-flows.spec.ts | 226 + tests/smoke/create-note-backing-file.spec.ts | 182 + tests/smoke/create-note-crash.spec.ts | 39 + .../create-note-default-properties.spec.ts | 17 + tests/smoke/create-note-in-folder.spec.ts | 44 + .../create-open-relationship-note.spec.ts | 117 + tests/smoke/deep-link-routing.spec.ts | 63 + tests/smoke/delete-note-nonblocking.spec.ts | 79 + tests/smoke/editor-block-handle-zoom.spec.ts | 96 + tests/smoke/editor-block-reorder.spec.ts | 313 + tests/smoke/editor-code-block-theme.spec.ts | 99 + tests/smoke/editor-find-replace.spec.ts | 62 + .../editor-formatting-persistence.spec.ts | 325 + .../editor-large-note-performance.spec.ts | 145 + .../editor-mode-modified-ordering.spec.ts | 187 + .../editor-mode-position-preservation.spec.ts | 193 + .../editor-save-tooltip-stability.spec.ts | 79 + tests/smoke/exact-match-search.spec.ts | 78 + tests/smoke/example.spec.ts | 71 + tests/smoke/fenced-code-copy.spec.ts | 152 + .../filter-wikilink-autocomplete.spec.ts | 46 + tests/smoke/fix-ai-chat-empty-body-v3.spec.ts | 40 + .../fix-archived-trashed-detection-v2.spec.ts | 109 + .../fix-codemirror-zoom-cursor-v2.spec.ts | 105 + tests/smoke/fix-crash-create-note.spec.ts | 140 + tests/smoke/flip-canonical-type-field.spec.ts | 50 + tests/smoke/focus-no-freeze.spec.ts | 50 + .../smoke/fresh-install-regression-qa.spec.ts | 182 + .../fresh-start-telemetry-onboarding.spec.ts | 136 + tests/smoke/frontmatter-date-picker.spec.ts | 146 + tests/smoke/frontmatter-parsing-fix.spec.ts | 48 + tests/smoke/generate-commit-message.spec.ts | 24 + tests/smoke/getting-started-template.spec.ts | 68 + tests/smoke/git-divergence-conflicts.spec.ts | 90 + tests/smoke/git-no-remote-commit-only.spec.ts | 59 + tests/smoke/gitignored-visibility.spec.ts | 126 + tests/smoke/h1-auto-rename-setting.spec.ts | 62 + tests/smoke/h1-title-decoupled.spec.ts | 178 + tests/smoke/h1-untitled-auto-rename.spec.ts | 498 + tests/smoke/helpers.ts | 118 + tests/smoke/image-toolbar-hover.spec.ts | 169 + tests/smoke/improve-note-open-latency.spec.ts | 71 + .../inline-wikilink-editor-regression.spec.ts | 143 + tests/smoke/inlineWikilinkEditorHelpers.ts | 129 + tests/smoke/keyboard-command-routing.spec.ts | 307 + .../smoke/korean-list-ime-composition.spec.ts | 73 + tests/smoke/latex-math-notes.spec.ts | 189 + tests/smoke/linkify-init-warnings.spec.ts | 63 + .../list-marker-paste-regression.spec.ts | 87 + .../literal-asterisk-paste-regression.spec.ts | 63 + tests/smoke/long-note-editor-scaling.spec.ts | 131 + .../manual-commit-modal-autogit-off.spec.ts | 192 + tests/smoke/mcp-config-copy.spec.ts | 38 + tests/smoke/mermaid-diagram-notes.spec.ts | 622 + .../missing-active-vault-recovery.spec.ts | 113 + .../missing-string-metadata-open-note.spec.ts | 143 + tests/smoke/missing-type-warning.spec.ts | 61 + tests/smoke/mixed-rich-text-keypress.spec.ts | 335 + .../mounted-workspaces-unified-graph.spec.ts | 221 + tests/smoke/move-vault-cache.spec.ts | 33 + tests/smoke/multi-selection-shortcuts.spec.ts | 132 + tests/smoke/multibyte-search-snippet.spec.ts | 72 + .../smoke/multimedia-embed-url-input.spec.ts | 49 + .../new-note-heading-paste-regression.spec.ts | 162 + .../new-note-immediate-editability.spec.ts | 111 + .../smoke/new-note-title-caret-click.spec.ts | 76 + tests/smoke/non-git-vault-init.spec.ts | 32 + .../non-markdown-wikilink-duplicates.spec.ts | 131 + tests/smoke/note-history-edit-loop.spec.ts | 79 + .../note-list-search-keyboard-toggle.spec.ts | 67 + .../note-list-search-visible-content.spec.ts | 107 + .../note-status-indicator-stability.spec.ts | 85 + tests/smoke/note-width-modes.spec.ts | 91 + tests/smoke/number-property.spec.ts | 75 + tests/smoke/offline-onboarding-status.spec.ts | 72 + .../onboarding-vault-registration.spec.ts | 205 + tests/smoke/persist-editor-mode.spec.ts | 58 + tests/smoke/plain-filename-autolink.spec.ts | 108 + tests/smoke/pull-refresh-open-note.spec.ts | 175 + tests/smoke/quick-open-create-note.spec.ts | 52 + ...raw-editor-frontmatter-propagation.spec.ts | 122 + .../raw-editor-sync-to-blocknote.spec.ts | 134 + .../smoke/raw-editor-type-propagation.spec.ts | 105 + .../raw-mode-pasted-text-roundtrip.spec.ts | 86 + tests/smoke/raw-yml-view-save.spec.ts | 184 + tests/smoke/release-channel-settings.spec.ts | 52 + tests/smoke/remove-relationship-keys.spec.ts | 65 + .../smoke/repair-vault-flat-structure.spec.ts | 44 + .../smoke/repair-vault-no-legacy-dirs.spec.ts | 58 + .../smoke/rich-editor-block-selection.spec.ts | 115 + tests/smoke/rich-editor-reload-typing.spec.ts | 476 + tests/smoke/rtl-editor-direction.spec.ts | 135 + tests/smoke/save-before-note-switch.spec.ts | 274 + ...show-deleted-notes-in-changes-view.spec.ts | 52 + .../smoke/sidebar-duplicate-sections.spec.ts | 36 + tests/smoke/sidebar-type-picker-sync.spec.ts | 87 + .../stale-getting-started-switcher.spec.ts | 135 + .../startup-empty-vault-recovery.spec.ts | 152 + .../smoke/table-cell-paste-regression.spec.ts | 74 + tests/smoke/table-focus-regression.spec.ts | 59 + tests/smoke/table-hover-crash.spec.ts | 521 + tests/smoke/table-resize-crash.spec.ts | 79 + tests/smoke/telemetry-consent.spec.ts | 25 + tests/smoke/testBridge.ts | 109 + tests/smoke/tldraw-whiteboard-notes.spec.ts | 380 + tests/smoke/type-create-note.spec.ts | 45 + tests/smoke/type-derived-properties.spec.ts | 79 + tests/smoke/type-rename-metadata.spec.ts | 45 + tests/smoke/vault-ai-guidance-restore.spec.ts | 81 + tests/smoke/vault-loading-skeleton.spec.ts | 149 + .../vault-switch-clears-stale-editor.spec.ts | 215 + tests/smoke/vault-switcher-bottom-bar.spec.ts | 220 + tests/smoke/view-item-hover-count.spec.ts | 102 + tests/smoke/visible-type-property.spec.ts | 39 + .../smoke/wikilink-cmd-click-history.spec.ts | 218 + tests/smoke/wikilink-path-fix.spec.ts | 79 + .../wikilink-traditional-chinese.spec.ts | 54 + tests/smoke/windows-save-path-failure.spec.ts | 84 + trademarks.md | 45 + tsconfig.app.json | 34 + tsconfig.json | 13 + tsconfig.node.json | 27 + ui-design.pen | 15532 +++++++++ vite.config.ts | 1045 + 2115 files changed, 439815 insertions(+) create mode 100644 .chunk/README.md create mode 100644 .chunk/config.json create mode 100644 .chunk/install-playwright-browsers.mjs create mode 100644 .chunk/run-playwright-shards.sh create mode 100644 .chunk/run-playwright-smoke.sh create mode 100644 .chunk/run-rust-gate.sh create mode 100755 .chunk/run-sidecar-gates-local.sh create mode 100644 .chunk/run-sidecar-gates.sh create mode 100755 .chunk/run-sidecar-lane.sh create mode 100644 .claude/commands/create-adr.md create mode 100644 .claude/commands/laputa-done.md create mode 100644 .claude/commands/laputa-next-task.md create mode 100644 .claude/settings.local.json create mode 100644 .codacy.yaml create mode 100644 .codescene-thresholds create mode 100644 .codesceneignore create mode 100644 .codescenerc create mode 100644 .editor-performance-thresholds.json create mode 100644 .env.example create mode 100644 .githooks-info create mode 100644 .github/FUNDING.yml create mode 100644 .github/HOOKS.md create mode 100644 .github/SETUP.md create mode 100755 .github/hooks/install-hooks.sh create mode 100644 .github/hooks/pre-commit create mode 100644 .github/scripts/configure-windows-authenticode.ps1 create mode 100644 .github/scripts/prefetch-tauri-nsis.ps1 create mode 100644 .github/workflows/README.md create mode 100644 .github/workflows/auto-update-prs.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/deploy-docs.yml create mode 100644 .github/workflows/release-build-artifacts.yml create mode 100644 .github/workflows/release-stable.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100755 .husky/pre-commit create mode 100755 .husky/pre-push create mode 100644 .semgrepignore create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 CONTRIBUTING.md create mode 100644 GEMINI.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 UPSTREAM.md create mode 100644 biome.json create mode 100644 components.json create mode 100644 demo-vault-v2/.fixture-manifest.json create mode 100644 demo-vault-v2/.gitignore create mode 100644 demo-vault-v2/24q4-laputa-start.md create mode 100644 demo-vault-v2/24q4.md create mode 100644 demo-vault-v2/25q1-laputa-v1.md create mode 100644 demo-vault-v2/25q1.md create mode 100644 demo-vault-v2/25q2-laputa-v2.md create mode 100644 demo-vault-v2/25q2.md create mode 100644 demo-vault-v2/area-building.md create mode 100644 demo-vault-v2/attachments/laputa-reference.png create mode 100644 demo-vault-v2/event-team-sync-2025-01-13.md create mode 100644 demo-vault-v2/laputa-qa-reference.md create mode 100644 demo-vault-v2/measure-close-rate.md create mode 100644 demo-vault-v2/measure-sponsorship-mrr.md create mode 100644 demo-vault-v2/note-on-clear-prose.md create mode 100644 demo-vault-v2/person-luca-rossi.md create mode 100644 demo-vault-v2/person-matteo-cellini.md create mode 100644 demo-vault-v2/procedure-quarterly-sponsor-outreach.md create mode 100644 demo-vault-v2/procedure-sponsor-onboarding.md create mode 100644 demo-vault-v2/refactoring-business-plan.md create mode 100644 demo-vault-v2/responsibility-sponsorships.md create mode 100644 demo-vault-v2/rtl-mixed-direction-qa.md create mode 100644 demo-vault-v2/tolaria-sheet-prototype-sample.md create mode 100644 demo-vault-v2/topic-writing.md create mode 100644 demo-vault-v2/type/area.md create mode 100644 demo-vault-v2/type/event.md create mode 100644 demo-vault-v2/type/measure.md create mode 100644 demo-vault-v2/type/note.md create mode 100644 demo-vault-v2/type/person.md create mode 100644 demo-vault-v2/type/procedure.md create mode 100644 demo-vault-v2/type/project.md create mode 100644 demo-vault-v2/type/quarter.md create mode 100644 demo-vault-v2/type/responsibility.md create mode 100644 demo-vault-v2/type/topic.md create mode 100644 demo-vault-v2/views/active-projects.yml create mode 100644 demo-vault-v2/writing-for-clarity-vs-writing-for-credit.md create mode 100644 demo-vault-v2/writing-weekly-rhythm.md create mode 100644 design/add-property-inline.pen create mode 100644 design/ai-agent-panel-ui.pen create mode 100644 design/ai-agent-wiring.pen create mode 100644 design/align-property-dropdown.pen create mode 100644 design/archived-note-indicator.pen create mode 100644 design/auto-pull-vault.pen create mode 100644 design/autocomplete-type-color.pen create mode 100644 design/build-number-status-bar.pen create mode 100644 design/command-palette-type-aware.pen create mode 100644 design/date-picker-shadcn.pen create mode 100644 design/design-full-layouts.pen create mode 100644 design/drag-drop-images.pen create mode 100644 design/fix-property-dropdown-narrow.pen create mode 100644 design/getting-started-vault.pen create mode 100644 design/git-status-bar.pen create mode 100644 design/github-oauth-fix.pen create mode 100644 design/gs-git-init.pen create mode 100644 design/gs-wikilinks-fix.pen create mode 100644 design/hide-backlinks-empty.pen create mode 100644 design/keyboard-first-nav.pen create mode 100644 design/mcp-autodetect-status-bar.pen create mode 100644 design/new-note-creation.pen create mode 100644 design/note-list-title-position.pen create mode 100644 design/note-subtitle-metadata.pen create mode 100644 design/note-templates.pen create mode 100644 design/note-type-labels.pen create mode 100644 design/notelist-multiselect.pen create mode 100644 design/notes-type-icon-search.pen create mode 100644 design/properties-panel-header.pen create mode 100644 design/property-value-input.pen create mode 100644 design/raw-editor-mode.pen create mode 100644 design/relationship-x-cosmetic.pen create mode 100644 design/search-bundle-qmd.pen create mode 100644 design/search-results-subtitle.pen create mode 100644 design/search-subtitle-metadata.pen create mode 100644 design/smart-property-display.pen create mode 100644 design/sort-picker-properties.pen create mode 100644 design/status-color-picker.pen create mode 100644 design/status-property-dropdown.pen create mode 100644 design/sync-conflict-resolution.pen create mode 100644 design/tab-responsive-width.pen create mode 100644 design/tags-property-type.pen create mode 100644 design/themes-editable.pen create mode 100644 design/theming-system.pen create mode 100644 design/trash-management.pen create mode 100644 design/trashed-note-editor.pen create mode 100644 design/ui-design-cleanup.pen create mode 100644 design/vault-agents-md.pen create mode 100644 design/vault-management-remove.pen create mode 100644 design/zoom-shortcuts.pen create mode 100644 docs/ABSTRACTIONS.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/GETTING-STARTED.md create mode 100644 docs/PUBLIC-DOCS-PLAN.md create mode 100644 docs/VISION.md create mode 100644 docs/adr/0001-tauri-react-stack.md create mode 100644 docs/adr/0002-filesystem-source-of-truth.md create mode 100644 docs/adr/0003-single-note-model.md create mode 100644 docs/adr/0004-vault-vs-app-settings-storage.md create mode 100644 docs/adr/0005-tauri-ios-for-ipad.md create mode 100644 docs/adr/0006-flat-vault-structure.md create mode 100644 docs/adr/0007-title-filename-sync.md create mode 100644 docs/adr/0008-underscore-system-properties.md create mode 100644 docs/adr/0009-keyword-only-search.md create mode 100644 docs/adr/0010-dynamic-wikilink-relationship-detection.md create mode 100644 docs/adr/0011-mcp-server-for-ai-integration.md create mode 100644 docs/adr/0012-claude-cli-for-ai-agent.md create mode 100644 docs/adr/0013-remove-theming-system.md create mode 100644 docs/adr/0014-git-based-vault-cache.md create mode 100644 docs/adr/0015-auto-save-with-debounce.md create mode 100644 docs/adr/0016-sentry-posthog-telemetry.md create mode 100644 docs/adr/0018-codescene-code-health-gates.md create mode 100644 docs/adr/0019-github-device-flow-oauth.md create mode 100644 docs/adr/0020-keyboard-first-design.md create mode 100644 docs/adr/0021-push-to-main-workflow.md create mode 100644 docs/adr/0022-blocknote-rich-text-editor.md create mode 100644 docs/adr/0023-repair-vault-auto-bootstrap.md create mode 100644 docs/adr/0024-cache-outside-vault.md create mode 100644 docs/adr/0025-type-field-canonical.md create mode 100644 docs/adr/0026-props-down-no-global-state.md create mode 100644 docs/adr/0027-dual-ai-architecture.md create mode 100644 docs/adr/0028-cli-agent-only-no-api-key.md create mode 100644 docs/adr/0029-domain-command-builder-pattern.md create mode 100644 docs/adr/0030-rust-commands-module-split.md create mode 100644 docs/adr/0031-full-app-for-note-windows.md create mode 100644 docs/adr/0032-status-bar-for-git-actions.md create mode 100644 docs/adr/0033-subfolder-scanning-and-folder-tree.md create mode 100644 docs/adr/0034-git-repo-required-for-vault.md create mode 100644 docs/adr/0035-path-suffix-wikilink-resolution.md create mode 100644 docs/adr/0036-external-rename-detection-via-git-diff.md create mode 100644 docs/adr/0037-codemirror-language-markdown-highlighting.md create mode 100644 docs/adr/0038-frontmatter-backed-favorites.md create mode 100644 docs/adr/0039-git-history-for-note-dates.md create mode 100644 docs/adr/0040-custom-views-yml-filter-engine.md create mode 100644 docs/adr/0041-filekind-all-files-in-vault-scanner.md create mode 100644 docs/adr/0042-posthog-release-channels-feature-flags.md create mode 100644 docs/adr/0042-trash-auto-purge-safety-model.md create mode 100644 docs/adr/0043-reactive-vault-state-on-save.md create mode 100644 docs/adr/0044-h1-as-title-primary-source.md create mode 100644 docs/adr/0045-permanent-delete-no-trash.md create mode 100644 docs/adr/0046-starter-vault-cloned-from-github.md create mode 100644 docs/adr/0047-regex-mode-for-view-filter-conditions.md create mode 100644 docs/adr/0048-relative-date-expressions-in-view-filters.md create mode 100644 docs/adr/0049-per-note-icon-property.md create mode 100644 docs/adr/0050-deterministic-shortcut-command-routing.md create mode 100644 docs/adr/0051-shared-shortcut-manifest-for-testable-routing.md create mode 100644 docs/adr/0052-renderer-first-shortcut-execution-with-native-menu-dedupe.md create mode 100644 docs/adr/0053-webview-init-prevention-for-browser-reserved-shortcuts.md create mode 100644 docs/adr/0054-deterministic-shortcut-qa-matrix.md create mode 100644 docs/adr/0055-h1-is-the-only-editor-title-surface.md create mode 100644 docs/adr/0056-system-git-cli-auth-no-provider-oauth.md create mode 100644 docs/adr/0057-alpha-stable-release-channels-and-beta-cohorts.md create mode 100644 docs/adr/0058-claude-code-first-launch-onboarding-gate.md create mode 100644 docs/adr/0059-local-only-git-commits-without-remote.md create mode 100644 docs/adr/0060-network-aware-ui-gating-for-remote-features.md create mode 100644 docs/adr/0061-ai-prompt-bridge-event-bus.md create mode 100644 docs/adr/0062-selectable-cli-ai-agents.md create mode 100644 docs/adr/0063-blocknote-code-block-package-for-editor-highlighting.md create mode 100644 docs/adr/0064-ratcheted-codescene-thresholds.md create mode 100644 docs/adr/0065-root-managed-ai-guidance-files.md create mode 100644 docs/adr/0066-calendar-semver-versioning-for-alpha-and-stable-releases.md create mode 100644 docs/adr/0067-autogit-idle-and-inactive-checkpoints.md create mode 100644 docs/adr/0068-h1-only-title-surface-with-optional-untitled-auto-rename.md create mode 100644 docs/adr/0069-neighborhood-mode-for-note-list-relationship-browsing.md create mode 100644 docs/adr/0070-starter-vaults-local-first-with-explicit-remote-connection.md create mode 100644 docs/adr/0071-external-vault-refresh-and-clean-tab-reopen.md create mode 100644 docs/adr/0072-confirmed-vault-paths-gate-startup-state.md create mode 100644 docs/adr/0073-persistent-linkify-protocol-registry-across-editor-remounts.md create mode 100644 docs/adr/0074-explicit-external-ai-tool-setup-and-least-privilege-desktop-scope.md create mode 100644 docs/adr/0075-crash-safe-note-rename-transactions.md create mode 100644 docs/adr/0076-note-retargeting-separates-type-and-folder-moves.md create mode 100644 docs/adr/0077-concurrent-safe-vault-cache-replacement.md create mode 100644 docs/adr/0078-scoped-unsigned-fallback-for-app-managed-git-commits.md create mode 100644 docs/adr/0079-linux-window-chrome-and-menu-reuse.md create mode 100644 docs/adr/0080-cross-platform-desktop-release-artifacts-and-portable-vault-names.md create mode 100644 docs/adr/0081-internal-light-dark-theme-runtime.md create mode 100644 docs/adr/0082-markdown-durable-math-notes.md create mode 100644 docs/adr/0083-dual-architecture-macos-release-artifacts.md create mode 100644 docs/adr/0084-app-localization-foundation.md create mode 100644 docs/adr/0085-non-git-vault-support.md create mode 100644 docs/adr/0086-in-app-image-file-preview.md create mode 100644 docs/adr/0087-json-catalogs-and-lara-cli-localization.md create mode 100644 docs/adr/0088-markdown-durable-mermaid-diagrams.md create mode 100644 docs/adr/0089-active-vault-filesystem-watcher.md create mode 100644 docs/adr/0090-pi-cli-agent-adapter.md create mode 100644 docs/adr/0091-gemini-cli-external-ai-setup.md create mode 100644 docs/adr/0092-vault-ai-agent-permission-modes.md create mode 100644 docs/adr/0093-shared-cli-agent-runtime-adapters.md create mode 100644 docs/adr/0094-gitignored-content-visibility-boundary-filter.md create mode 100644 docs/adr/0095-saved-view-order-field.md create mode 100644 docs/adr/0096-root-created-type-documents.md create mode 100644 docs/adr/0097-gemini-cli-agent-adapter.md create mode 100644 docs/adr/0098-in-app-image-and-pdf-file-previews.md create mode 100644 docs/adr/0099-cumulative-vault-asset-scope.md create mode 100644 docs/adr/0100-synthetic-vault-root-folder-row.md create mode 100644 docs/adr/0101-categorical-product-analytics-events.md create mode 100644 docs/adr/0102-low-end-safe-autosave-idle-window.md create mode 100644 docs/adr/0103-adapter-specific-ai-permission-semantics.md create mode 100644 docs/adr/0104-tauri-frontend-readiness-watchdog.md create mode 100644 docs/adr/0105-editor-correctness-and-responsiveness-contract.md create mode 100644 docs/adr/0106-shared-app-command-manifest.md create mode 100644 docs/adr/0107-markdown-durable-tldraw-whiteboards.md create mode 100644 docs/adr/0107-pointer-owned-editor-block-reordering.md create mode 100644 docs/adr/0108-direct-model-ai-targets.md create mode 100644 docs/adr/0108-sanitized-rendered-markup-and-safe-regex.md create mode 100644 docs/adr/0109-debounced-worker-derived-editor-indexes.md create mode 100644 docs/adr/0110-in-app-media-and-pdf-file-previews.md create mode 100644 docs/adr/0111-path-aware-external-vault-refresh-with-focused-editor-preservation.md create mode 100644 docs/adr/0112-system-theme-mode.md create mode 100644 docs/adr/0113-shared-renderer-attachment-path-normalization.md create mode 100644 docs/adr/0114-mounted-workspaces-unified-graph.md create mode 100644 docs/adr/0115-scoped-react-context-for-shared-ui-preferences.md create mode 100644 docs/adr/0116-rich-raw-transition-and-serialization-ownership.md create mode 100644 docs/adr/0117-appimage-fcitx-gtk3-frontend-bundle.md create mode 100644 docs/adr/0118-entry-scoped-note-windows-without-vault-index-scans.md create mode 100644 docs/adr/0119-vault-neutral-mcp-registration-with-mounted-workspace-guidance.md create mode 100644 docs/adr/0120-stable-appimage-mcp-server-path-with-opencode-registration.md create mode 100644 docs/adr/0121-appimage-external-fallback-for-audio-and-video-previews.md create mode 100644 docs/adr/0122-scalar-array-frontmatter-properties.md create mode 100644 docs/adr/0123-full-vault-graph-for-secondary-note-windows.md create mode 100644 docs/adr/0124-cached-secondary-note-window-startup.md create mode 100644 docs/adr/0126-renderer-action-history.md create mode 100644 docs/adr/0127-native-ai-workspace-window.md create mode 100644 docs/adr/0128-lightweight-ai-workspace-window.md create mode 100644 docs/adr/0129-tolaria-vault-item-deep-links.md create mode 100644 docs/adr/0130-windows-authenticode-release-signing.md create mode 100644 docs/adr/0131-reusable-release-artifact-build-workflow.md create mode 100644 docs/adr/0132-alpha-authenticode-soft-gate.md create mode 100644 docs/adr/0133-request-scoped-ai-stream-events.md create mode 100644 docs/adr/0134-direct-shiki-language-registrations.md create mode 100644 docs/adr/0134-sheet-nodes-with-plain-text-workbook-storage.md create mode 100644 docs/adr/0135-clean-active-note-refresh-after-external-edit.md create mode 100644 docs/adr/0136-macos-webview-pdf-export.md create mode 100644 docs/adr/0137-shared-rich-editor-input-transforms.md create mode 100644 docs/adr/0138-authenticode-required-for-all-release-channels.md create mode 100644 docs/adr/0139-temporary-windows-authenticode-soft-gate.md create mode 100644 docs/adr/0140-extension-based-raw-text-syntax-highlighting.md create mode 100644 docs/adr/0141-scoped-linux-webkit-rendering-safeguards.md create mode 100644 docs/adr/0142-rich-editor-prosemirror-decoration-dependency.md create mode 100644 docs/adr/0143-shared-focus-ownership-guard.md create mode 100644 docs/adr/0144-collections-and-presentations.md create mode 100644 docs/adr/0145-xdg-backed-app-config-path.md create mode 100644 docs/adr/0146-cached-main-window-startup-with-empty-reload-recovery.md create mode 100644 docs/adr/0147-antigravity-cli-agent-adapter.md create mode 100644 docs/adr/0148-cancellable-ai-agent-streams.md create mode 100644 docs/adr/0149-shared-app-config-policy-manifest.md create mode 100644 docs/adr/0150-github-copilot-cli-agent-adapter.md create mode 100644 docs/adr/0151-antigravity-add-dir-workspace-flag.md create mode 100644 docs/adr/0152-wsl2-git-provider.md create mode 100644 docs/adr/README.md create mode 100644 docs/adr/canary-release-channel-and-local-feature-flags.md create mode 100644 e2e/ai-chat-screenshot.spec.ts create mode 100644 e2e/ai-chat.spec.ts create mode 100644 e2e/app.spec.ts create mode 100644 e2e/auto-save.spec.ts create mode 100644 e2e/core-flows.spec.ts create mode 100644 e2e/create-note.spec.ts create mode 100644 e2e/create-type.spec.ts create mode 100644 e2e/debug-content-bug.spec.ts create mode 100644 e2e/editor-min-width.spec.ts create mode 100644 e2e/filtering.spec.ts create mode 100644 e2e/find-selectors.spec.ts create mode 100644 e2e/image-drag-drop.spec.ts create mode 100644 e2e/image-upload.spec.ts create mode 100644 e2e/keyboard-shortcuts.spec.ts create mode 100644 e2e/quick-open.spec.ts create mode 100644 e2e/rename-tab.spec.ts create mode 100644 e2e/screenshot.spec.ts create mode 100644 e2e/settings-oauth.spec.ts create mode 100644 e2e/sidebar-collapse.spec.ts create mode 100644 e2e/vault-picker-local.spec.ts create mode 100644 e2e/visual-verify.spec.ts create mode 100644 e2e/zero-shift-closeup.spec.ts create mode 100644 e2e/zero-shift-detail.spec.ts create mode 100644 e2e/zero-shift.spec.ts create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 lara.lock create mode 100644 lara.yaml create mode 100644 mcp-server/agent-instructions.js create mode 100644 mcp-server/app-config-policy.json create mode 100644 mcp-server/index.js create mode 100644 mcp-server/package-lock.json create mode 100644 mcp-server/package.json create mode 100644 mcp-server/test.js create mode 100644 mcp-server/tool-service.js create mode 100644 mcp-server/tool-service.test.js create mode 100644 mcp-server/vault-path.js create mode 100644 mcp-server/vault.js create mode 100644 mcp-server/ws-bridge.js create mode 100644 package.json create mode 100644 patches/@blocknote__code-block@0.46.2.patch create mode 100644 patches/@blocknote__core@0.46.2.patch create mode 100644 patches/@blocknote__react@0.46.2.patch create mode 100644 patches/@tiptap__extension-link@3.19.0.patch create mode 100644 patches/prosemirror-tables@1.8.5.patch create mode 100644 playwright.config.ts create mode 100644 playwright.integration.config.ts create mode 100644 playwright.smoke.config.ts create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 public/ai-agent-icons/SOURCES.md create mode 100644 public/ai-agent-icons/claude-code.svg create mode 100644 public/ai-agent-icons/codex.svg create mode 100644 public/ai-agent-icons/copilot.svg create mode 100644 public/ai-agent-icons/gemini.svg create mode 100644 public/ai-agent-icons/hermes.svg create mode 100644 public/ai-agent-icons/kiro.svg create mode 100644 public/ai-agent-icons/opencode.svg create mode 100644 public/ai-agent-icons/pi.svg create mode 100644 public/favicon.svg create mode 100644 release-notes/stable-v2026.5.2.md create mode 100644 release-notes/v2026-05-04.md create mode 100644 release-notes/v2026-05-06.md create mode 100644 release-notes/v2026-05-07.md create mode 100644 release-notes/v2026-05-12.md create mode 100644 release-notes/v2026-05-14.md create mode 100644 release-notes/v2026-05-18.md create mode 100644 release-notes/v2026-05-21.md create mode 100644 release-notes/v2026-05-25.md create mode 100644 release-notes/v2026-05-29.md create mode 100644 release-notes/v2026-06-01.md create mode 100644 release-notes/v2026-06-06.md create mode 100644 release-notes/v2026-06-10.md create mode 100644 release-notes/v2026-06-14.md create mode 100644 release-notes/v2026-06-23.md create mode 100644 release-notes/v2026-06-26.md create mode 100644 release-notes/v2026-07-01.md create mode 100644 scripts/appimage-launcher-tools.mjs create mode 100644 scripts/appimage-launcher-tools.test.mjs create mode 100644 scripts/build-agent-docs.mjs create mode 100644 scripts/build-agent-docs.test.mjs create mode 100644 scripts/build-release-download-page.ts create mode 100644 scripts/build-release-history-page.ts create mode 100644 scripts/bundle-mcp-server.mjs create mode 100644 scripts/editor-performance-benchmark.mjs create mode 100644 scripts/editor-performance-thresholds.mjs create mode 100644 scripts/generate_demo_vault.py create mode 100644 scripts/playwright-smoke-server.mjs create mode 100644 scripts/run-vitest-coverage-shards.mjs create mode 100644 scripts/run-vitest-coverage.mjs create mode 100644 scripts/serve-demo.mjs create mode 100644 scripts/validate-locales.mjs create mode 100644 site/.vitepress/config.ts create mode 100644 site/.vitepress/theme/LandingHome.vue create mode 100644 site/.vitepress/theme/Layout.vue create mode 100644 site/.vitepress/theme/assets/RefactoringSans.otf create mode 100644 site/.vitepress/theme/index.ts create mode 100644 site/.vitepress/theme/styles.css create mode 100644 site/concepts/ai.md create mode 100644 site/concepts/editor.md create mode 100644 site/concepts/files-and-media.md create mode 100644 site/concepts/git.md create mode 100644 site/concepts/inbox.md create mode 100644 site/concepts/notes.md create mode 100644 site/concepts/properties.md create mode 100644 site/concepts/relationships.md create mode 100644 site/concepts/spreadsheets.md create mode 100644 site/concepts/types.md create mode 100644 site/concepts/vaults.md create mode 100644 site/guides/build-custom-views.md create mode 100644 site/guides/capture-a-note.md create mode 100644 site/guides/commit-and-push.md create mode 100644 site/guides/configure-ai-models.md create mode 100644 site/guides/connect-a-git-remote.md create mode 100644 site/guides/create-types.md create mode 100644 site/guides/manage-display-preferences.md create mode 100644 site/guides/organize-inbox.md create mode 100644 site/guides/use-ai-panel.md create mode 100644 site/guides/use-command-palette.md create mode 100644 site/guides/use-media-previews.md create mode 100644 site/guides/use-spreadsheets.md create mode 100644 site/guides/use-table-of-contents.md create mode 100644 site/guides/use-wikilinks.md create mode 100644 site/index.md create mode 100644 site/public/CNAME create mode 100644 site/public/landing/Ai chat 3.png create mode 100644 site/public/landing/Block editor.png create mode 100644 site/public/landing/Luca hello.jpeg create mode 100644 site/public/landing/ananth.webp create mode 100644 site/public/landing/favicon.png create mode 100644 site/public/landing/git-history.png create mode 100644 site/public/landing/gregor.webp create mode 100644 site/public/landing/jordan.webp create mode 100644 site/public/landing/les-saintes.jpg create mode 100644 site/public/landing/pulse.png create mode 100644 site/public/landing/relationships.png create mode 100644 site/public/landing/simply-files.png create mode 100644 site/public/landing/sponsors/SOURCES.md create mode 100644 site/public/landing/sponsors/circleci-dark.svg create mode 100644 site/public/landing/sponsors/circleci-light.svg create mode 100644 site/public/landing/sponsors/codacy-dark.svg create mode 100644 site/public/landing/sponsors/codacy-light.svg create mode 100644 site/public/landing/sponsors/codescene-dark.svg create mode 100644 site/public/landing/sponsors/codescene-light.svg create mode 100644 site/public/landing/sponsors/unblocked-dark.svg create mode 100644 site/public/landing/sponsors/unblocked-light.svg create mode 100644 site/public/landing/tolaria-icon.png create mode 100644 site/public/landing/tolaria-screenshot-dark.png create mode 100644 site/public/landing/tolaria-screenshot.png create mode 100644 site/public/landing/track changes and push.png create mode 100644 site/public/landing/wikilinks.png create mode 100644 site/public/landing/yaml frontmatter.png create mode 100644 site/public/tolaria-icon.svg create mode 100644 site/reference/contribute.md create mode 100644 site/reference/docs-maintenance.md create mode 100644 site/reference/file-layout.md create mode 100644 site/reference/frontmatter-fields.md create mode 100644 site/reference/keyboard-shortcuts.md create mode 100644 site/reference/release-channels.md create mode 100644 site/reference/spreadsheet-format.md create mode 100644 site/reference/spreadsheet-functions.md create mode 100644 site/reference/supported-platforms.md create mode 100644 site/reference/view-filters.md create mode 100644 site/start/first-launch.md create mode 100644 site/start/getting-started-vault.md create mode 100644 site/start/install.md create mode 100644 site/start/open-or-create-vault.md create mode 100644 site/templates/portent.md create mode 100644 site/troubleshooting/ai-agent-not-found.md create mode 100644 site/troubleshooting/git-auth.md create mode 100644 site/troubleshooting/model-provider-connection.md create mode 100644 site/troubleshooting/sync-conflicts.md create mode 100644 site/troubleshooting/vault-not-loading.md create mode 100644 src-tauri/.gitignore create mode 100644 src-tauri/Cargo.lock create mode 100644 src-tauri/Cargo.toml create mode 100644 src-tauri/Info.plist create mode 100644 src-tauri/build.rs create mode 100644 src-tauri/capabilities/default.json create mode 100644 src-tauri/capabilities/mobile.json create mode 100644 src-tauri/gen/apple/.gitignore create mode 100644 src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png create mode 100644 src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png create mode 100644 src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png create mode 100644 src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png create mode 100644 src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png create mode 100644 src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png create mode 100644 src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png create mode 100644 src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png create mode 100644 src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png create mode 100644 src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png create mode 100644 src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png create mode 100644 src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png create mode 100644 src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png create mode 100644 src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png create mode 100644 src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png create mode 100644 src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png create mode 100644 src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png create mode 100644 src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png create mode 100644 src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 src-tauri/gen/apple/Assets.xcassets/Contents.json create mode 100644 src-tauri/gen/apple/ExportOptions.plist create mode 100644 src-tauri/gen/apple/LaunchScreen.storyboard create mode 100644 src-tauri/gen/apple/Podfile create mode 100644 src-tauri/gen/apple/Sources/laputa/bindings/bindings.h create mode 100644 src-tauri/gen/apple/Sources/laputa/main.mm create mode 100755 src-tauri/gen/apple/assets/mcp-server/index.js create mode 100644 src-tauri/gen/apple/assets/mcp-server/package.json create mode 100755 src-tauri/gen/apple/assets/mcp-server/ws-bridge.js create mode 100644 src-tauri/gen/apple/laputa.xcodeproj/project.pbxproj create mode 100644 src-tauri/gen/apple/laputa.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 src-tauri/gen/apple/laputa.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 src-tauri/gen/apple/laputa.xcodeproj/xcshareddata/xcschemes/laputa_iOS.xcscheme create mode 100644 src-tauri/gen/apple/laputa_iOS/Info.plist create mode 100644 src-tauri/gen/apple/laputa_iOS/laputa_iOS.entitlements create mode 100644 src-tauri/gen/apple/project.yml create mode 100644 src-tauri/icons/128x128.png create mode 100644 src-tauri/icons/128x128@2x.png create mode 100644 src-tauri/icons/256x256.png create mode 100644 src-tauri/icons/32x32.png create mode 100644 src-tauri/icons/512x512-dark.png create mode 100644 src-tauri/icons/512x512.png create mode 100644 src-tauri/icons/64x64.png create mode 100644 src-tauri/icons/Square107x107Logo.png create mode 100644 src-tauri/icons/Square142x142Logo.png create mode 100644 src-tauri/icons/Square150x150Logo.png create mode 100644 src-tauri/icons/Square284x284Logo.png create mode 100644 src-tauri/icons/Square30x30Logo.png create mode 100644 src-tauri/icons/Square310x310Logo.png create mode 100644 src-tauri/icons/Square44x44Logo.png create mode 100644 src-tauri/icons/Square71x71Logo.png create mode 100644 src-tauri/icons/Square89x89Logo.png create mode 100644 src-tauri/icons/StoreLogo.png create mode 100644 src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 src-tauri/icons/android/mipmap-hdpi/ic_launcher.png create mode 100644 src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png create mode 100644 src-tauri/icons/android/mipmap-mdpi/ic_launcher.png create mode 100644 src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png create mode 100644 src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png create mode 100644 src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png create mode 100644 src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png create mode 100644 src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 src-tauri/icons/android/values/ic_launcher_background.xml create mode 100644 src-tauri/icons/icon-source-dark.svg create mode 100644 src-tauri/icons/icon-source.png create mode 100644 src-tauri/icons/icon.icns create mode 100644 src-tauri/icons/icon.ico create mode 100644 src-tauri/icons/icon.png create mode 100644 src-tauri/icons/ios/AppIcon-20x20@1x.png create mode 100644 src-tauri/icons/ios/AppIcon-20x20@2x-1.png create mode 100644 src-tauri/icons/ios/AppIcon-20x20@2x.png create mode 100644 src-tauri/icons/ios/AppIcon-20x20@3x.png create mode 100644 src-tauri/icons/ios/AppIcon-29x29@1x.png create mode 100644 src-tauri/icons/ios/AppIcon-29x29@2x-1.png create mode 100644 src-tauri/icons/ios/AppIcon-29x29@2x.png create mode 100644 src-tauri/icons/ios/AppIcon-29x29@3x.png create mode 100644 src-tauri/icons/ios/AppIcon-40x40@1x.png create mode 100644 src-tauri/icons/ios/AppIcon-40x40@2x-1.png create mode 100644 src-tauri/icons/ios/AppIcon-40x40@2x.png create mode 100644 src-tauri/icons/ios/AppIcon-40x40@3x.png create mode 100644 src-tauri/icons/ios/AppIcon-512@2x.png create mode 100644 src-tauri/icons/ios/AppIcon-60x60@2x.png create mode 100644 src-tauri/icons/ios/AppIcon-60x60@3x.png create mode 100644 src-tauri/icons/ios/AppIcon-76x76@1x.png create mode 100644 src-tauri/icons/ios/AppIcon-76x76@2x.png create mode 100644 src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png create mode 100644 src-tauri/resources/agent-docs/AGENTS.md create mode 100644 src-tauri/resources/agent-docs/all.md create mode 100644 src-tauri/resources/agent-docs/concepts.md create mode 100644 src-tauri/resources/agent-docs/guides.md create mode 100644 src-tauri/resources/agent-docs/home.md create mode 100644 src-tauri/resources/agent-docs/index.md create mode 100644 src-tauri/resources/agent-docs/pages/concepts/ai.md create mode 100644 src-tauri/resources/agent-docs/pages/concepts/editor.md create mode 100644 src-tauri/resources/agent-docs/pages/concepts/files-and-media.md create mode 100644 src-tauri/resources/agent-docs/pages/concepts/git.md create mode 100644 src-tauri/resources/agent-docs/pages/concepts/inbox.md create mode 100644 src-tauri/resources/agent-docs/pages/concepts/notes.md create mode 100644 src-tauri/resources/agent-docs/pages/concepts/properties.md create mode 100644 src-tauri/resources/agent-docs/pages/concepts/relationships.md create mode 100644 src-tauri/resources/agent-docs/pages/concepts/spreadsheets.md create mode 100644 src-tauri/resources/agent-docs/pages/concepts/types.md create mode 100644 src-tauri/resources/agent-docs/pages/concepts/vaults.md create mode 100644 src-tauri/resources/agent-docs/pages/guides/build-custom-views.md create mode 100644 src-tauri/resources/agent-docs/pages/guides/capture-a-note.md create mode 100644 src-tauri/resources/agent-docs/pages/guides/commit-and-push.md create mode 100644 src-tauri/resources/agent-docs/pages/guides/configure-ai-models.md create mode 100644 src-tauri/resources/agent-docs/pages/guides/connect-a-git-remote.md create mode 100644 src-tauri/resources/agent-docs/pages/guides/create-types.md create mode 100644 src-tauri/resources/agent-docs/pages/guides/manage-display-preferences.md create mode 100644 src-tauri/resources/agent-docs/pages/guides/organize-inbox.md create mode 100644 src-tauri/resources/agent-docs/pages/guides/use-ai-panel.md create mode 100644 src-tauri/resources/agent-docs/pages/guides/use-command-palette.md create mode 100644 src-tauri/resources/agent-docs/pages/guides/use-media-previews.md create mode 100644 src-tauri/resources/agent-docs/pages/guides/use-spreadsheets.md create mode 100644 src-tauri/resources/agent-docs/pages/guides/use-table-of-contents.md create mode 100644 src-tauri/resources/agent-docs/pages/guides/use-wikilinks.md create mode 100644 src-tauri/resources/agent-docs/pages/index.md create mode 100644 src-tauri/resources/agent-docs/pages/reference/contribute.md create mode 100644 src-tauri/resources/agent-docs/pages/reference/docs-maintenance.md create mode 100644 src-tauri/resources/agent-docs/pages/reference/file-layout.md create mode 100644 src-tauri/resources/agent-docs/pages/reference/frontmatter-fields.md create mode 100644 src-tauri/resources/agent-docs/pages/reference/keyboard-shortcuts.md create mode 100644 src-tauri/resources/agent-docs/pages/reference/release-channels.md create mode 100644 src-tauri/resources/agent-docs/pages/reference/spreadsheet-format.md create mode 100644 src-tauri/resources/agent-docs/pages/reference/spreadsheet-functions.md create mode 100644 src-tauri/resources/agent-docs/pages/reference/supported-platforms.md create mode 100644 src-tauri/resources/agent-docs/pages/reference/view-filters.md create mode 100644 src-tauri/resources/agent-docs/pages/start/first-launch.md create mode 100644 src-tauri/resources/agent-docs/pages/start/getting-started-vault.md create mode 100644 src-tauri/resources/agent-docs/pages/start/install.md create mode 100644 src-tauri/resources/agent-docs/pages/start/open-or-create-vault.md create mode 100644 src-tauri/resources/agent-docs/pages/templates/portent.md create mode 100644 src-tauri/resources/agent-docs/pages/troubleshooting/ai-agent-not-found.md create mode 100644 src-tauri/resources/agent-docs/pages/troubleshooting/git-auth.md create mode 100644 src-tauri/resources/agent-docs/pages/troubleshooting/model-provider-connection.md create mode 100644 src-tauri/resources/agent-docs/pages/troubleshooting/sync-conflicts.md create mode 100644 src-tauri/resources/agent-docs/pages/troubleshooting/vault-not-loading.md create mode 100644 src-tauri/resources/agent-docs/reference.md create mode 100644 src-tauri/resources/agent-docs/search-index.json create mode 100644 src-tauri/resources/agent-docs/start.md create mode 100644 src-tauri/resources/agent-docs/templates.md create mode 100644 src-tauri/resources/agent-docs/troubleshooting.md create mode 100644 src-tauri/src/ai_agent_processes.rs create mode 100644 src-tauri/src/ai_agents.rs create mode 100644 src-tauri/src/ai_model_tools.rs create mode 100644 src-tauri/src/ai_models.rs create mode 100644 src-tauri/src/antigravity_cli.rs create mode 100644 src-tauri/src/antigravity_config.rs create mode 100644 src-tauri/src/antigravity_discovery.rs create mode 100644 src-tauri/src/app_config.rs create mode 100644 src-tauri/src/app_icon.rs create mode 100644 src-tauri/src/app_updater.rs create mode 100644 src-tauri/src/claude_cli.rs create mode 100644 src-tauri/src/claude_invocation.rs create mode 100644 src-tauri/src/cli_agent_runtime.rs create mode 100644 src-tauri/src/cli_agent_runtime/line_stream.rs create mode 100644 src-tauri/src/cli_agent_runtime/mcp_config.rs create mode 100644 src-tauri/src/cli_agent_runtime/shell_env.rs create mode 100644 src-tauri/src/cli_agent_runtime/windows_cmd_shim.rs create mode 100644 src-tauri/src/codex_cli.rs create mode 100644 src-tauri/src/commands/ai.rs create mode 100644 src-tauri/src/commands/app_icon.rs create mode 100644 src-tauri/src/commands/clipboard.rs create mode 100644 src-tauri/src/commands/delete.rs create mode 100644 src-tauri/src/commands/folders.rs create mode 100644 src-tauri/src/commands/git.rs create mode 100644 src-tauri/src/commands/git_clone.rs create mode 100644 src-tauri/src/commands/git_connect.rs create mode 100644 src-tauri/src/commands/memory.rs create mode 100644 src-tauri/src/commands/mod.rs create mode 100644 src-tauri/src/commands/pdf_export.rs create mode 100644 src-tauri/src/commands/runtime.rs create mode 100644 src-tauri/src/commands/sheet.rs create mode 100644 src-tauri/src/commands/system.rs create mode 100644 src-tauri/src/commands/vault.rs create mode 100644 src-tauri/src/commands/vault/boundary.rs create mode 100644 src-tauri/src/commands/vault/file_cmds.rs create mode 100644 src-tauri/src/commands/vault/frontmatter_cmds.rs create mode 100644 src-tauri/src/commands/vault/lifecycle_cmds.rs create mode 100644 src-tauri/src/commands/vault/rename_cmds.rs create mode 100644 src-tauri/src/commands/vault/scan_cmds.rs create mode 100644 src-tauri/src/commands/vault/view_cmds.rs create mode 100644 src-tauri/src/commands/version.rs create mode 100644 src-tauri/src/copilot_cli.rs create mode 100644 src-tauri/src/copilot_discovery.rs create mode 100644 src-tauri/src/frontmatter/keys.rs create mode 100644 src-tauri/src/frontmatter/mod.rs create mode 100644 src-tauri/src/frontmatter/ops.rs create mode 100644 src-tauri/src/frontmatter/ops_update_tests.rs create mode 100644 src-tauri/src/frontmatter/yaml.rs create mode 100644 src-tauri/src/git/author.rs create mode 100644 src-tauri/src/git/clone.rs create mode 100644 src-tauri/src/git/command.rs create mode 100644 src-tauri/src/git/commit.rs create mode 100644 src-tauri/src/git/conflict.rs create mode 100644 src-tauri/src/git/connect.rs create mode 100644 src-tauri/src/git/credentials.rs create mode 100644 src-tauri/src/git/dates.rs create mode 100644 src-tauri/src/git/file_url.rs create mode 100644 src-tauri/src/git/history.rs create mode 100644 src-tauri/src/git/mod.rs create mode 100644 src-tauri/src/git/provider.rs create mode 100644 src-tauri/src/git/pulse.rs create mode 100644 src-tauri/src/git/remote.rs create mode 100644 src-tauri/src/git/remote_branch_tests.rs create mode 100644 src-tauri/src/git/remote_config.rs create mode 100644 src-tauri/src/git/remote_status.rs create mode 100644 src-tauri/src/git/remote_url.rs create mode 100644 src-tauri/src/git/status.rs create mode 100644 src-tauri/src/git/upstream.rs create mode 100644 src-tauri/src/hermes_cli.rs create mode 100644 src-tauri/src/hermes_discovery.rs create mode 100644 src-tauri/src/kiro_cli.rs create mode 100644 src-tauri/src/kiro_discovery.rs create mode 100644 src-tauri/src/lib.rs create mode 100644 src-tauri/src/linux_appimage.rs create mode 100644 src-tauri/src/main.rs create mode 100644 src-tauri/src/mcp.rs create mode 100644 src-tauri/src/mcp/extraction.rs create mode 100644 src-tauri/src/mcp/opencode.rs create mode 100644 src-tauri/src/mcp/paths.rs create mode 100644 src-tauri/src/mcp/runtime.rs create mode 100644 src-tauri/src/mcp/subprocess.rs create mode 100644 src-tauri/src/menu.rs create mode 100644 src-tauri/src/opencode_cli.rs create mode 100644 src-tauri/src/opencode_config.rs create mode 100644 src-tauri/src/opencode_discovery.rs create mode 100644 src-tauri/src/opencode_events.rs create mode 100644 src-tauri/src/opencode_events_tests.rs create mode 100644 src-tauri/src/pi_cli.rs create mode 100644 src-tauri/src/pi_config.rs create mode 100644 src-tauri/src/pi_discovery.rs create mode 100644 src-tauri/src/pi_events.rs create mode 100644 src-tauri/src/pi_events_tests.rs create mode 100644 src-tauri/src/search.rs create mode 100644 src-tauri/src/settings.rs create mode 100644 src-tauri/src/telemetry.rs create mode 100644 src-tauri/src/vault/cache.rs create mode 100644 src-tauri/src/vault/config_seed.rs create mode 100644 src-tauri/src/vault/entry.rs create mode 100644 src-tauri/src/vault/file.rs create mode 100644 src-tauri/src/vault/filename_rules.rs create mode 100644 src-tauri/src/vault/folders.rs create mode 100644 src-tauri/src/vault/frontmatter.rs create mode 100644 src-tauri/src/vault/frontmatter_regression_tests.rs create mode 100644 src-tauri/src/vault/getting_started.rs create mode 100644 src-tauri/src/vault/ignored.rs create mode 100644 src-tauri/src/vault/image.rs create mode 100644 src-tauri/src/vault/migration.rs create mode 100644 src-tauri/src/vault/mod.rs create mode 100644 src-tauri/src/vault/mod_tests.rs create mode 100644 src-tauri/src/vault/mod_tests/archival_metadata.rs create mode 100644 src-tauri/src/vault/mod_tests/basics.rs create mode 100644 src-tauri/src/vault/mod_tests/complex_frontmatter.rs create mode 100644 src-tauri/src/vault/mod_tests/display_metadata.rs create mode 100644 src-tauri/src/vault/mod_tests/folder_and_file_kind.rs create mode 100644 src-tauri/src/vault/mod_tests/journal_type_visibility.rs create mode 100644 src-tauri/src/vault/mod_tests/real_vault_consistency.rs create mode 100644 src-tauri/src/vault/mod_tests/relationships.rs create mode 100644 src-tauri/src/vault/mod_tests/scan_and_file_access.rs create mode 100644 src-tauri/src/vault/mod_tests/type_and_links.rs create mode 100644 src-tauri/src/vault/modified_dates_tests.rs create mode 100644 src-tauri/src/vault/parsing.rs create mode 100644 src-tauri/src/vault/parsing_tests.rs create mode 100644 src-tauri/src/vault/path_identity.rs create mode 100644 src-tauri/src/vault/relationship_key_tests.rs create mode 100644 src-tauri/src/vault/rename.rs create mode 100644 src-tauri/src/vault/rename_transaction.rs create mode 100644 src-tauri/src/vault/system_metadata_tests.rs create mode 100644 src-tauri/src/vault/title_sync.rs create mode 100644 src-tauri/src/vault/trash.rs create mode 100644 src-tauri/src/vault/type_templates.rs create mode 100644 src-tauri/src/vault/view_date_filters.rs create mode 100644 src-tauri/src/vault/view_migration.rs create mode 100644 src-tauri/src/vault/view_relationships.rs create mode 100644 src-tauri/src/vault/view_tests.rs create mode 100644 src-tauri/src/vault/view_value_conversions.rs create mode 100644 src-tauri/src/vault/views.rs create mode 100644 src-tauri/src/vault_list.rs create mode 100644 src-tauri/src/vault_watcher.rs create mode 100644 src-tauri/src/window_state.rs create mode 100644 src-tauri/tauri.conf.json create mode 100644 src-tauri/tests/git_connect_no_remote.rs create mode 100644 src/App.css create mode 100644 src/App.note-window-properties.test.tsx create mode 100644 src/App.test.tsx create mode 100644 src/App.tsx create mode 100644 src/assets/sponsors/circleci-dark.svg create mode 100644 src/assets/sponsors/circleci-light.svg create mode 100644 src/assets/sponsors/codacy-dark.svg create mode 100644 src/assets/sponsors/codacy-light.svg create mode 100644 src/assets/sponsors/codescene-dark.svg create mode 100644 src/assets/sponsors/codescene-light.svg create mode 100644 src/assets/sponsors/unblocked-dark.svg create mode 100644 src/assets/sponsors/unblocked-light.svg create mode 100644 src/assets/tolaria-icon.svg create mode 100644 src/collections/collectionFromSelection.test.ts create mode 100644 src/collections/collectionFromSelection.ts create mode 100644 src/collections/collectionTypes.ts create mode 100644 src/collections/presentationConfig.ts create mode 100644 src/collections/resolveCollectionEntries.test.ts create mode 100644 src/collections/resolveCollectionEntries.ts create mode 100644 src/components/AccentColorPicker.tsx create mode 100644 src/components/AddPropertyForm.tsx create mode 100644 src/components/AddRemoteModal.test.tsx create mode 100644 src/components/AddRemoteModal.tsx create mode 100644 src/components/AiActionCard.test.tsx create mode 100644 src/components/AiActionCard.tsx create mode 100644 src/components/AiAgentIcon.tsx create mode 100644 src/components/AiAgentsOnboardingPrompt.test.tsx create mode 100644 src/components/AiAgentsOnboardingPrompt.tsx create mode 100644 src/components/AiMessage.test.tsx create mode 100644 src/components/AiMessage.tsx create mode 100644 src/components/AiPanel.test.tsx create mode 100644 src/components/AiPanel.tsx create mode 100644 src/components/AiPanelChrome.performance.test.tsx create mode 100644 src/components/AiPanelChrome.tsx create mode 100644 src/components/AiProviderSettings.tsx create mode 100644 src/components/AiWorkspace.test.tsx create mode 100644 src/components/AiWorkspace.tsx create mode 100644 src/components/AiWorkspaceChrome.tsx create mode 100644 src/components/AiWorkspaceFloatingButton.test.tsx create mode 100644 src/components/AiWorkspaceFloatingButton.tsx create mode 100644 src/components/AiWorkspaceResizeHandles.tsx create mode 100644 src/components/AiWorkspaceSideHeader.tsx create mode 100644 src/components/AiWorkspaceSidebar.tsx create mode 100644 src/components/AiWorkspaceWindowApp.tsx create mode 100644 src/components/AppAiWorkspaceSurface.tsx create mode 100644 src/components/ArchivedNoteBanner.test.tsx create mode 100644 src/components/ArchivedNoteBanner.tsx create mode 100644 src/components/BreadcrumbBar.test.tsx create mode 100644 src/components/BreadcrumbBar.tsx create mode 100644 src/components/BreadcrumbBar.visibility.test.tsx create mode 100644 src/components/BulkActionBar.test.tsx create mode 100644 src/components/BulkActionBar.tsx create mode 100644 src/components/ClaudeCodeOnboardingPrompt.test.tsx create mode 100644 src/components/ClaudeCodeOnboardingPrompt.tsx create mode 100644 src/components/CloneVaultModal.test.tsx create mode 100644 src/components/CloneVaultModal.tsx create mode 100644 src/components/ColorInput.test.tsx create mode 100644 src/components/ColorInput.tsx create mode 100644 src/components/CommandPalette.test.tsx create mode 100644 src/components/CommandPalette.tsx create mode 100644 src/components/CommandPaletteAiMode.tsx create mode 100644 src/components/CommitDialog.test.tsx create mode 100644 src/components/CommitDialog.tsx create mode 100644 src/components/ConfirmDeleteDialog.test.tsx create mode 100644 src/components/ConfirmDeleteDialog.tsx create mode 100644 src/components/ConflictNoteBanner.test.tsx create mode 100644 src/components/ConflictNoteBanner.tsx create mode 100644 src/components/ConflictResolverModal.extra.test.tsx create mode 100644 src/components/ConflictResolverModal.keyboard.test.tsx create mode 100644 src/components/ConflictResolverModal.test.tsx create mode 100644 src/components/ConflictResolverModal.tsx create mode 100644 src/components/CreateNoteDialog.test.tsx create mode 100644 src/components/CreateNoteDialog.tsx create mode 100644 src/components/CreateTypeDialog.test.tsx create mode 100644 src/components/CreateTypeDialog.tsx create mode 100644 src/components/CreateViewDialog.test.tsx create mode 100644 src/components/CreateViewDialog.tsx create mode 100644 src/components/DeleteProgressNotice.test.tsx create mode 100644 src/components/DeleteProgressNotice.tsx create mode 100644 src/components/DiffView.test.tsx create mode 100644 src/components/DiffView.tsx create mode 100644 src/components/DynamicPropertiesPanel.systemMetadata.test.tsx create mode 100644 src/components/DynamicPropertiesPanel.test.tsx create mode 100644 src/components/DynamicPropertiesPanel.tsx create mode 100644 src/components/EditableValue.test.tsx create mode 100644 src/components/EditableValue.tsx create mode 100644 src/components/Editor.css create mode 100644 src/components/Editor.raw-mode-sync.test.ts create mode 100644 src/components/Editor.test.tsx create mode 100644 src/components/Editor.tsx create mode 100644 src/components/EditorContent.tsx create mode 100644 src/components/EditorMemoryProbe.tsx create mode 100644 src/components/EditorRightPanel.test.tsx create mode 100644 src/components/EditorRightPanel.tsx create mode 100644 src/components/EditorTheme.css create mode 100644 src/components/EmojiPicker.tsx create mode 100644 src/components/FeedbackDialog.test.tsx create mode 100644 src/components/FeedbackDialog.tsx create mode 100644 src/components/FilePreview.test.tsx create mode 100644 src/components/FilePreview.tsx create mode 100644 src/components/FilterBuilder.test.tsx create mode 100644 src/components/FilterBuilder.tsx create mode 100644 src/components/FilterFieldCombobox.tsx create mode 100644 src/components/FolderTree.test.tsx create mode 100644 src/components/FolderTree.tsx create mode 100644 src/components/FormatSelector.test.tsx create mode 100644 src/components/FormatSelector.tsx create mode 100644 src/components/FrontendReadyMarker.tsx create mode 100644 src/components/GitProviderSettingsRows.tsx create mode 100644 src/components/GitRepositorySelect.tsx create mode 100644 src/components/GitRequiredModal.test.tsx create mode 100644 src/components/GitRequiredModal.tsx create mode 100644 src/components/GitSettingsSection.tsx create mode 100644 src/components/HiddenEditorMemoryProbe.tsx create mode 100644 src/components/IconEditableValue.test.tsx create mode 100644 src/components/IconEditableValue.tsx create mode 100644 src/components/ImageLightbox.test.tsx create mode 100644 src/components/ImageLightbox.tsx create mode 100644 src/components/InlineWikilinkInput.tsx create mode 100644 src/components/InlineWikilinkParts.tsx create mode 100644 src/components/Inspector.test.tsx create mode 100644 src/components/Inspector.tsx create mode 100644 src/components/InspectorPanels.test.tsx create mode 100644 src/components/InspectorPanels.tsx create mode 100644 src/components/LinuxMenuButton.test.tsx create mode 100644 src/components/LinuxMenuButton.tsx create mode 100644 src/components/LinuxTitlebar.test.tsx create mode 100644 src/components/LinuxTitlebar.tsx create mode 100644 src/components/MarkdownContent.test.tsx create mode 100644 src/components/MarkdownContent.tsx create mode 100644 src/components/MarkdownContent.webkit.test.tsx create mode 100644 src/components/McpSetupDialog.test.tsx create mode 100644 src/components/McpSetupDialog.tsx create mode 100644 src/components/MermaidDiagram.test.tsx create mode 100644 src/components/MermaidDiagram.tsx create mode 100644 src/components/NoteAutocomplete.test.tsx create mode 100644 src/components/NoteAutocomplete.tsx create mode 100644 src/components/NoteIcon.test.tsx create mode 100644 src/components/NoteIcon.tsx create mode 100644 src/components/NoteItem.test.tsx create mode 100644 src/components/NoteItem.tsx create mode 100644 src/components/NoteList.behavior.test.tsx create mode 100644 src/components/NoteList.changes.test.tsx create mode 100644 src/components/NoteList.contextMenu.test.tsx create mode 100644 src/components/NoteList.keyboard.test.tsx create mode 100644 src/components/NoteList.rendering.test.tsx create mode 100644 src/components/NoteList.sorting.test.tsx create mode 100644 src/components/NoteList.tsx create mode 100644 src/components/NoteSearchList.test.tsx create mode 100644 src/components/NoteSearchList.tsx create mode 100644 src/components/NoteTitleIcon.test.tsx create mode 100644 src/components/NoteTitleIcon.tsx create mode 100644 src/components/OnboardingShell.tsx create mode 100644 src/components/PrivacySettingsSection.tsx create mode 100644 src/components/PropertyValueCells.extra.test.tsx create mode 100644 src/components/PropertyValueCells.test.tsx create mode 100644 src/components/PropertyValueCells.tsx create mode 100644 src/components/PulseView.test.tsx create mode 100644 src/components/PulseView.tsx create mode 100644 src/components/QuickOpenPalette.test.tsx create mode 100644 src/components/QuickOpenPalette.tsx create mode 100644 src/components/RawEditorFindBar.test.tsx create mode 100644 src/components/RawEditorFindBar.tsx create mode 100644 src/components/RawEditorView.behavior.test.tsx create mode 100644 src/components/RawEditorView.coverage.test.tsx create mode 100644 src/components/RawEditorView.test.tsx create mode 100644 src/components/RawEditorView.tsx create mode 100644 src/components/RenameDetectedBanner.test.tsx create mode 100644 src/components/RenameDetectedBanner.tsx create mode 100644 src/components/ResizeHandle.test.tsx create mode 100644 src/components/ResizeHandle.tsx create mode 100644 src/components/SafeMarkup.test.tsx create mode 100644 src/components/SafeMarkup.tsx create mode 100644 src/components/SearchPanel.test.tsx create mode 100644 src/components/SearchPanel.tsx create mode 100644 src/components/SettingsBodyNav.tsx create mode 100644 src/components/SettingsControls.tsx create mode 100644 src/components/SettingsFooter.tsx create mode 100644 src/components/SettingsPanel.test.tsx create mode 100644 src/components/SettingsPanel.tsx create mode 100644 src/components/SheetContextMenu.tsx create mode 100644 src/components/SheetEditor.css create mode 100644 src/components/SheetEditor.frozenViewport.test.tsx create mode 100644 src/components/SheetEditor.serialization.test.tsx create mode 100644 src/components/SheetEditor.test.tsx create mode 100644 src/components/SheetEditor.testUtils.tsx create mode 100644 src/components/SheetEditor.tsx create mode 100644 src/components/SheetFormulaAutocompleteMenu.test.tsx create mode 100644 src/components/SheetFormulaAutocompleteMenu.tsx create mode 100644 src/components/Sidebar.test.ts create mode 100644 src/components/Sidebar.test.tsx create mode 100644 src/components/Sidebar.tsx create mode 100644 src/components/Sidebar.typeActions.test.tsx create mode 100644 src/components/Sidebar.typeVisibilityWorkspaces.test.tsx create mode 100644 src/components/Sidebar.viewActions.test.tsx create mode 100644 src/components/SidebarParts.tsx create mode 100644 src/components/SingleEditorView.test.tsx create mode 100644 src/components/SingleEditorView.testUtils.tsx create mode 100644 src/components/SingleEditorView.tsx create mode 100644 src/components/SortDropdown.tsx create mode 100644 src/components/StartupScreen.tsx create mode 100644 src/components/StatusBar.test.tsx create mode 100644 src/components/StatusBar.tsx create mode 100644 src/components/StatusDropdown.extra.test.tsx create mode 100644 src/components/StatusDropdown.test.tsx create mode 100644 src/components/StatusDropdown.tsx create mode 100644 src/components/TableOfContentsPanel.test.tsx create mode 100644 src/components/TableOfContentsPanel.tsx create mode 100644 src/components/TagsDropdown.test.tsx create mode 100644 src/components/TagsDropdown.tsx create mode 100644 src/components/TelemetryConsentDialog.test.tsx create mode 100644 src/components/TelemetryConsentDialog.tsx create mode 100644 src/components/TldrawWhiteboard.test.tsx create mode 100644 src/components/TldrawWhiteboard.tsx create mode 100644 src/components/Toast.tsx create mode 100644 src/components/TypeCustomizePopover.test.tsx create mode 100644 src/components/TypeCustomizePopover.tsx create mode 100644 src/components/TypeSelector.test.tsx create mode 100644 src/components/TypeSelector.tsx create mode 100644 src/components/UpdateBanner.test.tsx create mode 100644 src/components/UpdateBanner.tsx create mode 100644 src/components/VaultContentSettingsSection.tsx create mode 100644 src/components/WelcomeScreen.test.tsx create mode 100644 src/components/WelcomeScreen.tsx create mode 100644 src/components/WikilinkChatInput.test.tsx create mode 100644 src/components/WikilinkChatInput.tsx create mode 100644 src/components/WikilinkSuggestionMenu.css create mode 100644 src/components/WikilinkSuggestionMenu.test.tsx create mode 100644 src/components/WikilinkSuggestionMenu.tsx create mode 100644 src/components/WorkspaceInitialsBadge.tsx create mode 100644 src/components/WorkspaceMoveButtons.tsx create mode 100644 src/components/WorkspaceSelector.test.tsx create mode 100644 src/components/WorkspaceSelector.tsx create mode 100644 src/components/WorkspaceSettingsRows.tsx create mode 100644 src/components/WorkspaceSettingsSection.tsx create mode 100644 src/components/aiWorkspaceConversations.ts create mode 100644 src/components/aiWorkspaceSizing.ts create mode 100644 src/components/aiWorkspaceTargetGroups.ts create mode 100644 src/components/anchoredDropdown.test.ts create mode 100644 src/components/anchoredDropdown.ts create mode 100644 src/components/arrowLigaturesExtension.test.ts create mode 100644 src/components/arrowLigaturesExtension.ts create mode 100644 src/components/blockNoteCursorTarget.ts create mode 100644 src/components/blockNoteFormattingToolbarHoverGuard.extra.test.ts create mode 100644 src/components/blockNoteFormattingToolbarHoverGuard.test.ts create mode 100644 src/components/blockNoteFormattingToolbarHoverGuard.ts create mode 100644 src/components/blockNoteRenderRecovery.test.ts create mode 100644 src/components/blockNoteRenderRecovery.ts create mode 100644 src/components/blockNoteRenderRecoveryDocument.test.ts create mode 100644 src/components/blockNoteRenderRecoveryDocument.ts create mode 100644 src/components/blockNoteSideMenuHoverGuard.test.ts create mode 100644 src/components/blockNoteSideMenuHoverGuard.ts create mode 100644 src/components/codeBlockOptions.ts create mode 100644 src/components/contextMenuPosition.ts create mode 100644 src/components/dateDisplayPreferenceFlow.test.tsx create mode 100644 src/components/editor-content/EditorContentLayout.test.tsx create mode 100644 src/components/editor-content/EditorContentLayout.tsx create mode 100644 src/components/editor-content/editorContentState.test.ts create mode 100644 src/components/editor-content/editorContentState.ts create mode 100644 src/components/editor-content/useEditorContentModel.ts create mode 100644 src/components/editorAttachmentActions.test.ts create mode 100644 src/components/editorAttachmentActions.ts create mode 100644 src/components/editorContentFlushRegistration.test.tsx create mode 100644 src/components/editorContentFlushRegistration.ts create mode 100644 src/components/editorExternalChangeEvents.ts create mode 100644 src/components/editorImageInsertion.test.ts create mode 100644 src/components/editorImageInsertion.ts create mode 100644 src/components/editorMemoryProbeRuntime.ts create mode 100644 src/components/editorMemoryProbeTypes.ts create mode 100644 src/components/editorModePosition.test.ts create mode 100644 src/components/editorModePosition.ts create mode 100644 src/components/editorRawModeSync.arrowLigatures.test.ts create mode 100644 src/components/editorRawModeSync.images.test.ts create mode 100644 src/components/editorRawModeSync.mermaid.test.ts create mode 100644 src/components/editorRawModeSync.ts create mode 100644 src/components/editorRichCopy.test.ts create mode 100644 src/components/editorRichCopy.ts create mode 100644 src/components/editorSchema.codeBlockLanguages.test.ts create mode 100644 src/components/editorSchema.markdownHighlight.test.ts create mode 100644 src/components/editorSchema.math.test.tsx create mode 100644 src/components/editorSchema.media.test.ts create mode 100644 src/components/editorSchema.mermaid.test.ts create mode 100644 src/components/editorSchema.tsx create mode 100644 src/components/editorSchema.webkit.test.ts create mode 100644 src/components/editorTiptapSelection.test.ts create mode 100644 src/components/editorTiptapSelection.ts create mode 100644 src/components/filter-builder/DateValueInput.test.tsx create mode 100644 src/components/filter-builder/DateValueInput.tsx create mode 100644 src/components/filter-builder/FilterFieldCombobox.test.tsx create mode 100644 src/components/filter-builder/FilterFieldOptionsList.tsx create mode 100644 src/components/folder-tree/FolderContextMenu.test.tsx create mode 100644 src/components/folder-tree/FolderContextMenu.tsx create mode 100644 src/components/folder-tree/FolderItemRow.tsx create mode 100644 src/components/folder-tree/FolderNameInput.tsx create mode 100644 src/components/folder-tree/FolderTreeRow.tsx create mode 100644 src/components/folder-tree/folderTreeLayout.ts create mode 100644 src/components/folder-tree/folderTreeUtils.ts create mode 100644 src/components/folder-tree/useFolderContextMenu.ts create mode 100644 src/components/folder-tree/useFolderRowInteractions.ts create mode 100644 src/components/folder-tree/useFolderTreeDisclosure.ts create mode 100644 src/components/freshListItemPaste.ts create mode 100644 src/components/imeCompositionKeyGuardExtension.test.ts create mode 100644 src/components/imeCompositionKeyGuardExtension.ts create mode 100644 src/components/inlineWikilinkBeforeInput.ts create mode 100644 src/components/inlineWikilinkDom.test.ts create mode 100644 src/components/inlineWikilinkDom.ts create mode 100644 src/components/inlineWikilinkDropText.ts create mode 100644 src/components/inlineWikilinkEdits.ts create mode 100644 src/components/inlineWikilinkKeydown.ts create mode 100644 src/components/inlineWikilinkPasteRecovery.test.ts create mode 100644 src/components/inlineWikilinkPasteRecovery.ts create mode 100644 src/components/inlineWikilinkRemountState.test.ts create mode 100644 src/components/inlineWikilinkRemountState.ts create mode 100644 src/components/inlineWikilinkSuggestions.test.ts create mode 100644 src/components/inlineWikilinkSuggestions.ts create mode 100644 src/components/inlineWikilinkText.ts create mode 100644 src/components/inlineWikilinkTokens.ts create mode 100644 src/components/inspector/BacklinksPanel.tsx create mode 100644 src/components/inspector/GitHistoryPanel.tsx create mode 100644 src/components/inspector/InspectorChrome.tsx create mode 100644 src/components/inspector/InstancesPanel.tsx create mode 100644 src/components/inspector/LinkButton.tsx create mode 100644 src/components/inspector/NoteInfoPanel.test.tsx create mode 100644 src/components/inspector/NoteInfoPanel.tsx create mode 100644 src/components/inspector/ReferencedByPanel.tsx create mode 100644 src/components/inspector/RelationshipsPanel.tsx create mode 100644 src/components/inspector/relationshipDisplayLabels.test.tsx create mode 100644 src/components/inspector/relationshipLayoutRegression.test.tsx create mode 100644 src/components/inspector/shared.test.ts create mode 100644 src/components/inspector/shared.ts create mode 100644 src/components/inspector/useInspectorData.test.ts create mode 100644 src/components/inspector/useInspectorData.ts create mode 100644 src/components/inspector/useInspectorPropertyActions.ts create mode 100644 src/components/markdownHighlightInputExtension.test.ts create mode 100644 src/components/markdownHighlightInputExtension.ts create mode 100644 src/components/markdownHighlightShortcutExtension.test.ts create mode 100644 src/components/markdownHighlightShortcutExtension.ts create mode 100644 src/components/mathInputExtension.test.ts create mode 100644 src/components/mathInputExtension.ts create mode 100644 src/components/note-item/ChangeNoteContent.tsx create mode 100644 src/components/note-item/PropertyChips.tsx create mode 100644 src/components/note-item/propertyChipValues.ts create mode 100644 src/components/note-item/typeIcon.ts create mode 100644 src/components/note-list/FilterPills.tsx create mode 100644 src/components/note-list/InboxFilterPills.tsx create mode 100644 src/components/note-list/ListPropertiesPopover.tsx create mode 100644 src/components/note-list/NoteListChangesMenu.tsx create mode 100644 src/components/note-list/NoteListChangesMenuView.tsx create mode 100644 src/components/note-list/NoteListContextMenu.tsx create mode 100644 src/components/note-list/NoteListContextMenuView.tsx create mode 100644 src/components/note-list/NoteListHeader.expand.test.tsx create mode 100644 src/components/note-list/NoteListHeader.tsx create mode 100644 src/components/note-list/NoteListLayout.tsx create mode 100644 src/components/note-list/NoteListSearchKeyboard.test.tsx create mode 100644 src/components/note-list/NoteListViews.tsx create mode 100644 src/components/note-list/PinnedCard.tsx create mode 100644 src/components/note-list/RelationshipGroupSection.test.tsx create mode 100644 src/components/note-list/RelationshipGroupSection.tsx create mode 100644 src/components/note-list/TrashWarningBanner.tsx create mode 100644 src/components/note-list/multiSelectionCommands.ts create mode 100644 src/components/note-list/noteListFullTextSearch.ts create mode 100644 src/components/note-list/noteListHooks.extra.test.tsx create mode 100644 src/components/note-list/noteListHooks.ts create mode 100644 src/components/note-list/noteListPropertiesEvents.ts create mode 100644 src/components/note-list/noteListSearch.ts create mode 100644 src/components/note-list/noteListUtils.test.ts create mode 100644 src/components/note-list/noteListUtils.ts create mode 100644 src/components/note-list/useMultiSelectKeyboard.ts create mode 100644 src/components/note-list/useNoteListModel.tsx create mode 100644 src/components/note-list/useNoteListSearchState.ts create mode 100644 src/components/note-retargeting/NoteRetargetingDialogs.tsx create mode 100644 src/components/note-retargeting/RetargetNoteDialog.tsx create mode 100644 src/components/noteIconPropertyEvents.ts create mode 100644 src/components/propertyChipStyles.test.tsx create mode 100644 src/components/propertyChipStyles.ts create mode 100644 src/components/propertyDropdownUtils.test.ts create mode 100644 src/components/propertyDropdownUtils.ts create mode 100644 src/components/propertyPanelLayout.test.tsx create mode 100644 src/components/propertyPanelLayout.ts create mode 100644 src/components/propertyTypeIconControl.test.tsx create mode 100644 src/components/richEditorBlockNoteOptions.test.ts create mode 100644 src/components/richEditorBlockNoteOptions.ts create mode 100644 src/components/richEditorBlockSelectionClipboard.test.ts create mode 100644 src/components/richEditorBlockSelectionClipboard.ts create mode 100644 src/components/richEditorBlockSelectionDocument.test.ts create mode 100644 src/components/richEditorBlockSelectionDocument.ts create mode 100644 src/components/richEditorBlockSelectionExtension.test.ts create mode 100644 src/components/richEditorBlockSelectionExtension.ts create mode 100644 src/components/richEditorBlockSelectionTypes.ts create mode 100644 src/components/richEditorBlockTypeCommands.test.ts create mode 100644 src/components/richEditorBlockTypeCommands.ts create mode 100644 src/components/richEditorDispatchPerformance.test.ts create mode 100644 src/components/richEditorDispatchPerformance.ts create mode 100644 src/components/richEditorInputTransform.ts create mode 100644 src/components/richEditorInputTransformExtension.test.ts create mode 100644 src/components/richEditorInputTransformExtension.ts create mode 100644 src/components/richEditorPaste.ts create mode 100644 src/components/richEditorRecoveryClassifier.test.ts create mode 100644 src/components/richEditorRecoveryClassifier.ts create mode 100644 src/components/richEditorTextDirection.test.ts create mode 100644 src/components/richEditorTextDirection.ts create mode 100644 src/components/richEditorTransformErrorRecoveryExtension.test.ts create mode 100644 src/components/richEditorTransformErrorRecoveryExtension.ts create mode 100644 src/components/settingsPreferenceTracking.ts create mode 100644 src/components/settingsSectionIds.ts create mode 100644 src/components/sheet-editor/sheetAutocompleteKeyActions.ts create mode 100644 src/components/sheet-editor/sheetEditorActiveSheetTextInput.ts create mode 100644 src/components/sheet-editor/sheetEditorAutocompleteState.ts create mode 100644 src/components/sheet-editor/sheetEditorFocusOwnership.ts create mode 100644 src/components/sheet-editor/sheetEditorFormulaText.ts create mode 100644 src/components/sheet-editor/sheetEditorHelpers.ts create mode 100644 src/components/sheet-editor/sheetEditorInputDom.ts create mode 100644 src/components/sheet-editor/sheetEditorInputEvents.test.ts create mode 100644 src/components/sheet-editor/sheetEditorInputEvents.ts create mode 100644 src/components/sheet-editor/sheetEditorInputTarget.ts create mode 100644 src/components/sheet-editor/sheetEditorKeyboard.ts create mode 100644 src/components/sheet-editor/sheetEditorKeyboardEvents.ts create mode 100644 src/components/sheet-editor/sheetEditorKeyboardFocus.ts create mode 100644 src/components/sheet-editor/sheetEditorPlainKeyGuards.ts create mode 100644 src/components/sheet-editor/sheetEditorPointer.ts create mode 100644 src/components/sheet-editor/sheetEditorSerializeKeys.ts create mode 100644 src/components/sheet-editor/sheetEditorSpreadsheetKeys.ts create mode 100644 src/components/sheet-editor/sheetEditorTargetGuards.ts create mode 100644 src/components/sheet-editor/sheetEditorTypes.ts create mode 100644 src/components/sheet-editor/sheetEditorVisibleFormulaInput.ts create mode 100644 src/components/sheet-editor/sheetEditorVisibleSheetTextInput.test.ts create mode 100644 src/components/sheet-editor/sheetEditorVisibleSheetTextInput.ts create mode 100644 src/components/sheet-editor/sheetEditorWikilinkAutocomplete.ts create mode 100644 src/components/sheet-editor/sheetInlineAutocompleteTypes.ts create mode 100644 src/components/sheet-editor/useGuardedWorkbookFocus.test.tsx create mode 100644 src/components/sheet-editor/useGuardedWorkbookFocus.ts create mode 100644 src/components/sheet-editor/useSheetCellInputCommit.ts create mode 100644 src/components/sheet-editor/useSheetClipboardActions.test.tsx create mode 100644 src/components/sheet-editor/useSheetClipboardActions.ts create mode 100644 src/components/sheet-editor/useSheetContextMenuActions.ts create mode 100644 src/components/sheet-editor/useSheetContextMenuCapture.ts create mode 100644 src/components/sheet-editor/useSheetEditorController.ts create mode 100644 src/components/sheet-editor/useSheetExternalFormulaResolution.ts create mode 100644 src/components/sheet-editor/useSheetFormulaAutocomplete.ts create mode 100644 src/components/sheet-editor/useSheetInlineAutocompletes.ts create mode 100644 src/components/sheet-editor/useSheetInputActivityHandlers.ts create mode 100644 src/components/sheet-editor/useSheetKeyboardFocus.test.tsx create mode 100644 src/components/sheet-editor/useSheetKeyboardFocus.ts create mode 100644 src/components/sheet-editor/useSheetKeyboardHandlers.ts create mode 100644 src/components/sheet-editor/useSheetKeyboardReleaseOutside.ts create mode 100644 src/components/sheet-editor/useSheetPointerCoordinatePatching.ts create mode 100644 src/components/sheet-editor/useSheetPointerHandlers.ts create mode 100644 src/components/sheet-editor/useSheetSelectionChrome.ts create mode 100644 src/components/sheet-editor/useSheetWikilinkAutocomplete.ts create mode 100644 src/components/sheet-editor/useSheetWorkbookController.ts create mode 100644 src/components/sidebar/FavoritesSection.tsx create mode 100644 src/components/sidebar/SidebarGroupHeader.tsx create mode 100644 src/components/sidebar/SidebarLoadingSections.tsx create mode 100644 src/components/sidebar/SidebarSections.tsx create mode 100644 src/components/sidebar/SidebarTitleBar.test.tsx create mode 100644 src/components/sidebar/SidebarTopNav.tsx create mode 100644 src/components/sidebar/SidebarViewActions.tsx create mode 100644 src/components/sidebar/SidebarViewItem.tsx create mode 100644 src/components/sidebar/sidebarHooks.ts create mode 100644 src/components/sidebar/sidebarStyles.ts create mode 100644 src/components/sidebar/useSidebarTypeInteractions.ts create mode 100644 src/components/sidebar/useSidebarViewItemInteractions.ts create mode 100644 src/components/status-bar/AiAgentsBadge.test.tsx create mode 100644 src/components/status-bar/AiAgentsBadge.tsx create mode 100644 src/components/status-bar/StatusBarBadges.extra.test.tsx create mode 100644 src/components/status-bar/StatusBarBadges.tsx create mode 100644 src/components/status-bar/StatusBarSections.tsx create mode 100644 src/components/status-bar/VaultMenu.applyMountedChange.test.ts create mode 100644 src/components/status-bar/VaultMenu.tsx create mode 100644 src/components/status-bar/styles.ts create mode 100644 src/components/status-bar/types.ts create mode 100644 src/components/status-bar/useDismissibleLayer.ts create mode 100644 src/components/status-bar/vaultMenuMountedChange.ts create mode 100644 src/components/tableOfContents.worker.ts create mode 100644 src/components/tableOfContentsModel.ts create mode 100644 src/components/tableOfContentsWorkerClient.ts create mode 100644 src/components/tableResizeState.ts create mode 100644 src/components/titleHeadingInteractions.test.ts create mode 100644 src/components/titleHeadingInteractions.ts create mode 100644 src/components/titleHeadingPasteTarget.ts create mode 100644 src/components/tldrawBlockProps.test.ts create mode 100644 src/components/tldrawBlockProps.ts create mode 100644 src/components/tldrawTextMeasurementGuard.test.ts create mode 100644 src/components/tldrawTextMeasurementGuard.ts create mode 100644 src/components/tolariaBlockNoteDom.ts create mode 100644 src/components/tolariaBlockNoteSideMenu.test.tsx create mode 100644 src/components/tolariaBlockNoteSideMenu.tsx create mode 100644 src/components/tolariaBlockReorder.ts create mode 100644 src/components/tolariaCollapsedSections.ts create mode 100644 src/components/tolariaEditorFormatting.behavior.test.tsx create mode 100644 src/components/tolariaEditorFormatting.test.tsx create mode 100644 src/components/tolariaEditorFormatting.tsx create mode 100644 src/components/tolariaEditorFormattingConfig.ts create mode 100644 src/components/tolariaSideMenuAlignment.ts create mode 100644 src/components/tolariaSideMenuBlocks.ts create mode 100644 src/components/ui/action-tooltip.recovery.test.tsx create mode 100644 src/components/ui/action-tooltip.tsx create mode 100644 src/components/ui/actionTooltipRecovery.ts create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/calendar.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/dropdown-menu.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/overlayPresence.test.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 src/components/ui/scroll-area.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/separator.tsx create mode 100644 src/components/ui/switch.tsx create mode 100644 src/components/ui/tabs.tsx create mode 100644 src/components/ui/textControls.test.tsx create mode 100644 src/components/ui/textarea.tsx create mode 100644 src/components/ui/tooltip.tsx create mode 100644 src/components/useAiPanelContextSnapshot.ts create mode 100644 src/components/useAiPanelController.ts create mode 100644 src/components/useAiPanelFocus.ts create mode 100644 src/components/useAiPanelPromptQueue.test.tsx create mode 100644 src/components/useAiPanelPromptQueue.ts create mode 100644 src/components/useEditorComposing.ts create mode 100644 src/components/useEditorLinkActivation.test.tsx create mode 100644 src/components/useEditorLinkActivation.ts create mode 100644 src/components/useEditorMemoryProbeController.ts create mode 100644 src/components/useEditorModePositionSync.test.tsx create mode 100644 src/components/useEditorModePositionSync.ts create mode 100644 src/components/useEditorPdfExport.ts create mode 100644 src/components/useFilenameAutolinkGuard.extra.test.tsx create mode 100644 src/components/useFilenameAutolinkGuard.test.tsx create mode 100644 src/components/useFilenameAutolinkGuard.ts create mode 100644 src/components/useInlineWikilinkSelection.ts create mode 100644 src/components/useInlineWikilinkSuggestionsState.ts create mode 100644 src/components/useNativePathDrop.ts create mode 100644 src/components/useNoteListSort.test.tsx create mode 100644 src/components/useQueuedAiPrompt.ts create mode 100644 src/components/useRawModeWithFlush.test.tsx create mode 100644 src/components/useRawModeWithFlush.ts create mode 100644 src/components/useRichEditorSheetTransition.ts create mode 100644 src/components/useRightPanelExclusion.ts create mode 100644 src/components/useSettingsPanelFocus.ts create mode 100644 src/components/useTurnCurrentBlockIntoCommand.test.tsx create mode 100644 src/components/useTurnCurrentBlockIntoCommand.ts create mode 100644 src/constants/appStorage.test.ts create mode 100644 src/constants/appStorage.ts create mode 100644 src/constants/feedback.ts create mode 100644 src/extensions/frontmatterHighlight.test.ts create mode 100644 src/extensions/frontmatterHighlight.ts create mode 100644 src/extensions/markdownHighlight.test.ts create mode 100644 src/extensions/markdownHighlight.ts create mode 100644 src/extensions/rawEditorLanguage.ts create mode 100644 src/extensions/zoomCursorFix.behavior.test.ts create mode 100644 src/extensions/zoomCursorFix.extra.test.ts create mode 100644 src/extensions/zoomCursorFix.test.ts create mode 100644 src/extensions/zoomCursorFix.ts create mode 100644 src/hooks/appCommandCatalog.test.ts create mode 100644 src/hooks/appCommandCatalog.ts create mode 100644 src/hooks/appCommandDispatcher.test.ts create mode 100644 src/hooks/appCommandDispatcher.ts create mode 100644 src/hooks/appKeyboardShortcuts.editorFind.test.ts create mode 100644 src/hooks/appKeyboardShortcuts.ts create mode 100644 src/hooks/commands/aiAgentCommands.test.ts create mode 100644 src/hooks/commands/aiAgentCommands.ts create mode 100644 src/hooks/commands/editorFindCommands.ts create mode 100644 src/hooks/commands/filterCommands.ts create mode 100644 src/hooks/commands/gitCommands.test.ts create mode 100644 src/hooks/commands/gitCommands.ts create mode 100644 src/hooks/commands/index.ts create mode 100644 src/hooks/commands/localizeCommands.ts create mode 100644 src/hooks/commands/navigationCommands.ts create mode 100644 src/hooks/commands/noteCommands.ts create mode 100644 src/hooks/commands/settingsCommands.test.ts create mode 100644 src/hooks/commands/settingsCommands.ts create mode 100644 src/hooks/commands/typeCommands.ts create mode 100644 src/hooks/commands/types.ts create mode 100644 src/hooks/commands/viewCommands.ts create mode 100644 src/hooks/editorBlockRepair.test.ts create mode 100644 src/hooks/editorBlockRepair.ts create mode 100644 src/hooks/editorBlockResolution.performance.test.ts create mode 100644 src/hooks/editorBlockResolution.ts create mode 100644 src/hooks/editorChangeDebounce.ts create mode 100644 src/hooks/editorContentSwapApply.test.ts create mode 100644 src/hooks/editorContentSwapApply.ts create mode 100644 src/hooks/editorDocumentState.ts create mode 100644 src/hooks/editorDomSelection.ts create mode 100644 src/hooks/editorFastMarkdownBlocks.test.ts create mode 100644 src/hooks/editorFastMarkdownBlocks.ts create mode 100644 src/hooks/editorFastMarkdownBlocks.worker.ts create mode 100644 src/hooks/editorFocusOwnership.test.tsx create mode 100644 src/hooks/editorFocusOwnership.ts create mode 100644 src/hooks/editorFocusUtils.extra.test.ts create mode 100644 src/hooks/editorFocusUtils.ts create mode 100644 src/hooks/editorMarkdownParseFallback.test.ts create mode 100644 src/hooks/editorMarkdownParseFallback.ts create mode 100644 src/hooks/editorParsedBlockCache.ts create mode 100644 src/hooks/editorParsedBlockPreload.test.tsx create mode 100644 src/hooks/editorParsedBlockPreload.ts create mode 100644 src/hooks/editorParsedBlockSwap.ts create mode 100644 src/hooks/editorSwapToken.ts create mode 100644 src/hooks/editorTabContent.test.ts create mode 100644 src/hooks/editorTabContent.ts create mode 100644 src/hooks/editorTabSwapLifecycle.ts create mode 100644 src/hooks/editorTiptapSelection.ts create mode 100644 src/hooks/editorTitleHeadingText.ts create mode 100644 src/hooks/editorTitleSelection.ts create mode 100644 src/hooks/focusOwnershipGuard.ts create mode 100644 src/hooks/folder-actions/folderActionUtils.ts create mode 100644 src/hooks/folder-actions/useFolderDelete.ts create mode 100644 src/hooks/folder-actions/useFolderRename.ts create mode 100644 src/hooks/frontmatterOps.systemMetadata.test.ts create mode 100644 src/hooks/frontmatterOps.test.ts create mode 100644 src/hooks/frontmatterOps.ts create mode 100644 src/hooks/mockFrontmatterHelpers.test.ts create mode 100644 src/hooks/mockFrontmatterHelpers.ts create mode 100644 src/hooks/noteContentCache.ts create mode 100644 src/hooks/noteCreationRequests.ts create mode 100644 src/hooks/rawEditorEntryState.ts create mode 100644 src/hooks/useActionHistory.test.ts create mode 100644 src/hooks/useActionHistory.ts create mode 100644 src/hooks/useAiActivity.test.ts create mode 100644 src/hooks/useAiActivity.ts create mode 100644 src/hooks/useAiAgentPreferences.test.ts create mode 100644 src/hooks/useAiAgentPreferences.ts create mode 100644 src/hooks/useAiAgentsOnboarding.ts create mode 100644 src/hooks/useAiAgentsStatus.test.ts create mode 100644 src/hooks/useAiAgentsStatus.ts create mode 100644 src/hooks/useAiWorkspacePublishedContext.ts create mode 100644 src/hooks/useAiWorkspaceWindowBridgeEvents.ts create mode 100644 src/hooks/useAppAiWorkspaceBridge.ts create mode 100644 src/hooks/useAppCommandAiActions.ts create mode 100644 src/hooks/useAppCommands.ts create mode 100644 src/hooks/useAppKeyboard.test.ts create mode 100644 src/hooks/useAppKeyboard.ts create mode 100644 src/hooks/useAppNavigation.test.ts create mode 100644 src/hooks/useAppNavigation.ts create mode 100644 src/hooks/useAppPreferences.ts create mode 100644 src/hooks/useAppSave.test.ts create mode 100644 src/hooks/useAppSave.ts create mode 100644 src/hooks/useAppViewActions.ts create mode 100644 src/hooks/useAppWindowControls.ts create mode 100644 src/hooks/useAutoGit.test.ts create mode 100644 src/hooks/useAutoGit.ts create mode 100644 src/hooks/useAutoGitWork.ts create mode 100644 src/hooks/useAutoSync.extra.test.ts create mode 100644 src/hooks/useAutoSync.test.ts create mode 100644 src/hooks/useAutoSync.ts create mode 100644 src/hooks/useBuildNumber.test.ts create mode 100644 src/hooks/useBuildNumber.ts create mode 100644 src/hooks/useBulkActions.test.ts create mode 100644 src/hooks/useBulkActions.ts create mode 100644 src/hooks/useClaudeCodeOnboarding.test.ts create mode 100644 src/hooks/useClaudeCodeOnboarding.ts create mode 100644 src/hooks/useClaudeCodeStatus.test.ts create mode 100644 src/hooks/useClaudeCodeStatus.ts create mode 100644 src/hooks/useCliAiAgent.test.ts create mode 100644 src/hooks/useCliAiAgent.ts create mode 100644 src/hooks/useCodeMirror.arrowLigatures.test.ts create mode 100644 src/hooks/useCodeMirror.test.ts create mode 100644 src/hooks/useCodeMirror.ts create mode 100644 src/hooks/useCommandRegistry.editorFind.test.ts create mode 100644 src/hooks/useCommandRegistry.test.ts create mode 100644 src/hooks/useCommandRegistry.ts create mode 100644 src/hooks/useCommitFlow.test.ts create mode 100644 src/hooks/useCommitFlow.ts create mode 100644 src/hooks/useConflictFlow.test.ts create mode 100644 src/hooks/useConflictFlow.ts create mode 100644 src/hooks/useConflictResolver.test.ts create mode 100644 src/hooks/useConflictResolver.ts create mode 100644 src/hooks/useDeepLinks.test.tsx create mode 100644 src/hooks/useDeepLinks.ts create mode 100644 src/hooks/useDeleteActions.test.ts create mode 100644 src/hooks/useDeleteActions.ts create mode 100644 src/hooks/useDialogs.ts create mode 100644 src/hooks/useDiffMode.test.ts create mode 100644 src/hooks/useDiffMode.ts create mode 100644 src/hooks/useDocumentThemeMode.test.ts create mode 100644 src/hooks/useDocumentThemeMode.ts create mode 100644 src/hooks/useDragRegion.test.tsx create mode 100644 src/hooks/useDragRegion.ts create mode 100644 src/hooks/useEditorContentPathSignal.ts create mode 100644 src/hooks/useEditorFocus.test.ts create mode 100644 src/hooks/useEditorFocus.ts create mode 100644 src/hooks/useEditorSave.test.ts create mode 100644 src/hooks/useEditorSave.ts create mode 100644 src/hooks/useEditorSaveWithLinks.test.ts create mode 100644 src/hooks/useEditorSaveWithLinks.ts create mode 100644 src/hooks/useEditorTabSwap.performance.test.ts create mode 100644 src/hooks/useEditorTabSwap.rename.test.ts create mode 100644 src/hooks/useEditorTabSwap.selection.test.ts create mode 100644 src/hooks/useEditorTabSwap.test.ts create mode 100644 src/hooks/useEditorTabSwap.ts create mode 100644 src/hooks/useEntryActions.actionHistory.test.ts create mode 100644 src/hooks/useEntryActions.auto-create.test.ts create mode 100644 src/hooks/useEntryActions.history.test.ts create mode 100644 src/hooks/useEntryActions.test.ts create mode 100644 src/hooks/useEntryActions.ts create mode 100644 src/hooks/useEntryActions.typeVisibility.test.ts create mode 100644 src/hooks/useFeatureFlag.test.ts create mode 100644 src/hooks/useFeatureFlag.ts create mode 100644 src/hooks/useFileActions.ts create mode 100644 src/hooks/useFolderActions.test.ts create mode 100644 src/hooks/useFolderActions.ts create mode 100644 src/hooks/useGettingStartedClone.test.ts create mode 100644 src/hooks/useGettingStartedClone.ts create mode 100644 src/hooks/useGitFileWorkflows.ts create mode 100644 src/hooks/useGitHistory.test.ts create mode 100644 src/hooks/useGitHistory.ts create mode 100644 src/hooks/useGitRepositories.test.ts create mode 100644 src/hooks/useGitRepositories.ts create mode 100644 src/hooks/useGitSetupState.test.tsx create mode 100644 src/hooks/useGitSetupState.ts create mode 100644 src/hooks/useImageDrop.test.ts create mode 100644 src/hooks/useImageDrop.ts create mode 100644 src/hooks/useImageLightbox.test.tsx create mode 100644 src/hooks/useImageLightbox.ts create mode 100644 src/hooks/useInboxOrganizeAdvance.test.ts create mode 100644 src/hooks/useInboxOrganizeAdvance.ts create mode 100644 src/hooks/useKeyboardNavigation.test.ts create mode 100644 src/hooks/useKeyboardNavigation.ts create mode 100644 src/hooks/useLayoutPanels.test.ts create mode 100644 src/hooks/useLayoutPanels.ts create mode 100644 src/hooks/useMainWindowSizeConstraints.test.ts create mode 100644 src/hooks/useMainWindowSizeConstraints.ts create mode 100644 src/hooks/useMcpBridge.ts create mode 100644 src/hooks/useMcpSetupDialogController.ts create mode 100644 src/hooks/useMcpStatus.test.ts create mode 100644 src/hooks/useMcpStatus.ts create mode 100644 src/hooks/useMenuEvents.editorFind.test.ts create mode 100644 src/hooks/useMenuEvents.noteListSearch.test.ts create mode 100644 src/hooks/useMenuEvents.test.ts create mode 100644 src/hooks/useMenuEvents.ts create mode 100644 src/hooks/useMultiSelect.ts create mode 100644 src/hooks/useNavigationGestures.ts create mode 100644 src/hooks/useNavigationHistory.test.ts create mode 100644 src/hooks/useNavigationHistory.ts create mode 100644 src/hooks/useNeighborhoodSelection.test.ts create mode 100644 src/hooks/useNeighborhoodSelection.ts create mode 100644 src/hooks/useNetworkStatus.test.ts create mode 100644 src/hooks/useNetworkStatus.ts create mode 100644 src/hooks/useNoteActions.hook.test.ts create mode 100644 src/hooks/useNoteActions.missing-path.test.ts create mode 100644 src/hooks/useNoteActions.persistence.test.ts create mode 100644 src/hooks/useNoteActions.rename-guard.test.ts create mode 100644 src/hooks/useNoteActions.ts create mode 100644 src/hooks/useNoteActions.typeSync.test.tsx create mode 100644 src/hooks/useNoteCreation.extra.test.ts create mode 100644 src/hooks/useNoteCreation.test.ts create mode 100644 src/hooks/useNoteCreation.ts create mode 100644 src/hooks/useNoteGitUrls.test.ts create mode 100644 src/hooks/useNoteGitUrls.ts create mode 100644 src/hooks/useNoteLayout.test.ts create mode 100644 src/hooks/useNoteLayout.ts create mode 100644 src/hooks/useNoteListKeyboard.test.ts create mode 100644 src/hooks/useNoteListKeyboard.ts create mode 100644 src/hooks/useNoteRename.test.ts create mode 100644 src/hooks/useNoteRename.ts create mode 100644 src/hooks/useNoteRetargeting.test.ts create mode 100644 src/hooks/useNoteRetargeting.ts create mode 100644 src/hooks/useNoteRetargetingUi.ts create mode 100644 src/hooks/useNoteSearch.test.ts create mode 100644 src/hooks/useNoteSearch.ts create mode 100644 src/hooks/useNoteWidthMode.ts create mode 100644 src/hooks/useNoteWindowLifecycle.test.tsx create mode 100644 src/hooks/useNoteWindowLifecycle.ts create mode 100644 src/hooks/useOnboarding.test.ts create mode 100644 src/hooks/useOnboarding.ts create mode 100644 src/hooks/usePropertyPanelState.extra.test.ts create mode 100644 src/hooks/usePropertyPanelState.test.ts create mode 100644 src/hooks/usePropertyPanelState.ts create mode 100644 src/hooks/useRawMode.test.ts create mode 100644 src/hooks/useRawMode.ts create mode 100644 src/hooks/useRenameVaultRefresh.test.ts create mode 100644 src/hooks/useSaveNote.test.ts create mode 100644 src/hooks/useSaveNote.ts create mode 100644 src/hooks/useSavedViewOrdering.test.ts create mode 100644 src/hooks/useSavedViewOrdering.ts create mode 100644 src/hooks/useSettings.test.ts create mode 100644 src/hooks/useSettings.ts create mode 100644 src/hooks/useSheetWikilinkNavigation.ts create mode 100644 src/hooks/useStartupScreenState.ts create mode 100644 src/hooks/useStatusBarAddRemote.test.ts create mode 100644 src/hooks/useStatusBarAddRemote.ts create mode 100644 src/hooks/useTabManagement.test.ts create mode 100644 src/hooks/useTabManagement.ts create mode 100644 src/hooks/useTauriDragDropEvent.test.ts create mode 100644 src/hooks/useTauriDragDropEvent.ts create mode 100644 src/hooks/useTelemetry.test.ts create mode 100644 src/hooks/useTelemetry.ts create mode 100644 src/hooks/useTheme.test.ts create mode 100644 src/hooks/useTheme.ts create mode 100644 src/hooks/useThemeMode.test.ts create mode 100644 src/hooks/useThemeMode.ts create mode 100644 src/hooks/useUnavailableVaultState.ts create mode 100644 src/hooks/useUnifiedSearch.ts create mode 100644 src/hooks/useUpdater.test.ts create mode 100644 src/hooks/useUpdater.ts create mode 100644 src/hooks/useVaultAiGuidanceStatus.test.ts create mode 100644 src/hooks/useVaultAiGuidanceStatus.ts create mode 100644 src/hooks/useVaultBridge.test.ts create mode 100644 src/hooks/useVaultBridge.ts create mode 100644 src/hooks/useVaultConfig.ts create mode 100644 src/hooks/useVaultLoader.empty-cache.test.ts create mode 100644 src/hooks/useVaultLoader.extra.test.ts create mode 100644 src/hooks/useVaultLoader.startup.test.ts create mode 100644 src/hooks/useVaultLoader.test.ts create mode 100644 src/hooks/useVaultLoader.ts create mode 100644 src/hooks/useVaultLoader.workspaces.test.ts create mode 100644 src/hooks/useVaultOpenedTelemetry.ts create mode 100644 src/hooks/useVaultRenameDetection.ts create mode 100644 src/hooks/useVaultReorderAction.ts create mode 100644 src/hooks/useVaultSwitcher.test.ts create mode 100644 src/hooks/useVaultSwitcher.ts create mode 100644 src/hooks/useVaultWatcher.test.ts create mode 100644 src/hooks/useVaultWatcher.ts create mode 100644 src/hooks/useViewMode.test.ts create mode 100644 src/hooks/useViewMode.ts create mode 100644 src/hooks/useWorkspaceGraphState.ts create mode 100644 src/hooks/useWorkspaceIdentityActions.ts create mode 100644 src/hooks/useZoom.test.ts create mode 100644 src/hooks/useZoom.ts create mode 100644 src/hooks/vaultLoaderCommands.ts create mode 100644 src/hooks/vaultLoaderCommands.views.test.ts create mode 100644 src/hooks/vaultStateReset.ts create mode 100644 src/hooks/vaultSwitcherSanitization.ts create mode 100644 src/hooks/vaultWorkspaceEntries.ts create mode 100644 src/index.css create mode 100644 src/indexBootDiagnostics.test.ts create mode 100644 src/lib/agentDocsPath.ts create mode 100644 src/lib/aiAgentConversation.test.ts create mode 100644 src/lib/aiAgentConversation.ts create mode 100644 src/lib/aiAgentFileOperations.test.ts create mode 100644 src/lib/aiAgentFileOperations.ts create mode 100644 src/lib/aiAgentMessageState.ts create mode 100644 src/lib/aiAgentPermissionMode.test.ts create mode 100644 src/lib/aiAgentPermissionMode.ts create mode 100644 src/lib/aiAgentSession.test.ts create mode 100644 src/lib/aiAgentSession.ts create mode 100644 src/lib/aiAgentStreamCallbacks.test.ts create mode 100644 src/lib/aiAgentStreamCallbacks.ts create mode 100644 src/lib/aiAgents.test.ts create mode 100644 src/lib/aiAgents.ts create mode 100644 src/lib/aiFeatures.test.ts create mode 100644 src/lib/aiFeatures.ts create mode 100644 src/lib/aiTargets.test.ts create mode 100644 src/lib/aiTargets.ts create mode 100644 src/lib/aiWorkspaceSessionStore.test.ts create mode 100644 src/lib/aiWorkspaceSessionStore.ts create mode 100644 src/lib/aiWorkspaceWindowSharedContext.test.ts create mode 100644 src/lib/aiWorkspaceWindowSharedContext.ts create mode 100644 src/lib/appIconTheme.test.ts create mode 100644 src/lib/appIconTheme.ts create mode 100644 src/lib/appUpdater.ts create mode 100644 src/lib/automaticUpdateChecks.ts create mode 100644 src/lib/blockNoteChecklist.regression.test.ts create mode 100644 src/lib/blockNoteCodeBlockControl.regression.test.ts create mode 100644 src/lib/blockNoteCopyCompatibility.regression.test.ts create mode 100644 src/lib/blockNoteLinkClick.regression.test.ts create mode 100644 src/lib/blockNotePopover.regression.test.ts create mode 100644 src/lib/blockNoteSideMenu.regression.test.ts create mode 100644 src/lib/blockNoteSuggestionMenu.regression.test.ts create mode 100644 src/lib/blockNoteSuggestionWrapper.regression.test.tsx create mode 100644 src/lib/blockNoteTableHandles.regression.test.ts create mode 100644 src/lib/crossWindowPersistedStore.test.ts create mode 100644 src/lib/crossWindowPersistedStore.ts create mode 100644 src/lib/feedbackDiagnostics.test.ts create mode 100644 src/lib/feedbackDiagnostics.ts create mode 100644 src/lib/feedbackDialogOpener.ts create mode 100644 src/lib/gitSettings.ts create mode 100644 src/lib/gitignoredVisibility.ts create mode 100644 src/lib/gitignoredVisibilityEvents.ts create mode 100644 src/lib/i18n.test.ts create mode 100644 src/lib/i18n.ts create mode 100644 src/lib/locales/be-BY.json create mode 100644 src/lib/locales/be-Latn.json create mode 100644 src/lib/locales/de-DE.json create mode 100644 src/lib/locales/en.json create mode 100644 src/lib/locales/es-419.json create mode 100644 src/lib/locales/es-ES.json create mode 100644 src/lib/locales/fr-FR.json create mode 100644 src/lib/locales/id-ID.json create mode 100644 src/lib/locales/it-IT.json create mode 100644 src/lib/locales/ja-JP.json create mode 100644 src/lib/locales/ko-KR.json create mode 100644 src/lib/locales/pl-PL.json create mode 100644 src/lib/locales/pt-BR.json create mode 100644 src/lib/locales/pt-PT.json create mode 100644 src/lib/locales/ru-RU.json create mode 100644 src/lib/locales/sk-SK.json create mode 100644 src/lib/locales/sv-SE.json create mode 100644 src/lib/locales/uk-UA.json create mode 100644 src/lib/locales/vi.json create mode 100644 src/lib/locales/zh-CN.json create mode 100644 src/lib/locales/zh-TW.json create mode 100644 src/lib/localizedStreamError.ts create mode 100644 src/lib/nativeTextAssistance.ts create mode 100644 src/lib/productAnalytics.ts create mode 100644 src/lib/releaseChannel.test.ts create mode 100644 src/lib/releaseChannel.ts create mode 100644 src/lib/runtimeStyleNonce.ts create mode 100644 src/lib/sensitiveTextRedaction.ts create mode 100644 src/lib/telemetry.test.ts create mode 100644 src/lib/telemetry.ts create mode 100644 src/lib/telemetryConfig.test.ts create mode 100644 src/lib/telemetryConfig.ts create mode 100644 src/lib/themeMode.test.ts create mode 100644 src/lib/themeMode.ts create mode 100644 src/lib/utils.ts create mode 100644 src/lib/vaultAiGuidance.test.ts create mode 100644 src/lib/vaultAiGuidance.ts create mode 100644 src/main.test.ts create mode 100644 src/main.tsx create mode 100644 src/mock-tauri/index.test.ts create mode 100644 src/mock-tauri/index.ts create mode 100644 src/mock-tauri/mock-content.ts create mode 100644 src/mock-tauri/mock-entries.ts create mode 100644 src/mock-tauri/mock-handlers.coverage.test.ts create mode 100644 src/mock-tauri/mock-handlers.more.test.ts create mode 100644 src/mock-tauri/mock-handlers.test.ts create mode 100644 src/mock-tauri/mock-handlers.ts create mode 100644 src/mock-tauri/vault-api.test.ts create mode 100644 src/mock-tauri/vault-api.ts create mode 100644 src/shared/aiModelProviderCatalog.json create mode 100644 src/shared/appCommandManifest.json create mode 100644 src/test-utils/noteListTestUtils.tsx create mode 100644 src/test/setup.test.ts create mode 100644 src/test/setup.ts create mode 100644 src/theme.json create mode 100644 src/types.ts create mode 100644 src/types/laputaTestBridge.ts create mode 100644 src/utils/__tests__/emoji.test.ts create mode 100644 src/utils/addRemoteEvents.ts create mode 100644 src/utils/ai-agent.test.ts create mode 100644 src/utils/ai-agent.ts create mode 100644 src/utils/ai-chat.test.ts create mode 100644 src/utils/ai-chat.ts create mode 100644 src/utils/ai-context.test.ts create mode 100644 src/utils/ai-context.ts create mode 100644 src/utils/ai-reference-content.ts create mode 100644 src/utils/aiConversationTitle.test.ts create mode 100644 src/utils/aiConversationTitle.ts create mode 100644 src/utils/aiPromptBridge.ts create mode 100644 src/utils/aiProviderSecrets.ts create mode 100644 src/utils/aiStreamEvents.ts create mode 100644 src/utils/allNotesFileVisibility.ts create mode 100644 src/utils/appOrchestration.test.ts create mode 100644 src/utils/appOrchestration.ts create mode 100644 src/utils/arrowLigatures.test.ts create mode 100644 src/utils/arrowLigatures.ts create mode 100644 src/utils/autoGitWork.test.ts create mode 100644 src/utils/autoGitWork.ts create mode 100644 src/utils/autoSave.test.ts create mode 100644 src/utils/autoSave.ts create mode 100644 src/utils/automaticCommitMessage.test.ts create mode 100644 src/utils/automaticCommitMessage.ts create mode 100644 src/utils/blockNoteDirectMarkdown.test.ts create mode 100644 src/utils/blockNoteDirectMarkdown.ts create mode 100644 src/utils/calendarVersion.test.ts create mode 100644 src/utils/calendarVersion.ts create mode 100644 src/utils/chatWikilinks.ts create mode 100644 src/utils/clipboardText.test.ts create mode 100644 src/utils/clipboardText.ts create mode 100644 src/utils/codeBlockLanguage.test.ts create mode 100644 src/utils/codeBlockLanguage.ts create mode 100644 src/utils/codeBlockLanguageCatalog.ts create mode 100644 src/utils/colorUtils.test.ts create mode 100644 src/utils/colorUtils.ts create mode 100644 src/utils/commitEntryAction.test.ts create mode 100644 src/utils/commitEntryAction.ts create mode 100644 src/utils/commitMessage.test.ts create mode 100644 src/utils/commitMessage.ts create mode 100644 src/utils/commitMessageDraft.test.ts create mode 100644 src/utils/commitMessageDraft.ts create mode 100644 src/utils/compact-markdown.test.ts create mode 100644 src/utils/compact-markdown.ts create mode 100644 src/utils/configMigration.test.ts create mode 100644 src/utils/configMigration.ts create mode 100644 src/utils/cssFunctionalColor.ts create mode 100644 src/utils/dateDisplay.test.ts create mode 100644 src/utils/dateDisplay.ts create mode 100644 src/utils/dateStringParts.ts create mode 100644 src/utils/deepLinks.test.ts create mode 100644 src/utils/deepLinks.ts create mode 100644 src/utils/domScroll.ts create mode 100644 src/utils/durableMarkdownBlocks.ts create mode 100644 src/utils/editorDurableMarkdown.test.ts create mode 100644 src/utils/editorDurableMarkdown.ts create mode 100644 src/utils/editorFind.test.ts create mode 100644 src/utils/editorFind.ts create mode 100644 src/utils/editorFindEvents.ts create mode 100644 src/utils/editorLinkAutolink.test.ts create mode 100644 src/utils/editorLinkAutolink.ts create mode 100644 src/utils/editorPerformanceTrace.ts create mode 100644 src/utils/emoji.ts create mode 100644 src/utils/fileAttachmentMarkdown.test.ts create mode 100644 src/utils/fileAttachmentMarkdown.ts create mode 100644 src/utils/filePreview.ts create mode 100644 src/utils/filterDates.test.ts create mode 100644 src/utils/filterDates.ts create mode 100644 src/utils/frontendReady.test.ts create mode 100644 src/utils/frontendReady.ts create mode 100644 src/utils/frontmatter.test.ts create mode 100644 src/utils/frontmatter.ts create mode 100644 src/utils/fuzzyMatch.test.ts create mode 100644 src/utils/fuzzyMatch.ts create mode 100644 src/utils/gettingStartedVault.test.ts create mode 100644 src/utils/gettingStartedVault.ts create mode 100644 src/utils/gitRepositories.test.ts create mode 100644 src/utils/gitRepositories.ts create mode 100644 src/utils/guanghuPortalMarkdown.test.ts create mode 100644 src/utils/guanghuPortalMarkdown.ts create mode 100644 src/utils/iconRegistry.test.ts create mode 100644 src/utils/iconRegistry.ts create mode 100644 src/utils/imageLightboxTarget.test.ts create mode 100644 src/utils/imageLightboxTarget.ts create mode 100644 src/utils/initializeNoteProperties.test.ts create mode 100644 src/utils/initializeNoteProperties.ts create mode 100644 src/utils/inverseRelationshipLabels.ts create mode 100644 src/utils/legacyTypes.ts create mode 100644 src/utils/markdownFences.test.ts create mode 100644 src/utils/markdownFences.ts create mode 100644 src/utils/markdownHighlightMarkdown.test.ts create mode 100644 src/utils/markdownHighlightMarkdown.ts create mode 100644 src/utils/mathMarkdown.test.ts create mode 100644 src/utils/mathMarkdown.ts create mode 100644 src/utils/mediaPreviewRuntime.ts create mode 100644 src/utils/mermaidMarkdown.test.ts create mode 100644 src/utils/mermaidMarkdown.ts create mode 100644 src/utils/mouseMovement.test.ts create mode 100644 src/utils/mouseMovement.ts create mode 100644 src/utils/neighborhoodHistory.test.ts create mode 100644 src/utils/neighborhoodHistory.ts create mode 100644 src/utils/noteFormat.test.ts create mode 100644 src/utils/noteFormat.ts create mode 100644 src/utils/noteIcon.ts create mode 100644 src/utils/noteListHelpers.extra.test.ts create mode 100644 src/utils/noteListHelpers.test.ts create mode 100644 src/utils/noteListHelpers.ts create mode 100644 src/utils/noteListHelpers.view-workspaces.test.ts create mode 100644 src/utils/noteListSearchEvents.ts create mode 100644 src/utils/noteOpenPerformance.extra.test.ts create mode 100644 src/utils/noteOpenPerformance.test.ts create mode 100644 src/utils/noteOpenPerformance.ts create mode 100644 src/utils/notePathIdentity.test.ts create mode 100644 src/utils/notePathIdentity.ts create mode 100644 src/utils/notePdfExport.test.ts create mode 100644 src/utils/notePdfExport.ts create mode 100644 src/utils/noteSlug.test.ts create mode 100644 src/utils/noteSlug.ts create mode 100644 src/utils/noteTitle.test.ts create mode 100644 src/utils/noteTitle.ts create mode 100644 src/utils/noteWidth.test.ts create mode 100644 src/utils/noteWidth.ts create mode 100644 src/utils/openAiWorkspaceWindow.test.ts create mode 100644 src/utils/openAiWorkspaceWindow.ts create mode 100644 src/utils/openNoteWindow.test.ts create mode 100644 src/utils/openNoteWindow.ts create mode 100644 src/utils/organizationWorkflow.test.ts create mode 100644 src/utils/organizationWorkflow.ts create mode 100644 src/utils/plainTextPaste.test.ts create mode 100644 src/utils/plainTextPaste.ts create mode 100644 src/utils/platform.test.ts create mode 100644 src/utils/platform.ts create mode 100644 src/utils/pointerButtons.ts create mode 100644 src/utils/propertyLabels.test.ts create mode 100644 src/utils/propertyLabels.ts create mode 100644 src/utils/propertyTypes.test.ts create mode 100644 src/utils/propertyTypes.ts create mode 100644 src/utils/pulledVaultRefresh.test.ts create mode 100644 src/utils/pulledVaultRefresh.ts create mode 100644 src/utils/rawEditorLanguage.test.ts create mode 100644 src/utils/rawEditorLanguage.ts create mode 100644 src/utils/rawEditorUtils.test.ts create mode 100644 src/utils/rawEditorUtils.ts create mode 100644 src/utils/regexCapabilities.ts create mode 100644 src/utils/releaseDownloadPage.test.ts create mode 100644 src/utils/releaseDownloadPage.ts create mode 100644 src/utils/releaseHistoryPage.test.ts create mode 100644 src/utils/releaseHistoryPage.ts create mode 100644 src/utils/richEditorBlockTypes.ts create mode 100644 src/utils/richEditorMarkdown.ts create mode 100644 src/utils/safeRegex.ts create mode 100644 src/utils/sheetBrowserScheduling.ts create mode 100644 src/utils/sheetClipboard.test.ts create mode 100644 src/utils/sheetClipboard.ts create mode 100644 src/utils/sheetContextMenuState.test.ts create mode 100644 src/utils/sheetContextMenuState.ts create mode 100644 src/utils/sheetCsv.test.ts create mode 100644 src/utils/sheetCsv.ts create mode 100644 src/utils/sheetDirtyState.test.ts create mode 100644 src/utils/sheetDirtyState.ts create mode 100644 src/utils/sheetExternalFormulaWorker.ts create mode 100644 src/utils/sheetExternalReferences.test.ts create mode 100644 src/utils/sheetExternalReferences.ts create mode 100644 src/utils/sheetFormulaAutocomplete.test.ts create mode 100644 src/utils/sheetFormulaAutocomplete.ts create mode 100644 src/utils/sheetFrontmatterProperties.ts create mode 100644 src/utils/sheetInitialWorkbookReadiness.ts create mode 100644 src/utils/sheetIronCalcColor.test.ts create mode 100644 src/utils/sheetIronCalcColor.ts create mode 100644 src/utils/sheetMarkdownCell.test.ts create mode 100644 src/utils/sheetMarkdownCell.ts create mode 100644 src/utils/sheetMetadata.test.ts create mode 100644 src/utils/sheetMetadata.ts create mode 100644 src/utils/sheetPointerCoordinates.ts create mode 100644 src/utils/sheetPointerHitTest.ts create mode 100644 src/utils/sheetSelection.ts create mode 100644 src/utils/sheetSelectionChrome.ts create mode 100644 src/utils/sheetWikilinkModelBridge.ts create mode 100644 src/utils/sheetWikilinks.test.ts create mode 100644 src/utils/sheetWikilinks.ts create mode 100644 src/utils/sheetWorkbook.test.ts create mode 100644 src/utils/sheetWorkbook.ts create mode 100644 src/utils/sidebarSections.ts create mode 100644 src/utils/statusStyles.test.ts create mode 100644 src/utils/statusStyles.ts create mode 100644 src/utils/streamAiAgent.test.ts create mode 100644 src/utils/streamAiAgent.ts create mode 100644 src/utils/streamAiModel.test.ts create mode 100644 src/utils/streamAiModel.ts create mode 100644 src/utils/suggestionEnrichment.test.ts create mode 100644 src/utils/suggestionEnrichment.ts create mode 100644 src/utils/systemMetadata.ts create mode 100644 src/utils/tabEntrySync.test.ts create mode 100644 src/utils/tabEntrySync.ts create mode 100644 src/utils/tableOfContents.test.ts create mode 100644 src/utils/tableOfContents.ts create mode 100644 src/utils/tagStyles.test.ts create mode 100644 src/utils/tagStyles.ts create mode 100644 src/utils/tauriCsp.test.ts create mode 100644 src/utils/tauriDragDropConfig.test.ts create mode 100644 src/utils/tauriEventCleanup.ts create mode 100644 src/utils/tauriLinuxBundleConfig.test.ts create mode 100644 src/utils/tauriWindowControlPermissions.test.ts create mode 100644 src/utils/tldrawMarkdown.test.ts create mode 100644 src/utils/tldrawMarkdown.ts create mode 100644 src/utils/typeColors.test.ts create mode 100644 src/utils/typeColors.ts create mode 100644 src/utils/typeDefinitions.ts create mode 100644 src/utils/typeDeletion.test.ts create mode 100644 src/utils/typeDeletion.ts create mode 100644 src/utils/typeVisibility.ts create mode 100644 src/utils/url.test.ts create mode 100644 src/utils/url.ts create mode 100644 src/utils/vault-dialog.test.ts create mode 100644 src/utils/vault-dialog.ts create mode 100644 src/utils/vaultAttachments.test.ts create mode 100644 src/utils/vaultAttachments.ts create mode 100644 src/utils/vaultCollections.test.ts create mode 100644 src/utils/vaultCollections.ts create mode 100644 src/utils/vaultConfigStore.test.ts create mode 100644 src/utils/vaultConfigStore.ts create mode 100644 src/utils/vaultErrors.ts create mode 100644 src/utils/vaultImages.test.ts create mode 100644 src/utils/vaultImages.ts create mode 100644 src/utils/vaultListStore.ts create mode 100644 src/utils/vaultMetadataNormalization.test.ts create mode 100644 src/utils/vaultMetadataNormalization.ts create mode 100644 src/utils/vaultOrdering.test.ts create mode 100644 src/utils/vaultOrdering.ts create mode 100644 src/utils/vaultPathContainment.test.ts create mode 100644 src/utils/vaultPathContainment.ts create mode 100644 src/utils/vaultTypes.ts create mode 100644 src/utils/viewFilename.test.ts create mode 100644 src/utils/viewFilename.ts create mode 100644 src/utils/viewFilterArrayFields.ts create mode 100644 src/utils/viewFilterArrayProperties.ts create mode 100644 src/utils/viewFilters.arrayProperties.test.ts create mode 100644 src/utils/viewFilters.relativeDates.test.ts create mode 100644 src/utils/viewFilters.test.ts create mode 100644 src/utils/viewFilters.ts create mode 100644 src/utils/viewIdentity.ts create mode 100644 src/utils/viewOrdering.test.ts create mode 100644 src/utils/viewOrdering.ts create mode 100644 src/utils/viewTargetVault.test.ts create mode 100644 src/utils/viewTargetVault.ts create mode 100644 src/utils/whiteboardPlatformPermissionRejection.ts create mode 100644 src/utils/wikilink.test.ts create mode 100644 src/utils/wikilink.ts create mode 100644 src/utils/wikilinkColors.test.ts create mode 100644 src/utils/wikilinkColors.ts create mode 100644 src/utils/wikilinkSuggestions.test.ts create mode 100644 src/utils/wikilinkSuggestions.ts create mode 100644 src/utils/wikilinks.table.test.ts create mode 100644 src/utils/wikilinks.test.ts create mode 100644 src/utils/wikilinks.ts create mode 100644 src/utils/windowMode.test.ts create mode 100644 src/utils/windowMode.ts create mode 100644 src/utils/workspaces.test.ts create mode 100644 src/utils/workspaces.ts create mode 100644 tests/fixtures/test-vault/event/team-meeting.md create mode 100644 tests/fixtures/test-vault/note/archived-note.md create mode 100644 tests/fixtures/test-vault/note/note-b.md create mode 100644 tests/fixtures/test-vault/note/note-c.md create mode 100644 tests/fixtures/test-vault/note/trashed-note.md create mode 100644 tests/fixtures/test-vault/project/alpha-project.md create mode 100644 tests/fixtures/test-vault/quarter/spring-2026.md create mode 100644 tests/fixtures/test-vault/type/event.md create mode 100644 tests/fixtures/test-vault/type/note.md create mode 100644 tests/fixtures/test-vault/type/project.md create mode 100644 tests/helpers/fixtureVault.ts create mode 100644 tests/helpers/fixtureVaultDesktopBridge.ts create mode 100644 tests/integration/vault-workflows.spec.ts create mode 100644 tests/smoke/add-ds-store-files-to-gitignore.spec.ts create mode 100644 tests/smoke/add-remote-from-no-remote.spec.ts create mode 100644 tests/smoke/ai-chat-history.spec.ts create mode 100644 tests/smoke/ai-chat-wikilink-clickable.spec.ts create mode 100644 tests/smoke/ai-notes-visibility-fix.spec.ts create mode 100644 tests/smoke/ai-panel-shortcut.spec.ts create mode 100644 tests/smoke/autogit-checkpoints.spec.ts create mode 100644 tests/smoke/autosave-low-end-typing.spec.ts create mode 100644 tests/smoke/better-sync-error-ux.spec.ts create mode 100644 tests/smoke/blocknote-serializer-blank-lines.spec.ts create mode 100644 tests/smoke/blocknote-slash-menu-mouse.spec.ts create mode 100644 tests/smoke/blocknote-slash-menu-styling.spec.ts create mode 100644 tests/smoke/breadcrumb-filename-rename.spec.ts create mode 100644 tests/smoke/breadcrumb-icon-size.spec.ts create mode 100644 tests/smoke/cache-invalidation-vault-open.spec.ts create mode 100644 tests/smoke/changing-type-data-corruption.spec.ts create mode 100644 tests/smoke/checklist-pull-refresh-regression.spec.ts create mode 100644 tests/smoke/collision-create-flows.spec.ts create mode 100644 tests/smoke/command-palette-ai-mode-regression.spec.ts create mode 100644 tests/smoke/command-palette-hides-legacy-journal.spec.ts create mode 100644 tests/smoke/command-palette-new-note-dedupe.spec.ts create mode 100644 tests/smoke/contribute-modal.spec.ts create mode 100644 tests/smoke/create-empty-vault-flows.spec.ts create mode 100644 tests/smoke/create-note-backing-file.spec.ts create mode 100644 tests/smoke/create-note-crash.spec.ts create mode 100644 tests/smoke/create-note-default-properties.spec.ts create mode 100644 tests/smoke/create-note-in-folder.spec.ts create mode 100644 tests/smoke/create-open-relationship-note.spec.ts create mode 100644 tests/smoke/deep-link-routing.spec.ts create mode 100644 tests/smoke/delete-note-nonblocking.spec.ts create mode 100644 tests/smoke/editor-block-handle-zoom.spec.ts create mode 100644 tests/smoke/editor-block-reorder.spec.ts create mode 100644 tests/smoke/editor-code-block-theme.spec.ts create mode 100644 tests/smoke/editor-find-replace.spec.ts create mode 100644 tests/smoke/editor-formatting-persistence.spec.ts create mode 100644 tests/smoke/editor-large-note-performance.spec.ts create mode 100644 tests/smoke/editor-mode-modified-ordering.spec.ts create mode 100644 tests/smoke/editor-mode-position-preservation.spec.ts create mode 100644 tests/smoke/editor-save-tooltip-stability.spec.ts create mode 100644 tests/smoke/exact-match-search.spec.ts create mode 100644 tests/smoke/example.spec.ts create mode 100644 tests/smoke/fenced-code-copy.spec.ts create mode 100644 tests/smoke/filter-wikilink-autocomplete.spec.ts create mode 100644 tests/smoke/fix-ai-chat-empty-body-v3.spec.ts create mode 100644 tests/smoke/fix-archived-trashed-detection-v2.spec.ts create mode 100644 tests/smoke/fix-codemirror-zoom-cursor-v2.spec.ts create mode 100644 tests/smoke/fix-crash-create-note.spec.ts create mode 100644 tests/smoke/flip-canonical-type-field.spec.ts create mode 100644 tests/smoke/focus-no-freeze.spec.ts create mode 100644 tests/smoke/fresh-install-regression-qa.spec.ts create mode 100644 tests/smoke/fresh-start-telemetry-onboarding.spec.ts create mode 100644 tests/smoke/frontmatter-date-picker.spec.ts create mode 100644 tests/smoke/frontmatter-parsing-fix.spec.ts create mode 100644 tests/smoke/generate-commit-message.spec.ts create mode 100644 tests/smoke/getting-started-template.spec.ts create mode 100644 tests/smoke/git-divergence-conflicts.spec.ts create mode 100644 tests/smoke/git-no-remote-commit-only.spec.ts create mode 100644 tests/smoke/gitignored-visibility.spec.ts create mode 100644 tests/smoke/h1-auto-rename-setting.spec.ts create mode 100644 tests/smoke/h1-title-decoupled.spec.ts create mode 100644 tests/smoke/h1-untitled-auto-rename.spec.ts create mode 100644 tests/smoke/helpers.ts create mode 100644 tests/smoke/image-toolbar-hover.spec.ts create mode 100644 tests/smoke/improve-note-open-latency.spec.ts create mode 100644 tests/smoke/inline-wikilink-editor-regression.spec.ts create mode 100644 tests/smoke/inlineWikilinkEditorHelpers.ts create mode 100644 tests/smoke/keyboard-command-routing.spec.ts create mode 100644 tests/smoke/korean-list-ime-composition.spec.ts create mode 100644 tests/smoke/latex-math-notes.spec.ts create mode 100644 tests/smoke/linkify-init-warnings.spec.ts create mode 100644 tests/smoke/list-marker-paste-regression.spec.ts create mode 100644 tests/smoke/literal-asterisk-paste-regression.spec.ts create mode 100644 tests/smoke/long-note-editor-scaling.spec.ts create mode 100644 tests/smoke/manual-commit-modal-autogit-off.spec.ts create mode 100644 tests/smoke/mcp-config-copy.spec.ts create mode 100644 tests/smoke/mermaid-diagram-notes.spec.ts create mode 100644 tests/smoke/missing-active-vault-recovery.spec.ts create mode 100644 tests/smoke/missing-string-metadata-open-note.spec.ts create mode 100644 tests/smoke/missing-type-warning.spec.ts create mode 100644 tests/smoke/mixed-rich-text-keypress.spec.ts create mode 100644 tests/smoke/mounted-workspaces-unified-graph.spec.ts create mode 100644 tests/smoke/move-vault-cache.spec.ts create mode 100644 tests/smoke/multi-selection-shortcuts.spec.ts create mode 100644 tests/smoke/multibyte-search-snippet.spec.ts create mode 100644 tests/smoke/multimedia-embed-url-input.spec.ts create mode 100644 tests/smoke/new-note-heading-paste-regression.spec.ts create mode 100644 tests/smoke/new-note-immediate-editability.spec.ts create mode 100644 tests/smoke/new-note-title-caret-click.spec.ts create mode 100644 tests/smoke/non-git-vault-init.spec.ts create mode 100644 tests/smoke/non-markdown-wikilink-duplicates.spec.ts create mode 100644 tests/smoke/note-history-edit-loop.spec.ts create mode 100644 tests/smoke/note-list-search-keyboard-toggle.spec.ts create mode 100644 tests/smoke/note-list-search-visible-content.spec.ts create mode 100644 tests/smoke/note-status-indicator-stability.spec.ts create mode 100644 tests/smoke/note-width-modes.spec.ts create mode 100644 tests/smoke/number-property.spec.ts create mode 100644 tests/smoke/offline-onboarding-status.spec.ts create mode 100644 tests/smoke/onboarding-vault-registration.spec.ts create mode 100644 tests/smoke/persist-editor-mode.spec.ts create mode 100644 tests/smoke/plain-filename-autolink.spec.ts create mode 100644 tests/smoke/pull-refresh-open-note.spec.ts create mode 100644 tests/smoke/quick-open-create-note.spec.ts create mode 100644 tests/smoke/raw-editor-frontmatter-propagation.spec.ts create mode 100644 tests/smoke/raw-editor-sync-to-blocknote.spec.ts create mode 100644 tests/smoke/raw-editor-type-propagation.spec.ts create mode 100644 tests/smoke/raw-mode-pasted-text-roundtrip.spec.ts create mode 100644 tests/smoke/raw-yml-view-save.spec.ts create mode 100644 tests/smoke/release-channel-settings.spec.ts create mode 100644 tests/smoke/remove-relationship-keys.spec.ts create mode 100644 tests/smoke/repair-vault-flat-structure.spec.ts create mode 100644 tests/smoke/repair-vault-no-legacy-dirs.spec.ts create mode 100644 tests/smoke/rich-editor-block-selection.spec.ts create mode 100644 tests/smoke/rich-editor-reload-typing.spec.ts create mode 100644 tests/smoke/rtl-editor-direction.spec.ts create mode 100644 tests/smoke/save-before-note-switch.spec.ts create mode 100644 tests/smoke/show-deleted-notes-in-changes-view.spec.ts create mode 100644 tests/smoke/sidebar-duplicate-sections.spec.ts create mode 100644 tests/smoke/sidebar-type-picker-sync.spec.ts create mode 100644 tests/smoke/stale-getting-started-switcher.spec.ts create mode 100644 tests/smoke/startup-empty-vault-recovery.spec.ts create mode 100644 tests/smoke/table-cell-paste-regression.spec.ts create mode 100644 tests/smoke/table-focus-regression.spec.ts create mode 100644 tests/smoke/table-hover-crash.spec.ts create mode 100644 tests/smoke/table-resize-crash.spec.ts create mode 100644 tests/smoke/telemetry-consent.spec.ts create mode 100644 tests/smoke/testBridge.ts create mode 100644 tests/smoke/tldraw-whiteboard-notes.spec.ts create mode 100644 tests/smoke/type-create-note.spec.ts create mode 100644 tests/smoke/type-derived-properties.spec.ts create mode 100644 tests/smoke/type-rename-metadata.spec.ts create mode 100644 tests/smoke/vault-ai-guidance-restore.spec.ts create mode 100644 tests/smoke/vault-loading-skeleton.spec.ts create mode 100644 tests/smoke/vault-switch-clears-stale-editor.spec.ts create mode 100644 tests/smoke/vault-switcher-bottom-bar.spec.ts create mode 100644 tests/smoke/view-item-hover-count.spec.ts create mode 100644 tests/smoke/visible-type-property.spec.ts create mode 100644 tests/smoke/wikilink-cmd-click-history.spec.ts create mode 100644 tests/smoke/wikilink-path-fix.spec.ts create mode 100644 tests/smoke/wikilink-traditional-chinese.spec.ts create mode 100644 tests/smoke/windows-save-path-failure.spec.ts create mode 100644 trademarks.md create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 ui-design.pen create mode 100644 vite.config.ts diff --git a/.chunk/README.md b/.chunk/README.md new file mode 100644 index 0000000..e8a84d2 --- /dev/null +++ b/.chunk/README.md @@ -0,0 +1,63 @@ +# Chunk Sidecar Validation + +This repo uses Chunk as an experimental inner-loop validation runner for the same gates enforced by the local git hooks. + +## Setup + +Install the CLI: + +```bash +brew install CircleCI-Public/circleci/chunk +``` + +If Homebrew cannot install the formula, use the matching archive from the Chunk CLI GitHub releases and place the `chunk` binary on `PATH`. + +Authenticate before remote sidecar runs: + +```bash +chunk auth set circleci +``` + +`chunk validate` can run locally without CircleCI auth. Remote sidecars require `CIRCLECI_TOKEN` or stored CircleCI auth. + +## Commands + +```bash +chunk validate --list +chunk validate lint +chunk validate typecheck +chunk validate frontend-coverage +chunk validate --remote lint +bash .chunk/run-sidecar-gates-local.sh true +``` + +Use `chunk sidecar setup --name tolaria-hooks` for the first remote environment setup. Once the environment passes, create a snapshot and launch future sidecars from that snapshot to avoid reinstalling Node, Rust, Tauri Linux dependencies, Playwright Chromium, and `cargo-llvm-cov` every time. + +Keep native macOS Tauri QA outside Chunk. The sidecar is Linux-based and is meant to catch portable lint, build, unit coverage, Rust, and Playwright smoke failures before the full local pre-push hook runs. Browser provisioning uses `.chunk/install-playwright-browsers.mjs` because Playwright's built-in installer can outlive the current Chunk SSH session while extracting large archives. + +## Fast Pre-Push Path + +The local pre-push hook prefers the sidecar fast path when Chunk is available. It targets three independent sidecars by name and always passes `--sidecar-id` after resolution, avoiding races with Chunk's global active-sidecar state: + +- `tolaria-hooks-frontend-2`: lint and build first, then frontend coverage with `FRONTEND_COVERAGE_SHARDS=2` and `VITEST_COVERAGE_MAX_WORKERS=2`. +- `tolaria-hooks-rust`: clippy, rustfmt, and `cargo llvm-cov` with `CARGO_BUILD_JOBS=2`. +- `tolaria-hooks-playwright`: curated Playwright smoke with one shared Vite server, eight shards, and four concurrent shard workers. + +Set `LAPUTA_PREPUSH_LOCAL=1` to force the old local path. Use `SIDECAR_FRONTEND_ID`, `SIDECAR_RUST_ID`, or `SIDECAR_PLAYWRIGHT_ID` to pin a lane to a specific sidecar when duplicate names exist in CircleCI. Use `SIDECAR_SNAPSHOT_ID=` when provisioning missing lane sidecars from a prepared snapshot. + +The lane launcher uses `chunk sidecar exec` only to start detached remote lane processes with `setsid -f`, then polls status files. Long foreground `exec` commands hit CircleCI's sidecar API deadline; plain background jobs are still tracked by `exec` and can time out. + +```bash +bash .chunk/run-sidecar-gates-local.sh true +``` + +Latest measured passing run with two frontend coverage shards: lane runtime 318s including sync, frontend completed in 313s, and Playwright smoke completed in 104s. The previous single-coverage frontend path took 441s with 457s external wall time, so the two-shard experiment reduced the sidecar long pole by about 29%. A previous Playwright run exposed `tests/smoke/h1-title-decoupled.spec.ts` as flaky on sidecar, so it remains in regression coverage but is no longer part of the curated smoke lane. + +To compare the frontend coverage experiment against the old single coverage run, use: + +```bash +FRONTEND_COVERAGE_SHARDS=1 bash .chunk/run-sidecar-gates-local.sh false +FRONTEND_COVERAGE_SHARDS=2 bash .chunk/run-sidecar-gates-local.sh false +``` + +The default sidecar fast path uses two frontend coverage shards. Each shard disables per-shard coverage thresholds, then the merged V8/Istanbul coverage map is checked once against the same 70% line/function/branch/statement thresholds. diff --git a/.chunk/config.json b/.chunk/config.json new file mode 100644 index 0000000..b381f0b --- /dev/null +++ b/.chunk/config.json @@ -0,0 +1,157 @@ +{ + "commands": [ + { + "name": "lint", + "run": "pnpm lint", + "role": "gate", + "fileExt": ".ts,.tsx,.js,.jsx,.mjs,.json", + "timeout": 120, + "limit": 3 + }, + { + "name": "typecheck", + "run": "npx tsc --noEmit", + "role": "gate", + "fileExt": ".ts,.tsx", + "timeout": 180, + "limit": 3 + }, + { + "name": "build", + "run": "pnpm build", + "role": "gate", + "fileExt": ".ts,.tsx,.css,.html,.json", + "timeout": 300, + "limit": 2 + }, + { + "name": "frontend-coverage", + "run": "pnpm test:coverage --silent", + "role": "gate", + "fileExt": ".ts,.tsx", + "timeout": 600, + "limit": 2 + }, + { + "name": "rust-lint", + "run": "cargo clippy --manifest-path=src-tauri/Cargo.toml -- -D warnings && cargo fmt --manifest-path=src-tauri/Cargo.toml -- --check", + "role": "gate", + "fileExt": ".rs", + "timeout": 300, + "limit": 2 + }, + { + "name": "rust-coverage", + "run": "cargo llvm-cov --manifest-path src-tauri/Cargo.toml --no-clean --ignore-filename-regex \"lib\\.rs|main\\.rs|menu\\.rs\" --fail-under-lines 85 -- --test-threads=1", + "role": "gate", + "fileExt": ".rs", + "timeout": 900, + "limit": 1 + }, + { + "name": "smoke-1", + "run": "bash .chunk/run-playwright-smoke.sh 1/8", + "role": "gate", + "fileExt": ".ts,.tsx", + "timeout": 240, + "limit": 1 + }, + { + "name": "smoke-2", + "run": "bash .chunk/run-playwright-smoke.sh 2/8", + "role": "gate", + "fileExt": ".ts,.tsx", + "timeout": 240, + "limit": 1 + }, + { + "name": "smoke-3", + "run": "bash .chunk/run-playwright-smoke.sh 3/8", + "role": "gate", + "fileExt": ".ts,.tsx", + "timeout": 240, + "limit": 1 + }, + { + "name": "smoke-4", + "run": "bash .chunk/run-playwright-smoke.sh 4/8", + "role": "gate", + "fileExt": ".ts,.tsx", + "timeout": 240, + "limit": 1 + }, + { + "name": "smoke-5", + "run": "bash .chunk/run-playwright-smoke.sh 5/8", + "role": "gate", + "fileExt": ".ts,.tsx", + "timeout": 240, + "limit": 1 + }, + { + "name": "smoke-6", + "run": "bash .chunk/run-playwright-smoke.sh 6/8", + "role": "gate", + "fileExt": ".ts,.tsx", + "timeout": 240, + "limit": 1 + }, + { + "name": "smoke-7", + "run": "bash .chunk/run-playwright-smoke.sh 7/8", + "role": "gate", + "fileExt": ".ts,.tsx", + "timeout": 240, + "limit": 1 + }, + { + "name": "smoke-8", + "run": "bash .chunk/run-playwright-smoke.sh 8/8", + "role": "gate", + "fileExt": ".ts,.tsx", + "timeout": 240, + "limit": 1 + } + ], + "stopHookMaxAttempts": 2, + "vcs": { + "org": "refactoringhq", + "repo": "tolaria" + }, + "orgID": "39f93336-5295-4c6c-845f-e692c1d3f968", + "environment": { + "stack": "javascript-rust-tauri", + "setup": [ + { + "name": "system", + "command": "sudo apt-get update && sudo apt-get install -y --no-install-recommends build-essential curl file git libwebkit2gtk-4.1-dev libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev libsoup-3.0-dev patchelf pkg-config unzip wget xvfb && sudo rm -rf /var/lib/apt/lists/*" + }, + { + "name": "rust", + "command": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal && . \"$HOME/.cargo/env\" && rustup default stable && rustup component add clippy rustfmt && cargo install cargo-llvm-cov --locked" + }, + { + "name": "node", + "command": "curl -fsSL https://nodejs.org/dist/v26.3.0/node-v26.3.0-linux-x64.tar.xz | sudo tar -xJ -C /usr/local --strip-components=1 && node --version && npm --version" + }, + { + "name": "pnpm", + "command": "sudo npm install -g pnpm@10.33.0 && pnpm --version" + }, + { + "name": "install", + "command": "mkdir -p \"$HOME/Documents\" && pnpm install --frozen-lockfile && . \"$HOME/.cargo/env\" && cargo fetch --manifest-path src-tauri/Cargo.toml" + }, + { + "name": "playwright-deps", + "command": "pnpm exec playwright install-deps chromium" + }, + { + "name": "playwright", + "command": "node .chunk/install-playwright-browsers.mjs" + } + ], + "image": "cimg/node", + "image_version": "26.3.0" + } +} diff --git a/.chunk/install-playwright-browsers.mjs b/.chunk/install-playwright-browsers.mjs new file mode 100644 index 0000000..804a032 --- /dev/null +++ b/.chunk/install-playwright-browsers.mjs @@ -0,0 +1,100 @@ +#!/usr/bin/env node +/* global console */ +import { execFileSync, spawnSync } from 'node:child_process' +import { + existsSync, + mkdirSync, + readdirSync, + renameSync, + rmSync, + writeFileSync, +} from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +function run(command, args) { + const result = spawnSync(command, args, { stdio: 'inherit' }) + if (result.error) throw result.error + if (result.status !== 0) { + throw new Error(`${command} exited with status ${result.status}`) + } +} + +function parsePlan(planText) { + const entries = [] + const seen = new Set() + + for (const section of planText.split(/\n(?=\S)/)) { + const label = section.split('\n')[0]?.trim() ?? '' + const location = readField(section, 'Install location') + const url = readField(section, 'Download url') + rememberEntry(entries, seen, { label, location, url }) + } + + return entries +} + +function readField(text, name) { + const match = text.match(new RegExp(`^\\s*${name}:\\s*(.+)$`, 'm')) + return match?.[1].trim() ?? '' +} + +function rememberEntry(entries, seen, entry) { + const key = `${entry.location}\n${entry.url}` + if (!entry.location || !entry.url || seen.has(key)) return + seen.add(key) + entries.push(entry) +} + +function installBrowser(entry) { + const markerPath = join(entry.location, 'INSTALLATION_COMPLETE') + if (existsSync(markerPath)) { + console.log(`Already installed: ${entry.label}`) + return + } + + const workDir = join(tmpdir(), `playwright-${Date.now()}`) + const archivePath = join(workDir, 'browser.zip') + const extractDir = join(workDir, 'extract') + + rmSync(entry.location, { force: true, recursive: true }) + mkdirSync(extractDir, { recursive: true }) + + console.log(`Downloading ${entry.label}`) + run('curl', [ + '-fL', + '--retry', + '3', + '--connect-timeout', + '20', + '-o', + archivePath, + entry.url, + ]) + + console.log(`Extracting ${entry.label}`) + run('unzip', ['-q', archivePath, '-d', extractDir]) + + mkdirSync(entry.location, { recursive: true }) + for (const child of readdirSync(extractDir)) { + renameSync(join(extractDir, child), join(entry.location, child)) + } + + writeFileSync(markerPath, '') + rmSync(workDir, { force: true, recursive: true }) +} + +const planText = execFileSync( + 'npx', + ['playwright', 'install', '--dry-run', 'chromium'], + { encoding: 'utf8' }, +) +const entries = parsePlan(planText) + +if (entries.length === 0) { + throw new Error('Playwright did not report any browser archives to install') +} + +for (const entry of entries) { + installBrowser(entry) +} diff --git a/.chunk/run-playwright-shards.sh b/.chunk/run-playwright-shards.sh new file mode 100644 index 0000000..4a9dcbf --- /dev/null +++ b/.chunk/run-playwright-shards.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +set -euo pipefail + +total_shards="${1:-${PLAYWRIGHT_SHARDS:-8}}" +concurrency="${PLAYWRIGHT_CONCURRENCY:-$total_shards}" +log_dir="${TMPDIR:-/tmp}/tolaria-playwright-shards-$$" +batch_pids=() +shared_server="${PLAYWRIGHT_SHARED_SERVER:-1}" +server_port="${PLAYWRIGHT_SMOKE_PORT:-41741}" +base_url="${BASE_URL:-http://127.0.0.1:${server_port}}" +server_port="$(node -e 'console.log(new URL(process.argv[1]).port || "41741")' "$base_url")" +server_pid="" + +if [[ ! "$total_shards" =~ ^[1-9][0-9]*$ ]]; then + printf 'Usage: %s [total-shards]\n' "$0" >&2 + exit 2 +fi + +if [[ ! "$concurrency" =~ ^[1-9][0-9]*$ ]]; then + printf 'PLAYWRIGHT_CONCURRENCY must be a positive integer\n' >&2 + exit 2 +fi + +mkdir -p "$log_dir" + +stop_shared_server() { + if [[ -z "$server_pid" ]]; then + return + fi + + kill -TERM "$server_pid" 2>/dev/null || true + sleep 2 + kill -KILL "$server_pid" 2>/dev/null || true + server_pid="" +} + +cleanup() { + local status=$? + trap - EXIT INT TERM + + for pid in "${batch_pids[@]:-}"; do + kill -TERM "$pid" 2>/dev/null || true + done + + sleep 2 + + for pid in "${batch_pids[@]:-}"; do + kill -KILL "$pid" 2>/dev/null || true + done + + stop_shared_server + + exit "$status" +} + +trap cleanup EXIT INT TERM + +wait_for_shared_server() { + local deadline=$((SECONDS + 30)) + + until curl -fsS "$base_url" >/dev/null 2>&1; do + if [[ -n "$server_pid" ]] && ! kill -0 "$server_pid" 2>/dev/null; then + printf '[chunk-playwright] shared server exited before becoming ready\n' >&2 + sed 's/^/[shared-server] /' "${log_dir}/shared-server.log" >&2 || true + return 1 + fi + + if [[ "$SECONDS" -ge "$deadline" ]]; then + printf '[chunk-playwright] shared server did not become ready at %s\n' "$base_url" >&2 + sed 's/^/[shared-server] /' "${log_dir}/shared-server.log" >&2 || true + return 1 + fi + + sleep 1 + done +} + +start_shared_server() { + if [[ "$shared_server" != "1" ]]; then + return + fi + + printf '[chunk-playwright] starting shared server at %s\n' "$base_url" + TOLARIA_VITE_CACHE_DIR="${TOLARIA_VITE_CACHE_DIR:-${TMPDIR:-/tmp}/tolaria-vite-smoke-shared}" \ + node scripts/playwright-smoke-server.mjs "$server_port" >"${log_dir}/shared-server.log" 2>&1 & + server_pid="$!" + wait_for_shared_server +} + +run_batch() { + local first_shard="$1" + local failures=0 + local names=() + local shard="$first_shard" + + batch_pids=() + + while [[ "$shard" -le "$total_shards" && "${#batch_pids[@]}" -lt "$concurrency" ]]; do + local name="smoke-${shard}-${total_shards}" + local log_file="${log_dir}/${name}.log" + + printf '[chunk-playwright] starting %s\n' "$name" + if [[ "$shared_server" == "1" ]]; then + BASE_URL="$base_url" PLAYWRIGHT_REUSE_SERVER=1 PLAYWRIGHT_SMOKE_PORT="$server_port" \ + bash .chunk/run-playwright-smoke.sh "${shard}/${total_shards}" >"$log_file" 2>&1 & + else + bash .chunk/run-playwright-smoke.sh "${shard}/${total_shards}" >"$log_file" 2>&1 & + fi + batch_pids+=("$!") + names+=("$name") + shard=$((shard + 1)) + done + + for index in "${!batch_pids[@]}"; do + local pid="${batch_pids[$index]}" + local name="${names[$index]}" + local log_file="${log_dir}/${name}.log" + + if wait "$pid"; then + printf '[chunk-playwright] %s passed\n' "$name" + else + failures=1 + printf '[chunk-playwright] %s failed\n' "$name" + fi + + sed "s/^/[${name}] /" "$log_file" + done + + batch_pids=() + return "$failures" +} + +next_shard=1 +failed=0 + +start_shared_server + +while [[ "$next_shard" -le "$total_shards" ]]; do + if ! run_batch "$next_shard"; then + failed=1 + fi + next_shard=$((next_shard + concurrency)) +done + +stop_shared_server +exit "$failed" diff --git a/.chunk/run-playwright-smoke.sh b/.chunk/run-playwright-smoke.sh new file mode 100644 index 0000000..f4d35dd --- /dev/null +++ b/.chunk/run-playwright-smoke.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +set -euo pipefail + +shard="${1:-}" +shard_label="all" +base_url="${BASE_URL:-}" +port="${PLAYWRIGHT_SMOKE_PORT:-41741}" + +if [[ -n "$shard" ]]; then + if [[ ! "$shard" =~ ^[1-9][0-9]*/[1-9][0-9]*$ ]]; then + printf 'Usage: %s [shard/total]\n' "$0" >&2 + exit 2 + fi + + shard_index="${shard%%/*}" + shard_label="${shard//\//-}" + if [[ -z "$base_url" ]]; then + port=$((41740 + shard_index)) + fi +fi + +base_url="${base_url:-http://127.0.0.1:${port}}" +port="$(node -e 'console.log(new URL(process.argv[1]).port || "41741")' "$base_url")" + +log_file="${TMPDIR:-/tmp}/tolaria-playwright-smoke-${shard_label}.log" +playwright_pid="" + +cleanup() { + local status=$? + + if [[ -n "$playwright_pid" ]]; then + kill -TERM "$playwright_pid" 2>/dev/null || true + sleep 2 + kill -KILL "$playwright_pid" 2>/dev/null || true + fi + + if [[ "${PLAYWRIGHT_REUSE_SERVER:-}" != "1" ]]; then + pkill -TERM -f "playwright-smoke-server.mjs ${port}" 2>/dev/null || true + pkill -TERM -f "vite.js --host 127.0.0.1 --port ${port}" 2>/dev/null || true + fi + + exit "$status" +} + +trap cleanup INT TERM + +mapfile -t smoke_files < <(node <<'NODE' +const fs = require('node:fs') + +const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')) +const script = packageJson.scripts && packageJson.scripts['playwright:smoke'] +const prefix = 'playwright test --config playwright.smoke.config.ts ' + +if (!script || !script.startsWith(prefix)) { + throw new Error('Unexpected playwright:smoke script format') +} + +for (const file of script.slice(prefix.length).trim().split(/\s+/)) { + if (file) { + console.log(file) + } +} +NODE +) + +: > "$log_file" +playwright_args=( + test + --config playwright.smoke.config.ts + "${smoke_files[@]}" + --reporter=line +) + +if [[ -n "$shard" ]]; then + playwright_args+=(--shard "$shard") +fi + +printf '[chunk-playwright] running %s curated smoke files on port %s' "${#smoke_files[@]}" "$port" +if [[ -n "$shard" ]]; then + printf ' with shard %s' "$shard" +fi +printf '\n' + +set +e +env CI=true BASE_URL="$base_url" PLAYWRIGHT_REUSE_SERVER="${PLAYWRIGHT_REUSE_SERVER:-}" pnpm exec playwright "${playwright_args[@]}" \ + > >(tee "$log_file") 2>&1 & +playwright_pid=$! +wait "$playwright_pid" +playwright_status=$? +playwright_pid="" +set -e + +printf '[chunk-playwright] smoke %s exited with status %s\n' "$shard_label" "$playwright_status" +exit "$playwright_status" diff --git a/.chunk/run-rust-gate.sh b/.chunk/run-rust-gate.sh new file mode 100644 index 0000000..619e1c2 --- /dev/null +++ b/.chunk/run-rust-gate.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +start_time=$(date +%s) + +log_rust() { + printf '[sidecar-rust +%ss] %s\n' "$(($(date +%s) - start_time))" "$*" +} + +mkdir -p "$HOME/Documents" +# shellcheck disable=SC1091 +. "$HOME/.cargo/env" +export CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-2}" + +log_rust 'clippy started' +cargo clippy --manifest-path=src-tauri/Cargo.toml -- -D warnings +log_rust 'clippy passed' + +log_rust 'rustfmt started' +cargo fmt --manifest-path=src-tauri/Cargo.toml -- --check +log_rust 'rustfmt passed' + +log_rust 'coverage started' +cargo llvm-cov \ + --manifest-path src-tauri/Cargo.toml \ + --no-clean \ + --ignore-filename-regex "lib\\.rs|main\\.rs|menu\\.rs" \ + --fail-under-lines 85 \ + -- --test-threads=1 +log_rust "completed in $(($(date +%s) - start_time))s" diff --git a/.chunk/run-sidecar-gates-local.sh b/.chunk/run-sidecar-gates-local.sh new file mode 100755 index 0000000..13d0008 --- /dev/null +++ b/.chunk/run-sidecar-gates-local.sh @@ -0,0 +1,395 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly unavailable_status=86 + +rust_changed="${1:-true}" +playwright_shards="${PLAYWRIGHT_SHARDS:-8}" +playwright_concurrency="${PLAYWRIGHT_CONCURRENCY:-4}" +playwright_shared_server="${PLAYWRIGHT_SHARED_SERVER:-1}" +vitest_coverage_max_workers="${VITEST_COVERAGE_MAX_WORKERS:-${SIDECAR_VITEST_MAX_WORKERS:-2}}" +frontend_coverage_shards="${FRONTEND_COVERAGE_SHARDS:-${SIDECAR_FRONTEND_COVERAGE_SHARDS:-2}}" +cargo_build_jobs="${CARGO_BUILD_JOBS:-2}" +timeout_seconds="${SIDECAR_GATE_TIMEOUT:-1800}" +poll_interval="${SIDECAR_GATE_POLL_INTERVAL:-20}" +launch_timeout="${SIDECAR_GATE_LAUNCH_TIMEOUT:-45}" +launch_attempts="${SIDECAR_GATE_LAUNCH_ATTEMPTS:-3}" +remote_workdir="${SIDECAR_REMOTE_WORKDIR:-/home/user/tolaria}" +sidecar_prefix="${SIDECAR_NAME_PREFIX:-tolaria-hooks}" +frontend_name="${SIDECAR_FRONTEND_NAME:-${sidecar_prefix}-frontend-2}" +rust_name="${SIDECAR_RUST_NAME:-${sidecar_prefix}-rust}" +playwright_name="${SIDECAR_PLAYWRIGHT_NAME:-${sidecar_prefix}-playwright}" +state_dir="$(mktemp -d "${TMPDIR:-/tmp}/tolaria-sidecar-lanes.XXXXXX")" +lanes_file="${state_dir}/lanes.tsv" + +: > "$lanes_file" + +cleanup_state() { + rm -rf "$state_dir" +} + +trap cleanup_state EXIT + +resolve_chunk_bin() { + if [[ -n "${CHUNK_BIN:-}" && -x "$CHUNK_BIN" ]]; then + return 0 + fi + + if command -v chunk >/dev/null 2>&1; then + CHUNK_BIN="$(command -v chunk)" + return 0 + fi + + if [[ -x "$HOME/.local/bin/chunk" ]]; then + CHUNK_BIN="$HOME/.local/bin/chunk" + return 0 + fi + + return 1 +} + +load_org_id() { + if [[ -n "${CIRCLECI_ORG_ID:-}" ]]; then + printf '%s\n' "$CIRCLECI_ORG_ID" + return + fi + + node -e "const fs=require('node:fs'); const c=JSON.parse(fs.readFileSync('.chunk/config.json','utf8')); if (c.orgID) process.stdout.write(c.orgID)" +} + +sidecar_id_by_name() { + local name="$1" + + "$CHUNK_BIN" sidecar list 2>/dev/null | awk -v name="$name" '$1 == name { print $2; found=1; exit } END { exit found ? 0 : 1 }' +} + +create_sidecar_from_snapshot() { + local name="$1" + + if [[ -z "${SIDECAR_SNAPSHOT_ID:-}" ]]; then + return 1 + fi + + echo " -> Creating ${name} from snapshot ${SIDECAR_SNAPSHOT_ID}" >&2 + "$CHUNK_BIN" sidecar create --name "$name" --org-id "$org_id" --image "$SIDECAR_SNAPSHOT_ID" >/dev/null +} + +ensure_sidecar() { + local name="$1" + local explicit_id="${2:-}" + local id + + if [[ -n "$explicit_id" ]]; then + printf '%s\n' "$explicit_id" + return 0 + fi + + if id="$(sidecar_id_by_name "$name")"; then + printf '%s\n' "$id" + return 0 + fi + + echo " -> Sidecar ${name} not found; provisioning it..." >&2 + if ! create_sidecar_from_snapshot "$name"; then + "$CHUNK_BIN" sidecar setup --name "$name" --org-id "$org_id" >/dev/null + fi + + sidecar_id_by_name "$name" +} + +sync_lane() { + local lane="$1" + local sidecar_id="$2" + local log_file="${state_dir}/sync-${lane}.log" + + { + echo "[sidecar-sync] ${lane}: syncing to ${sidecar_id}" + if "$CHUNK_BIN" sidecar sync --sidecar-id "$sidecar_id" --workdir "$remote_workdir"; then + exit 0 + fi + + echo "[sidecar-sync] ${lane}: sync failed, rerunning setup once" + "$CHUNK_BIN" sidecar setup --sidecar-id "$sidecar_id" --dir . + "$CHUNK_BIN" sidecar sync --sidecar-id "$sidecar_id" --workdir "$remote_workdir" + } >"$log_file" 2>&1 +} + +sync_all_lanes() { + local failures=0 + local frontend_pid rust_pid playwright_pid + + sync_lane frontend "$frontend_id" & + frontend_pid=$! + sync_lane rust "$rust_id" & + rust_pid=$! + sync_lane playwright "$playwright_id" & + playwright_pid=$! + + for lane_pid in "$frontend_pid" "$rust_pid" "$playwright_pid"; do + if ! wait "$lane_pid"; then + failures=1 + fi + done + + if [[ "$failures" != "0" ]]; then + echo "Chunk sidecar sync failed" + sed 's/^/ /' "${state_dir}"/sync-*.log 2>/dev/null || true + return 1 + fi +} + +record_lane() { + local lane="$1" + local sidecar_id="$2" + local status_file="$3" + local log_file="$4" + local pid_file="$5" + + printf '%s\t%s\t%s\t%s\t%s\n' "$lane" "$sidecar_id" "$status_file" "$log_file" "$pid_file" >> "$lanes_file" +} + +launch_lane() { + local lane="$1" + local sidecar_id="$2" + local run_id="tolaria-${lane}-$(date +%s)-$$" + local remote_status="/tmp/${run_id}.status" + local remote_log="/tmp/${run_id}.log" + local remote_pid="/tmp/${run_id}.pid" + local remote_launcher="/tmp/${run_id}.launcher.log" + local launch_output_file="${state_dir}/launch-${lane}.log" + local launch_attempt=1 + + while [[ "$launch_attempt" -le "$launch_attempts" ]]; do + local launch_status=0 + local launch_pid + local launch_elapsed=0 + + echo " -> Launching ${lane} on ${sidecar_id} (attempt ${launch_attempt}/${launch_attempts})" + "$CHUNK_BIN" sidecar exec \ + --sidecar-id "$sidecar_id" \ + --command bash \ + --args -lc \ + --args "cd '$remote_workdir' && export RUST_CHANGED='$rust_changed' PLAYWRIGHT_SHARDS='$playwright_shards' PLAYWRIGHT_CONCURRENCY='$playwright_concurrency' PLAYWRIGHT_SHARED_SERVER='$playwright_shared_server' VITEST_COVERAGE_MAX_WORKERS='$vitest_coverage_max_workers' FRONTEND_COVERAGE_SHARDS='$frontend_coverage_shards' CARGO_BUILD_JOBS='$cargo_build_jobs' && rm -f '$remote_status' '$remote_log' '$remote_pid' '$remote_launcher' && nohup setsid -f bash -lc 'bash .chunk/run-sidecar-lane.sh $lane >\"$remote_log\" 2>&1; printf \"%s\\n\" \"\$?\" >\"$remote_status\"' '$remote_launcher' 2>&1" \ + >"$launch_output_file" 2>&1 & + launch_pid=$! + + while kill -0 "$launch_pid" 2>/dev/null; do + if [[ "$launch_elapsed" -ge "$launch_timeout" ]]; then + launch_status=124 + kill "$launch_pid" 2>/dev/null || true + sleep 1 + kill -KILL "$launch_pid" 2>/dev/null || true + wait "$launch_pid" 2>/dev/null || true + break + fi + sleep 1 + launch_elapsed=$((launch_elapsed + 1)) + done + + if [[ "$launch_status" == "0" ]]; then + wait "$launch_pid" || launch_status=$? + fi + + if [[ "$launch_status" == "0" ]]; then + record_lane "$lane" "$sidecar_id" "$remote_status" "$remote_log" "$remote_pid" + return 0 + fi + + cat "$launch_output_file" + if launch_state="$(poll_lane_status "$sidecar_id" "$remote_status" "$remote_log" "$remote_pid" 2>/dev/null)"; then + launch_state="$(printf '%s\n' "$launch_state" | tail -1 | tr -d '[:space:]')" + if [[ "$launch_state" == "running" || "$launch_state" == "0" || "$launch_state" =~ ^[1-9][0-9]*$ ]]; then + echo " -> ${lane} appears to have started despite launch status ${launch_status}" + record_lane "$lane" "$sidecar_id" "$remote_status" "$remote_log" "$remote_pid" + return 0 + fi + fi + echo "Chunk sidecar launch failed for ${lane} with status ${launch_status}" + if [[ "$launch_attempt" -lt "$launch_attempts" ]]; then + sleep 5 + fi + launch_attempt=$((launch_attempt + 1)) + done + + return 1 +} + +fetch_remote_log_tail() { + local lane="$1" + local sidecar_id="$2" + local log_file="$3" + + echo "" + echo "----- ${lane} log tail -----" + "$CHUNK_BIN" sidecar exec \ + --sidecar-id "$sidecar_id" \ + --command bash \ + --args -lc \ + --args "tail -180 '$log_file' 2>/dev/null || true" 2>&1 || true +} + +stop_lane() { + local sidecar_id="$1" + local pid_file="$2" + local log_file="$3" + + "$CHUNK_BIN" sidecar exec \ + --sidecar-id "$sidecar_id" \ + --command bash \ + --args -lc \ + --args "if [ -s '$pid_file' ]; then pid=\$(cat '$pid_file'); kill -TERM -\"\$pid\" 2>/dev/null || kill -TERM \"\$pid\" 2>/dev/null || true; sleep 3; kill -KILL -\"\$pid\" 2>/dev/null || kill -KILL \"\$pid\" 2>/dev/null || true; fi; ps -eo pid=,command= | awk -v target='$log_file' '\$0 ~ target && \$0 !~ /awk/ { print \$1 }' | xargs -r kill -TERM 2>/dev/null || true" \ + >/dev/null 2>&1 || true +} + +stop_all_lanes() { + while IFS=$'\t' read -r _lane sidecar_id _status_file log_file pid_file; do + stop_lane "$sidecar_id" "$pid_file" "$log_file" + done < "$lanes_file" +} + +poll_lane_status() { + local sidecar_id="$1" + local status_file="$2" + local log_file="$3" + local pid_file="$4" + + "$CHUNK_BIN" sidecar exec \ + --sidecar-id "$sidecar_id" \ + --command bash \ + --args -lc \ + --args "if [ -f '$status_file' ]; then cat '$status_file'; elif test -s '$pid_file' && kill -0 \"\$(cat '$pid_file')\" 2>/dev/null; then echo running; elif ps -eo pid=,command= | awk -v target='$log_file' '\$0 ~ target && \$0 !~ /awk/ { found=1 } END { exit found ? 0 : 1 }'; then echo running; else echo missing; fi" 2>&1 +} + +poll_lanes() { + local started_at="$1" + local last_heartbeat=0 + local completed_file="${state_dir}/completed" + local failed=0 + + : > "$completed_file" + + while true; do + local all_done=1 + + while IFS=$'\t' read -r lane sidecar_id status_file log_file pid_file; do + if grep -qx "$lane" "$completed_file"; then + continue + fi + + local poll_status=0 + local poll_output + local status + + poll_output="$(poll_lane_status "$sidecar_id" "$status_file" "$log_file" "$pid_file")" || poll_status=$? + if [[ "$poll_status" != "0" ]]; then + echo " -> ${lane} status poll failed: $(printf '%s\n' "$poll_output" | head -1)" + all_done=0 + continue + fi + + status="$(printf '%s\n' "$poll_output" | tail -1 | tr -d '[:space:]')" + case "$status" in + 0) + echo "$lane" >> "$completed_file" + echo " -> ${lane} passed" + fetch_remote_log_tail "$lane" "$sidecar_id" "$log_file" + ;; + running|"") + all_done=0 + ;; + missing) + echo " -> ${lane} stopped before writing a status file" + fetch_remote_log_tail "$lane" "$sidecar_id" "$log_file" + failed=1 + echo "$lane" >> "$completed_file" + ;; + *) + echo " -> ${lane} failed with status ${status}" + fetch_remote_log_tail "$lane" "$sidecar_id" "$log_file" + failed=1 + echo "$lane" >> "$completed_file" + ;; + esac + done < "$lanes_file" + + if [[ "$failed" != "0" ]]; then + stop_all_lanes + return 1 + fi + + if [[ "$all_done" == "1" ]]; then + return 0 + fi + + local elapsed + elapsed=$(($(date +%s) - started_at)) + if [[ "$elapsed" -ge "$timeout_seconds" ]]; then + echo "Chunk sidecar lanes timed out after ${elapsed}s" + stop_all_lanes + return 124 + fi + + if [[ $((elapsed - last_heartbeat)) -ge 60 ]]; then + echo " -> Sidecar lanes still running (${elapsed}s elapsed)" + last_heartbeat="$elapsed" + fi + + sleep "$poll_interval" + done +} + +if ! resolve_chunk_bin; then + echo "Chunk CLI not found" + exit "$unavailable_status" +fi + +org_id="$(load_org_id)" +if [[ -z "$org_id" ]]; then + echo "Chunk org id not found" + exit "$unavailable_status" +fi + +echo "Chunk sidecar lanes:" +echo " frontend: ${frontend_name}" +echo " rust: ${rust_name}" +echo " playwright: ${playwright_name}" +if [[ -n "${SIDECAR_FRONTEND_ID:-}${SIDECAR_RUST_ID:-}${SIDECAR_PLAYWRIGHT_ID:-}" ]]; then + echo " explicit sidecar IDs are set for one or more lanes" +fi +echo " vitest workers=${vitest_coverage_max_workers}; frontend coverage shards=${frontend_coverage_shards}; playwright shards=${playwright_shards}; concurrency=${playwright_concurrency}; cargo jobs=${cargo_build_jobs}" + +if ! frontend_id="$(ensure_sidecar "$frontend_name" "${SIDECAR_FRONTEND_ID:-}")"; then + echo "Chunk frontend sidecar unavailable" + exit "$unavailable_status" +fi +if ! rust_id="$(ensure_sidecar "$rust_name" "${SIDECAR_RUST_ID:-}")"; then + echo "Chunk rust sidecar unavailable" + exit "$unavailable_status" +fi +if ! playwright_id="$(ensure_sidecar "$playwright_name" "${SIDECAR_PLAYWRIGHT_ID:-}")"; then + echo "Chunk Playwright sidecar unavailable" + exit "$unavailable_status" +fi + +echo "Syncing worktree to sidecar lanes..." +if ! sync_all_lanes; then + exit "$unavailable_status" +fi + +started_at=$(date +%s) +echo "Launching sidecar lanes..." +launch_lane frontend "$frontend_id" +if [[ "$rust_changed" == "true" ]]; then + launch_lane rust "$rust_id" +else + echo " -> Rust lane skipped because RUST_CHANGED=false" +fi +launch_lane playwright "$playwright_id" + +if ! poll_lanes "$started_at"; then + exit 1 +fi + +elapsed=$(($(date +%s) - started_at)) +echo "Chunk sidecar lanes passed in ${elapsed}s" +exit 0 diff --git a/.chunk/run-sidecar-gates.sh b/.chunk/run-sidecar-gates.sh new file mode 100644 index 0000000..1c45fcd --- /dev/null +++ b/.chunk/run-sidecar-gates.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +set -euo pipefail + +start_time=$(date +%s) +log_dir="${TMPDIR:-/tmp}/tolaria-sidecar-gates-$$" +rust_changed="${RUST_CHANGED:-true}" +rust_phase="${SIDECAR_RUST_PHASE:-after-coverage}" +playwright_phase="${SIDECAR_PLAYWRIGHT_PHASE:-after}" +playwright_shards="${PLAYWRIGHT_SHARDS:-8}" + +mkdir -p "$log_dir" + +if [[ -z "${VITEST_COVERAGE_MAX_WORKERS:-}" ]]; then + if [[ "$playwright_phase" == "early" ]]; then + export VITEST_COVERAGE_MAX_WORKERS="${SIDECAR_EARLY_VITEST_MAX_WORKERS:-2}" + else + export VITEST_COVERAGE_MAX_WORKERS="${SIDECAR_VITEST_MAX_WORKERS:-3}" + fi +fi + +elapsed_seconds() { + printf '%s' "$(($(date +%s) - start_time))" +} + +log_gate() { + printf '[sidecar-gates +%ss] %s\n' "$(elapsed_seconds)" "$*" +} + +run_job() { + local name="$1" + shift + local log_file="${log_dir}/${name}.log" + + ( + set +e + log_gate "${name} started" + "$@" 2>&1 | tee "$log_file" + local status=${PIPESTATUS[0]} + log_gate "${name} exited with status ${status}" + exit "$status" + ) & + printf '%s:%s\n' "$name" "$!" >> "$jobs_file" +} + +terminate_jobs() { + local pids + pids="$(jobs -pr || true)" + if [[ -n "$pids" ]]; then + kill $pids 2>/dev/null || true + sleep 2 + kill -KILL $pids 2>/dev/null || true + fi +} + +jobs_file="${log_dir}/jobs" +: > "$jobs_file" +trap terminate_jobs INT TERM + +wait_for_jobs() { + local failures=0 + + while IFS=: read -r name pid; do + if wait "$pid"; then + printf '[sidecar-gates] %s passed\n' "$name" + else + failures=1 + printf '[sidecar-gates] %s failed\n' "$name" + fi + done < "$jobs_file" + + return "$failures" +} + +log_gate "rust phase=${rust_phase}; playwright phase=${playwright_phase}; shards=${playwright_shards}; concurrency=${PLAYWRIGHT_CONCURRENCY:-${playwright_shards}}; vitest workers=${VITEST_COVERAGE_MAX_WORKERS:-4}" + +run_job frontend-lint pnpm lint +run_job frontend-build pnpm build +run_job frontend-coverage pnpm test:coverage --silent + +if [[ "$rust_changed" == "true" && "$rust_phase" == "early" ]]; then + run_job rust bash .chunk/run-rust-gate.sh +elif [[ "$rust_changed" != "true" ]]; then + log_gate 'rust skipped because RUST_CHANGED=false' +fi + +if [[ "$playwright_phase" == "early" ]]; then + run_job playwright-smoke bash .chunk/run-playwright-shards.sh "$playwright_shards" +fi + +if ! wait_for_jobs; then + if [[ "$playwright_phase" == "early" ]]; then + log_gate 'one or more sidecar gates failed' + else + log_gate 'stopping before Playwright because a prerequisite gate failed' + fi + exit 1 +fi + +if [[ "$playwright_phase" != "early" || ( "$rust_changed" == "true" && "$rust_phase" != "early" ) ]]; then + : > "$jobs_file" + if [[ "$rust_changed" == "true" && "$rust_phase" != "early" ]]; then + run_job rust bash .chunk/run-rust-gate.sh + fi + if [[ "$playwright_phase" != "early" ]]; then + run_job playwright-smoke bash .chunk/run-playwright-shards.sh "$playwright_shards" + fi + + if ! wait_for_jobs; then + exit 1 + fi +fi + +log_gate "completed in $(elapsed_seconds)s" + +exit 0 diff --git a/.chunk/run-sidecar-lane.sh b/.chunk/run-sidecar-lane.sh new file mode 100755 index 0000000..7f41d46 --- /dev/null +++ b/.chunk/run-sidecar-lane.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +set -euo pipefail + +lane="${1:-}" +start_time=$(date +%s) + +elapsed_seconds() { + printf '%s' "$(($(date +%s) - start_time))" +} + +log_lane() { + printf '[sidecar-%s +%ss] %s\n' "$lane" "$(elapsed_seconds)" "$*" +} + +run_job() { + local name="$1" + shift + local log_file="${log_dir}/${name}.log" + + ( + set +e + log_lane "${name} started" + "$@" 2>&1 | tee "$log_file" + local status=${PIPESTATUS[0]} + log_lane "${name} exited with status ${status}" + exit "$status" + ) & + printf '%s:%s\n' "$name" "$!" >> "$jobs_file" +} + +sync_node_dependencies() { + log_lane "syncing pnpm dependencies with lockfile" + pnpm install --frozen-lockfile +} + +terminate_jobs() { + local pids + pids="$(jobs -pr || true)" + if [[ -n "$pids" ]]; then + kill $pids 2>/dev/null || true + sleep 2 + kill -KILL $pids 2>/dev/null || true + fi +} + +wait_for_jobs() { + local failures=0 + + while IFS=: read -r name pid; do + if wait "$pid"; then + printf '[sidecar-%s] %s passed\n' "$lane" "$name" + else + failures=1 + printf '[sidecar-%s] %s failed\n' "$lane" "$name" + fi + done < "$jobs_file" + + return "$failures" +} + +run_frontend_coverage_job() { + local shard_count="${FRONTEND_COVERAGE_SHARDS:-1}" + + if [[ ! "$shard_count" =~ ^[1-9][0-9]*$ ]]; then + printf 'FRONTEND_COVERAGE_SHARDS must be a positive integer\n' >&2 + return 2 + fi + + if [[ "$shard_count" == "1" ]]; then + run_job frontend-coverage pnpm test:coverage --silent + return + fi + + run_job frontend-coverage node scripts/run-vitest-coverage-shards.mjs --silent +} + +run_frontend_lane() { + log_dir="${TMPDIR:-/tmp}/tolaria-sidecar-frontend-$$" + jobs_file="${log_dir}/jobs" + mkdir -p "$log_dir" + : > "$jobs_file" + trap terminate_jobs INT TERM + + sync_node_dependencies + + export VITEST_COVERAGE_MAX_WORKERS="${VITEST_COVERAGE_MAX_WORKERS:-2}" + log_lane "vitest workers=${VITEST_COVERAGE_MAX_WORKERS}; coverage shards=${FRONTEND_COVERAGE_SHARDS:-1}" + run_job frontend-lint pnpm lint + run_job frontend-build pnpm build + + if ! wait_for_jobs; then + return 1 + fi + + : > "$jobs_file" + run_frontend_coverage_job + wait_for_jobs +} + +run_rust_lane() { + if [[ "${RUST_CHANGED:-true}" != "true" ]]; then + log_lane 'skipped because RUST_CHANGED=false' + return 0 + fi + + bash .chunk/run-rust-gate.sh +} + +run_playwright_lane() { + sync_node_dependencies + + export PLAYWRIGHT_SHARED_SERVER="${PLAYWRIGHT_SHARED_SERVER:-1}" + export PLAYWRIGHT_CONCURRENCY="${PLAYWRIGHT_CONCURRENCY:-4}" + local shards="${PLAYWRIGHT_SHARDS:-8}" + + log_lane "shards=${shards}; concurrency=${PLAYWRIGHT_CONCURRENCY}; shared server=${PLAYWRIGHT_SHARED_SERVER}" + bash .chunk/run-playwright-shards.sh "$shards" +} + +case "$lane" in + frontend) + run_frontend_lane + ;; + rust) + run_rust_lane + ;; + playwright) + run_playwright_lane + ;; + *) + printf 'Usage: %s frontend|rust|playwright\n' "$0" >&2 + exit 2 + ;; +esac + +log_lane "completed in $(elapsed_seconds)s" diff --git a/.claude/commands/create-adr.md b/.claude/commands/create-adr.md new file mode 100644 index 0000000..1cbcd6a --- /dev/null +++ b/.claude/commands/create-adr.md @@ -0,0 +1,133 @@ +# Create Architecture Decision Record + +Use this command when you need to document an architectural decision made during a task. + +Inspired by [adr-tools](https://github.com/npryce/adr-tools) (Nygard format), adapted for Laputa's frontmatter-based note format. + +## When to use this + +Create an ADR when your work involves any of these: +- Choosing a storage strategy (vault vs app settings vs database) +- Adding or removing a major dependency +- Supporting a new platform or target +- Introducing or removing a core abstraction +- Making a cross-cutting decision that affects how future code should be written + +Do NOT create ADRs for: bug fixes, UI styling, refactors that preserve behavior, or test additions. + +## Creating a new ADR + +### 1. Find the next ID + +```bash +ls docs/adr/*.md | grep -oP '\d{4}' | sort -n | tail -1 | xargs -I{} printf '%04d\n' $(({} + 1)) +``` + +If no files exist yet, start at `0001`. + +### 2. Create the file + +Filename: `docs/adr/NNNN-short-kebab-title.md` + +Template: + +```markdown +--- +type: ADR +id: "NNNN" +title: "Short decision title" +status: active +date: YYYY-MM-DD +--- + +## Context + +The issue motivating this decision, and any context that influences or constrains it. + +## Decision + +**The change we're proposing or have agreed to implement.** State it clearly in one or two sentences — bold so it stands out. + +## Options considered + +- **Option A** (chosen): brief description — pros / cons +- **Option B**: brief description — pros / cons +- **Option C**: brief description — pros / cons + +## Consequences + +What becomes easier or harder as a result? +What risks does this introduce that will need to be mitigated? +What would trigger re-evaluation of this decision? + +## Advice + +*(optional)* Input received before making this decision — who was consulted, what they said. +Omit this section if the decision was made without external input. +``` + +### 3. Update the index + +Add a row to `docs/adr/README.md`: + +```markdown +| [NNNN](NNNN-short-kebab-title.md) | Title | active | +``` + +### 4. Include in the same commit as the feature + +```bash +git add docs/adr/NNNN-*.md docs/adr/README.md +# fold into the feature commit — do not create a separate commit just for the ADR +``` + +--- + +## Superseding an existing ADR + +Equivalent of `adr new -s ` from adr-tools — do this in two steps: + +### Step 1: Mark the old ADR as superseded + +Edit the existing file — add `superseded_by` and update `status`: + +```yaml +--- +type: ADR +id: "000N" +title: "Old decision title" +status: superseded # ← change from active +superseded_by: "NNNN" # ← add this +date: YYYY-MM-DD +--- +``` + +**Never edit the content sections** of an active ADR — only the status metadata. + +### Step 2: Create the new ADR + +Follow the steps above. In the **Context** section, reference the superseded ADR: + +```markdown +## Context + +Supersedes [ADR-000N](000N-old-title.md). + +[explain why the old decision no longer holds] +``` + +### Step 3: Update the README index + +Change the old row's status to `superseded`, add the new row. + +--- + +## Best practices (from adr-tools / Nygard) + +- **One decision per ADR** — if you find yourself writing "and also", split it +- **Write Decision first** — if you can't state it in 1-2 sentences, the decision is too vague +- **Context is the "why now"** — what forced this decision to be made today? +- **Consequences should include negatives** — a one-sided ADR is a red flag +- **Committed = immutable** — once pushed, the content doesn't change; only status metadata does +- **If in doubt, create one** — cheaper to have an unnecessary ADR than to lose context +- Date = today's date, `YYYY-MM-DD` diff --git a/.claude/commands/laputa-done.md b/.claude/commands/laputa-done.md new file mode 100644 index 0000000..1529e6c --- /dev/null +++ b/.claude/commands/laputa-done.md @@ -0,0 +1,42 @@ +# /laputa-done + +Mark a Laputa task as done: add completion comment, move to In Review, then self-dispatch the next task. + +Run this after Phase 1 (Playwright) and Phase 2 (native app QA) both pass **and `git push origin main` has succeeded**. + +⚠️ A task is NOT done until the push succeeds. If the push is blocked by the pre-push hook (clippy, tests, CodeScene, build): +- Read the error +- Fix it (never use `--no-verify`) +- Commit the fix and push again +- Repeat until push exits with code 0 + +## Steps + +**1. Add completion comment to the task** + +Summarize what was done — this is the context Luca and Brian will read in Todoist: + +```bash +curl -s -X POST "https://api.todoist.com/api/v1/comments" \ + -H "Authorization: Bearer $TODOIST_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "task_id": "$ARGUMENTS", + "content": "✅ Implementation complete.\n\n**What changed:** [brief summary of the implementation]\n**ADR:** [if an ADR was created, reference it here; otherwise omit]\n**Playwright:** all tests pass\n**Native QA:** tested with pnpm tauri dev — [describe what was tested and what was observed]" + }' +``` + +**2. Move task to In Review** + +```bash +curl -s -X POST "https://api.todoist.com/api/v1/tasks/$ARGUMENTS/move" \ + -H "Authorization: Bearer $TODOIST_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"section_id": "6g3XjX33FF4Vj86M"}' +``` + +**3. Pick the next task** + +Run `/laputa-next-task` to get the next task and start working on it immediately. + +If there are no tasks, `/laputa-next-task` will wait 10 minutes and retry automatically. Do NOT exit — stay alive and let it loop. diff --git a/.claude/commands/laputa-next-task.md b/.claude/commands/laputa-next-task.md new file mode 100644 index 0000000..22fba6b --- /dev/null +++ b/.claude/commands/laputa-next-task.md @@ -0,0 +1,60 @@ +# /laputa-next-task + +Pick the next Laputa task from Todoist and move it to In Progress. + +Priority order: **To Rework** first, then **Open** (sorted by Todoist priority p1→p4). + +## Steps + +1. Fetch tasks from To Rework (`6g6QqvR9rRpvJWvv`), then Open (`6g3XjWR832hVHhCM`) +2. **Sort by priority — this is mandatory.** Todoist returns tasks in arbitrary order. You must sort them yourself: + - Todoist priority field: `4` = p1 (urgent), `3` = p2, `2` = p3, `1` = p4 + - Sort descending by `priority` field (4 first, 1 last) + - To Rework tasks always come before Open tasks regardless of priority + - **Never pick a p3/p4 task if a p1/p2 task exists in the same section** +3. Take the first task from the sorted list +4. Move it to In Progress (`6g3XjWjfmJFcGgHM`) via Todoist API: + +```bash +curl -s -X POST "https://api.todoist.com/api/v1/tasks//move" \ + -H "Authorization: Bearer $TODOIST_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"section_id": "6g3XjWjfmJFcGgHM"}' +``` + +5. Add a "started" comment to the task: + +```bash +curl -s -X POST "https://api.todoist.com/api/v1/comments" \ + -H "Authorization: Bearer $TODOIST_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"task_id": "", "content": "🚀 Starting work. [Brief description of approach or what needs to be fixed]"}' +``` + +6. Fetch the full task details (description, comments) from Todoist: + +```bash +curl -s "https://api.todoist.com/api/v1/tasks/" \ + -H "Authorization: Bearer $TODOIST_API_KEY" + +curl -s "https://api.todoist.com/api/v1/comments?task_id=" \ + -H "Authorization: Bearer $TODOIST_API_KEY" +``` + +6. For To Rework tasks: read the ❌ QA failed comment — it tells you exactly what to fix +7. Output: task ID, title, and full description so you can start working immediately + +If no tasks are available in either section → wait 10 minutes and try again (loop forever): + +```bash +while true; do + # ... check tasks ... + if no_tasks; then + sleep 600 # 10 minutes + else + break # got a task, proceed + fi +done +``` + +Do NOT exit when there are no tasks. Keep looping until a task appears. This keeps Claude Code alive permanently — the watchdog is a safety net only, not the primary dispatcher. diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..fba3ea8 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "mcp__codescene__*", + "Read(*)", + "Bash(cat*)", + "Bash(ls*)", + "Write(*)" + ] + } +} diff --git a/.codacy.yaml b/.codacy.yaml new file mode 100644 index 0000000..d39189e --- /dev/null +++ b/.codacy.yaml @@ -0,0 +1,21 @@ +--- +exclude_paths: + - ".chunk/**" + - "coverage/**" + - "dist/**" + - "e2e/**" + - "node_modules/**" + - "scripts/**" + - "site/.vitepress/cache/**" + - "site/.vitepress/dist/**" + - "src/test/**" + - "src-tauri/gen/**" + - "src-tauri/resources/agent-docs/**" + - "src-tauri/resources/mcp-server/**" + - "src-tauri/target/**" + - "target/**" + - "test-results/**" + - "tests/**" + - "**/*.test.ts" + - "**/*.test.tsx" + - "vite.config.ts" diff --git a/.codescene-thresholds b/.codescene-thresholds new file mode 100644 index 0000000..c317ae7 --- /dev/null +++ b/.codescene-thresholds @@ -0,0 +1,2 @@ +HOTSPOT_THRESHOLD=10.0 +AVERAGE_THRESHOLD=9.99 diff --git a/.codesceneignore b/.codesceneignore new file mode 100644 index 0000000..dc59d40 --- /dev/null +++ b/.codesceneignore @@ -0,0 +1,5 @@ +# Exclude third-party tools and their dependencies from CodeScene analysis +tools/ +e2e/ +tests/ +scripts/ diff --git a/.codescenerc b/.codescenerc new file mode 100644 index 0000000..9867481 --- /dev/null +++ b/.codescenerc @@ -0,0 +1,9 @@ +{ + "exclude": [ + "tools/", + "scripts/", + "src-tauri/gen/", + "coverage/", + "dist/" + ] +} diff --git a/.editor-performance-thresholds.json b/.editor-performance-thresholds.json new file mode 100644 index 0000000..e48915e --- /dev/null +++ b/.editor-performance-thresholds.json @@ -0,0 +1,83 @@ +{ + "scenarios": { + "small": { + "contentBytes": 1451, + "metrics": { + "blockApplyMs": { + "baselineMs": 12.3, + "maxMs": 37 + }, + "blockResolveMs": { + "baselineMs": 11.6, + "maxMs": 37 + }, + "editFrameMs": { + "baselineMs": 3.2, + "maxMs": 16 + }, + "editorVisibleMs": { + "baselineMs": 223.2, + "maxMs": 282 + }, + "firstContentMs": { + "baselineMs": 237.4, + "maxMs": 293 + }, + "fullAppliedMs": { + "baselineMs": 240.6, + "maxMs": 294 + }, + "noteOpenEditorSwapMs": { + "baselineMs": 90.4, + "maxMs": 115 + }, + "noteOpenTotalMs": { + "baselineMs": 138.7, + "maxMs": 180 + } + }, + "sectionCount": 5 + }, + "large": { + "contentBytes": 130377, + "metrics": { + "blockApplyMs": { + "baselineMs": 279.1, + "maxMs": 362 + }, + "blockResolveMs": { + "baselineMs": 37.8, + "maxMs": 63 + }, + "editFrameMs": { + "baselineMs": 5, + "maxMs": 16 + }, + "editorVisibleMs": { + "baselineMs": 197.9, + "maxMs": 261 + }, + "firstContentMs": { + "baselineMs": 305.1, + "maxMs": 401 + }, + "fullAppliedMs": { + "baselineMs": 508.4, + "maxMs": 664 + }, + "noteOpenEditorSwapMs": { + "baselineMs": 384.3, + "maxMs": 498 + }, + "noteOpenTotalMs": { + "baselineMs": 448.4, + "maxMs": 587 + } + }, + "sectionCount": 460 + } + }, + "version": 1, + "updatedAt": "2026-06-29T07:53:27.234Z", + "description": "Ratcheted editor performance budgets for synthetic small and large note opens. Lower is better; maxMs values should only move down unless intentionally rebaselined." +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6979548 --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# Copy to .env.local and fill in real values. +# These are never committed — .env.local is gitignored. +# Release workflows must mirror these values into GitHub Actions secrets: +# - VITE_SENTRY_DSN +# - SENTRY_DSN (same value as VITE_SENTRY_DSN for Rust-side crash reporting) +# - VITE_POSTHOG_KEY +# - VITE_POSTHOG_HOST + +# Sentry DSN (https://sentry.io → Project → Settings → Client Keys) +VITE_SENTRY_DSN= + +# PostHog (https://posthog.com → Project → Settings → Project API Key) +VITE_POSTHOG_KEY= +VITE_POSTHOG_HOST=https://eu.i.posthog.com + +# Lara CLI (https://github.com/translated/lara-cli) +LARA_ACCESS_KEY_ID= +LARA_ACCESS_KEY_SECRET= diff --git a/.githooks-info b/.githooks-info new file mode 100644 index 0000000..f5a66e8 --- /dev/null +++ b/.githooks-info @@ -0,0 +1,3 @@ +# Git Hooks + +See .github/HOOKS.md for details. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..6fe0494 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +custom: https://refactoring.fm/ diff --git a/.github/HOOKS.md b/.github/HOOKS.md new file mode 100644 index 0000000..604693f --- /dev/null +++ b/.github/HOOKS.md @@ -0,0 +1,64 @@ +# Git Hooks + +This repo uses Husky hooks from `.husky/`. Those files are the source of truth. + +## Installation + +`pnpm install` runs the `prepare` script and installs the hooks into `.git/hooks`. + +If you need to reinstall them manually: + +```bash +pnpm exec husky +``` + +The hooks expect `node` and `pnpm` to be available. If they are installed via `nvm`, the hooks will try to load `~/.nvm/nvm.sh` automatically. + +## Policy + +- Commit on `main` only. +- Push from `main` to `origin/main` only. +- Never use `--no-verify`. +- `.codescene-thresholds` is a ratchet. It can only move up. + +## Pre-commit + +`.husky/pre-commit` blocks commits unless all of the following are true: + +- `HEAD` is attached to `main` +- staged TypeScript files pass `pnpm lint --quiet` +- TypeScript passes `npx tsc --noEmit` +- frontend tests pass via `pnpm test --run --silent` +- current CodeScene Hotspot and Average health are both at or above `.codescene-thresholds` + +If `CODESCENE_PAT` or `CODESCENE_PROJECT_ID` is missing, the CodeScene portion is skipped, but the rest of the hook still runs. + +## Pre-push + +`.husky/pre-push` blocks pushes unless all of the following are true: + +- the current branch is `main` +- every pushed branch ref is `refs/heads/main -> refs/heads/main` +- TypeScript and the Vite build pass +- frontend coverage passes +- Rust lint and Rust coverage pass when `src-tauri/` changed +- the curated Playwright core smoke lane passes via `pnpm playwright:smoke` +- current CodeScene Hotspot and Average health are both at or above `.codescene-thresholds` + +If the remote CodeScene scores are better than the current thresholds, the hook updates `.codescene-thresholds`, stages it, and stops the push. Commit that file normally, then push again. The hook does not auto-commit or bypass itself. + +## Legacy Files + +The legacy `pre-commit` file under `.github/hooks/` is archival only. Do not copy it into `.git/hooks`; use Husky and `.husky/` instead. The old design `post-commit` auto-implementation hook was removed because it depended on obsolete one-off scripts. `install-hooks.sh` remains as a reinstall helper that runs Husky. + +## Troubleshooting + +If a hook cannot find `node` or `pnpm`: + +```bash +export NVM_DIR="$HOME/.nvm" +. "$NVM_DIR/nvm.sh" +nvm use node +``` + +Then retry the commit or push. diff --git a/.github/SETUP.md b/.github/SETUP.md new file mode 100644 index 0000000..d56b055 --- /dev/null +++ b/.github/SETUP.md @@ -0,0 +1,227 @@ +# CI/CD Setup Guide + +## Quick Start + +### 1. Add GitHub Secrets + +Nel repository GitHub (Settings → Secrets and variables → Actions → New repository secret): + +**CODESCENE_TOKEN** +``` + +``` + +**CODESCENE_PROJECT_ID** +Trova l'ID del progetto nella dashboard CodeScene (URL: `https://codescene.io/projects//...`) + +**VITE_SENTRY_DSN** +``` + +``` + +**SENTRY_DSN** +``` + +``` + +**VITE_POSTHOG_KEY** +``` + +``` + +**VITE_POSTHOG_HOST** +``` +https://eu.i.posthog.com +``` + +**Windows Authenticode release signing** + +Windows release artifacts can be Authenticode-signed when a trusted code-signing certificate is available. Until Windows certificate provisioning is complete, the release workflow warns and publishes Windows artifacts with Tauri updater signatures only. + +To enable Authenticode, configure a trusted certificate exported as base64 PFX data: + +``` +WINDOWS_CODE_SIGNING_CERTIFICATE= +WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD= +``` + +Optional: + +``` +WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT= +WINDOWS_CODE_SIGNING_TIMESTAMP_URL=https://timestamp.digicert.com +``` + +Legacy aliases `WINDOWS_CERTIFICATE`, `WINDOWS_CERTIFICATE_PASSWORD`, `WINDOWS_CERTIFICATE_THUMBPRINT`, and `WINDOWS_TIMESTAMP_URL` are still accepted by the signing script. Do not use a self-signed certificate for public releases; Windows Authenticode release signing needs a certificate from a trusted CA or signing service. + +### 2. Enable GitHub Actions + +- Vai su Settings → Actions → General +- Assicurati che "Allow all actions and reusable workflows" sia selezionato + +### 3. Configure Branch Protection (Optional ma Raccomandato) + +Settings → Branches → Add branch protection rule: + +**Branch name pattern**: `main` + +Abilita: +- ✅ Require status checks to pass before merging + - Select: `Tests & Quality Checks` +- ✅ Require branches to be up to date before merging +- ✅ Do not allow bypassing the above settings + +Questo forza tutti i check a passare prima di poter fare merge su main. + +### 4. Test Locally Prima di Pushare + +```bash +# Full test suite +pnpm test && cargo test --manifest-path=src-tauri/Cargo.toml + +# Coverage +pnpm test:coverage + +# Lint +pnpm lint +cargo clippy --manifest-path=src-tauri/Cargo.toml + +# Format check +cargo fmt --manifest-path=src-tauri/Cargo.toml -- --check +``` + +## What Gets Checked + +### ✅ Tests +- Frontend: Vitest +- Backend: `cargo test` + +### 📊 Coverage +- Threshold: 70% (lines, functions, branches, statements) +- Configurabile in `vite.config.ts` + +### 🏥 Code Health +- CodeScene delta analysis +- **Fail se code health diminuisce** +- Confronta HEAD vs base branch + +### 📡 Telemetry In Release Builds +- `release.yml` e `release-stable.yml` devono ricevere `VITE_SENTRY_DSN`, `SENTRY_DSN`, `VITE_POSTHOG_KEY`, `VITE_POSTHOG_HOST` +- `VITE_SENTRY_DSN` inizializza il frontend Sentry bundle +- `SENTRY_DSN` inizializza Sentry nel binary Rust/Tauri +- `VITE_POSTHOG_KEY` / `VITE_POSTHOG_HOST` permettono ai build distribuiti di inizializzare PostHog quando l'utente abilita analytics + +### 📝 Documentation +- **Warning se modifichi `src/` o `src-tauri/` ma non aggiorni `docs/`** +- Non blocca il merge, solo un reminder +- Skip il check con `[skip docs]` nel commit message +- Aggiorna docs solo se la modifica invalida qualcosa già documentato + +### 🎨 Lint & Format +- ESLint per frontend +- Clippy + rustfmt per Rust + +## Workflow File + +Il workflow è in `.github/workflows/ci.yml`. + +**Trigger**: +- Push su `main` o `experiment/*` +- Pull request verso `main` + +**Runner**: `macos-latest` (necessario per Tauri + Rust) + +## Customization + +### Soglie Coverage + +Modifica `vite.config.ts`: + +```typescript +coverage: { + thresholds: { + lines: 80, // Aumenta se vuoi più coverage + functions: 80, + branches: 80, + statements: 80, + } +} +``` + +### Documentation Check + +Il check **avvisa** (non fallisce) se: +1. Modifichi file in `src/` o `src-tauri/` +2. NON modifichi nulla in `docs/` + +**Quando aggiornare docs:** +- Cambi architettura → aggiorna `docs/ARCHITECTURE.md` +- Cambi astrazioni chiave → aggiorna `docs/ABSTRACTIONS.md` +- Cambi theme system → aggiorna `docs/THEMING.md` +- Bug fix / refactor interno → `[skip docs]` nel commit message + +**Skip il check:** +```bash +git commit -m "fix: editor scroll bug [skip docs]" +``` + +### CodeScene Fail Threshold + +Nel workflow, modifica: + +```yaml +- name: CodeScene Delta Analysis + uses: codescene-oss/codescene-delta-analysis-action@v1 + with: + fail-on-declining-code-health: true # Cambia a false per warning-only + minimum-code-health-score: 8.0 # Aggiungi per soglia assoluta +``` + +## Troubleshooting + +### CodeScene fails con "Project not found" +- Verifica che `CODESCENE_PROJECT_ID` sia corretto +- Controlla che il token abbia accesso al progetto + +### Coverage check fails +- Verifica che `@vitest/coverage-v8` sia installato: `pnpm add -D @vitest/coverage-v8` +- Le soglie sono configurabili in `vite.config.ts` + +### Docs check avvisa anche se non serve aggiornare docs +- È solo un warning, non blocca +- Skip con `[skip docs]` nel commit message +- Oppure ignora — è un reminder, non un requisito + +### Workflow non si attiva +- Verifica che il file sia in `.github/workflows/ci.yml` +- Controlla che GitHub Actions sia abilitato nelle settings +- Il workflow parte solo su push/PR verso `main` o branch `experiment/*` + +## Example CI Pass + +``` +✅ Run frontend tests +✅ Run Rust tests +✅ Run frontend coverage (75% lines, 73% functions) +✅ CodeScene Delta Analysis (code health: 9.2 → 9.3) +✅ Check docs are updated (docs/ARCHITECTURE.md modified) +✅ Lint frontend +✅ Clippy (Rust) +✅ Format check (Rust) +``` + +## Example CI Warning + +``` +⚠️ Code files changed but docs/ not updated + Changed code files: + - src/components/Editor.tsx + - src-tauri/src/vault.rs + + If this change affects architecture/abstractions/design documented in docs/, + please update the relevant documentation files. + + To skip this check, include [skip docs] in your commit message. +``` + +Questo è solo un reminder. Se la modifica non invalida la documentazione esistente, puoi ignorarlo o usare `[skip docs]`. diff --git a/.github/hooks/install-hooks.sh b/.github/hooks/install-hooks.sh new file mode 100755 index 0000000..5b952c3 --- /dev/null +++ b/.github/hooks/install-hooks.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -e + +if ! command -v pnpm >/dev/null 2>&1 || ! command -v node >/dev/null 2>&1; then + export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" + if [ -s "$NVM_DIR/nvm.sh" ]; then + # shellcheck disable=SC1090 + . "$NVM_DIR/nvm.sh" --no-use + nvm use --silent node >/dev/null 2>&1 || true + fi +fi + +if ! command -v pnpm >/dev/null 2>&1 || ! command -v node >/dev/null 2>&1; then + echo "❌ node and pnpm must be available to install Husky hooks" + exit 1 +fi + +echo "Installing Husky hooks from .husky/ ..." +pnpm exec husky +echo "✅ Husky hooks installed" +echo "" +echo "Source of truth:" +echo " - .husky/pre-commit" +echo " - .husky/pre-push" +echo "" +echo "Never use --no-verify in this repo." diff --git a/.github/hooks/pre-commit b/.github/hooks/pre-commit new file mode 100644 index 0000000..94e6aed --- /dev/null +++ b/.github/hooks/pre-commit @@ -0,0 +1,81 @@ +#!/bin/bash +# Pre-commit hook: CodeScene Code Health Check +# Copy to .git/hooks/pre-commit and make executable + +set -e + +# Allow bypass with --no-verify or [skip codescene] in commit message +if git log -1 --pretty=%B 2>/dev/null | grep -qi '\[skip codescene\]'; then + echo "⏭️ CodeScene check skipped (commit message contains [skip codescene])" + exit 0 +fi + +echo "🔍 Running CodeScene Code Health check..." + +# Check if we have staged files to analyze +STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(ts|tsx|rs)$' || true) + +if [ -z "$STAGED_FILES" ]; then + echo "✅ No TypeScript/Rust files staged, skipping CodeScene check" + exit 0 +fi + +# Get current branch +CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) + +# Determine base branch for comparison +if [ "$CURRENT_BRANCH" = "main" ]; then + BASE_REF="HEAD~1" +else + BASE_REF="origin/main" +fi + +echo " Comparing against: $BASE_REF" + +# Check if we have CodeScene configured (MCP or CLI) +CODESCENE_MCP_CONFIG="$HOME/.claude/mcp.json" +CODESCENE_TOKEN_FILE="$HOME/.codescene/token" + +if [ ! -f "$CODESCENE_MCP_CONFIG" ] && [ ! -f "$CODESCENE_TOKEN_FILE" ]; then + echo "⚠️ CodeScene not configured" + echo " Install CodeScene MCP (configured in ~/.claude/mcp.json)" + echo " Or place token at ~/.codescene/token" + echo " Proceeding without check (use 'git commit --no-verify' to skip this warning)" + exit 0 +fi + +# Simple health check using git diff stats +echo " Analyzing code changes..." + +# Get file changes +LINES_ADDED=$(git diff --cached --numstat | awk '{sum+=$1} END {print sum}') +LINES_REMOVED=$(git diff --cached --numstat | awk '{sum+=$2} END {print sum}') + +# Check for large files (potential complexity) +LARGE_FILES=$(git diff --cached --numstat | awk '$1 > 500 || $2 > 500 {print $3}') + +if [ ! -z "$LARGE_FILES" ]; then + echo "⚠️ Large file changes detected (>500 lines):" + echo "$LARGE_FILES" | sed 's/^/ - /' + echo "" + echo " Consider:" + echo " - Breaking into smaller commits" + echo " - Reviewing with Claude Code + CodeScene MCP" + echo " - Running: claude 'Review code health of staged changes'" + echo "" + read -p " Continue anyway? (y/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "❌ Commit aborted" + exit 1 + fi +fi + +echo "✅ CodeScene check passed" +echo " +$LINES_ADDED -$LINES_REMOVED lines" +echo "" +echo " 💡 For detailed code health analysis, run:" +echo " claude 'Check code health of this commit with CodeScene MCP'" +echo "" + +exit 0 diff --git a/.github/scripts/configure-windows-authenticode.ps1 b/.github/scripts/configure-windows-authenticode.ps1 new file mode 100644 index 0000000..f5aade3 --- /dev/null +++ b/.github/scripts/configure-windows-authenticode.ps1 @@ -0,0 +1,115 @@ +param( + [string]$ConfigPath = "src-tauri/tauri.windows-signing.conf.json" +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +function Read-FirstEnv { + param([string[]]$Names) + + foreach ($Name in $Names) { + $Value = [Environment]::GetEnvironmentVariable($Name) + if (-not [string]::IsNullOrWhiteSpace($Value)) { + return $Value.Trim() + } + } + + throw "Set one of these environment variables: $($Names -join ', ')" +} + +function Read-OptionalEnv { + param( + [string[]]$Names, + [string]$DefaultValue + ) + + foreach ($Name in $Names) { + $Value = [Environment]::GetEnvironmentVariable($Name) + if (-not [string]::IsNullOrWhiteSpace($Value)) { + return $Value.Trim() + } + } + + return $DefaultValue +} + +function Normalize-Thumbprint { + param([string]$Thumbprint) + + return ($Thumbprint -replace "\s", "").ToUpperInvariant() +} + +function Convert-CertificateSecretToBytes { + param([string]$CertificateSecret) + + $Base64Lines = $CertificateSecret -split "\r?\n" | + Where-Object { $_ -notmatch "^-+BEGIN " -and $_ -notmatch "^-+END " } + $CertificateBase64 = ($Base64Lines -join "") -replace "\s", "" + + try { + return [Convert]::FromBase64String($CertificateBase64) + } catch { + throw "Windows code-signing certificate must be base64-encoded PFX data." + } +} + +$CertificateSecret = Read-FirstEnv @("WINDOWS_CODE_SIGNING_CERTIFICATE", "WINDOWS_CERTIFICATE") +$CertificatePassword = Read-FirstEnv @("WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD", "WINDOWS_CERTIFICATE_PASSWORD") +$ConfiguredThumbprint = Read-OptionalEnv @("WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT", "WINDOWS_CERTIFICATE_THUMBPRINT") "" +$DigestAlgorithm = Read-OptionalEnv @("WINDOWS_CODE_SIGNING_DIGEST_ALGORITHM") "sha256" +$TimestampUrl = Read-OptionalEnv @("WINDOWS_CODE_SIGNING_TIMESTAMP_URL", "WINDOWS_TIMESTAMP_URL") "http://timestamp.digicert.com" + +$TempRoot = Join-Path ([IO.Path]::GetTempPath()) "tolaria-windows-signing" +if (-not [string]::IsNullOrWhiteSpace($env:RUNNER_TEMP)) { + $TempRoot = Join-Path $env:RUNNER_TEMP "tolaria-windows-signing" +} +New-Item -ItemType Directory -Force -Path $TempRoot | Out-Null + +$PfxPath = Join-Path $TempRoot "certificate.pfx" +[IO.File]::WriteAllBytes($PfxPath, (Convert-CertificateSecretToBytes $CertificateSecret)) + +$SecurePassword = ConvertTo-SecureString -String $CertificatePassword -Force -AsPlainText +$ImportedCertificates = @(Import-PfxCertificate -FilePath $PfxPath -CertStoreLocation Cert:\CurrentUser\My -Password $SecurePassword) +Remove-Item -Force -ErrorAction SilentlyContinue $PfxPath + +$ImportedCertificate = $ImportedCertificates | Where-Object { $_.HasPrivateKey } | Select-Object -First 1 +if ($null -eq $ImportedCertificate) { + throw "The imported Windows code-signing certificate does not include a private key." +} + +if ([string]::IsNullOrWhiteSpace($ConfiguredThumbprint)) { + $CertificateThumbprint = Normalize-Thumbprint $ImportedCertificate.Thumbprint +} else { + $CertificateThumbprint = Normalize-Thumbprint $ConfiguredThumbprint +} + +$StoreCertificate = Get-ChildItem Cert:\CurrentUser\My | + Where-Object { (Normalize-Thumbprint $_.Thumbprint) -eq $CertificateThumbprint } | + Select-Object -First 1 +if ($null -eq $StoreCertificate) { + throw "The requested Windows code-signing certificate thumbprint was not found in Cert:\CurrentUser\My." +} + +$Config = @{ + bundle = @{ + windows = @{ + certificateThumbprint = $CertificateThumbprint + digestAlgorithm = $DigestAlgorithm + timestampUrl = $TimestampUrl + } + } +} + +$ResolvedConfigPath = Resolve-Path -Path (Split-Path -Parent $ConfigPath) -ErrorAction SilentlyContinue +if ($null -eq $ResolvedConfigPath) { + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $ConfigPath) | Out-Null +} + +$Config | ConvertTo-Json -Depth 10 | Set-Content -Path $ConfigPath -Encoding utf8NoBOM + +if (-not [string]::IsNullOrWhiteSpace($env:GITHUB_ENV)) { + "WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT=$CertificateThumbprint" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 +} + +Write-Host "Prepared Windows Authenticode signing config at $ConfigPath." diff --git a/.github/scripts/prefetch-tauri-nsis.ps1 b/.github/scripts/prefetch-tauri-nsis.ps1 new file mode 100644 index 0000000..79da404 --- /dev/null +++ b/.github/scripts/prefetch-tauri-nsis.ps1 @@ -0,0 +1,137 @@ +$ErrorActionPreference = "Stop" + +$nsisUrl = "https://github.com/tauri-apps/binary-releases/releases/download/nsis-3.11/nsis-3.11.zip" +$nsisSha1 = "EF7FF767E5CBD9EDD22ADD3A32C9B8F4500BB10D" +$tauriUtilsUrl = "https://github.com/tauri-apps/nsis-tauri-utils/releases/download/nsis_tauri_utils-v0.5.3/nsis_tauri_utils.dll" +$tauriUtilsSha1 = "75197FEE3C6A814FE035788D1C34EAD39349B860" +$tauriUtilsRelativePath = "Plugins\x86-unicode\additional\nsis_tauri_utils.dll" + +$nsisRequiredFiles = @( + "makensis.exe", + "Bin\makensis.exe", + "Stubs\lzma-x86-unicode", + "Stubs\lzma_solid-x86-unicode", + "Include\MUI2.nsh", + "Include\FileFunc.nsh", + "Include\x64.nsh", + "Include\nsDialogs.nsh", + "Include\WinMessages.nsh", + "Include\Win\COM.nsh", + "Include\Win\Propkey.nsh", + "Include\Win\RestartManager.nsh" +) + +function Get-UpperSha1 { + param([Parameter(Mandatory = $true)][string]$Path) + + return (Get-FileHash -Algorithm SHA1 -LiteralPath $Path).Hash.ToUpperInvariant() +} + +function Test-FileSha1 { + param( + [Parameter(Mandatory = $true)][string]$Path, + [Parameter(Mandatory = $true)][string]$ExpectedSha1 + ) + + return (Test-Path -LiteralPath $Path) -and ((Get-UpperSha1 -Path $Path) -eq $ExpectedSha1) +} + +function Save-VerifiedDownload { + param( + [Parameter(Mandatory = $true)][string]$Uri, + [Parameter(Mandatory = $true)][string]$OutFile, + [Parameter(Mandatory = $true)][string]$ExpectedSha1 + ) + + $parent = Split-Path -Parent $OutFile + New-Item -ItemType Directory -Force -Path $parent | Out-Null + + $tempFile = "$OutFile.download" + for ($attempt = 1; $attempt -le 5; $attempt++) { + try { + Remove-Item -Force -ErrorAction SilentlyContinue -LiteralPath $tempFile + Invoke-WebRequest -Uri $Uri -OutFile $tempFile -TimeoutSec 120 + + $actualSha1 = Get-UpperSha1 -Path $tempFile + if ($actualSha1 -ne $ExpectedSha1) { + throw "SHA1 mismatch for $Uri. Expected $ExpectedSha1, got $actualSha1." + } + + Move-Item -Force -LiteralPath $tempFile -Destination $OutFile + return + } catch { + Remove-Item -Force -ErrorAction SilentlyContinue -LiteralPath $tempFile + if ($attempt -eq 5) { + throw + } + + $delaySeconds = [Math]::Min(30, 5 * $attempt) + Write-Warning "Download attempt ${attempt} failed: $($_.Exception.Message). Retrying in ${delaySeconds}s." + Start-Sleep -Seconds $delaySeconds + } + } +} + +function Find-MissingFile { + param( + [Parameter(Mandatory = $true)][string]$Root, + [Parameter(Mandatory = $true)][string[]]$RelativePaths + ) + + foreach ($relativePath in $RelativePaths) { + if (-not (Test-Path -LiteralPath (Join-Path $Root $relativePath))) { + return $relativePath + } + } + + return $null +} + +if ([string]::IsNullOrWhiteSpace($env:LOCALAPPDATA)) { + throw "LOCALAPPDATA is required to resolve Tauri's Windows tool cache." +} + +$tauriToolsPath = Join-Path $env:LOCALAPPDATA "tauri" +$nsisPath = Join-Path $tauriToolsPath "NSIS" +$downloadRoot = if ([string]::IsNullOrWhiteSpace($env:RUNNER_TEMP)) { + [System.IO.Path]::GetTempPath() +} else { + $env:RUNNER_TEMP +} + +New-Item -ItemType Directory -Force -Path $tauriToolsPath | Out-Null + +$missingNsisFile = Find-MissingFile -Root $nsisPath -RelativePaths $nsisRequiredFiles +if ($missingNsisFile) { + Write-Host "Tauri NSIS cache is missing $missingNsisFile; downloading NSIS 3.11." + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue -LiteralPath $nsisPath + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue -LiteralPath (Join-Path $tauriToolsPath "nsis-3.11") + + $zipPath = Join-Path $downloadRoot "nsis-3.11.zip" + Save-VerifiedDownload -Uri $nsisUrl -OutFile $zipPath -ExpectedSha1 $nsisSha1 + Expand-Archive -Force -LiteralPath $zipPath -DestinationPath $tauriToolsPath + + $extractedNsisPath = Join-Path $tauriToolsPath "nsis-3.11" + if (-not (Test-Path -LiteralPath $extractedNsisPath)) { + throw "Downloaded NSIS archive did not contain the expected nsis-3.11 directory." + } + + Move-Item -Force -LiteralPath $extractedNsisPath -Destination $nsisPath +} else { + Write-Host "Tauri NSIS cache already contains NSIS 3.11." +} + +$tauriUtilsPath = Join-Path $nsisPath $tauriUtilsRelativePath +if (-not (Test-FileSha1 -Path $tauriUtilsPath -ExpectedSha1 $tauriUtilsSha1)) { + Write-Host "Downloading Tauri NSIS utility plugin." + Save-VerifiedDownload -Uri $tauriUtilsUrl -OutFile $tauriUtilsPath -ExpectedSha1 $tauriUtilsSha1 +} else { + Write-Host "Tauri NSIS utility plugin is already cached." +} + +$missingFile = Find-MissingFile -Root $nsisPath -RelativePaths ($nsisRequiredFiles + @($tauriUtilsRelativePath)) +if ($missingFile) { + throw "Tauri NSIS toolchain is incomplete after prefetch; missing $missingFile." +} + +Write-Host "Tauri NSIS toolchain ready at $nsisPath." diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..250e5e0 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,133 @@ +# CI/CD Setup + +## GitHub Actions Workflow + +Il workflow `ci.yml` esegue i seguenti check automatici: + +### 1. Tests +- Frontend: `pnpm test` +- Rust backend: `cargo test` + +### 2. Test Coverage +- Frontend: vitest con coverage reporting +- Upload automatico su Codecov dai report LCOV frontend + Rust +- Threshold configurabile in `vitest.config.ts` + +### 3. Code Health (CodeScene) +- Delta analysis su ogni PR/push +- Fail se il code health diminuisce +- Richiede secrets configurati (vedi sotto) + +### 4. Documentation Check +- Verifica che se cambia codice in `src/` o `src-tauri/`, anche `docs/` viene aggiornato +- **Warning only** — non blocca il merge, solo un reminder +- Skip con `[skip docs]` nel commit message +- Aggiorna docs solo se la modifica invalida architettura/astrazioni/design già documentati + +### 5. Lint & Format +- ESLint per frontend +- Clippy + rustfmt per Rust + +## Setup Required + +### CodeScene Secrets +Aggiungi questi secrets nel repository GitHub (Settings → Secrets → Actions): + +``` +CODESCENE_TOKEN= +CODESCENE_PROJECT_ID= +``` + +Il PAT di CodeScene è lo stesso che usi localmente (~/.codescene/token). +Il project ID lo trovi nella dashboard CodeScene. + +### Codecov Setup +- Installa/attiva il repo in Codecov una volta sola tramite GitHub App / import del repository. +- Nessun `CODECOV_TOKEN` richiesto in GitHub Actions: `ci.yml` usa OIDC (`id-token: write` + `use_oidc: true`). +- Il workflow carica `coverage/lcov.info` (Vitest) e `coverage/rust.lcov` (cargo-llvm-cov). +- L'action Codecov resta con integrity validation attiva. Se Codecov ruota la chiave GPG del CLI, aggiorna il pin dell'action invece di usare `skip_validation`. + +### Telemetry Secrets For Release Builds +Aggiungi anche questi secrets per i workflow `release.yml` e `release-stable.yml`: + +``` +VITE_SENTRY_DSN= +SENTRY_DSN= +VITE_POSTHOG_KEY= +VITE_POSTHOG_HOST=https://eu.i.posthog.com +``` + +Senza questi valori, i build distribuiti possono mantenere i toggle telemetry nelle Settings ma non inizializzare davvero PostHog/Sentry. + +### Windows Authenticode Secrets For Release Builds +Windows alpha e stable release builds usano sempre le firme Tauri updater. Se i secret Authenticode sono presenti, il workflow firma anche gli installer Windows e verifica le firme; se mancano, emette un warning e pubblica gli artifact Windows senza Authenticode finche' il certificato non e' pronto. + +``` +WINDOWS_CODE_SIGNING_CERTIFICATE= +WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD= +``` + +Opzionale: + +``` +WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT= +WINDOWS_CODE_SIGNING_TIMESTAMP_URL=https://timestamp.digicert.com +``` + +Il certificato deve essere un certificato di code signing trusted; un certificato self-signed non e' adatto per i release artifact pubblici. + +### Coverage Thresholds +Configura in `vitest.config.ts`: + +```typescript +export default defineConfig({ + test: { + coverage: { + lines: 80, + functions: 80, + branches: 80, + statements: 80, + // Fail CI se sotto threshold + thresholds: { + lines: 80, + functions: 80, + branches: 80, + statements: 80 + } + } + } +}) +``` + +## Local Testing + +Prima di pushare, puoi testare localmente: + +```bash +# Run all tests +pnpm test && cargo test + +# Check coverage +pnpm test:coverage + +# Lint +pnpm lint +cargo clippy +cargo fmt --check + +# CodeScene (local) +codescene delta-analysis --base-revision origin/main +``` + +## Workflow Triggers + +- **Push**: su `main` +- **Pull Request**: verso `main` +- **Manuale**: `workflow_dispatch` + +Nota: l'upload a Codecov gira su push a `main` e sulle PR dello stesso repository. Le PR da fork saltano l'upload per evitare problemi di permessi OIDC. + +## Status Checks + +Tutti i check devono passare prima di poter fare merge. +Se un check fallisce, vedrai il dettaglio nei logs di GitHub Actions. diff --git a/.github/workflows/auto-update-prs.yml b/.github/workflows/auto-update-prs.yml new file mode 100644 index 0000000..b055ad8 --- /dev/null +++ b/.github/workflows/auto-update-prs.yml @@ -0,0 +1,38 @@ +name: Auto-update PR branches + +# When main advances, automatically update all open PR branches +# so they stay up to date and can be auto-merged without manual rebase. + +on: + push: + branches: [main] + +jobs: + update-prs: + name: Update open PR branches + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Update all open PR branches + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Get all open PRs targeting main + PRS=$(gh pr list --base main --state open --json number,headRefName --jq '.[]') + + echo "$PRS" | while IFS= read -r pr; do + PR_NUM=$(echo "$pr" | jq -r '.number') + BRANCH=$(echo "$pr" | jq -r '.headRefName') + + echo "Updating PR #$PR_NUM ($BRANCH)..." + # GitHub native update — does a merge of main into the branch + gh pr update-branch "$PR_NUM" 2>&1 && echo "✅ #$PR_NUM updated" || echo "⚠️ #$PR_NUM skipped (already up to date or conflict)" + done diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..09af790 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,296 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + id-token: write + +env: + # Bump this when Tauri/Rust target artifacts capture stale absolute paths. + RUST_TARGET_CACHE_VERSION: v2026-04-14-tolaria + # Keep large production frontend builds below CI runner memory limits. + NODE_OPTIONS: --max-old-space-size=4096 + +jobs: + frontend-static-quality: + name: Frontend Static Quality Checks + runs-on: macos-15 + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for CodeScene + + - name: Setup pnpm + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + # Keep frontend and Rust quality gates in separate macOS jobs so the + # expensive Rust target cache restore no longer blocks the frontend lane. + # ── 0. Build check (catches type errors and bundler failures) ───────── + - name: TypeScript type check + run: pnpm exec tsc --noEmit + + - name: Vite build check + # TypeScript is checked explicitly above; run Vite directly here to avoid + # paying for the package build script's duplicate `tsc -b` pass. + run: pnpm exec vite build + + - name: Check whether docs build is needed + id: docs-changes + shell: bash + run: | + set -euo pipefail + BASE_SHA="${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}" + if [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ -z "$BASE_SHA" ] || [[ "$BASE_SHA" =~ ^0+$ ]]; then + echo "should-build=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + if git diff --name-only "$BASE_SHA" HEAD | grep -qE '^(docs/|site/|scripts/build-agent-docs\.mjs|package\.json|pnpm-lock\.yaml|\.github/workflows/ci\.yml)'; then + echo "should-build=true" >> "$GITHUB_OUTPUT" + else + echo "should-build=false" >> "$GITHUB_OUTPUT" + fi + + - name: Docs build check + if: steps.docs-changes.outputs.should-build == 'true' + run: pnpm docs:build + + # ── 1. Code Health (CodeScene — Hotspot + Average Code Health gates) ── + # Enforces minimum floors on BOTH hotspot and average code health. + # Thresholds come from .codescene-thresholds so CI and local hooks match. + - name: Code Health gates + env: + CODESCENE_PAT: ${{ secrets.CODESCENE_PAT }} + CODESCENE_PROJECT_ID: ${{ secrets.CODESCENE_PROJECT_ID }} + run: | + HOTSPOT_THRESHOLD=$(grep '^HOTSPOT_THRESHOLD=' .codescene-thresholds | cut -d= -f2) + AVERAGE_THRESHOLD=$(grep '^AVERAGE_THRESHOLD=' .codescene-thresholds | cut -d= -f2) + API_RESPONSE=$(curl -sf \ + -H "Authorization: Bearer $CODESCENE_PAT" \ + -H "Accept: application/json" \ + "https://api.codescene.io/v2/projects/$CODESCENE_PROJECT_ID") + HOTSPOT_SCORE=$(echo "$API_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['analysis']['hotspot_code_health']['now'])") + AVERAGE_SCORE=$(echo "$API_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['analysis']['code_health']['now'])") + echo "Hotspot Code Health: $HOTSPOT_SCORE (threshold: $HOTSPOT_THRESHOLD)" + echo "Average Code Health: $AVERAGE_SCORE (threshold: $AVERAGE_THRESHOLD)" + python3 -c " + hotspot = float('$HOTSPOT_SCORE') + average = float('$AVERAGE_SCORE') + ht = float('$HOTSPOT_THRESHOLD') + at = float('$AVERAGE_THRESHOLD') + failed = False + if hotspot < ht: + print(f'❌ Hotspot Code Health {hotspot:.2f} is below threshold {ht}') + failed = True + else: + print(f'✅ Hotspot Code Health {hotspot:.2f} ≥ {ht}') + if average < at: + print(f'❌ Average Code Health {average:.2f} is below threshold {at}') + failed = True + else: + print(f'✅ Average Code Health {average:.2f} ≥ {at}') + if failed: + exit(1) + " + + # ── 2. Documentation check (warning only — does not fail build) ─────── + - name: Check docs are updated + continue-on-error: true + run: | + if git log -1 --pretty=%B | grep -i '\[skip docs\]' > /dev/null; then + echo "⏭️ Documentation check skipped" + exit 0 + fi + if git diff --name-only origin/main | grep -E '^(src/|src-tauri/)' > /dev/null; then + if ! git diff --name-only origin/main | grep -E '^docs/' > /dev/null; then + echo "⚠️ Code files changed but docs/ not updated" + git diff --name-only origin/main | grep -E '^(src/|src-tauri/)' + echo "If this change affects architecture/abstractions/theme documented in docs/, update them." + echo "To suppress: include [skip docs] in your commit message." + fi + fi + echo "✅ Documentation check passed" + + # ── 3. Lint & format ────────────────────────────────────────────────── + - name: Lint frontend + run: pnpm lint + + frontend-tests: + name: Frontend Tests & Coverage + runs-on: macos-15 + + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + # The coverage command runs the canonical frontend test suite. + - name: Bundle MCP server resources (required by Tauri build) + run: node scripts/bundle-mcp-server.mjs + + - name: Frontend tests + coverage (≥70% lines/functions/branches/statements) + run: pnpm test:coverage + # Thresholds configured in vite.config.ts — exits non-zero if coverage drops + + - name: Upload frontend coverage to Codecov + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + uses: codecov/codecov-action@5975040f7f7d40edaff8d784b576fd65ae95c073 + with: + use_oidc: true + fail_ci_if_error: true + disable_search: true + files: ./coverage/lcov.info + flags: frontend + verbose: true + # OIDC avoids long-lived CODECOV_TOKEN secrets. + + rust-quality: + name: Rust Tests & Quality Checks + runs-on: macos-15 + + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 + with: + components: rustfmt, clippy, llvm-tools-preview + + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + src-tauri/target + key: ${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}- + + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@e5de28abeb52d916c5e5875d54b21a9e738b61ec + + - name: Rust tests + coverage (≥85% lines) + run: | + mkdir -p coverage + cargo llvm-cov \ + --manifest-path src-tauri/Cargo.toml \ + --ignore-filename-regex 'lib\.rs|main\.rs|menu\.rs' \ + --lcov \ + --output-path coverage/rust.lcov \ + --fail-under-lines 85 + # cargo-llvm-cov exits non-zero if line coverage drops below 85% + # lib.rs/main.rs/menu.rs are Tauri boilerplate -- not meaningfully unit-testable. + + - name: Upload Rust coverage to Codecov + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + uses: codecov/codecov-action@5975040f7f7d40edaff8d784b576fd65ae95c073 + with: + use_oidc: true + fail_ci_if_error: true + disable_search: true + files: ./coverage/rust.lcov + flags: rust + verbose: true + # OIDC avoids long-lived CODECOV_TOKEN secrets. + + - name: Clippy (Rust) + run: cargo clippy --manifest-path=src-tauri/Cargo.toml -- -D warnings + + - name: Format check (Rust) + run: cargo fmt --manifest-path=src-tauri/Cargo.toml -- --check + + linux-build: + name: Linux build verification + # Keep the normal push CI lane under the 10-minute target. The release + # workflows already perform the full Linux/AppImage build after main + # pushes, so this slower compatibility check stays available for PRs and + # manual diagnostics without blocking every direct push. + if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + + - name: Install Tauri Linux system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libsoup-3.0-dev \ + libxdo-dev \ + libssl-dev \ + libayatana-appindicator3-dev \ + libfuse2 \ + librsvg2-dev \ + patchelf \ + build-essential \ + file + + - name: Setup pnpm + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'pnpm' + + - name: Setup Rust + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 + with: + components: clippy + + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + src-tauri/target + key: ${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Frontend build + run: pnpm build + + - name: Cargo check + run: cargo check --manifest-path=src-tauri/Cargo.toml + + - name: Clippy + run: cargo clippy --manifest-path=src-tauri/Cargo.toml -- -D warnings diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..d82adb2 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,104 @@ +name: Deploy docs + +on: + push: + branches: [main] + paths: + - ".github/workflows/deploy-docs.yml" + - "package.json" + - "pnpm-lock.yaml" + - "scripts/build-agent-docs.mjs" + - "site/**" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +env: + NODE_OPTIONS: --max-old-space-size=4096 + +jobs: + build: + name: Build VitePress site + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: pnpm + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: + bun-version: latest + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build docs and download pages + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + pnpm docs:build + + DIST="site/.vitepress/dist" + mkdir -p "$DIST/alpha" "$DIST/stable" "$DIST/download" "$DIST/releases" "$DIST/stable/download" + + gh api -H "Accept: application/vnd.github.html+json" repos/${{ github.repository }}/releases --paginate > "$DIST/releases.json" + + STABLE_TAG="$(gh release list --repo ${{ github.repository }} --limit 100 --json tagName,isDraft,isPrerelease --jq '[.[] | select(.isDraft == false and .isPrerelease == false)][0].tagName // ""')" + if [ -n "$STABLE_TAG" ]; then + gh release download --repo ${{ github.repository }} "$STABLE_TAG" --pattern "stable-latest.json" --output "$DIST/stable/latest.json" || echo '{}' > "$DIST/stable/latest.json" + else + echo '{}' > "$DIST/stable/latest.json" + fi + + ALPHA_TAG="$(gh release list --repo ${{ github.repository }} --limit 100 --json tagName,isDraft,isPrerelease --jq '[.[] | select(.isDraft == false and .isPrerelease == true)][0].tagName // ""')" + if [ -n "$ALPHA_TAG" ]; then + gh release download --repo ${{ github.repository }} "$ALPHA_TAG" --pattern "alpha-latest.json" --output "$DIST/alpha/latest.json" || echo '{}' > "$DIST/alpha/latest.json" + else + echo '{}' > "$DIST/alpha/latest.json" + fi + + bun scripts/build-release-download-page.ts --latest-json "$DIST/stable/latest.json" --releases-json "$DIST/releases.json" --output-file "$DIST/download/index.html" + bun scripts/build-release-history-page.ts --releases-json "$DIST/releases.json" --output-file "$DIST/releases/index.html" + cp "$DIST/download/index.html" "$DIST/stable/download/index.html" + cp "$DIST/alpha/latest.json" "$DIST/latest.json" + cp "$DIST/alpha/latest.json" "$DIST/latest-canary.json" + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v4 + with: + path: site/.vitepress/dist + + deploy: + name: Deploy to GitHub Pages + needs: build + runs-on: ubuntu-24.04 + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release-build-artifacts.yml b/.github/workflows/release-build-artifacts.yml new file mode 100644 index 0000000..a5ffb6a --- /dev/null +++ b/.github/workflows/release-build-artifacts.yml @@ -0,0 +1,619 @@ +name: Release build artifacts + +on: + workflow_call: + inputs: + version: + required: true + type: string + macos_bundles: + required: false + type: string + default: "" + upload_macos_dmg: + required: true + type: boolean + +env: + # Bump this when Tauri/Rust target artifacts capture stale absolute paths. + RUST_TARGET_CACHE_VERSION: v2026-04-14-tolaria + # The production Vite bundle can exceed Node's default ~2GB heap on + # macOS arm64 runners while Tauri runs beforeBuildCommand. + NODE_OPTIONS: --max-old-space-size=4096 + +jobs: + build: + name: Build (${{ matrix.arch }}) + runs-on: macos-15 + strategy: + fail-fast: true + matrix: + include: + - arch: aarch64 + target: aarch64-apple-darwin + - arch: x86_64 + target: x86_64-apple-darwin + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "pnpm" + + - name: Setup Bun (required for bundle-qmd.sh) + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: + bun-version: latest + + - name: Setup Rust + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 + with: + targets: ${{ matrix.target }} + + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + src-tauri/target + key: ${{ runner.os }}-release-cargo-${{ matrix.target }}-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-release-cargo-${{ matrix.target }}-${{ env.RUST_TARGET_CACHE_VERSION }}- + + - name: Install frontend dependencies + run: pnpm install --frozen-lockfile + + - name: Clear cached bundle artifacts + run: | + rm -rf src-tauri/target/${{ matrix.target }}/release/bundle + + - name: Set version + run: | + VERSION="${{ inputs.version }}" + jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json + sed -i '' "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml + + - name: Import Apple Developer certificate into keychain + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + run: | + CERT_PATH="$RUNNER_TEMP/apple_cert.p12" + KEYCHAIN_PATH="$RUNNER_TEMP/laputa-signing.keychain-db" + KEYCHAIN_PASSWORD="$(uuidgen)" + echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH" + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" + security list-keychain -d user -s "$KEYCHAIN_PATH" + security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV" + + - name: Validate telemetry env + env: + VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }} + VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }} + run: | + python3 <<'PY' + import os + import re + import sys + from urllib.parse import urlparse + + DISALLOWED_PLACEHOLDERS = { + "", + "-", + "_", + "false", + "true", + "null", + "undefined", + "none", + "disabled", + } + + def normalize(name: str) -> str: + value = os.getenv(name, "").strip() + if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'): + value = value[1:-1].strip() + return value + + def normalize_http_like(value: str) -> str: + if "://" in value: + return value + return f"https://{value}" + + def normalize_hostname(hostname: str) -> str: + normalized = hostname.strip().rstrip('.').lower() + if normalized.startswith('[') and normalized.endswith(']'): + normalized = normalized[1:-1] + return normalized + + def is_ip_address(hostname: str) -> bool: + if re.fullmatch(r"(?:\d{1,3}\.){3}\d{1,3}", hostname): + return all(0 <= int(part) <= 255 for part in hostname.split('.')) + return ':' in hostname and re.fullmatch(r"[\da-f:]+", hostname, re.IGNORECASE) is not None + + def is_allowed_hostname(hostname: str) -> bool: + normalized = normalize_hostname(hostname) + if not normalized or normalized in DISALLOWED_PLACEHOLDERS: + return False + if normalized == 'localhost': + return True + return '.' in normalized or is_ip_address(normalized) + + def is_http_url(value: str) -> bool: + parsed = urlparse(normalize_http_like(value)) + return parsed.scheme in {"http", "https"} and is_allowed_hostname(parsed.hostname or "") + + values = { + name: normalize(name) + for name in ( + "VITE_SENTRY_DSN", + "SENTRY_DSN", + "VITE_POSTHOG_KEY", + "VITE_POSTHOG_HOST", + ) + } + errors = [] + + for name in ("VITE_SENTRY_DSN", "SENTRY_DSN", "VITE_POSTHOG_HOST"): + value = values[name] + if value.lower() in DISALLOWED_PLACEHOLDERS: + errors.append(f"{name} must be set to a real value, not a placeholder") + elif not is_http_url(value): + errors.append(f"{name} must be a valid http(s) URL with a non-placeholder host") + + if values["VITE_POSTHOG_KEY"].lower() in DISALLOWED_PLACEHOLDERS: + errors.append("VITE_POSTHOG_KEY must be set to a real project API key, not a placeholder") + + if errors: + print("Telemetry env validation failed:", file=sys.stderr) + for error in errors: + print(f"- {error}", file=sys.stderr) + raise SystemExit(1) + + print("Telemetry env validation passed.") + PY + + - name: Build Tauri app (with signing + notarization) + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }} + VITE_SENTRY_RELEASE: ${{ inputs.version }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }} + VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }} + run: | + MACOS_BUNDLES="${{ inputs.macos_bundles }}" + if [ -n "$MACOS_BUNDLES" ]; then + pnpm tauri build --target ${{ matrix.target }} --bundles "$MACOS_BUNDLES" + else + pnpm tauri build --target ${{ matrix.target }} + fi + + - name: Upload .dmg + if: ${{ inputs.upload_macos_dmg }} + uses: actions/upload-artifact@v4 + with: + name: dmg-${{ matrix.arch }} + path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg + retention-days: 1 + + - name: Upload updater artifacts (.tar.gz + .sig) + uses: actions/upload-artifact@v4 + with: + name: updater-${{ matrix.arch }} + path: | + src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz + src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz.sig + retention-days: 1 + + build-linux: + name: Build (linux-x86_64) + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + + - name: Install Tauri Linux system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libsoup-3.0-dev \ + libxdo-dev \ + libssl-dev \ + libayatana-appindicator3-dev \ + fcitx5-frontend-gtk3 \ + libfuse2 \ + librsvg2-dev \ + curl \ + wget \ + patchelf \ + build-essential \ + file \ + cpio \ + rpm + + - name: Setup pnpm + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "pnpm" + + - name: Setup Rust + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 + with: + targets: x86_64-unknown-linux-gnu + + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + src-tauri/target + key: ${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}- + + - name: Install frontend dependencies + run: pnpm install --frozen-lockfile + + - name: Clear cached bundle artifacts + run: | + rm -rf src-tauri/target/x86_64-unknown-linux-gnu/release/bundle + + - name: Set version + run: | + VERSION="${{ inputs.version }}" + jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json + sed -i "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml + + - name: Build Tauri app (Linux bundles) + env: + APPIMAGE_EXTRACT_AND_RUN: 1 + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }} + VITE_SENTRY_RELEASE: ${{ inputs.version }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }} + VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }} + run: | + pnpm tauri build --target x86_64-unknown-linux-gnu --bundles deb,rpm,appimage + + - name: Validate Linux bundles + run: | + shopt -s nullglob + appimages=( + src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage + ) + installers=( + "${appimages[@]}" + src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb + src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm + ) + signatures=( + src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig + src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig + src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig + ) + if [ ${#appimages[@]} -eq 0 ]; then + echo "::error::Linux build produced no AppImage bundle." + exit 1 + fi + if [ ${#installers[@]} -eq 0 ]; then + echo "::error::Linux build produced no AppImage, deb or rpm bundle." + exit 1 + fi + if [ ${#signatures[@]} -eq 0 ]; then + echo "::error::Linux build produced no updater signature (.sig) artifact." + exit 1 + fi + + validate_desktop_categories() { + local package_path="$1" + local extract_dir="$2" + + rm -rf "$extract_dir" + mkdir -p "$extract_dir" + + case "$package_path" in + *.deb) + dpkg-deb -x "$package_path" "$extract_dir" + ;; + *.rpm) + ( + cd "$extract_dir" + rpm2cpio "$GITHUB_WORKSPACE/$package_path" | cpio -id --quiet + ) + ;; + *) + echo "::error::Unsupported package format for desktop entry validation: $package_path" + exit 1 + ;; + esac + + mapfile -t desktop_files < <(find "$extract_dir/usr/share/applications" -type f -name "*.desktop" 2>/dev/null) + if [ ${#desktop_files[@]} -eq 0 ]; then + echo "::error::$package_path did not include a desktop entry under /usr/share/applications." + exit 1 + fi + + for desktop_file in "${desktop_files[@]}"; do + local categories + categories=$(grep "^Categories=" "$desktop_file" | cut -d= -f2- || true) + if [ -z "$categories" ]; then + echo "::error::$package_path has an empty Categories field in $(basename "$desktop_file")." + exit 1 + fi + if [[ "$categories" != *";" ]]; then + echo "::error::$package_path has a Categories field that is not semicolon-terminated: $categories" + exit 1 + fi + case ";$categories" in + *";Office;"*|*";Utility;"*) + ;; + *) + echo "::error::$package_path has an unexpected launcher category: $categories" + exit 1 + ;; + esac + done + } + + for deb in src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb; do + validate_desktop_categories "$deb" "$RUNNER_TEMP/tolaria-deb-desktop" + done + + for rpm_package in src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm; do + validate_desktop_categories "$rpm_package" "$RUNNER_TEMP/tolaria-rpm-desktop" + done + + - name: Upload Linux bundles + uses: actions/upload-artifact@v4 + with: + name: linux-x86_64-bundles + path: | + src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb + src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig + src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm + src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage + src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig + src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz + src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig + if-no-files-found: error + retention-days: 1 + + build-windows: + name: Build (windows-x86_64) + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "pnpm" + + - name: Setup Rust + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 + with: + targets: x86_64-pc-windows-msvc + + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~\.cargo\registry + ~\.cargo\git + src-tauri\target + key: ${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}- + + - name: Cache Tauri Windows tools + uses: actions/cache@v4 + with: + path: ~\AppData\Local\tauri + key: ${{ runner.os }}-tauri-tools-nsis-3.11-nsis-tauri-utils-0.5.3 + + - name: Prefetch Tauri NSIS toolchain + shell: pwsh + run: ./.github/scripts/prefetch-tauri-nsis.ps1 + + - name: Install frontend dependencies + run: pnpm install --frozen-lockfile + + - name: Clear cached Windows bundle artifacts + shell: pwsh + run: | + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue "src-tauri/target/x86_64-pc-windows-msvc/release/bundle" + + - name: Set version + shell: pwsh + run: | + $version = "${{ inputs.version }}" + $tauri = Get-Content "src-tauri/tauri.conf.json" | ConvertFrom-Json + $tauri.version = $version + $tauri | ConvertTo-Json -Depth 100 | Set-Content "src-tauri/tauri.conf.json" + (Get-Content "src-tauri/Cargo.toml") -replace '^version = ".*"$', "version = `"$version`"" | Set-Content "src-tauri/Cargo.toml" + + - name: Validate Windows release env + id: windows-signing + shell: bash + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + WINDOWS_CODE_SIGNING_CERTIFICATE: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE }} + WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD }} + WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} + WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} + run: | + for name in TAURI_SIGNING_PRIVATE_KEY TAURI_KEY_PASSWORD; do + if [ -z "${!name}" ]; then + echo "::error::$name is required to build signed Windows updater artifacts." + exit 1 + fi + done + + has_certificate=false + has_password=false + if [ -n "$WINDOWS_CODE_SIGNING_CERTIFICATE" ] || [ -n "$WINDOWS_CERTIFICATE" ]; then + has_certificate=true + fi + if [ -n "$WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD" ] || [ -n "$WINDOWS_CERTIFICATE_PASSWORD" ]; then + has_password=true + fi + + if [ "$has_certificate" != "$has_password" ]; then + echo "::error::Windows Authenticode signing is partially configured. Set both certificate and password secrets, or remove both to build with Tauri updater signatures only." + exit 1 + fi + + if [ "$has_certificate" = "true" ]; then + echo "authenticode_available=true" >> "$GITHUB_OUTPUT" + else + echo "::warning::Windows Authenticode certificate secrets are not configured. Building Windows artifacts without Authenticode signatures; Tauri updater signatures are still required." + echo "authenticode_available=false" >> "$GITHUB_OUTPUT" + fi + + - name: Prepare Windows Authenticode signing + if: ${{ steps.windows-signing.outputs.authenticode_available == 'true' }} + shell: pwsh + env: + WINDOWS_CODE_SIGNING_CERTIFICATE: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE }} + WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD }} + WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT }} + WINDOWS_CODE_SIGNING_TIMESTAMP_URL: ${{ secrets.WINDOWS_CODE_SIGNING_TIMESTAMP_URL }} + WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} + WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} + WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} + WINDOWS_TIMESTAMP_URL: ${{ secrets.WINDOWS_TIMESTAMP_URL }} + run: ./.github/scripts/configure-windows-authenticode.ps1 + + - name: Build Tauri app (Windows bundles) + shell: pwsh + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }} + VITE_SENTRY_RELEASE: ${{ inputs.version }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }} + VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }} + run: | + if ("${{ steps.windows-signing.outputs.authenticode_available }}" -eq "true") { + pnpm tauri build --target x86_64-pc-windows-msvc --bundles nsis --config src-tauri/tauri.windows-signing.conf.json + } else { + pnpm tauri build --target x86_64-pc-windows-msvc --bundles nsis + } + + - name: Validate Windows Authenticode signatures + if: ${{ steps.windows-signing.outputs.authenticode_available == 'true' }} + shell: pwsh + run: | + $expectedThumbprint = $env:WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT + if ([string]::IsNullOrWhiteSpace($expectedThumbprint)) { + throw "WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT was not exported by the signing setup step." + } + $expectedThumbprint = ($expectedThumbprint -replace "\s", "").ToUpperInvariant() + $paths = @() + $paths += Get-ChildItem -Path "src-tauri/target/x86_64-pc-windows-msvc/release" -Filter "*.exe" -File -ErrorAction SilentlyContinue + $paths += Get-ChildItem -Path "src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis" -Filter "*.exe" -File -ErrorAction SilentlyContinue + $paths += Get-ChildItem -Path "src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi" -Filter "*.msi" -File -ErrorAction SilentlyContinue + $paths = @($paths | Sort-Object FullName -Unique) + if ($paths.Count -eq 0) { + throw "No Windows executable or installer artifacts found to verify." + } + foreach ($path in $paths) { + $signature = Get-AuthenticodeSignature -FilePath $path.FullName + if ($signature.Status -ne "Valid") { + throw "Invalid Authenticode signature for $($path.FullName): $($signature.Status)" + } + if ($null -eq $signature.SignerCertificate) { + throw "Missing signer certificate for $($path.FullName)." + } + $actualThumbprint = ($signature.SignerCertificate.Thumbprint -replace "\s", "").ToUpperInvariant() + if ($actualThumbprint -ne $expectedThumbprint) { + throw "Unexpected signer thumbprint for $($path.FullName): $actualThumbprint" + } + Write-Host "Authenticode signature OK: $($path.FullName)" + } + + - name: Validate Windows bundles + shell: bash + run: | + shopt -s nullglob + installers=( + src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe + src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi + ) + signatures=( + src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe.sig + src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.nsis.zip.sig + src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig + src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.zip.sig + ) + if [ ${#installers[@]} -eq 0 ]; then + echo "::error::Windows build produced no installable NSIS or MSI bundle." + exit 1 + fi + for installer in "${installers[@]}"; do + if [[ "$(basename "$installer")" != *"${{ inputs.version }}"* ]]; then + echo "::error::Windows build produced an installer for a different version: $(basename "$installer")" + exit 1 + fi + done + if [ ${#signatures[@]} -eq 0 ]; then + echo "::error::Windows build produced no updater signature (.sig) artifact." + exit 1 + fi + + - name: Upload Windows bundles + uses: actions/upload-artifact@v4 + with: + name: windows-x86_64-bundles + path: | + src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe + src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe.sig + src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip + src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip.sig + src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi + src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig + src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip + src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip.sig + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/release-stable.yml b/.github/workflows/release-stable.yml new file mode 100644 index 0000000..bd843ca --- /dev/null +++ b/.github/workflows/release-stable.yml @@ -0,0 +1,300 @@ +name: Release (Stable) + +on: + push: + tags: + - 'stable-v*' + - 'v20*' + +concurrency: + group: release-stable-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ───────────────────────────────────────────────────────────── + # Phase 1: Compute the stable version string once + # ───────────────────────────────────────────────────────────── + version: + name: Compute stable version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.ver.outputs.version }} + display_version: ${{ steps.ver.outputs.display_version }} + tag: ${{ steps.ver.outputs.tag }} + steps: + - id: ver + shell: bash + run: | + python3 <<'PY' > version.env + import os + import re + from datetime import date + + tag = os.environ["GITHUB_REF_NAME"] + legacy_match = re.fullmatch(r"stable-v(\d{4})\.(\d{1,2})\.(\d{1,2})", tag) + date_match = re.fullmatch(r"v(\d{4})-(\d{2})-(\d{2})", tag) + + if date_match: + year, month, day = map(int, date_match.groups()) + date(year, month, day) + version = f"{year}.{month}.{day}" + display_version = tag + elif legacy_match: + year, month, day = map(int, legacy_match.groups()) + date(year, month, day) + version = f"{year}.{month}.{day}" + display_version = version + else: + raise SystemExit(f"Stable tags must use vYYYY-MM-DD or stable-vYYYY.M.D, got {tag}") + + print(f"version={version}") + print(f"display_version={display_version}") + print(f"tag={tag}") + PY + + cat version.env >> "$GITHUB_OUTPUT" + DISPLAY_VERSION=$(grep '^display_version=' version.env | cut -d= -f2-) + echo "### Stable version: \`$DISPLAY_VERSION\`" >> "$GITHUB_STEP_SUMMARY" + + # ------------------------------------------------------------- + # Phase 2: Build shared release artifacts + # ------------------------------------------------------------- + build-artifacts: + name: Build release artifacts + needs: version + uses: ./.github/workflows/release-build-artifacts.yml + with: + version: ${{ needs.version.outputs.version }} + macos_bundles: "" + upload_macos_dmg: true + secrets: inherit + + # ───────────────────────────────────────────────────────────── + # Phase 3: Publish GitHub Release + # ───────────────────────────────────────────────────────────── + release: + name: GitHub Release (stable) + needs: [version, build-artifacts] + runs-on: ubuntu-latest + permissions: + contents: write + pages: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + + - name: Normalize macOS release artifact names + run: | + normalize_macos_artifacts() { + local arch="$1" + local normalized_updater="$2" + local normalized_dmg="$3" + local updater_dir="updater-${arch}" + local updater_file + updater_file=$(find "$updater_dir" -maxdepth 1 -name "*.app.tar.gz" -print -quit) + if [ -z "$updater_file" ]; then + echo "::error::Missing macOS updater artifact in ${updater_dir}" >&2 + return 1 + fi + + local sig_file="${updater_file}.sig" + if [ ! -f "$sig_file" ]; then + echo "::error::Missing macOS updater signature for ${updater_file}" >&2 + return 1 + fi + + local normalized_sig="${normalized_updater}.sig" + if [ "$updater_file" != "$normalized_updater" ]; then + mv "$updater_file" "$normalized_updater" + fi + if [ "$sig_file" != "$normalized_sig" ]; then + mv "$sig_file" "$normalized_sig" + fi + + local dmg_dir="dmg-${arch}" + local dmg_file + dmg_file=$(find "$dmg_dir" -maxdepth 1 -name "*.dmg" -print -quit) + if [ -z "$dmg_file" ]; then + echo "::error::Missing macOS DMG artifact in ${dmg_dir}" >&2 + return 1 + fi + + if [ "$dmg_file" != "$normalized_dmg" ]; then + mv "$dmg_file" "$normalized_dmg" + fi + } + + normalize_macos_artifacts aarch64 \ + "updater-aarch64/Tolaria_${{ needs.version.outputs.version }}_macOS_Silicon.app.tar.gz" \ + "dmg-aarch64/Tolaria_${{ needs.version.outputs.version }}_macOS_Silicon.dmg" + normalize_macos_artifacts x86_64 \ + "updater-x86_64/Tolaria_${{ needs.version.outputs.version }}_macOS_Intel.app.tar.gz" \ + "dmg-x86_64/Tolaria_${{ needs.version.outputs.version }}_macOS_Intel.dmg" + + - name: Generate release notes + run: | + NOTES_FILE="release-notes/${{ needs.version.outputs.tag }}.md" + if [ -f "$NOTES_FILE" ]; then + cat "$NOTES_FILE" > release_notes.md + else + PREV_TAG=$(git for-each-ref --sort=-creatordate --format='%(refname:short)' refs/tags/v20* refs/tags/stable-v* | grep -vx "${{ needs.version.outputs.tag }}" | head -n 1 || echo "") + if [ -z "$PREV_TAG" ]; then + NOTES=$(git log --oneline --no-merges -20) + else + NOTES=$(git log --oneline --no-merges "${PREV_TAG}..${{ needs.version.outputs.tag }}") + fi + { + echo "## What's Changed" + echo "" + echo "$NOTES" | while IFS= read -r line; do echo "- $line"; done + } > release_notes.md + fi + { + echo "" + echo "---" + echo "**Stable release — manually promoted from \`main\`**" + echo "" + echo "**Includes macOS (Apple Silicon and Intel), Windows x64, and Linux x64 bundles**" + echo "" + echo "*Built from \`$(git rev-parse --short ${{ needs.version.outputs.tag }})\` on $(date -u +%Y-%m-%d)*" + } >> release_notes.md + + - name: Build stable-latest.json + run: | + VERSION="${{ needs.version.outputs.version }}" + TAG="${{ needs.version.outputs.tag }}" + REPO="${GITHUB_REPOSITORY}" + REPO_NAME="${REPO#*/}" + PAGES_URL="https://refactoringhq.github.io/${REPO_NAME}/" + + find_required() { + local patterns=("$@") + for pattern in "${patterns[@]}"; do + set -- $pattern + if [ -e "$1" ]; then + printf '%s\n' "$1" + return 0 + fi + done + echo "::error::Missing required artifact matching one of: ${patterns[*]}" >&2 + return 1 + } + + ARM_SIG_FILE=$(find_required "updater-aarch64/*.app.tar.gz.sig") + ARM_UPDATER_FILE="${ARM_SIG_FILE%.sig}" + ARM_SIG=$(cat "$ARM_SIG_FILE") + ARM_TARBALL=$(basename "$ARM_UPDATER_FILE") + ARM_DMG=$(basename "$(find_required "dmg-aarch64/*.dmg")") + + INTEL_SIG_FILE=$(find_required "updater-x86_64/*.app.tar.gz.sig") + INTEL_UPDATER_FILE="${INTEL_SIG_FILE%.sig}" + INTEL_SIG=$(cat "$INTEL_SIG_FILE") + INTEL_TARBALL=$(basename "$INTEL_UPDATER_FILE") + INTEL_DMG=$(basename "$(find_required "dmg-x86_64/*.dmg")") + + LINUX_SIG_FILE=$(find_required "linux-x86_64-bundles/*/*.AppImage.sig" "linux-x86_64-bundles/*/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*/*.deb.sig" "linux-x86_64-bundles/*.AppImage.sig" "linux-x86_64-bundles/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*.deb.sig") + LINUX_UPDATER_FILE="${LINUX_SIG_FILE%.sig}" + LINUX_SIG=$(cat "$LINUX_SIG_FILE") + LINUX_UPDATER=$(basename "$LINUX_UPDATER_FILE") + LINUX_DOWNLOAD=$(basename "$(find_required "linux-x86_64-bundles/*/*.AppImage" "linux-x86_64-bundles/*/*.deb" "linux-x86_64-bundles/*/*.AppImage.tar.gz" "linux-x86_64-bundles/*.AppImage" "linux-x86_64-bundles/*.deb" "linux-x86_64-bundles/*.AppImage.tar.gz")") + + WINDOWS_SIG_FILE=$(find_required "windows-x86_64-bundles/*/*-setup.exe.sig" "windows-x86_64-bundles/*/*.msi.sig" "windows-x86_64-bundles/*/*.nsis.zip.sig" "windows-x86_64-bundles/*/*.msi.zip.sig" "windows-x86_64-bundles/*-setup.exe.sig" "windows-x86_64-bundles/*.msi.sig" "windows-x86_64-bundles/*.nsis.zip.sig" "windows-x86_64-bundles/*.msi.zip.sig") + WINDOWS_UPDATER_FILE="${WINDOWS_SIG_FILE%.sig}" + WINDOWS_SIG=$(cat "$WINDOWS_SIG_FILE") + WINDOWS_UPDATER=$(basename "$WINDOWS_UPDATER_FILE") + WINDOWS_DOWNLOAD=$(basename "$(find_required "windows-x86_64-bundles/*/*-setup.exe" "windows-x86_64-bundles/*/*.msi" "windows-x86_64-bundles/*/*.nsis.zip" "windows-x86_64-bundles/*/*.msi.zip" "windows-x86_64-bundles/*-setup.exe" "windows-x86_64-bundles/*.msi" "windows-x86_64-bundles/*.nsis.zip" "windows-x86_64-bundles/*.msi.zip")") + + cat > stable-latest.json << EOF + { + "version": "${VERSION}", + "notes": "Stable release. See ${PAGES_URL} for full release notes.", + "pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", + "platforms": { + "darwin-aarch64": { + "signature": "${ARM_SIG}", + "url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_TARBALL}", + "dmg_url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_DMG}" + }, + "darwin-x86_64": { + "signature": "${INTEL_SIG}", + "url": "https://github.com/${REPO}/releases/download/${TAG}/${INTEL_TARBALL}", + "dmg_url": "https://github.com/${REPO}/releases/download/${TAG}/${INTEL_DMG}" + }, + "linux-x86_64": { + "signature": "${LINUX_SIG}", + "url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_UPDATER}", + "download_url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_DOWNLOAD}" + }, + "windows-x86_64": { + "signature": "${WINDOWS_SIG}", + "url": "https://github.com/${REPO}/releases/download/${TAG}/${WINDOWS_UPDATER}", + "download_url": "https://github.com/${REPO}/releases/download/${TAG}/${WINDOWS_DOWNLOAD}" + } + } + } + EOF + echo "stable-latest.json:"; cat stable-latest.json + + - name: Publish GitHub Release + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 + with: + tag_name: ${{ needs.version.outputs.tag }} + name: Tolaria ${{ needs.version.outputs.display_version }} + body_path: release_notes.md + draft: false + prerelease: false + files: | + dmg-aarch64/*.dmg + updater-aarch64/*.app.tar.gz + updater-aarch64/*.app.tar.gz.sig + dmg-x86_64/*.dmg + updater-x86_64/*.app.tar.gz + updater-x86_64/*.app.tar.gz.sig + linux-x86_64-bundles/*.deb + linux-x86_64-bundles/*.deb.sig + linux-x86_64-bundles/*.rpm + linux-x86_64-bundles/*.AppImage + linux-x86_64-bundles/*.AppImage.sig + linux-x86_64-bundles/*.AppImage.tar.gz + linux-x86_64-bundles/*.AppImage.tar.gz.sig + linux-x86_64-bundles/*/*.deb + linux-x86_64-bundles/*/*.deb.sig + linux-x86_64-bundles/*/*.rpm + linux-x86_64-bundles/*/*.AppImage + linux-x86_64-bundles/*/*.AppImage.sig + linux-x86_64-bundles/*/*.AppImage.tar.gz + linux-x86_64-bundles/*/*.AppImage.tar.gz.sig + windows-x86_64-bundles/*.exe + windows-x86_64-bundles/*.exe.sig + windows-x86_64-bundles/*.msi + windows-x86_64-bundles/*.msi.sig + windows-x86_64-bundles/*.zip + windows-x86_64-bundles/*.zip.sig + windows-x86_64-bundles/*/*.exe + windows-x86_64-bundles/*/*.exe.sig + windows-x86_64-bundles/*/*.msi + windows-x86_64-bundles/*/*.msi.sig + windows-x86_64-bundles/*/*.zip + windows-x86_64-bundles/*/*.zip.sig + stable-latest.json + + # ───────────────────────────────────────────────────────────── + # Phase 4: Trigger the main-branch GitHub Pages deployment + # ───────────────────────────────────────────────────────────── + pages: + name: Update docs and release pages + needs: [version, release] + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - name: Dispatch docs deployment from main + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh workflow run deploy-docs.yml --repo ${{ github.repository }} --ref main + echo "Triggered deploy-docs.yml on main after publishing ${{ needs.version.outputs.tag }}." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..fc911e3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,410 @@ +name: Release (Alpha) + +on: + push: + branches: + - main + paths-ignore: + - ".husky/**" + - ".github/workflows/deploy-docs.yml" + - ".github/workflows/release.yml" + - "site/**" + +concurrency: + group: release-alpha-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ───────────────────────────────────────────────────────────── + # Phase 1: Compute the alpha version string once + # Alpha builds use calendar semver and stay newer than the latest stable tag. + # ───────────────────────────────────────────────────────────── + version: + name: Compute alpha version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.ver.outputs.version }} + display_version: ${{ steps.ver.outputs.display_version }} + tag: ${{ steps.ver.outputs.tag }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - id: ver + shell: bash + run: | + python3 <<'PY' > version.env + import re + import subprocess + from datetime import datetime, timedelta, timezone + + def lines(command: list[str]) -> list[str]: + output = subprocess.check_output(command, text=True).strip() + return [line for line in output.splitlines() if line] + + alpha_pattern = re.compile(r"^alpha-v(\d{4}\.\d{1,2}\.\d{1,2})-alpha\.(\d+)$") + + def parse_alpha_tag(tag: str) -> tuple[str, int] | None: + match = alpha_pattern.fullmatch(tag) + if not match: + return None + calendar_version, sequence = match.groups() + return calendar_version, int(sequence) + + def alpha_version(calendar_version: str, sequence: int) -> str: + return f"{calendar_version}-alpha.{sequence}" + + def alpha_tag(calendar_version: str, sequence: int) -> str: + return f"alpha-v{calendar_version}-alpha.{sequence:04d}" + + existing_tags = [ + tag for tag in lines(["git", "tag", "--points-at", "HEAD"]) + if tag.startswith("alpha-v") + ] + + if existing_tags: + tag = existing_tags[0] + parsed = parse_alpha_tag(tag) + version = alpha_version(*parsed) if parsed is not None else tag.removeprefix("alpha-v") + else: + today = datetime.now(timezone.utc).date() + stable_date = None + stable_patterns = ( + re.compile(r"^v(\d{4})-(\d{2})-(\d{2})$"), + re.compile(r"^stable-v(\d{4})\.(\d{1,2})\.(\d{1,2})$"), + ) + + stable_tags = lines([ + "git", "for-each-ref", "--sort=-creatordate", "--format=%(refname:short)", + "refs/tags/v20*", "refs/tags/stable-v*", + ]) + + for stable_tag in stable_tags: + match = next((pattern.fullmatch(stable_tag) for pattern in stable_patterns if pattern.fullmatch(stable_tag)), None) + if not match: + continue + + year, month, day = map(int, match.groups()) + try: + stable_date = datetime(year, month, day, tzinfo=timezone.utc).date() + except ValueError: + continue + break + + alpha_date = today if stable_date is None or today > stable_date else stable_date + timedelta(days=1) + calendar_version = f"{alpha_date.year}.{alpha_date.month}.{alpha_date.day}" + sequence = len(lines(["git", "tag", "--list", f"alpha-v{calendar_version}-alpha.*"])) + 1 + + version = alpha_version(calendar_version, sequence) + tag = alpha_tag(calendar_version, sequence) + + display_match = re.fullmatch(r"(\d{4})\.(\d{1,2})\.(\d{1,2})-alpha\.(\d+)", version) + display_version = ( + f"Alpha {int(display_match.group(1))}.{int(display_match.group(2))}.{int(display_match.group(3))}.{int(display_match.group(4))}" + if display_match + else version + ) + + print(f"version={version}") + print(f"display_version={display_version}") + print(f"tag={tag}") + PY + + cat version.env >> "$GITHUB_OUTPUT" + VERSION=$(grep '^version=' version.env | cut -d= -f2-) + DISPLAY_VERSION=$(grep '^display_version=' version.env | cut -d= -f2-) + echo "### Alpha version: \`$DISPLAY_VERSION\` (\`$VERSION\`)" >> "$GITHUB_STEP_SUMMARY" + + # ------------------------------------------------------------- + # Phase 2: Build shared release artifacts + # ------------------------------------------------------------- + build-artifacts: + name: Build release artifacts + needs: version + uses: ./.github/workflows/release-build-artifacts.yml + with: + version: ${{ needs.version.outputs.version }} + macos_bundles: app + upload_macos_dmg: false + secrets: inherit + + # ───────────────────────────────────────────────────────────── + # Phase 3: Publish GitHub Release + # No lipo/re-signing — use the per-arch artifacts directly + # ───────────────────────────────────────────────────────────── + release: + name: GitHub Release (alpha) + needs: [version, build-artifacts] + runs-on: ubuntu-latest + permissions: + contents: write + pages: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + + - name: Normalize macOS updater artifact names + run: | + normalize_updater() { + local arch="$1" + local normalized_updater="$2" + local artifact_dir="updater-${arch}" + local updater_file + updater_file=$(find "$artifact_dir" -maxdepth 1 -name "*.app.tar.gz" -print -quit) + if [ -z "$updater_file" ]; then + echo "::error::Missing macOS updater artifact in ${artifact_dir}" >&2 + return 1 + fi + + local sig_file="${updater_file}.sig" + if [ ! -f "$sig_file" ]; then + echo "::error::Missing macOS updater signature for ${updater_file}" >&2 + return 1 + fi + + local normalized_sig="${normalized_updater}.sig" + if [ "$updater_file" != "$normalized_updater" ]; then + mv "$updater_file" "$normalized_updater" + fi + if [ "$sig_file" != "$normalized_sig" ]; then + mv "$sig_file" "$normalized_sig" + fi + } + + normalize_updater aarch64 "updater-aarch64/Tolaria_${{ needs.version.outputs.version }}_macOS_Silicon.app.tar.gz" + normalize_updater x86_64 "updater-x86_64/Tolaria_${{ needs.version.outputs.version }}_macOS_Intel.app.tar.gz" + + - name: Generate release notes + run: | + PREV_TAG=$(python3 <<'PY' + import re + import subprocess + + current_tag = '${{ needs.version.outputs.tag }}' + pattern = re.compile(r'^alpha-v(\d{4})\.(\d{1,2})\.(\d{1,2})-alpha\.(\d+)$') + + output = subprocess.check_output(['git', 'tag', '--list', 'alpha-v*'], text=True).strip() + tags = [line for line in output.splitlines() if line and line != current_tag] + + parsed_tags = [] + for tag in tags: + match = pattern.fullmatch(tag) + if not match: + continue + year, month, day, sequence = map(int, match.groups()) + parsed_tags.append(((year, month, day, sequence), tag)) + + print(max(parsed_tags)[1] if parsed_tags else '') + PY + ) + if [ -z "$PREV_TAG" ]; then + NOTES=$(git log --oneline --no-merges -20) + else + NOTES=$(git log --oneline --no-merges "${PREV_TAG}..HEAD") + fi + { + echo "## What's Changed (Alpha)" + echo "" + echo "$NOTES" | while IFS= read -r line; do echo "- $line"; done + echo "" + echo "---" + echo "**Alpha build — updated on every push to \`main\`**" + echo "" + echo "**Includes macOS (Apple Silicon and Intel), Linux x64, and Windows x64 bundles**" + echo "" + echo "*Built from \`$(git rev-parse --short HEAD)\` on $(date -u +%Y-%m-%d)*" + } > release_notes.md + + - name: Build alpha-latest.json + run: | + VERSION="${{ needs.version.outputs.version }}" + TAG="${{ needs.version.outputs.tag }}" + REPO="${GITHUB_REPOSITORY}" + REPO_NAME="${REPO#*/}" + PAGES_URL="https://refactoringhq.github.io/${REPO_NAME}/" + + find_required() { + for pattern in "$@"; do + set -- $pattern + if [ -e "$1" ]; then + printf '%s\n' "$1" + return 0 + fi + done + return 1 + } + + ARM_SIG_FILE=$(find_required "updater-aarch64/*.app.tar.gz.sig") + ARM_UPDATER_FILE="${ARM_SIG_FILE%.sig}" + ARM_SIG=$(cat "$ARM_SIG_FILE") + ARM_UPDATER=$(basename "$ARM_UPDATER_FILE") + + INTEL_SIG_FILE=$(find_required "updater-x86_64/*.app.tar.gz.sig") + INTEL_UPDATER_FILE="${INTEL_SIG_FILE%.sig}" + INTEL_SIG=$(cat "$INTEL_SIG_FILE") + INTEL_UPDATER=$(basename "$INTEL_UPDATER_FILE") + + LINUX_SIG_FILE=$(find_required "linux-x86_64-bundles/*/*.AppImage.sig" "linux-x86_64-bundles/*/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*/*.deb.sig" "linux-x86_64-bundles/*.AppImage.sig" "linux-x86_64-bundles/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*.deb.sig") + LINUX_UPDATER_FILE="${LINUX_SIG_FILE%.sig}" + LINUX_SIG=$(cat "$LINUX_SIG_FILE") + LINUX_UPDATER=$(basename "$LINUX_UPDATER_FILE") + LINUX_DOWNLOAD=$(basename "$(find_required "linux-x86_64-bundles/*/*.AppImage" "linux-x86_64-bundles/*/*.deb" "linux-x86_64-bundles/*/*.AppImage.tar.gz" "linux-x86_64-bundles/*.AppImage" "linux-x86_64-bundles/*.deb" "linux-x86_64-bundles/*.AppImage.tar.gz")") + + WINDOWS_SIG_FILE=$(find_required "windows-x86_64-bundles/*/*-setup.exe.sig" "windows-x86_64-bundles/*/*.msi.sig" "windows-x86_64-bundles/*/*.nsis.zip.sig" "windows-x86_64-bundles/*/*.msi.zip.sig" "windows-x86_64-bundles/*-setup.exe.sig" "windows-x86_64-bundles/*.msi.sig" "windows-x86_64-bundles/*.nsis.zip.sig" "windows-x86_64-bundles/*.msi.zip.sig") + WINDOWS_UPDATER_FILE="${WINDOWS_SIG_FILE%.sig}" + WINDOWS_SIG=$(cat "$WINDOWS_SIG_FILE") + WINDOWS_UPDATER=$(basename "$WINDOWS_UPDATER_FILE") + WINDOWS_DOWNLOAD=$(basename "$(find_required "windows-x86_64-bundles/*/*-setup.exe" "windows-x86_64-bundles/*/*.msi" "windows-x86_64-bundles/*/*.nsis.zip" "windows-x86_64-bundles/*/*.msi.zip" "windows-x86_64-bundles/*-setup.exe" "windows-x86_64-bundles/*.msi" "windows-x86_64-bundles/*.nsis.zip" "windows-x86_64-bundles/*.msi.zip")") + + cat > alpha-latest.json << EOF + { + "version": "${VERSION}", + "notes": "Alpha build. See ${PAGES_URL} for full release notes.", + "pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", + "platforms": { + "darwin-aarch64": { + "signature": "${ARM_SIG}", + "url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_UPDATER}", + "download_url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_UPDATER}" + }, + "darwin-x86_64": { + "signature": "${INTEL_SIG}", + "url": "https://github.com/${REPO}/releases/download/${TAG}/${INTEL_UPDATER}", + "download_url": "https://github.com/${REPO}/releases/download/${TAG}/${INTEL_UPDATER}" + }, + "linux-x86_64": { + "signature": "${LINUX_SIG}", + "url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_UPDATER}", + "download_url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_DOWNLOAD}" + }, + "windows-x86_64": { + "signature": "${WINDOWS_SIG}", + "url": "https://github.com/${REPO}/releases/download/${TAG}/${WINDOWS_UPDATER}", + "download_url": "https://github.com/${REPO}/releases/download/${TAG}/${WINDOWS_DOWNLOAD}" + } + } + } + EOF + echo "alpha-latest.json:"; cat alpha-latest.json + + - name: Publish GitHub Release + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 + with: + tag_name: ${{ needs.version.outputs.tag }} + name: Tolaria ${{ needs.version.outputs.display_version }} + body_path: release_notes.md + draft: false + prerelease: true + files: | + updater-aarch64/*.app.tar.gz + updater-aarch64/*.app.tar.gz.sig + updater-x86_64/*.app.tar.gz + updater-x86_64/*.app.tar.gz.sig + linux-x86_64-bundles/*.deb + linux-x86_64-bundles/*.deb.sig + linux-x86_64-bundles/*.rpm + linux-x86_64-bundles/*.AppImage + linux-x86_64-bundles/*.AppImage.sig + linux-x86_64-bundles/*.AppImage.tar.gz + linux-x86_64-bundles/*.AppImage.tar.gz.sig + linux-x86_64-bundles/*/*.deb + linux-x86_64-bundles/*/*.deb.sig + linux-x86_64-bundles/*/*.rpm + linux-x86_64-bundles/*/*.AppImage + linux-x86_64-bundles/*/*.AppImage.sig + linux-x86_64-bundles/*/*.AppImage.tar.gz + linux-x86_64-bundles/*/*.AppImage.tar.gz.sig + windows-x86_64-bundles/*.exe + windows-x86_64-bundles/*.exe.sig + windows-x86_64-bundles/*.msi + windows-x86_64-bundles/*.msi.sig + windows-x86_64-bundles/*.zip + windows-x86_64-bundles/*.zip.sig + windows-x86_64-bundles/*/*.exe + windows-x86_64-bundles/*/*.exe.sig + windows-x86_64-bundles/*/*.msi + windows-x86_64-bundles/*/*.msi.sig + windows-x86_64-bundles/*/*.zip + windows-x86_64-bundles/*/*.zip.sig + alpha-latest.json + + # ───────────────────────────────────────────────────────────── + # Phase 4: Update GitHub Pages with docs, release history, and download assets + # ───────────────────────────────────────────────────────────── + pages: + name: Update docs and release pages + needs: [version, release] + runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + concurrency: + group: github-pages + cancel-in-progress: false + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'pnpm' + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: + bun-version: latest + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build docs and release pages + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VITEPRESS_BASE="/" pnpm docs:build + mkdir -p _site/alpha _site/stable _site/release-notes + cp -R site/.vitepress/dist/. _site/ + if [ -d release-notes ]; then cp release-notes/*.md _site/release-notes/ 2>/dev/null || true; fi + gh api -H "Accept: application/vnd.github.html+json" repos/${{ github.repository }}/releases --paginate > _site/releases.json + STABLE_TAG=$(gh release list --repo ${{ github.repository }} --exclude-drafts --exclude-pre-releases --limit 1 --json tagName --jq '.[0].tagName // ""') + + gh release download --repo ${{ github.repository }} "${{ needs.version.outputs.tag }}" --pattern "alpha-latest.json" --output _site/alpha/latest.json || echo '{}' > _site/alpha/latest.json + if [ -n "$STABLE_TAG" ]; then + gh release download --repo ${{ github.repository }} "$STABLE_TAG" --pattern "stable-latest.json" --output _site/stable/latest.json || echo '{}' > _site/stable/latest.json + else + echo '{}' > _site/stable/latest.json + fi + bun scripts/build-release-download-page.ts --latest-json _site/stable/latest.json --releases-json _site/releases.json --output-file _site/stable/download/index.html + bun scripts/build-release-history-page.ts --releases-json _site/releases.json --output-file _site/releases/index.html + mkdir -p _site/download + cp _site/stable/download/index.html _site/download/index.html + + cp _site/alpha/latest.json _site/latest.json + cp _site/alpha/latest.json _site/latest-canary.json + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v4 + with: + path: ./_site + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a89e22a --- /dev/null +++ b/.gitignore @@ -0,0 +1,81 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +site/.vitepress/cache/ +site/.vitepress/dist/ +_site/ +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Playwright +/test-results/ +/playwright-report/ + +# Coverage reports +/coverage/ + +# Demo vault and helper scripts +demo-vault/ +generated-fixtures/ +select_demo_notes*.py +final_selection.py + +# Claude Code task signals +.claude-done +.claude-blocked +src-tauri/target + +# Generated mcp-server bundle (built by scripts/bundle-mcp-server.mjs) +src-tauri/resources/mcp-server/ + +# Python cache +__pycache__/ +*.py[cod] + +# Dev screenshots +screenshots/ + +# Stale planning docs (keep locally if needed, not in repo) +REDESIGN-PLAN.md +SF-SYMBOLS-MIGRATION.md +CODE-HEALTH-REPORT.md + +# Local home dir artifact from worktree ops +(HOME)/ + +# Runtime / process files +.claude-pid + +# Generated vault index files (qmd/search artifacts) +.laputa-index.json + +# Tauri signing keys (never commit private keys) +*.key +*.key.pub + +# Local environment variables (never commit) +.env +.env.local +.env.*.local + +# Local Codacy CLI runtime/config generated by the MCP server +.codacy/ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..c3bf579 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,55 @@ +#!/bin/sh +# Pre-commit: fast local lint gate before commit. Full suite runs in pre-push/CI. +set -e + +ensure_node_tooling() { + if command -v node >/dev/null 2>&1 && command -v pnpm >/dev/null 2>&1; then + return 0 + fi + + NVM_DIR="${NVM_DIR:-$HOME/.nvm}" + if [ -s "$NVM_DIR/nvm.sh" ]; then + # shellcheck disable=SC1090 + . "$NVM_DIR/nvm.sh" --no-use + nvm use --silent node >/dev/null 2>&1 || true + fi + + if ! command -v node >/dev/null 2>&1 || ! command -v pnpm >/dev/null 2>&1; then + echo "❌ node and pnpm must be available before committing" + echo " Install them or make sure your nvm setup is available to git hooks." + exit 1 + fi +} + +ensure_node_tooling + +echo "🔍 Pre-commit checks..." + +STAGED_FILES=$(git diff --cached --name-only) +APP_CHANGED=false + +for FILE in $STAGED_FILES; do + case "$FILE" in + .github/workflows/*|.husky/*|docs/*|*.md) + ;; + *) + APP_CHANGED=true + ;; + esac +done + +if [ "$APP_CHANGED" = false ]; then + echo " → app checks skipped (docs/workflow/hooks only)" + echo "✅ Pre-commit passed" + exit 0 +fi + +# Lint only when frontend source files are staged. Typecheck and test coverage +# run in the pre-push gate. +STAGED_LINTABLE=$(echo "$STAGED_FILES" | grep -E '\.(ts|tsx|js|jsx|mjs)$' || true) +if [ -n "$STAGED_LINTABLE" ]; then + echo " → lint..." + pnpm lint --quiet +fi + +echo "✅ Pre-commit passed" diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..1b917b0 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,338 @@ +#!/bin/sh +# Pre-push: full CI checks run locally before any push. +# This replaces remote CI for normal task pushes. +# DO NOT skip with --no-verify (Claude Code is configured to never do this). +# +# ── Optimizations (Feb 2026) ───────────────────────────────────────────── +# +# 1. --no-clean on cargo llvm-cov: reuses previous instrumented build +# artifacts for incremental compilation. Reduces Rust coverage from +# ~8 min (full recompile) to ~30-60s (incremental rebuild). +# +# 2. Change detection: skips Rust checks entirely when no files under +# src-tauri/ changed. Saves ~1-2 min on frontend-only pushes. +# +# 3. Merged redundant test runs: frontend coverage (step 2) already runs +# all tests, so the separate test step was removed. +# +# 4. Fast-fail ordering: within Rust checks, fast lints (fmt ~2s, clippy +# ~15s) run before slow coverage (~30-60s) for quicker feedback. +# +# 5. Force full coverage: set LAPUTA_FULL_COVERAGE=1 to run cargo llvm-cov +# without --no-clean (clean rebuild, accurate baseline). +# +# Expected times (warm cache, incremental): +# Frontend only: ~1 min +# Frontend+Rust: ~2-3 min +# Full coverage: ~9-10 min (with LAPUTA_FULL_COVERAGE=1) +# ───────────────────────────────────────────────────────────────────────── +set -e + +ensure_cargo_tooling() { + if command -v cargo >/dev/null 2>&1; then + return 0 + fi + + if [ -s "$HOME/.cargo/env" ]; then + # shellcheck disable=SC1091 + . "$HOME/.cargo/env" + fi + + if ! command -v cargo >/dev/null 2>&1; then + echo "❌ cargo must be available before pushing" + echo " Install Rust via https://rustup.rs or ensure ~/.cargo/bin is in PATH." + exit 1 + fi +} + +ensure_node_tooling() { + if command -v node >/dev/null 2>&1 && command -v pnpm >/dev/null 2>&1; then + return 0 + fi + + NVM_DIR="${NVM_DIR:-$HOME/.nvm}" + if [ -s "$NVM_DIR/nvm.sh" ]; then + # shellcheck disable=SC1090 + . "$NVM_DIR/nvm.sh" --no-use + nvm use --silent node >/dev/null 2>&1 || true + fi + + if ! command -v node >/dev/null 2>&1 || ! command -v pnpm >/dev/null 2>&1; then + echo "❌ node and pnpm must be available before pushing" + echo " Install them or make sure your nvm setup is available to git hooks." + exit 1 + fi +} + +require_main_push() { + CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) + if [ "$CURRENT_BRANCH" != "main" ] && [ "$CURRENT_BRANCH" != "HEAD" ]; then + echo "❌ Pushes must happen from main or a detached HEAD that is pushed directly to main. Current branch: $CURRENT_BRANCH" + exit 1 + fi + + while IFS=' ' read -r LOCAL_REF LOCAL_SHA REMOTE_REF REMOTE_SHA; do + [ -z "$LOCAL_REF" ] && continue + + case "$LOCAL_REF:$REMOTE_REF" in + refs/heads/main:refs/heads/main) + ;; + HEAD:refs/heads/main) + ;; + refs/tags/*:refs/tags/*) + ;; + *) + echo "❌ Pushes must be main -> main only." + echo " Attempted: ${LOCAL_REF:-} -> ${REMOTE_REF:-}" + exit 1 + ;; + esac + done </dev/null || echo "") +RUST_CHANGED=true +APP_CHANGED=true +SITE_CHANGED=false + +if [ -n "$PUSH_TARGET" ]; then + CHANGED=$(git diff --name-only "$PUSH_TARGET"..HEAD) + APP_CHANGED=false + if ! echo "$CHANGED" | grep -qE '^(src-tauri/|Cargo)'; then + RUST_CHANGED=false + fi + for FILE in $CHANGED; do + case "$FILE" in + site/*) + SITE_CHANGED=true + ;; + .github/workflows/*|.husky/*|docs/*|*.md) + ;; + *) + APP_CHANGED=true + ;; + esac + done +fi + +if [ "$APP_CHANGED" = false ]; then + if [ "$SITE_CHANGED" = true ]; then + echo "" + echo "📚 Docs-only push detected; running docs build..." + pnpm docs:build + echo " ✅ Docs build OK" + else + echo "" + echo "⏭️ App checks skipped (docs/workflow/hooks only)" + fi + ELAPSED=$(($(date +%s) - START_TIME)) + echo "" + echo "✅ Pre-push passed in ${ELAPSED}s" + exit 0 +fi + +run_sidecar_automatic_checks() { + if [ "${LAPUTA_PREPUSH_LOCAL:-0}" = "1" ]; then + echo "☁️ Chunk sidecar checks disabled by LAPUTA_PREPUSH_LOCAL=1" + return 1 + fi + + if ! command -v bash >/dev/null 2>&1; then + echo "☁️ bash not found; falling back to local automatic checks" + return 1 + fi + + echo "" + echo "☁️ Running automatic checks on Chunk sidecar..." + if bash .chunk/run-sidecar-gates-local.sh "$RUST_CHANGED"; then + echo " ✅ Chunk sidecar automatic checks OK" + return 0 + else + SIDECAR_STATUS=$? + fi + + if [ "$SIDECAR_STATUS" -eq 86 ]; then + echo " ⚠️ Chunk sidecar unavailable; falling back to local automatic checks" + return 1 + fi + + echo " ❌ Chunk sidecar automatic checks FAILED" + exit "$SIDECAR_STATUS" +} + +if ! run_sidecar_automatic_checks; then + ensure_cargo_tooling + + # ── 0. Frontend lint ─────────────────────────────────────────────────── + echo "" + echo "🔎 [0/6] Frontend lint..." + pnpm lint + echo " ✅ Lint OK" + + # ── 1. TypeScript + Vite build ────────────────────────────────────────── + echo "" + echo "📦 [1/6] TypeScript + Vite build..." + pnpm build + echo " ✅ Build OK" + + # ── 2. Frontend coverage (≥70%) — includes all unit tests ─────────────── + echo "" + echo "📊 [2/6] Frontend tests + coverage (≥70%)..." + FRONTEND_COVERAGE_CONCURRENCY="${FRONTEND_COVERAGE_CONCURRENCY:-1}" \ + node scripts/run-vitest-coverage-shards.mjs --silent + echo " ✅ Frontend coverage OK" + + # ── 3. Rust lint (clippy + fmt) — fast, run before coverage ───────────── + echo "" + if [ "$RUST_CHANGED" = true ]; then + echo "🔧 [3/6] Clippy + rustfmt..." + cargo clippy --manifest-path=src-tauri/Cargo.toml -- -D warnings + cargo fmt --manifest-path=src-tauri/Cargo.toml -- --check + echo " ✅ Rust lint OK" + else + echo "⏭️ [3/6] Rust lint — skipped (no src-tauri/ changes)" + fi + + # ── 4. Rust coverage (≥85% lines) ────────────────────────────────────── + echo "" + if [ "$RUST_CHANGED" = true ]; then + LLVM_COV_FLAGS="--no-clean" + if [ "${LAPUTA_FULL_COVERAGE:-0}" = "1" ]; then + LLVM_COV_FLAGS="" + echo "🦀 [4/6] Rust coverage — FULL (LAPUTA_FULL_COVERAGE=1)..." + else + echo "🦀 [4/6] Rust coverage (≥85%, incremental)..." + fi + # Unset GIT_DIR so git tests create isolated repos without inheriting hook context + unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE + # shellcheck disable=SC2086 + cargo llvm-cov \ + --manifest-path src-tauri/Cargo.toml \ + $LLVM_COV_FLAGS \ + --ignore-filename-regex "lib\.rs|main\.rs|menu\.rs" \ + --fail-under-lines 85 \ + -- --test-threads=1 + echo " ✅ Rust coverage OK" + else + echo "⏭️ [4/6] Rust coverage — skipped (no src-tauri/ changes)" + fi + + # ── 5. Playwright core smoke lane (if any exist) ────────────────────── + echo "" + SMOKE_FILES=$(find tests/smoke tests/integration -name '*.spec.ts' 2>/dev/null | head -1) + if [ -n "$SMOKE_FILES" ]; then + echo "🎭 [5/6] Playwright core smoke tests..." + if ! pnpm playwright:smoke; then + echo " ❌ Core smoke tests FAILED" + exit 1 + fi + echo " ✅ Core smoke tests OK" + else + echo "⏭️ [5/6] Playwright core smoke tests — skipped (no tests/**/*.spec.ts)" + fi +fi + +# ── 6. CodeScene code health gate (ratchet) ────────────────────────────── +# Thresholds live in .codescene-thresholds and only ever go UP (ratchet). +# If remote scores improved, the hook updates the file and stops so the new +# floor is committed with normal verified hooks before the next push. +# If the remote baseline is already below threshold, allow recovery pushes to +# land; otherwise the stale remote score would block the refactors required to +# restore the gate. +THRESHOLDS_FILE="$(git rev-parse --show-toplevel)/.codescene-thresholds" +HOTSPOT_MIN=9.45 +AVERAGE_MIN=9.29 +if [ -f "$THRESHOLDS_FILE" ]; then + HOTSPOT_MIN=$(grep HOTSPOT_THRESHOLD "$THRESHOLDS_FILE" | cut -d= -f2) + AVERAGE_MIN=$(grep AVERAGE_THRESHOLD "$THRESHOLDS_FILE" | cut -d= -f2) +fi + +echo "" +echo "🏥 [6/6] CodeScene code health (Hotspot ≥${HOTSPOT_MIN} + Average ≥${AVERAGE_MIN})..." +if [ -z "$CODESCENE_PAT" ] || [ -z "$CODESCENE_PROJECT_ID" ]; then + echo " ⚠️ CODESCENE_PAT or CODESCENE_PROJECT_ID not set — skipping" +else + API_RESPONSE=$(curl -sf \ + -H "Authorization: Bearer $CODESCENE_PAT" \ + -H "Accept: application/json" \ + "https://api.codescene.io/v2/projects/$CODESCENE_PROJECT_ID" 2>/dev/null || echo "{}") + HOTSPOT_SCORE=$(echo "$API_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['analysis']['hotspot_code_health']['now'])" 2>/dev/null || echo "") + AVERAGE_SCORE=$(echo "$API_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['analysis']['code_health']['now'])" 2>/dev/null || echo "") + if [ -z "$HOTSPOT_SCORE" ] || [ -z "$AVERAGE_SCORE" ]; then + echo " ⚠️ Could not fetch remote scores — skipping (CI will enforce)" + else + echo " Remote Hotspot Code Health: $HOTSPOT_SCORE (threshold: $HOTSPOT_MIN)" + echo " Remote Average Code Health: $AVERAGE_SCORE (threshold: $AVERAGE_MIN)" + PYTHON_STATUS=0 + python3 -c " +import sys + +hotspot = float('$HOTSPOT_SCORE') +average = float('$AVERAGE_SCORE') +hotspot_min = float('$HOTSPOT_MIN') +average_min = float('$AVERAGE_MIN') +failed = False + +if hotspot < hotspot_min: + print(f'WARN: Hotspot Code Health {hotspot:.2f} < {hotspot_min} — remote baseline is currently red') + failed = True +else: + print(f'OK: Hotspot {hotspot:.2f} >= {hotspot_min}') + +if average < average_min: + print(f'WARN: Average Code Health {average:.2f} < {average_min} — remote baseline is currently red') + failed = True +else: + print(f'OK: Average {average:.2f} >= {average_min}') + +if failed: + print(' ⚠️ Recovery mode: allowing this push so refactors can land and restore the gate on a later analysis.') + sys.exit(0) + +import math +thresholds_file = '$THRESHOLDS_FILE' +new_hotspot = max(hotspot_min, math.floor(hotspot * 100) / 100) +new_average = max(average_min, math.floor(average * 100) / 100) +if new_hotspot > hotspot_min or new_average > average_min: + with open(thresholds_file, 'w') as f: + f.write(f'HOTSPOT_THRESHOLD={new_hotspot}\nAVERAGE_THRESHOLD={new_average}\n') + print(f' 📈 Ratchet updated: Hotspot {hotspot_min} → {new_hotspot}, Average {average_min} → {new_average}') + sys.exit(3) +" || PYTHON_STATUS=$? + if [ "$PYTHON_STATUS" -ne 0 ] && [ "$PYTHON_STATUS" -ne 3 ]; then + exit "$PYTHON_STATUS" + fi + if [ "$PYTHON_STATUS" -eq 3 ]; then + git add "$THRESHOLDS_FILE" + echo " ❌ Commit the updated .codescene-thresholds with a normal verified commit, then push again." + exit 1 + fi + fi +fi + +END_TIME=$(date +%s) +ELAPSED=$((END_TIME - START_TIME)) +MINUTES=$((ELAPSED / 60)) +SECONDS=$((ELAPSED % 60)) + +echo "" +echo "================================================" +echo "✅ All checks passed — pushing (${MINUTES}m ${SECONDS}s)" +echo "" diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 0000000..f79744f --- /dev/null +++ b/.semgrepignore @@ -0,0 +1,10 @@ +tests/ +e2e/ +node_modules/ +dist/ +coverage/ +test-results/ +src-tauri/target/ +target/ +src-tauri/gen/apple/assets/mcp-server/index.js +src-tauri/gen/apple/assets/mcp-server/ws-bridge.js diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0181340 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,194 @@ +# AGENTS.md — Tolaria App + +## 1. Development Process + +### Start working on a task + +**Before writing a single line of code:** run `mcp__codescene__code_health_score` to check the current codebase health against `.codescene-thresholds`. If the score is already below the threshold, **stop and refactor first** — find the worst files with the MCP, improve them, commit, then start the task. Never start feature work on a codebase that is already below the gate. + +- Read task description and all comments fully +- For To Rework: the ❌ QA failed comment tells you exactly what to fix +- Check `docs/adr/` for relevant architecture decisions before structural choices +- Check `docs/ARCHITECTURE.md` and `docs/ABSTRACTIONS.md` for relevant structural information +- For UI tasks: study app visual language and components first. Prioritize reusing existing components, assets, and variables over recreating them. +- If working on a Todoist task, add a comment: `🚀 Starting work on this task. [Brief description of approach]` + +### Commits & pushes + +- Local work may happen on `main`, in detached HEAD worktrees, or in other temporary local states. The production path is still direct-to-main: final verified work is pushed to `origin/main`, with no PR branch flow. +- Commit every 20–30 min: `feat:`, `fix:`, `refactor:`, `test:`, `docs:` +- Pre-commit is a lightweight lint gate only. Pre-push runs the full check suite (build + tests + coverage + core Playwright smoke + CodeScene), preferably on three Chunk sidecar lanes for automatic test/coverage work: frontend lint/build/coverage, Rust coverage, and Playwright smoke. The goal is lower wall-clock time than local hooks while keeping each heavy gate isolated; keep local Playwright mainly for authoring, focused reproduction, or sidecar outages. +- **A task is NOT done until `git push origin main` succeeds.** If the hook blocks: read the error, fix it (clippy, tests, CodeScene, build), commit the fix, push again. **⛔ NEVER use --no-verify** + +### TDD (mandatory) + +Red → Green → Refactor → Commit. One cycle per commit. For bugs: write failing regression test first, then fix. Exception: pure CSS/layout changes. + +**Test quality (Kent Beck's Desiderata):** Isolated · Deterministic · Fast · Behavioral · Structure-insensitive · Specific · Predictive. Fix flaky tests first. Prefer E2E over unit tests for user flows. + +### Localization (mandatory for UI copy) + +All user-facing UI labels/copy must live in `src/lib/locales/en.json` and be translated into every target listed in `lara.yaml`. When adding or changing interface copy: + +```bash +pnpm l10n:translate +``` + +Use `pnpm l10n:translate:force` only when intentionally regenerating existing translations. Commit `src/lib/locales/*.json`, `lara.yaml`/`lara.lock` changes if produced, and verify placeholders/product names stayed intact. + +### Product analytics (mandatory for meaningful features) + +New features should almost always emit a PostHog event so we can see whether users actually discover and use them. Skip instrumentation only for very small changes where a dedicated event would create noise. Use clear, stable event names, avoid PII or note content, and include only safe metadata that helps evaluate adoption and failures. + +When adding or changing a meaningful user-facing feature, include the event name(s) in the Todoist completion comment alongside QA, docs, and code health. If intentionally not instrumenting a feature, explain why in the completion comment. + +### Code health (mandatory) + +Pre-push enforces **Hotspot Code Health** and **Average Code Health** ≥ thresholds in `.codescene-thresholds`. Pre-commit is lint-only; CodeScene remains mandatory through the file-level review rules below and the pre-push ratchet gate. Thresholds are a **ratchet** — only go up. When pre-push sees improved remote scores, it updates `.codescene-thresholds`, stages it, and stops so you can commit the new floor with normal verified hooks before pushing again. Never add `// eslint-disable`, `#[allow(...)]`, or `as any`. + +**Release rule:** CodeScene is a before/after gate, not just a final score. Every task must record the starting CodeScene state before edits and the final state after edits. If touched code gets worse, refactor before committing. + +**⛔ NEVER edit `.codescene-thresholds` to lower the values.** If the gate blocks you, improve the code — do not lower the bar. + +**CodeScene access order:** use CodeScene MCP tools if available. If MCP is unavailable, use the installed `cs` CLI for file-level review/delta work, and use the CodeScene API (`CODESCENE_PAT` + `CODESCENE_PROJECT_ID`) for project-wide Hotspot/Average threshold checks from `.codescene-thresholds`. + +**Before editing any existing code file:** capture its current file-level CodeScene score. After your edits, re-run the same file-level review and verify the score is higher. If the file already starts at `10.0`, it must remain `10.0`. + +**New files:** every new **scorable code file** must reach CodeScene score `10.0` before commit. If CodeScene reports `null` / "no scorable code" for a new file, it must still have zero CodeScene findings/warnings. + +**Before every commit:** run CodeScene file-level review on every touched or newly created code file and verify the rule above. **Boy Scout Rule:** every file you touch must leave with a higher score, unless it was already `10.0`, in which case it must stay `10.0`. + +**If CodeScene gate blocks your push:** use `mcp__codescene__code_health_score` to find the worst file, refactor it, commit, push again. Do NOT stop or wait for laputa-refactor — that is a background loop, not a substitute for fixing your own regressions. + +### Security scan with Codacy (mandatory) + +Use Codacy as a security and static-analysis gate before a task is considered releasable. + +- Prefer the Codacy MCP inside Codex to inspect repository/file issues for every touched code file. +- If MCP is unavailable, use the local CLI wrapper, e.g. `.codacy/cli.sh analyze --format sarif`; choose the relevant tool when useful (`eslint`, `opengrep`, `trivy`, `lizard`). +- **Always fix Critical and High severity findings introduced by your change.** Do not move the task to In Review with new Critical/High Codacy issues. +- Review Medium findings. Fix them when they are real defects or security-sensitive; otherwise explain why they are acceptable in the completion comment. +- Never silence a Codacy rule just to pass the scan. Prefer small code changes that remove the finding. + +### Check suite (runs on every push) +```bash +pnpm lint && npx tsc --noEmit && pnpm test && pnpm test:coverage # frontend ≥70% +cargo test && cargo llvm-cov --manifest-path src-tauri/Cargo.toml --no-clean --fail-under-lines 85 +``` + +Coverage is a release gate, not a vanity metric: +- Frontend coverage must stay ≥70%. +- Rust line coverage must stay ≥85%. +- For bug fixes, add a regression test when practical. +- For new behavior, add targeted coverage close to the changed code; do not rely only on broad E2E coverage. + +### UI and native QA + +**Phase 1 — Playwright (only for core user flows):** + +Write Playwright test in `tests/smoke/.spec.ts` only if feature touches: vault open, note create/save/delete, search, wikilink navigation, git commit/push, conflict resolution. Tag a test with `@smoke` only if it protects a core pre-push workflow. Do NOT tag cosmetic or mock-heavy checks — keep those in the full regression lane. Prefer `.chunk/run-playwright-smoke.sh` on a Chunk sidecar for the curated smoke lane because local Playwright is expensive; keep `pnpm playwright:smoke` available for focused local reproduction. The curated smoke suite must stay under **5 minutes** when sharded on sidecars; use `pnpm playwright:regression` for the full Playwright pass. + +```bash +pnpm dev --port 5201 & +sleep 3 +BASE_URL="http://localhost:5201" npx playwright test tests/smoke/.spec.ts +``` + +**Phase 2 — Native app QA:** + +```bash +pnpm tauri dev & +sleep 10 +bash ~/.openclaw/skills/tolaria-qa/scripts/focus-app.sh laputa +bash ~/.openclaw/skills/tolaria-qa/scripts/screenshot.sh /tmp/qa-native.png +``` + +Use computer-use/browser-control style interaction for native UI QA when available: click, hover, drag, select, scroll, and type the way a real user would with the mouse and trackpad. For every UI feature, test the primary mouse-driven path first, then verify any relevant keyboard shortcut or keyboard-first workflow still works. Tolaria is still a keyboard-first app, but QA must not assume users only interact by keyboard. + +Use `osascript` for app focus, keyboard shortcuts, and keyboard-specific checks. **⚠️ WKWebView:** `osascript keystroke` can be blocked inside editor content — use computer use for native editor interaction when possible, and rely on Playwright for deterministic text-input coverage. Write result as Todoist comment (✅ or ❌). + +### Release-readiness checklist + +Before pushing or moving a task to In Review, verify the release gates and add a **completion comment** to the Todoist task. The comment must include: + +- What was implemented (a few lines covering logic and UX/UI). +- QA: what was tested and how (Playwright / native screenshot / osascript). +- Tests/coverage: commands run and final coverage result. +- CodeScene: before/after touched-file checks plus final Hotspot and Average scores after push; final scores must pass `.codescene-thresholds`. +- Coverage commands passed (`pnpm test:coverage` and `cargo llvm-cov ... --fail-under-lines 85`) or the change is docs-only. +- Codacy: MCP/CLI scan summary; confirm no new Critical/High findings. +- Localization: any user-facing copy lives in `src/lib/locales/en.json`, `pnpm l10n:translate` was run, and `pnpm l10n:validate` passes. If no copy changed, say “Localization: no UI copy changes”. +- PostHog: meaningful new user actions/events are instrumented with safe metadata; noisy/minor changes explicitly say “PostHog: no event needed because …”. +- Refactoring: any files refactored to meet the CodeScene gate, or "none needed". +- ADRs: any new/updated ADRs, or "none". +- Docs: any updated docs (`ARCHITECTURE.md`, `ABSTRACTIONS.md`, etc.), or "none". +- Demo vault dirt checked: `git status --short -- demo-vault demo-vault-v2` is empty unless fixture changes are intentional. + +### ADRs & docs + +ADRs live in `docs/adr/`. Create in the same commit as the code. Never edit existing — create a new one that supersedes. Use `/create-adr`. **When:** new dependency, storage strategy, platform target, core abstraction, cross-cutting pattern. **Not for:** bug fixes, styling, refactors. + +After any Tauri command, new component/hook, data model change, or new integration: update `docs/ARCHITECTURE.md`, `docs/ABSTRACTIONS.md`, and/or `docs/GETTING-STARTED.md` in the same commit. + +--- + +## 2. Product Rules + +### Demo vault hygiene (`demo-vault/`, `demo-vault-v2/`) + +Default to `demo-vault-v2/` for testing. + +- Treat `demo-vault/` and `demo-vault-v2/` as disposable QA fixtures unless the task explicitly changes demo content. +- If you create untracked notes, attachments, or other temporary files there for testing, delete them before the task is complete. +- If you modify tracked demo-vault files only to test or QA behavior, revert those edits before the final commit. +- Before declaring a task done, make sure `git status --short -- demo-vault demo-vault-v2` is empty unless demo fixture changes are part of the task. +- If a fresh run starts and the only local dirt is inside `demo-vault/` or `demo-vault-v2/`, clean those paths first and continue. That case is recoverable QA residue, not a blocker. + +### User vault (`~/Laputa/`) + +Default to `demo-vault-v2/`. If you must use `~/Laputa/` for testing: +- **Never commit or push** any test notes to the remote vault +- **Delete all test notes from disk** when done — do not leave untitled or temporary notes on the filesystem. Run `cd ~/Laputa && git checkout -- . && git clean -fd` to restore the vault to its last committed state. +- **Rationale:** test notes pollute the local vault over time, making it a collection of nonsensical untitled files. The vault must stay clean on disk, not just on the remote. + +### UI components — mandatory rules + +**Always use shadcn/ui components.** Never use raw HTML form elements (``, ``, etc.) for user-facing UI. Every interactive element must use the shadcn/ui equivalent: + +| Need | Use | +|---|---| +| Text input | `Input` from shadcn/ui | +| Dropdown/select | `Select` from shadcn/ui | +| Date picker | `Calendar` + `Popover` from shadcn/ui (NOT native ``) | +| Button | `Button` from shadcn/ui | +| Autocomplete/combobox | Reuse existing combobox components from the app (check `src/components/`) | +| Wikilink picker | Reuse the wikilink autocomplete component already used in the editor and Properties panel | +| Emoji picker | Reuse the emoji picker component already used for note/type icons | +| Color picker | Reuse the color swatch picker used for type customization | +| Toggle/switch | `Switch` or `ToggleGroup` from shadcn/ui | +| Dialog/modal | `Dialog` from shadcn/ui | + +**When in doubt:** search `src/components/` for an existing component before building new. **Visual language:** all new UI must feel native to Tolaria — if it looks like a browser default, it's wrong. + +--- + +## 3. Reference + +### macOS / Tauri gotchas + +- `Option+N` → special chars on macOS. Use `e.code` or `Cmd+N` +- Tauri menu accelerators: `MenuItemBuilder::new(label).accelerator("CmdOrCtrl+1")` +- `app.set_menu()` replaces the ENTIRE menu bar — include all submenus +- `mock-tauri.ts` silently swallows Tauri calls — not a substitute for native testing + +### QA scripts + +```bash +bash ~/.openclaw/skills/tolaria-qa/scripts/focus-app.sh Tolaria +bash ~/.openclaw/skills/tolaria-qa/scripts/screenshot.sh /tmp/out.png +bash ~/.openclaw/skills/tolaria-qa/scripts/shortcut.sh "command" "s" +``` + +### Diagrams + +Prefer Mermaid (`flowchart`, `sequenceDiagram`, `classDiagram`, `stateDiagram-v2`). ASCII only for spatial wireframe layouts. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6909a95 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,8 @@ +--- +type: Note +_organized: true +--- + +@AGENTS.md + +This file is only a Claude Code compatibility shim. Keep shared agent instructions in `AGENTS.md`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e8fc749 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# Contributing to Tolaria + +Thanks for being here! Tolaria is still early, and every bug report, idea, and contribution genuinely helps shape the app. + +## 🗳️ Where to share what + +To keep things clean: + +- 🐛 Bugs → GitHub Issues +- 💡 Feature requests / ideas → Canny • + +If you have a feature idea, please check Canny first and upvote it if it already exists. + +## 📥 Pull requests are welcome + +PRs are very welcome. + +A few things to keep in mind before opening one: + +- Bug fixes are always great +- Small improvements are great too +- For bigger features, please check Canny first before building + - Try to avoid things that are already marked **in progress** + - Requests marked **planned** are usually great contribution targets +- Keep PRs small, focused, and easy to review +- Include a short explanation of the problem and your solution +- Follow the dev process described in Tolaria’s `AGENTS.md` (tests, code health, etc.) +- Avoid bundling unrelated refactors into the same PR + +If you want to contribute a feature, the best place to start is here: + +## 📋 What makes a good bug report + +If you open a bug report on GitHub, it really helps to include: + +- your Tolaria version +- your OS version +- steps to reproduce +- what you expected to happen +- what actually happened +- screenshots or screen recordings if useful + +The clearer the report, the easier it is for us to reproduce and fix it. + +## 🙏 Thank you + +Tolaria is getting better because people care enough to try it, report what’s broken, suggest what’s missing, and contribute improvements. + +That means a lot. Thanks for helping build it. diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..e7433cc --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,8 @@ +--- +type: Note +_organized: true +--- + +@AGENTS.md + +This file is only a Gemini CLI compatibility shim. Keep shared agent instructions in `AGENTS.md`. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..77c48a4 --- /dev/null +++ b/README.md @@ -0,0 +1,171 @@ +# 🌊 光湖 + +> 基于 [Tolaria](https://github.com/refactoringhq/tolaria) 的独立开源分支。 +> 上游采用 AGPL-3.0-or-later,本项目继续遵守同一许可证并保留完整历史。 + +光湖由冰朔维护,现行源码仓库: +`https://guanghubingshuo.com/code/bingshuo/guanghu` + +本分支拥有独立的产品名称、应用标识与发布链,不会检查、下载或安装 +Tolaria 官方更新。在光湖自己的签名发布服务完成前,应用内更新保持关闭。 + +## 上游来源 + +Tolaria 提供了跨平台 Markdown 知识库的基础能力。光湖在此基础上继续开发 +第五域入口、编号导航、人格协作与本地知识系统。上游版权与贡献者署名保持不变。 + +--- + +## Tolaria 上游说明 + +Tolaria is a desktop app for macOS, Windows, and Linux for managing **markdown knowledge bases**. People use it for a variety of use cases: + +* Operate second brains and personal knowledge +* Organize company docs as context for AI +* Store OpenClaw/assistants memory and procedures + +Personally, I use it to **run my life** (hey 👋 [Luca here](http://x.com/lucaronin)). I have a massive workspace of 10,000+ notes, which are the result of my [Refactoring](https://refactoring.fm/) work + a ton of personal journaling and *second braining*. + +1776506856823-CleanShot_2026-04-18_at_12 06 57_2x + +## Sponsors + +Tolaria is supported by a small panel of tools that help keep the project healthy, tested, and ready for AI-assisted development. I use these tools every day. + + + + + + + + +
+ + + + Codacy + + + + + + + CodeScene + + + + + + + CircleCI + + + + + + + Unblocked + + +
+ +## Walkthroughs + +You can find some Loom walkthroughs below — they are short and to the point: +- [How I Organize My Own Tolaria Workspace](https://www.loom.com/share/bb3aaffa238b4be0bd62e4464bca2528) +- [My Inbox Workflow](https://www.loom.com/share/dffda263317b4fa8b47b59cdf9330571) +- [How I Save Web Resources to Tolaria](https://www.loom.com/share/8a3c1776f801402ebbf4d7b0f31e9882) + +## Principles + +- 📑 **Files-first** — Your notes are plain markdown files. They're portable, work with any editor, and require no export step. Your data belongs to you, not to any app. +- 🔌 **Git-first** — Every vault is a git repository. You get full version history, the ability to use any git remote, and zero dependency on Tolaria servers. +- 🛜 **Offline-first, zero lock-in** — No accounts, no subscriptions, no cloud dependencies. Your vault works completely offline and always will. If you stop using Tolaria, you lose nothing. +- 🔬 **Open source** — Tolaria is free and open source. I built this for [myself](https://x.com/lucaronin) and for sharing it with others. +- 📋 **Standards-based** — Notes are markdown files with YAML frontmatter. No proprietary formats, no locked-in data. Everything works with standard tools if you decide to move away from Tolaria. +- 🔍 **Types as lenses, not schemas** — Types in Tolaria are navigation aids, not enforcement mechanisms. There's no required fields, no validation, just helpful categories for finding notes. +- 🪄**AI-first but not AI-only** — A vault of files works very well with AI agents, but you are free to use whatever you want. We support Claude Code, Codex CLI, and Gemini CLI setup paths, but you can edit the vault with any AI you want. We provide an AGENTS file for your agents to figure out. +- ⌨️ **Keyboard-first** — Tolaria is designed for power-users who want to use keyboard as much as possible. A lot of how we designed the Editor and the Command Palette is based on this. +- 💪 **Built from real use** — Tolaria was created for manage my personal vault of 10,000+ notes, and I use it every day. Every feature exists because it solved a real problem. + +## Installation + +### Homebrew + +Install via Homebrew on macOS: + +```batch +brew install --cask tolaria +``` + +### Download from releases + +Download the [latest release here](https://refactoringhq.github.io/tolaria/download/) for macOS, Windows, or Linux. Windows installers are Authenticode-signed; company-managed devices may still require IT approval of the Tolaria publisher before first install. + +## Getting started + +When you open Tolaria for the first time you get the chance of cloning the [getting started vault](https://github.com/refactoringhq/tolaria-getting-started) — which gives you a walkthrough of the whole app. + +The public user docs live in [`site/`](site/) and are published to GitHub Pages. Start with [Install Tolaria](site/start/install.md), then [First Launch](site/start/first-launch.md). + +## Open source and local setup + +Tolaria is open source and built with Tauri, React, and TypeScript. If you want to run or contribute to the app locally, here is [how to get started](https://github.com/refactoringhq/tolaria/blob/main/docs/GETTING-STARTED.md). You can also find the gist below 👇 + +### Prerequisites + +- Node.js 20+ +- pnpm 8+ +- Rust stable +- macOS or Linux for development + +#### Linux system dependencies + +Tauri 2 on Linux requires WebKit2GTK 4.1 and GTK 3: + +- Arch / Manjaro: + ```bash + sudo pacman -S --needed webkit2gtk-4.1 base-devel curl wget file openssl \ + appmenu-gtk-module libappindicator-gtk3 librsvg + ``` +- Debian / Ubuntu (22.04+): + ```bash + sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file \ + libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev \ + libsoup-3.0-dev patchelf + ``` +- Fedora 38+: + ```bash + sudo dnf install webkit2gtk4.1-devel openssl-devel curl wget file \ + libappindicator-gtk3-devel librsvg2-devel + ``` + +The bundled MCP server still spawns the system `node` binary at runtime on Linux, so install Node from your distro package manager if you want the external AI tooling flow. + +### Quick start + +```bash +pnpm install +pnpm dev +``` + +Open `http://localhost:5173` for the browser-based mock mode, or run the native desktop app with: + +```bash +pnpm tauri dev +``` + +## Tech Docs + +- 📐 [ARCHITECTURE.md](docs/ARCHITECTURE.md) — System design, tech stack, data flow +- 🧩 [ABSTRACTIONS.md](docs/ABSTRACTIONS.md) — Core abstractions and models +- 🚀 [GETTING-STARTED.md](docs/GETTING-STARTED.md) — How to navigate the codebase +- 📚 [ADRs](docs/adr) — Architecture Decision Records + +## Security + +If you believe you have found a security issue, please report it privately as described in [SECURITY.md](./SECURITY.md). + +## License + +Tolaria is licensed under AGPL-3.0-or-later. The Tolaria name and logo remain covered by the project’s trademark policy. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..1d956d2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,55 @@ +# Security Policy + +Thanks for helping keep Tolaria safe. + +If you believe you have found a security vulnerability, **please do not open a public GitHub issue**. Report it privately instead. + +## Supported versions + +We currently support security fixes for: + +| Version | Supported | +| --- | --- | +| Latest stable release | ✅ | +| `main` branch | Best effort | +| Older releases / prereleases | ❌ | + +## Reporting a vulnerability + +Please use GitHub's private vulnerability reporting flow for this repository. + +Include as much of the following as you can: + +- a short description of the issue +- reproduction steps or a proof of concept +- affected version / commit, if known +- impact assessment +- any suggested mitigation + +If the issue involves sensitive user data, credentials, or a working exploit, keep the report private and do not post details publicly. + +## What to expect + +We will try to: + +- acknowledge receipt within a few business days +- reproduce and assess the report +- work on a fix or mitigation if the issue is valid +- coordinate public disclosure after users have had a reasonable chance to update + +## Disclosure guidelines + +Please give us a reasonable amount of time to investigate and ship a fix before publishing details. + +We appreciate responsible disclosure and good-faith research. + +## Out of scope + +The following are generally out of scope unless they demonstrate a real security impact: + +- missing best-practice headers or hardening with no practical exploit +- self-XSS or editor behavior that requires unrealistic user actions +- reports that only affect unsupported old builds +- purely theoretical issues with no plausible attack path + +If you are unsure whether something qualifies, please still report it privately. diff --git a/UPSTREAM.md b/UPSTREAM.md new file mode 100644 index 0000000..670fac2 --- /dev/null +++ b/UPSTREAM.md @@ -0,0 +1,14 @@ +# Upstream provenance + +光湖是 Tolaria 的独立开源分支。 + +- 上游项目: https://github.com/refactoringhq/tolaria +- 上游许可证: AGPL-3.0-or-later +- 本次分叉的本地基线: `af6891b` +- 第一笔光湖入口提交: `9bd612c` +- 光湖独立身份与更新隔离提交: `8939145` +- 光湖仓库: https://guanghubingshuo.com/code/bingshuo/guanghu + +由于创建仓库时 GitHub 网络不可用,光湖远端首次发布采用完整源码快照。 +上游作者、许可证和来源不因快照发布而改变。网络恢复后可补充完整上游历史镜像。 + diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..62bb433 --- /dev/null +++ b/biome.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json", + "files": { + "includes": [ + "**", + "!src-tauri/gen/**", + "!target/**", + "!dist/**", + "!node_modules/**" + ] + }, + "css": { + "parser": { + "tailwindDirectives": true + } + }, + "overrides": [ + { + "includes": ["site/**/*.vue"], + "linter": { + "rules": { + "correctness": { + "noUnusedImports": "off", + "noUnusedVariables": "off", + "useHookAtTopLevel": "off" + } + } + } + } + ], + "linter": { + "rules": { + "correctness": { + "useQwikValidLexicalScope": "off" + } + } + } +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..4fd0ad5 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "phosphor" +} diff --git a/demo-vault-v2/.fixture-manifest.json b/demo-vault-v2/.fixture-manifest.json new file mode 100644 index 0000000..b9a7686 --- /dev/null +++ b/demo-vault-v2/.fixture-manifest.json @@ -0,0 +1,61 @@ +{ + "name": "Tolaria QA fixture", + "purpose": "Curated local vault for native QA and developer flows. This is not the public Getting Started starter vault.", + "large_fixture": { + "generator": "python3 scripts/generate_demo_vault.py", + "default_output": "generated-fixtures/demo-vault-large" + }, + "scenarios": [ + { + "id": "exact-match-search", + "reason": "Quick Open should rank the exact title 'Writing' above prefix matches.", + "files": [ + "topic-writing.md", + "writing-for-clarity-vs-writing-for-credit.md", + "writing-weekly-rhythm.md" + ] + }, + { + "id": "relationship-rendering", + "reason": "Relationship keys should render in the inspector instead of as plain properties.", + "files": [ + "responsibility-sponsorships.md", + "measure-sponsorship-mrr.md", + "measure-close-rate.md", + "procedure-quarterly-sponsor-outreach.md", + "procedure-sponsor-onboarding.md", + "24q4-laputa-start.md", + "24q4.md", + "person-luca-rossi.md" + ] + }, + { + "id": "project-navigation", + "reason": "Projects, quarters, and a saved view give keyboard QA a compact but representative browsing path.", + "files": [ + "24q4.md", + "25q1.md", + "25q2.md", + "24q4-laputa-start.md", + "25q1-laputa-v1.md", + "25q2-laputa-v2.md", + "views/active-projects.yml" + ] + }, + { + "id": "attachment-rendering", + "reason": "A note with a real binary attachment keeps image/block QA anchored to the fixture.", + "files": [ + "laputa-qa-reference.md", + "attachments/laputa-reference.png" + ] + }, + { + "id": "rtl-mixed-direction", + "reason": "Arabic and mixed English/Arabic paragraphs keep rich editor and raw editor BiDi QA anchored to the fixture.", + "files": [ + "rtl-mixed-direction-qa.md" + ] + } + ] +} diff --git a/demo-vault-v2/.gitignore b/demo-vault-v2/.gitignore new file mode 100644 index 0000000..9ff2df4 --- /dev/null +++ b/demo-vault-v2/.gitignore @@ -0,0 +1,5 @@ +.git/ +.laputa/ +.laputa-index.json +.DS_Store + diff --git a/demo-vault-v2/24q4-laputa-start.md b/demo-vault-v2/24q4-laputa-start.md new file mode 100644 index 0000000..71479bc --- /dev/null +++ b/demo-vault-v2/24q4-laputa-start.md @@ -0,0 +1,17 @@ +--- +type: Project +aliases: + - "[[Start Laputa App Project]]" +belongs_to: "[[24q4]]" +owner: "[[person-luca-rossi]]" +status: Done +--- + +# Start Laputa App Project + +The original spike that proved Tolaria could read a markdown vault, render note metadata, and support keyboard-first navigation. + +- Set the initial four-panel layout. +- Proved the note list, editor, and inspector could coexist in one flow. +- Led directly into [[25q1-laputa-v1]]. + diff --git a/demo-vault-v2/24q4.md b/demo-vault-v2/24q4.md new file mode 100644 index 0000000..765b17e --- /dev/null +++ b/demo-vault-v2/24q4.md @@ -0,0 +1,16 @@ +--- +type: Quarter +aliases: + - "[[Q4 2024]]" +status: Done +has: + - "[[24q4-laputa-start]]" +--- + +# Q4 2024 + +The quarter where the Laputa prototype became real enough to replace sketches and notes. + +- Started [[24q4-laputa-start]] as the first working app spike. +- Captured the initial panel layout and editor decisions. + diff --git a/demo-vault-v2/25q1-laputa-v1.md b/demo-vault-v2/25q1-laputa-v1.md new file mode 100644 index 0000000..facbde0 --- /dev/null +++ b/demo-vault-v2/25q1-laputa-v1.md @@ -0,0 +1,17 @@ +--- +type: Project +aliases: + - "[[Laputa App V1]]" +belongs_to: "[[25q1]]" +owner: "[[person-luca-rossi]]" +status: Done +--- + +# Laputa App V1 + +The first usable release for daily browsing, quick open, and note-property editing. + +- Shipped the working command palette. +- Made the inspector practical for real frontmatter editing. +- Captured enough confidence to continue with [[25q2-laputa-v2]]. + diff --git a/demo-vault-v2/25q1.md b/demo-vault-v2/25q1.md new file mode 100644 index 0000000..a6ce6a7 --- /dev/null +++ b/demo-vault-v2/25q1.md @@ -0,0 +1,13 @@ +--- +type: Quarter +aliases: + - "[[Q1 2025]]" +status: Done +has: + - "[[25q1-laputa-v1]]" +--- + +# Q1 2025 + +The first period where Laputa was usable for daily navigation, quick open, and inspector flows. + diff --git a/demo-vault-v2/25q2-laputa-v2.md b/demo-vault-v2/25q2-laputa-v2.md new file mode 100644 index 0000000..2a40ac6 --- /dev/null +++ b/demo-vault-v2/25q2-laputa-v2.md @@ -0,0 +1,19 @@ +--- +type: Project +aliases: + - "[[Laputa App V2]]" +belongs_to: "[[25q2]]" +owner: "[[person-luca-rossi]]" +status: Active +related_to: + - "[[laputa-qa-reference]]" +--- + +# Laputa App V2 + +The active polish project used for current QA, especially richer editing, keyboard navigation, and attachment rendering. + +- Tightened the editor interaction model. +- Reduced friction in wikilink navigation. +- Uses [[laputa-qa-reference]] as a lightweight visual reference note. + diff --git a/demo-vault-v2/25q2.md b/demo-vault-v2/25q2.md new file mode 100644 index 0000000..08a42cc --- /dev/null +++ b/demo-vault-v2/25q2.md @@ -0,0 +1,13 @@ +--- +type: Quarter +aliases: + - "[[Q2 2025]]" +status: Active +has: + - "[[25q2-laputa-v2]]" +--- + +# Q2 2025 + +The polish cycle focused on richer editing, faster linking, and better keyboard QA. + diff --git a/demo-vault-v2/area-building.md b/demo-vault-v2/area-building.md new file mode 100644 index 0000000..1be6e50 --- /dev/null +++ b/demo-vault-v2/area-building.md @@ -0,0 +1,12 @@ +--- +type: Area +aliases: + - "[[Building]]" +has: + - "[[responsibility-sponsorships]]" +--- + +# Building + +The business-facing area used in QA to anchor a responsibility with linked procedures and metrics. + diff --git a/demo-vault-v2/attachments/laputa-reference.png b/demo-vault-v2/attachments/laputa-reference.png new file mode 100644 index 0000000000000000000000000000000000000000..17a288d3a2d4a7a9784911c6ce643d3664467100 GIT binary patch literal 705442 zcmbrlWmKF^(=Ix|;7$a0cNm=D?(PJi!QCZDfB?Z=gS$iU5G*7(!Gc=|!QGwNL-L&G zd*6M|kF)lkwPsrG>bkqTs;=s;?s>1KB8!Pega!ZrFy-Z>Gynh~EDHpnz{8T`z|TKm z397T4z8e65_4Vlsm%@xi3;-Z1*=Xsx>nSSv9fU#rax-$qNlI{3)AcJD03(~OG2z|4asJ3WlZ6>KS}Atmz<39KYcZ|&~xEXdC8<>kfZ#l`02 zYQ@eeARxfb0b&P%SYZ^bZa$9gX5OrhZqzUiXcT{VNI~2zTy31)ZJZn_o_Ly>J9)SZ z)6=6-{KHz@-QCs3{9o>lZfs_MYCN8ru)zk+ZU%N^=Vaq}>J|ls&|i8%aj@G{@$XAC zAufLke@pf@f8OM5=IRE~@^OX;(`!K7oIG4DAb&VKHTs8%q$|YC9Rd~sae#PPIY6u+ zP9gUH?a|Xa{-=SInY)>YADxSbllv>Mlf~bR;NL__Tuy=?!YZr9ul%=y8T1dO|G)kF)06-1oV2&QJXplioQDr& z#=*nNV+Q77<>un#Wi>bBHD~4H=Q0QLa`1C=gTeoD`P<~b*~pl~xbT2@K)gHxoZOsz z0zCX2|2FvZ@W1J`oIEV7pXONv^e@@})cv6qVt*37z0F^V{VDt-vVU9rmFHi=|InHL z|9S+r_*aLVJzVYo=qK2M9byl0fH=B8X%Oc>8U(fwbhmN0hy0KJh}*mWxBkEkL(tyL z(Mp)!n-vVPH1n``rx($7{L>6f0fxBQSUFO#Qoz*Z|243GcnPup$7}vChUNSpmiJ^E ze*oagDqs&_W8(T}Ot6xmxQDy7ldFi5hlLr1hLf9{jnEU(|5FXK$3LC@Kbl+^VEj9~ z|J4g`Gw1)XO&FQrADeP=)pByO7m+k`^faSj(119CA+8WtMhckC2>*wjP(0a&@PF6? z#UC3G{tvwuV*lTn{(lbS@7Q2~1%od3e*q3w_!lB!eGsO1g#n;aEom$aJXIii@|McV z047)-1ptI20wBV2aImik9Pz(-88`+2!k>B=PQyR@C;vqC_hSl6o=SgGFT~X?09r?Z zyp*_>_lNzCoh0zA%Z5+voj-0ylWu+;4p3aJh{2}FxX=Faj58}IXQ#QIGsBlq-;NON zxx6|&9L}AAo#of|Lo1PM{{-o#zJZt0@W8_(1T{G+nXi5ca;BDlX$H8I8-qqMcW(M&xziJRYtP6HS_JF6ZD9XlcAxdBJ#HH;`^@k&4 zvx)e5pZq-8=#@>w&Ltryn?YdUzTDsQRr^AWsvsX?Z3LQ-mu;|bZ*KPp*qV8~ylBX} zauNzW+C-mC&3Yy3wje47hjxTh39_w{Az1VWzDW-^4~Eiff96C{}d zfQt`}P$HTm0RpArEsr-Hn+q8zsW7>{*If&jy>BjCI{l%3>??0JMr!;gu1ue#*5zV} zzyV852oix8LLvuoqW-sQcX2B>$7_DrIRUQQ`2E{;a_{0dkqVP-P$h<#t`X(o;KA@X zyH1vLA(W`_VcfoUn{;DJ)Rff93K6Mb^P}0rRqr+8md=OW<)f8_Riih2(&8{ZIMs&m z0-jPsX|?H@_W={R0k`LcEuxR%syPA2MWViMyxk*)2e3*|G*HFiY3QP7Fbp$GLr3Bh zfpUb567Z;MavF4rG6Zsk#3VrJ=?pjk6)O*I?om!R=e7|gcpOJ)JFkC z{3msw{+jf7oQ>@C`Q~Iy#Q(gfcYUj$zfOIm4n=x1GXuD6eTY+p_t36o1V_Or9Vr9I z%MZw(q8S9Ef-{vIi=syXaGPvC@57_H$AP89#7T0Z>o=nhZtTm~eU)SWSKkb4=avY# zp5~AB?>u(74C!Y@^2Cvg`a`#J?|a+hRvypS?vP2_KN*C@8o{CPT0VZYad*n22%?Dr zGa-sw%23%+0>BCr7~D8iN*7MnMHDE%y@@7%k?HyfbUq;hR{14m5i~>*Iw9QnV%<@0Ge&_I2_olF+{7)aq{^<)v z{^QL?e;{;1b>;r}NYv-{v9PyStUKft$t*PkSLQRBFDNCGBN#}Gl!Yinn3giSc!}wj zMQw>0m2iDBxHJ(;!O6i$rHt>?xM*-`acvJB%gOM7Nw2KKOvCWty5~qxBNTbVn_ky6 z?aeM8q#b@fg;~y_QJ~q;mhNQUDQR96>SBu9o!4Rr9(?kE$!E!n&gw_OU(qV7Lf9lv zcE#8I*RD*`>-!XM1~vI$H)+z}2UIb!HQflUy_b+zx z+7YzS+_~tNM@X#0Ed3@&tv5$I6t3&Y8VQD5TVy z@>eqJE||zyqo^R(xb4cz=EeD@MZTlu_RIV1y_Q4cuE4GR$M08I)Fc2i2|5rib1eSe zdN_Wruu-ltS*|e&I~$g2+=CjQ9L2=w=lPFLR$F&=e3yK+M^4Lv?f(94O+QEURQa2q z<+-ZB5D$E6$Msk7)1>?P11`_ka|4bGa|5mmvB^99eiSQh;=O1?qGn7_Ehdyp$GNC% z)kg>uTooHin@NStR<-f9WM^f~OSolDC=NN%>?Gq-G3YV9<0#aKRPx~P+pNDLvl}Ce z%n^QJNyVZ)m_N@2`n~D+5;vFuqFNXdbBx4fyBI{|)7jMv*Ov#g&-HVUoLds zTTjjw8<+E139Xm5Z{7_}o>JlT;KK|JieSwf@&T+zkT+ zr(}pWj-xh3#M}0F5St}dhpyFwOuoV-l~nTqm9v0YLIFxaIS@zK22hWR{22?P3{$e* zWlld?NB_dz22qD>{PToVI#e{opyaU9bR|>{i^1sRa#sS)eO$cZQnifYtT;mJd9J?p z#z=}6aBtU{&FA4Uq~tQ>;xUiUJP&-^11!AmW*!gET8!VE^bNO+qcXxbN8uyl4=GUY{V_?4ZYf0yY$etgV=aK1UHN>H(xIiK4Qt7%XPr1k9#> zOM5tsc8`eJ+zdrcRquI3Dtvo!=cnh}fEHN%+cM=K30EN_MC$^crk9e2Fh!xUrvHKf zw|+BoH$gbQhh-zCy)7e18zdH56fLEoz$LiX4L1a24*QX;-wbfOFwps6W|`t(5c3Y; zM(k4(oFxg0ZzjM;L(g;@L`ZMeYd_>O`TVfkas*vbY%vya;!~N-D@T7C4^;ghsk{No z3!V?Rh}_L%2R^2W-u*7c_gPbXe{+@|u2ve-$CPMjN0E|2c<49-;JuwpZGf~iJOs?M z8&6WyuC>0G;X4F?=>U@>Z-`rZ_2~g}k}<2K+-_in;KaTpYTdF{iV4bRWlY=DZU=T_ z>ucWpm+O%!7USd$v^a2Fl#~p*1y${#+D`de@cOZ)3IIePRk0cxNJFfn-%PfKh6**W z7r@Q783PW(Xl@V=u9w)sdgU}*%gT3{v+TD6^NK!O(}s8Lqp2qJiPkV>>&zKJdUB4B zH&s5@H>FN62htUAHpNHQzuujhQy1~NI9~jD3?2$sBv^__ZqjV{ z%6DDBc@vB11N}S57@2LgpNTGT|}Y&92JdYGS7etspnonlAWfk(Ey(!Qag;^ zLc7XkzrLn_eZzO60U1w8TwE@DU?gZN8dxN^u{lvqLLm-H1nZFO@D|oqQ`|8}!cfEX~_jj^;)GYB^M95CjWOiZfNSff}gC?KKgwI**ouaF` zNB*;)+nSUFP&Ehv*4zdq7?#|q-01?^5@e~jIox=_K?Cg9Sq*g$Y+IKtLwN6|LVu(V zAw;*m(jV>NifXsEk&LAV4CMdH$HN^W>n9VY@+F()cfZh*l42^dAjD$wmkSMpqa3Zo zOaK&`?L!gYyNIYEBTM2+b(aa!0%BG?gRG+IiaYPGfX})%CUOaN>vb(d4l`1#Yg9v|oTpDTlH3xW>1kj$Otc+aH+d?Zu2dK#B zT~zb3wBpDUBYKiDN557BSR&=&`47F%Vr)2At7y$5nq|g?>jBaZ3}}x;TTlS235Nz= zr|d#l)tZyfaFmkc(ho}5+SC!ae4l6!EwI5t>-mxJJjv@ zqhYcr>u|y=1Kfs}C{&DGyqTzS)4M^evsS~<@`<*hAL$?6P+SrhwK~}}bxFe#UL36B zS!m);55jkA>F6fx<`-oVKTG^n!On2RL8CCBgjz5!KaS0%`1-*ljKVhLtsIIu6D=-K zQ{h`hh;I$yp6~ty+<2LRS86;Z6{y|E?yyOWLh==x;^N+%%kS(n{7&dE@<8Zgm&jF? zp(gj!m`zNd*7(2eWK-J8tI~&?YWrJ1zs8PZBNq}7@ptt;oljEDAt5Kd-AKAPIdPtt zmZX%{<>PDpnmp-AC#GPZalR=-FpkRBx$oc7c=jz@=1WlIwz$QM9TY~z1<^~-lg4ku zBT4XI#T@Qlpcdtq-08;T~bGT zlgGt1WzWK{=s_V6i#8-8lo~QU_*L$*Rz+Hh4z-1sWMJ_X*@5Ep77!KAh_X0&@5EaV zyQ9{w3hu3|0mZBmg-xo0S0N!hmR)E1_S#GP*2{%u(d)-3=&&3~XrVfN{a zCV>d%J|L=aAutxCoO@V(70)$jxll$-#5eGGgK;Bu6S0 zxAnH2Mu$$9E}~ z4NH=xzF2RybMYB2d^EWPfB@Y;f!tIe+Ke(LKOU-p#+_UBCT_)XOZeKY}<$2qJcbwJaap8^UXT|-T)WuRmV=r;IhxaLH1?0XsB;7h!LMm%$ zGcz)zr3#PI9%@)&Gf^1Aor%f_&yfLVmJr-SBDpt}|HGP%33>0nl_w^CUCSb*-)rFe9hawUI6+4t>5SOr?Y!yc87)vn4=-xdgMLZ_A_8tf*4tZWCi5+tBDLZX%W8>ub z{Mdbz8a1OResU>>+ezsw5lAw8>+#a{>W=-sa<%=ce>RP{>-PK4Prn1tx2(GOpOzd` ze-=`2rnh%hu~kLIRdz>weENM=VdH)rrnV**akt{Y{H zULV05ftXZdjD!3OG3Cyvc+m%y1xK6Nt68tk=Wp=oC&AyrDaru?ev_36IhRp(te`@`Z^O~+j8Jz5m} zxf0aF{7qu1s48sldBpZ-8D>H{Bh-0y+>z^ljP2xq-$x%O>f`P0?b7-)@i-JU20$fI zbzn%bHp=BT3}0c3PiO|N&Ks)M`L5*U#>J&r3>Z=pGQ709YcN`5-IQV04a&&!$(D#O zQ=#Aj!i2+M0ru9}Rnb9H%~9C$6{Vxb$lCBEmaxlOPBR41Jfj%hWda5($lFkinniEp zea?_8!l*)*ryW&LkP0eXv!>-QsB6vAXOP@6=TQf5>M@FODRdA8qv8JY>{o2|1mKn~2HN%pEE6(MW zsee8al+W2}3Y{Evf<-a>R$Cu-_1hgeCNVa!QvsAI83W@B4dJnNN9EfY9@?nech3bsEmlWcg*vyVR?SV%d(n^cQg;L-idCR z$&d>Uh+3x)KdO@w7eE|Yz#>^MzdJArjxXoO+=r7JtQW{rLSMHxecM_AMA?>+$zZ5|K%o(+(-fUQ^j8oTv159t}@O?%GKO_j3la2C>TB|Vv}ii^1I z-xdM{x;onf9K6NzywBG8 zR&LK@kkjH2>}GL+$~JQE2=W0%BbmQS#f2$a{Tzwj$R~f$uF*xsZlR+<7-f!HiMMUc zW^G?yaS&KM$YhmE_pwYgSd#SjcC|s8RI2!T_dLHy5x7h^MAGYM$Bm~?DS*P5J!-9n z8oB$H1I={DN6yRZV=NJH0ApzB2U8POx0t5w6d`S`xfg?0-^6sQf0k8ymLd$qzY#bj zJSYLjhwJi26v(8F^^-6STXC}K242%J&7cHjgJsu4Nc6>ta7!&ODpZJ>5oLnMojG(% zr4&j=0=Cms*~rLeEX3+i=ro6Gq*Vbl>+yYIdBgIh4C70PLqP*M%(em{sA5u+6BnYwe=<$7fz~f(gJdov8V!0IwrD!OsX<`J!hn|}Rli(SvKqq3}H&hlZsY)Ok6m6U$kI zZdEu)h?aDe#0y1~2ea~7Ir^n=D!$6w#XyarNM=OLU(GB^+)3`PFQfG+W44xvf~FGQ zX|u)CWTr64C+bsEBI3;SQTFag4vzTJTk2#$-y6sQm_5|=-`6jvR?1BYVh|3>UeHiP z>!?I)ZmN4o_FAe@mg8XH_eVvoPf3bORqgz;zOtp4cwe-%t)g2^vgnKAX6|k_9rnXW zg|Z`h0$wA7qi}*sQ(*sR1YTT5Q+7VjO@>Lke*pCH0>9JeZ1GQ2bu0)>%USgjUL(t-ucb$~r{%^s5)k1y&ab+QYfDDx5^OwXjr7NA8H7n1zG4>=}2BREioV5&jX!7)V1Pf5ObU1de z8T!=W>PfBB3n%4x7G8wNgDk0{F(k(>ejT5mdJ{&BtoYUPYrxNZwkMm3#!I5o;N~N0 zLW<|6pTo`LX-Q62zxv>TrI}wL_7yENgOQrrhWm=&lH?26vArXAb$;3J@p5>z_2K}( z#^24#ip2lhb3|pjBzDBVx=1XCcMkTy_acAzebEwld}-xWTK!R|4#fL$48r+->vFk}D~ z1wk4fp#xcS}HYZj}g3yFE#sR%vo8pwN z6bpLiQD{Tx4Go^yOlBQPDnc?+;K)yx6!*a-ODjc0#6BWmy$bR92(|2rXqZU9Rw*If ztI-M6r0E+p%*_;Jg(842sY<%xfe?XwS)%E4|M1_Q8sd4b!9TTPJy4M}%S&hO-Jv$KdgA78{!pOV*1UChq;U580eE;GuM z*zUc=AuPpfI}MVP4C~f3a(Y3j(91zQFjB_JoHl=HtKSKZ^#T_je$VeB3?_A09;T!F z6|vu# zJXXJVvFzwHtL&JThymK)OAV(VBqwT#q?t9}yML^(VyfxNo{7CSz5}HtN9VnbR`pvNTO83jDd@EHeL-Id30ekq zhSk*KhejFgNyl%_2fr}2=mB{Z_mp|`@jM>*;PS|^s-4c{M3+6yp$N69qbbI>c~jDE z>B*|--)z}WXTFrV3hf1`3HajV94Z}kiAo`L8Z6UjKyeFNki3Bm z6PVjU7}0ctitm$PwOjw0-)v6c#-*2W#~rel=am4S+|%J@l-VC8R2)_B&wX><3%ySg z^@Fb6EzfB8*By>_%%6DMw{M1#`H##NM|0S5%cgh@M5q;fJGYe}uEmB%hA`d{ z?*GVYbFcz=Sjd3(t${_a%{~^R4w1?E7=j^7mL43~*#}yl0C>+oXR-@&U;I?2Y?&P7Q8?fo{jDVy@^zq7%q!8F}V~jk1yLo6i;b-akK{Pq7z&9sh#6mJA zMALU{2VwcUD8Eh$p}6q_rz+A`Bw58k_F5kQ^8L33IU!|KkQ0pzkm)_zBRMIK+3OdC zu&u0cEUy1(lQDNnMSPiDk{E1%iJQpu*~4CxECvGaN*pejkE0P=Pn)O1^$PBs4CQT} zp#*B{h`mcX7Wg{*;bAuC?y<$mA9^^Cjzn*T4cqhvEdJf#5z7Bw%p zNCkxVF=UeXWKR=(Sn7r7l0Lr1l7})1BzvGPiUU!oJtC25LlatSr4C-rJ@#`wvNR%}$Y1wCJ9G!xS5wB&p;LL=OH z>UMKXDUQ*h!Rp`JgG+BT4Kl2_c-&XbuI_J!SN+yhJ3aQ=tpXnSZcHX&;iLO!e=WSK?Sw*`}(Z#on8*_f&%A#e>wV+hX#7SWZW+tyXJgZ?y z39*^EbZ`P&nxnhbc6-Z5-IXiEI0QR5CibN==T!{i?hUFFw)+rzOA;ndk!aCm>q|S+ zz2p2vK3mRO{;a#XSyB6CDUDhXF@{=f!W&k;ISd4C?YN0+jz*BUT^*>OE-cU5?U!TD z`jknFpNQ5YV}{%el`%2oNUoo-O`f)ua3N&^ke-oWlTd{e)M(}Qwr?*^riiS&x}6{^ zrsL!{+8nn^1+VO)K0FuYxN=qKRP|$NSQB9#(=WSxVnhv8dQ1!ku2DuNnb(XcnJGDu z7k#@w25Kh5#PYwt{`e(3UG2x|Po5bIQIb;vpZ$69Ha|(uIG&C|PiZB@#@1Cu?oVH8mlboTmO`s4Rs>E@pS>^+2{S{9h)lz+3Jz&bMaZIW{eVi%Cb!eLVE%53vxlxs zeJ8Afs!v<_wMR!Ev+j=lqGI%lDGj{(BDE?eprwk2w^sh%n~J$@ym==oIjeFZfmNxj zvt{UEgu7e?Zvby%j*FceDDh=FrwMHyS!SiI-9Z+;3|$^lOb+QAU{nwfo(+JQ(*V|< z|FHO!I8EQDA3{T(gMgwh66DI2=Jx5scyF=Tu$JSixgj=(1@o){8VHp{B0(aa&pa%) z^{dXf1XQUExh%6peXcZGl_7xog@jdi3U{^jX7|eEI>~9p_h8BCb+#xhx;yBU_-CQc zj$0X9+jSQLy}k{+-rflx9)*k!liB6nO@jJ2%U#W|^ilK^_#IU!skp|gvYVCz<_|`e z@ULXVGvuZ}G=T-@PCZ?VfHp3bpw|`g@)6vHN9`-{jefcb=i&{#-~~mK~JYJIrU76(}C$v;du8vWF}1 zs2v$;#>6=E{0PN*|Kk`V2wt_>RHsoimvh4LBdPZE_mBQsHi52WfX(8z3Y9)?tc;w{o< z-<$g#`|a3`0ynM>A12WGO!PNDJ*jBc-}UD3mx_yvtH)L7-Ualy=Ns}8r(sQ7Cgn%$ z7o!|~@N*v_V)CQg_Q~qb&1Vf~T&qV*s0@^9jIet-&XbeCyxjmHP2|O9K~u#bvmfX& zLH(?*#=yGx!Clm_fIeP5zF(@?C8ohf19OhpqP1VK2MmSaRzErMXBPB ziYh4EPrIZIk1L z_FPA}Wl4aqmiaF5-I_I`+r@FUmH=8S*-T$5Ld)l-(@jM(%C*YVT%SK<^_X(E?JMYL zNjkni)Q#$O=FzF4$opcQARiHy9|>oMQqEFmnUbzr@b%Wh*Qx5xgtiH{#|)RZ_)Tk? ztXlj8F}_-4lr5*+{dnM$Ui|*kM@8o98X!D~ny~-k`1kes9QK5t zEOH?hcMZKfod79{;0!S7yHvsYG*QM&gk zy3+EWcqX|J2n8&^=@f5SS_#X0{T2x2q`{kSIeW|E;Z19-lfph9aZ2(Gl6+XKbvZJM zYv+t!%*`u09ki-ABWi0fcia%%Hvoj__#|g2IK~lLxP4KmgtuqKB4tr^OX(uaV3L2} zSDyRL-Ag0(2~OW|f`0}}izlm0$KuWtjj}ftRFQ;kK4sPef(YGYWh82&l*S~h2V1JV z&59h1>1}?iZkwxxgk7DLP51Mykb7nZkkS2dVhsaX{WT!Y0(#;*7-13J$-7C+AYO3_k4ww)HYvzMXYv6oqOw{n|0 zhWR(z#=loBYhUF*u46+l#~vS5MdOf#wx%x1`EF_mr~NKb5Ukm#G$iT@Y(TPyB5fw$ zzhBOJ4j%8CYI=JVb%={OaQc`Y&rzE zB#0&9E<<-D|>#9tJHaAv8C|rBeT zEW83*b^Q-O+RVXGDy=a&5F@IN%)ARDHGUTjU6UmKg^&fGSb2zbfdtKRr6a5SGfHU` z3DjR)1-y?$DUcUBIyq|dq3^lnX%i?>z+fMhaI$3*U_;daWQ50owuOU6<%P1AdRg%` zWj}9?nU7^#0ff77eug9OJ5C=;j%7*xwwPEdUzOehgo}FiB*+-7KqF#IZhcOQaS$B(k zsfHeoc~D2n-lrqjKi5J|5!Db@a`se3ZwI@ed(em8VeGcvZyOfehMvm1^NKd?e2Nmu zgSPX}75djz^eteLY0GQcD*K(|^H!NtbaSqZRoj%VZFIgRQBGrQy>23y5?7(%)N%Vo zjpnyy?Jt%u!hVmxn4SlQ6One)nK1G>57||*v=J^-Q&YLRv*={?(@4ka^TOMpiZ!>z zv>Ypccz!TOqENQN4l!oBQJe8gqG7Tz41PQ&)57c;ea{tBqA_D_XT|-YhP$?c=v;w` zlpIN!K|D!?4@mu1HmLb};&#(lfEGlnO1)O>LM5qdbf|fTR`wR0P}Aj&>kX<2I!nOYbb6ywEojBAs(5&CsHo_kqZdH1p1o8Is6e*bdbjeY65ZR-vVFY@)VSWUtrj*c?hn23Q;riwPDImC=&Q@sg)d?1sOmks{2!$&AQPRHO^j3W|U{>({0XE3*E=?gHvP*%fMpMX>#aF(ct{QRUJP?&H5w9-;cLnGNH%7w_h`k=QB z52u_)R+oJCIx=k#p*V>M3#UMZysA@kEEHE;%wbt+mBt8ZwdorM%`88O`B$=x@{G28 zk)2H$y*QkLPz|vE&x(c|EPLXa=kvvIz>&sMiXS5D~!L+ z+IYrDq2j|(Bw&9M7DSOL|9-@Qfjsn`>*6|^T*5^E9{SISTY4_0-<^%@WrfI#zh2i= zmB?ZG1O{80f+aYG(j>dEu5dlb3G+7Od2$3a93v(?X#ihSS2Q1INg6y65*SiMx)V~| zKE18N6M9!aP!)@?`zyIz%vp14wzWN!Q7Uznmn**t(~4w9BcJ!Rwtp51BxS7_V`#+S zi;n9R+Ss6r6sMM`E-@}p4Y!B2MO%!CWIBg-xr*+vTw6T~V|P364I)mmxESpPE~=ev zVl>`ID~yLJ%juMXx}ZD)#^U7G>e%lF#JW_~6LSm+8HWyM5*XR(Dx?xK{hlubf$*iK zI%O+%9;B8sg`k#PBEm{p;z601;U?0!9e&q?{#csd+|rxd)nV&;yNmL%l5zu_Ky~Ws z7c%`bK*p3}xwCBMvvJ+l9+v(1F#7xRlXcl6JDoT)Sor-eQh9#bzcb6X5EpJG8 z{THeonTT>MJ#81--YwpJ9y@?-THV|DY%S*Uq|;pLM@VuiE-o#%YDC7yNc9K_vRFwX zjvEOBn2tePWm7GhrsaM_N=-6j-O-S4QC=s+48*|rP`ALO;)S;uyX5dgNo2je+Er~M ziq-$~Z1?B<=$@fyEpw&xgy=OdB)kMtx)NIlMgxje7tf7|Ja`HR2Ew8BO~x~ zBw*Ck1~3_lTP*kncB(elj9fIaC(EWyO5WFG&0pTnDZ%KXM;?TEBo(VVR>fAt5R>$_ zQO9#f@zzWj_+?EX);fBKXKMd^9uL4958XoAnfxR9nMuG7Xs8L|Q(0!Yln~W{(-|kxV8lo+2dE)I?HPlyq}e zs#8=qHXg|jN|2du;@s^bYM+H?PZswa36WL7w2M`uNGJp^MQ*BJ%Usr1rVT_V=|o5t zBw6GbAE_9+so{epY*7leYeaV6P|5VFg_D$<{aQ|xF$19h%=)Qi98>v|k=wI+6d}z(D zB%R1ibg*K)mV|toAXMY~r!gZ&HJWJ2?cA(Gs}DecWli+SGj1fPT&xiSZ7tD}m7>1_ zVse&W5tY&&vd4L7-|j>O0*wYG>n4?$9k*fC7hzq}N*DL05u7Hs{M$%hcTz>vx!VR8 z9Cfrxv&NvC6ivc`W^bx)uts?fGjRNVQTQaUY=$v1JrQzhdpndho!j`8+4obpvL~#mQ zBAQ~9umj|-8~VGRI4s2y1_>(EFR7?lfJ`8fB;UB(RrdFll(zf2(@MAex(yV0j1@;6 z5y0|Qj+^H}g;VbLlr|)uQbF(`@xpMhvThV^)WzI6}dI4zlY`-I+en_9@o&y0{~ zHi1QK4xH$TVLw`Yl7C!yE;p?@wk!rnGVz8Rjr53!C((zX{R?kSaj{%DTd~DyoXe`)8O}r<e$^v7NjqGmf{qZZWOJ7mrliZV%zwr%6EAzNly7(B;Wx8(j?R<1K3OGyANee7 zMuxVBfQ=AWa@vORx6<3g*l5B+_Azo&Ra3S^13HDxRndqbL@~~0Ic&wSL2C5mAKH6i zU|)&=n3`#)4*T4=(B)j|73Z(kPY?*2=O6{!C z{)T$t%*pEkcb`dN2`{LEfjEKR90|O{W_57fdN1UZsNrtAdy)x=8irXV1l|`9E zk9D1PEL+Jcu2z&K83kH$e1EI|$8I{lJi-JnjFxQZl4Zkzvd@>10Zy=&9Ulrk0(S-r zZJ~N_A~AK7fxqfD>oVPXx6)&+AsyF!p19QbTL8-UilT$0$htaw67mAte(l1vL^ysH z{%};85u;c{ohYsG4*@i|39(-3<8|u!U(NY!ic7_%DnCW#J8H+tYwPL6dIVJ9nmuE} zIA&$9zc-uT(`n*Tvo+RRGMq~%(w9E30Yr|*qLV?K=GK{O?-!09Z;zn2Tb%*#+cinv z%;@J{3&M5lTV-EB^@9(u?n-A%$7;sNRz(9`{qOHD_Xi)(4Q34w#xA$UoW?F6d@J`n zx>m0qW?uB9DopQMD*Bl@O;;-dCp#lai-L?aW?vE-j?_}c?avc6C(?S6@RRY1C} zdu06ZW4UtMUC7z{R`44Rmf)-KydxC{DktGUtg~Maj_U9R*#ZKZvGun5udCZBxTiTs z9AoJj_WAWYzQLJdJ%$OHkzF>ce;bKcRz0oMs#JcKxjKTTrom2oQ0MsR5(R{3I=p7) zPCjp|tr-y^*KjdWVJWE~?I?itXs}s4`I4sIF0>jKj*e20H9DfZ5AyEp7mEpX-nblk zIq&@}A0dxO>}UYjDkU%a0F) zhA7Pz8;z>j*%CZ25p3`d5D$EYoNddhIQmJY9aujgDy zOJ(y_Ou?V)E)Pl5TG2O1O&;zyhPzJMobGS8x-K_V{XNzx9$73l!=oZ zHB46jw%Bt$b<cY#~|Eq5}rlw|TvQU3gCXwiSN7m;Vr5J4oi!_wP>1Ay~ zLbn()3cmI6SXUdRQu%iT7|gMiUS`%C-K|T#S{I0W#(mwc1nvKVr(BKAl`bQ{|UQOMF4n!A~O zWGXQ#wI=MbKt;9PkENCL=k|1b60`$8?GjQn1mCIm;KfQHWMpH8Gvh`wL_1#HUqR=D z4q`}YrXS!DWo}hzM6;|t;tEX5bby5FMtd1w7>D7SPh#SiqJ01(NI@egSkdA*5ghu= zGm`X@GEtl&irvSL2t5OEm;+%9L+x;}X$iUZ7OQ@%SBn>(T~`Mcq=Pj`m9{PXQoAEs z*z9toxjq*SpXh6D%J1(3ulAs)#3qmUwZBWDhEPv|y{ZP|R6!m9OsLo`VbNGAyYmP0 z$f}~=cY}+f0e4QT-q6t&@~*S%8`-g|t(d;BRt^+8bVRCVV41CWLcY}1s0{5;22Srz zL-HpORR*4;wyhmcePw%-G>Zms+99%Gao&L@Q=8}ev0*TY^vi=md^H z>N3sZ%%g1GUM8U(ym>V|-ItO)I=@A_hITWP@r+$85%tAgMA#)GOevPQ)Ve@SxZ)md{rd zT}ct5Y0#9T7?vCqc71S@t}I*{KiyLFlT7mthT=Tyk-@84!2A%6)DDb1oeeKKv`n;) zG_R>frs!%r;Vk%B8&mnCwFW6^e;WVVqulli2@a>6@Y> zYoD(>Nyo;-#>BR5Ol;e>olJ}gC${ZOY$p?26DJ*8U*6yUyXbXp`r@p$&#v0FpQ@)s zj9gb}0_DXiL-Y!p)8o-@lnxJ^y#_l8+!pUYyaUd<7E_}Jq1@7Sf9ZhSI=3%hp9!-) z(j*-OU$18LKX=|=Tm5fodtMJD4W?EUBxbcN{BBM+{$r)!ESjsqV|*^Z|IrDrgYVTa z^7hLv-eu4A+%0mh0nshEsiR6EUKG8s6mPs17g#v=4QY_FM_>@suHFMlJe8uJ(7av` z2C9yIaI66Pym};pFg2QO3M}2y9!Tv$VKq1py$Zn8Cz&Ss^)`BP=*k@<%n}cWcTc() z;472p;tDf1`#w_*M(&y}XeB#HoPJry&;M4Gg$>;RvA);rn-byy4vlPV1 z$KA1#$R}xco!AcZB}LdNp~-NWVhVCsp0u&einC=uCg?dxNt2R4IDQc#4?R1^2Qwr4 zX5Q8tuncO$qtX2ntto>;s!B?5B~|#ocyVwmZ(AFIKl&Pb*hobRYRySu{^|F1jFMHEwF)`{2)R zGA7cxpDRC4bNzQY1w9^z{x4P5@o@f+mIbb%(qP5jk7J!*quZr|UYC@DerNZsxqseG z1Dp*sC~nZ?+#lS7z-hqxOHl2Q>^dqsFG$hL7}UJptfsq&%k%2w;lA}D9!F*+`VgJS zV3@++A6jSBKjGaRdthXPvg1f+PuKNP`9%+;3m^gE=!xIK5ro0?4r*|zkbR}2VwtUq zyr>D8WT%NA7=nt~TXF@##*oU$J12giFje^UMhB3ly=? zqsF~lbhnRr8>d}8;vvcf%>@P#89mqo{;0dC9-KydZHQXkjzcf{Hj;R`3p6tjFxd>Ke3>#Zo*3sZp4M3BM<};YHYxVxJZpfO^@x~IhxD>}*!!;-G!*!O$; zKR|9rPiAGd@o6T%Lbf~7*}&oHOI99^c7G{8^GB*+<_wDspGyhtqwy1=pH4Y_OiI31 zey$JQ%$82!%_7*yvEl~_E9~a^pvVxiy+yI2XL}SRK%-ocsPbNYJt`@Z4jv3kD%GTX zG|aq(HV5uKM_y3<85jVwWd9N+8<8WyC}|pM73h|S(&t;}wcqrSVLLFb6!#QFN+Mf{ zCAnQ!)!a@XNGIp_4)_Ipn?Lu-j9a@ux$z1(4h*mgtJx>@lZ*u$vE;zOMnNnKM8R&_ z{zWmjPYo}0MJXFX6vRw~*7tW1r|qb;=~WM-ZU_@BP|pXw{a~X?`Et#(-N%)L>~X@a zXY1wJea!E&CaFM2%z(wcq8>ghmh&!N!1LzgwsreC#o=QKE8qye*7q6s9~;NYlEGcS zbnMldPk^`23nGE_fS;(idGzu`S9SdTZZ&2_@Cr%cx0tTASyq~NZFgXHy6M?{% zy6XZ0{I1<&>-l1ABLZ7szJvzTD2fExMJ{Gh8dM*QL||)ob5m?wYR=n0zFt;wzwawC z9~cv6udor%ADWIi5(S+Yp2Sd*9MO z)o1m>QXBplR~Al~YW>ta{pdSiesEb-oyFTNa^BS7vI;L?i*t&|E^VTG3={$}kQB%4 zr^6s-PBEusGCe^_=3<+vwlWI2@@Xn3H#4anKmnDEQps2q-goB*SYttb1%YNTSgZg^ zYVHv>lhf^^^+j48lEl?6K0Il#oDV(N0i`XMQN5!qq+SbxDWG+2J;}+?xsjG7c}~RO z%ss&8t>F0INOYG-|P426O$1|J) zw&Ri((e3~06F(N&+2phRRzdHxQlhu>=GKoxETZ?lu2+woi$`<4L5_{%F8gY0=}l8r z4|CEw4|vFF$A#=c>kL55*Bqdw4)MHvG=^zHnjM)*t=}bhlO0$%>}^nrV{=n{4!gzS z_ZBO5Dqfh3Ry6DvsPs+S<_psbn!rx_OH#)AV!ub6L)%&0on4R(T6d~5>v=I%`2<5u zvlNT{;IMTBev&`(tKbp_{%(pezI+xrdzpwx1m!#3kMObX!!_XW`hodP63{!el3{fO z!=;*BOE0jCeFVY$Ixz+ssbM;11xJXy#LOYwXGK_`cXkgtTv%D$RDyUV&I~<)qe3YG zXLS=1Wra)i4uCVK&0-nC6t`|Q@eebQzca!uQ8<|78{s+o0LxiwMJiQl^7{t5!^0Ka zW=zfwef+Wy;HL+LpC_kT0-UEGtD#B^N*UCVWsG3etX|mt=@JV7LXUul<5v!$UR(I7 zc+C`^^bxK_kytG>lKsNz)ySE3+|R@ZILWJQo`zvk^h{so0H(eOdBjn|G?k)dMwm;( zKjLJ{d5R()m(^=douJ3o_YRky^0k041+B4pV0zzlY_@(~? zNVb$_=YMfnIq=IaNLDdFhV%CJdwF@S{oJno9O?0Tg`PQGz3q;dJykjylHjJVsKdx{ zoH!|=AWaq$QBJj9LA3#bxIGh~pwkgb;83L5XUiNP=j?ww<2DfDB8;w>eguE6(5!?! zAONi0T2sB+WB2Rr4xLaIMSYQ_ESH))7d@`2NCyu2?Pp3QHR7wyk>)o9F&P#EcZAy|Wi6Slu{a`9G6YRD zJa4y9xf-IE5mITFY(X3F$y%_?s=liulB$Yg_=@)Ti6w0@0rAg=1wYxq)^)*~I|6>J z!UicRSG-^P8gg0n(Q1;|abE_JSQytF*44+^a7tV?l$f!q1BBfHP{f4;6rvm7@V&82 zK`2wotl_W?R=4EN-|yh0<;7z}X^r&YFzWj@^0?`fIOU}h<)Ac2O3YMAley$s!SwG1W zKG}O}zu>Iu8TmiLqK#JvRDNaG&kJ6`w`t^_+xgW&4=b>Y`oE!tl3Ju;J;#7*sI-7XoVV&qpy?c^bvOYaDSz?IWNN3-lVk>gFztezf$tdMfpQ8l)@75)=Cuu&ZZ-s=*o8}=Emf3 zcjm$oJ*9K~gQ6J=pzh5-Sa-g!j=}o>W=;0Y!_*7$8;KD}Xa>f0b)IL=xAF41U>Kq= za};KWu1YXl0FE&;ezaH?!;c&OUvrQ6cv~|9hr6}j-`7stof|rf3?KetStiy5Q_XK9 z#>=HBSGs^9)Ntq};1<2-u)X^a74f@PX1`~-17x(#c3Qe6^-y&k6s(6?xV$xsWJk~d zY~Ux>&}_dUpok3zNCRByl6P+221EWp;D)&(AX%Ddu^a6Z1JFz^e$c@xrX=E$3zro$ zwP>JbNARs;izs7>4K_dG+3W1AIc|aHD_eXQJ+-##v~BGOxR~mByBdS;dBpo1SQhyF z9k0OiUn#?PRs{I6UMcfy-2DC%zMikmmp%9IU*iq~l&zHOv}4EX{_-M0m>A|H#H02u z;hW3}F^f)9*uD;9QFA9#ZfuSyqZq5`p`oB@=u# zDAbb>_6Y>BL5{=LP(>4w3CNy^qQjp75hSHU3&=02N~2X(A!QX2KjufPcJa_A$i78S zL5h@BY}yut*ZBFl`>5*?Bvs5^ctBNr6M~;xhxDl0*jwcFf5{Cv@c0<2ecQqUb-MyL zzo672O8dWh=9)p+!T;{|tH+)3bI)SiZ@-Z9q3xuAKtmkRaq2G$bk@jEKrpP*9)2(O z$rMFSH8Xg*E@ulfj1q=2z?mPF)P%I1~W5~aHl#n&E<`M?tiov`-ic z65&hd3OZ(BU&6G-1@%E7^ZW|wKCnS_a7>7FI5hq}M@^ET zCyAZth2tH5e4JthynprHI`}_4O-0%0CU}roF1u|P`XC3ltNp-hbxC{=q8Is&(F=T* zCa4(W8QMGe(Vt=w4k{`Z(2hL`j#m)an0D>h#kZcx+$Jo1Z1&EI!-*a;bR=cL4Za{l z?tiVD(GjtT`TOf8#~8ID);3EOW#z^=HsolkvqE^hz}W5eYx0dCCekyI13AkzR>r-Y zz0?9Y;H8X60mY2ibbNvdMqt2Scbc!}{K!2%OV~;TL1I*}q%BRtPfMi0KkD7~dnBC? zIm-y^H$dNUjTzZoE;@B7*ygQzNpsqy%O}HN0b1S0gxjn>n^E>r#p}S1A zo*aH>{GubClsv>#zGyubchrco_4{aQ!5l33oVxS~*a{z#0hl%UnnKb1y9rCooEmwe zE;*gVKU?yhLZw1p5{h%orbzssuTK8!@w$zYIEG|-mAy`E$|rq;2HKwSLLsm~MVo?n zw+)}{czdbXe;fe~AUUz)6?DTm8xP4i6Hxu`GQx)m2^RM}_k~W_E;9d`e*55NV~1oA zmEm?J&ZLwSsy@i7hriO_-@|L6zU!m!QNZ_R>+|mZ^I1diwt0sdmg}r4UOpS}pH%Xv zI4*o0+4ec}+5VWzeV_6X{Il%R^|#t@dD=#b0H?PIaT*HRl z_TI}NkiGQw5+6b?@Yl${ajCj>b2b;9B4*)W<1GVhyBO{OSVIY*v*lwrGHro1716^< z2A|OcIccz-tx?-$@YL;QNnFJ@gal>~3rF1fl4wuIY0#6x7&-`;V1mKx5FGO4+SHgT zM7)zrbem!)TPCr(;2+5cP9+#Yh-%AMx4+CC(`QIKHgPhtkImaQPa7@Hrd_6j)MP0H zEFgBefFe}JMbbT{U1N4&{&^KO>hixn0*yY)FIVxnuKkirRt z#IVD}lSQI$emKA-CrUPnbXYqn> zC%GS2>t7-F>jH=m1`s$Fv-V<80#7Sj1b^CE;`ap#A{(TUDSz2mPubV*|2XpL z4EM|;+9Xs8vVajpjQAq28b==3m?97(uhxGSRbmAZfC;-K{3HFWQVUZzr31KV zbnu!C&_UD<&wpU1BQ|zw*c@fKv7m^FdDwV99cpt*%up+D-tXK(1m~MkxY#nY!AE^L zxCjG_#jD)@o8>iNVra+aey-@IM5LP5UBVf4f{ERteyAnQ*VG3C;e~<-_}SRcSo_f_^t@YM`A?FkS9@9mc=yg>eIktIrnyE z)-SC_#at4ov}}wqr@>@?4pE*lYh=$XGYaQ11)Lj#flY|JbhuaP3_!9(RJvZ(0vy)J ze?qWJ`oGV9z6QquVxec5N8nYLH-1b|FIW#nssJ1(=rL$eG?aWUgiOV(Ji0whai7m$ z=VveGHcPk84BkdKKe2Z>yWghx9{;l)oXY-p;4)IU@!I`*-sSN5kN5KfdfWTxqpMBy zPbTLEz-}G0U0fChayOAmj>f39<;s$SnZA`S@;qd6|Io#fl;2vSPb`w$M_0crGK;x} z7Zz&YvR`H@(j*7odWA%NT!y)lR&+fqjc8;*=`yM%+2JmAxRv~#7KHL8d&xq&>x$5LXl+69)~=y zVP$!vkIpqY=^%O=2u8b%lwBqMkH zvND^h?`3(M1b&ZA>I?kgm$z9Aha-j3vV6`hge;j2hF}9<2RF=F0{!-kZ&junp5OfB zecqx|kqnasJv)G!r23&J6iPAXl8HcsZOogT(HLk%3c67yw+6CO2tk?`jwnW47r^?^(G%=dGI#$Pq@f?? zT<1e|x2+TzFF??zIdB-vY&04A+ff-$dr-!=PlpCwV9|*@b1d`U(Im4|P3iE)H z2f)!&6+7+K+fcGXk;J-`i%F<)j!Z@j?zI(xNkFXN0a((L^6clwE|}2EzxyPZ(8a3& zgeo1;Ias?o9%4>f`VvqSsc@9aKuGarR`4|6NK2!`0I8&ZE6Jq$j*mr$cd`@7$Pps0 zL@3ux0POglD&3Lb!LlSfA6xQn^|FevNCgjZh0w^ss>3X?YI&5sZIEglePV2X?7o2w;fC&E>&FPsJFv<2w-3qCGE!all z87~o1gkl7_)@6ey%E4QswTnxj4}xx!u);bzcZE8nLVLezI1s9q_vgCcd#i`jbMuV9 z-F5kYMQd7<0^rMRV)T)Ix%wFB@!hZeIQ}AY>GqkdSuaxjt`~TyVEj;^7tKlHg=CMVJnZ?qU);tN&CFAzE{` z)W4`>3#BWwm}s+Tt2J9h48cav;s+8qHkDD8v?3pM9Fc|*LX2cw$8l3@+kMD!L&k_z zq^ObYZkAuIgS$SlT5m;ZXT(S~re`7Y&fjL#oLTzGz!9@VmaXaJ-R$5a1}2IegYmdR zuVrmUGp@A$)jhv%{#O7fH|3Q9v*olDgQm8R9&yo>HICt0o;Q^vvg#zIDJg7j0t{5= z(w-2KMvttqnt)DmCp3Ej6a<%f8Hyw1Zi}L|cE8nzpRQYz^#@+57r8 zH(RB&k9`wngj{mRU8@sS?|_?#ya7<7+Oyq#*JYTmwz6V{K_O1m)!zt(&f8h2Xme~ zHKGr0VR)#w^ z?>ZiMC%<>`og_U&npSoI%OKdhZaQf)6gY~F?&m7~r4f=tMua51tuTqsM3KRS z;&xp;@*wU$afw7LuVnF(0pZAyo@eFn+a_Ll{>BT+KBFk)Mw#Q@l?i)P!cWdb*Sq?9 zGU=!dpE4b}#&?Tl)AwGIVXiv}_&2FDey9OZle)bufwr~DgSNgF6A`~MICI-u2G@dL za3U~Dc8Cd|%?a?Me-!&$JL<8HTcpgBdGIJ0y*6%WJ0L0_mTf8FnCi9p)N}6x3(93- zXNu54{ft3o1j%U0vv*6|+n;E<*);tJAs|$PIc=q`SIYzTb!^XmQ5?;(Y8D?50naoYRm+5ZZjo7B8qzq`iIgPD)zW&i87%G?0Rc-O^y?MJ7v z%xij)we`bOLT&h*gqdTVl3XU|7aNzc&1h1VWB@!Z#V^nym{^fdn>Y46@-3mW49(&m z+9jbu-HS~Zllu^x<@`PHpF*8c?l99b4r-Y_SUc5}505`(a=tW9o5T|rb1_UpnbNsMSBE0G~$WbM@6V`(KApiKY+ zPvT2jkqL&VB4<}#C0~YpVe|<6W3)?jkFpVtn}X_SoP`ntiZGE=qQH0Mf;SA3NHl3D zN{?hs)-ugHhF}RY<8Jb|(5fOp4lxSwwZO7Ox-hkZfT5|9FVEUBD|e7johJegL*?1@ zEx7fj@6#09|Bh5h*A9P#N>!($=*73v))Q7-s|Y#@R0t7~f97QiV7CJ|R zYPv|JB|MV5NWNj&^hT6nP#MHxWJsOst++b}ybmn<-!~e(j^w_Ve#-kJ=6nh00IdHb zv-FwQ=I1?Q_b2rB=KyDJkJocQt&jSB^=jHg@$8xhMR8pUbd#(b6@2N43Oa1ls4E(# zYBMhrb?=oeE>{`gV_Wwl7&JWo8TZvbIlG%C zs0%Tex#J#B;tgE?2gzbFH)k_W%YZzcp(}!StS1ayq`FUyiGj#z^MaQ6RXai;II)5 zXE;xqkELj~c1%)KX2IGiyY_LSs@Ivrsn@aB5o(+>HDPgLrl$oMbWF;PfKFTtpAR1k zn4H@5@z>C#%O-?K_@*63(e?<*F~IwMU<*{C{a}SyaB1&x5G3NCQR*>8*g+qfDxP9u zHue~8#@@xJdJm5zK2GycZAhXe*m>WbasFXi0WA_(WIWO{!$6zMJ0Ya2$xBC5@Zgr4 z4vhKc`*oh8FwMfDQ1oeZEkWOR0lNptQIRDnhc;$Fw0cEo{Sw(o1U-DVA;=m6O; z5xV77Eft(zdVv&jwV8}kuaGR+Tmx*i>MG63_Cc1>?1)HGO`q1N_HuWkLK;2n;*Or4 z5k1grM8Nw}Zou_ot*`a#-S*Js@P9ppJ>CD8pEbPd9&H0YC zH>>qfQZ{EXOa!x?GNv3ku{CEjb`JmD$!>oypUe0&vc(A5JiUq}1?Hc=U55*c{l>tb z+^n6QMb2qBW2Hr2d?6vq7>e0}RV0~o*_>+F(024VB(4N-d?=J#(RtZ;`*NGeQ&JX| zV56v(Y>9b&iC}5nq<&+yQ0o2f5oTgy!DTqFA(FDZMZShB@=!_2x^XBQ`p|YU4B3=H zCd)!I^<<-_D4S%+n(B3|#~i8i#@INe{q1)4x+7EWdhz1lqtBKGIYciKnXv#&DMfUBuAYi7P=kd@70RSZF3yNdBkAs{TLLsJ<XWPD)$ahwE#W|1n?p;5#JwTKjh|tFwXHECNzTcF4KiBbb1z-BKUt;}=&S}7> z8e!||sPMsgbKS{TBc0C{6_aa9W>n95qMOlWp0sgG!-azEO*kZoT8naoXQXH87}iey zfI`;N(BY&1T@9KFy!;!<9_tYtn!ZcU46b7vPT`UrIG7hKY%Ex^m>jvYYsq}Of*7di zAz`>9|LfId71@A7)x&kgezbLMA_4BZ%LLQnu%c1AoJ<~HrvsiIOaGWYzf|wzyyyxx zA=(I@>tP4ro6*E~)^gvWJ|X19FrjE=ByN6|h3eF#Q0#nr3H=s3<(a$&X{%NZqHvnz zT@-+lnt!}`hM7d03uhlplDnXq(}VCd9Y8k9Uff=;z*{=L%bc0Ksc+$_A1Gi2c8ZGs)z%YgL>mE5UtE9qyk6_6 z>{PyU>yZFI-xyt~cu-DU6%A>j-j4(6->8rMRNFr&6lb=-H*PMRsL%qJqD2f!k?1Jt zviG;%vj~$2K{fZG6zx;0N9&Vcfv?Mkx3uuOzftFu%=vd{s=~x7gMQAAK|XqWmt;ue zJ7el+JlR2jh?f{Gg+|=*uv;cqXljC4H8h$O5f_?Ve;^!nj2o{|_4^yD0vE_0E>Xwc z<6+5&&*H9X(zwY)kKFCC zSj`}w&Uau7z5XEqH0d8FcT0Izy?tH)zxmifQDcv393mjgziHoONQREw>=X!JcGYt` z!`lZ{BX4`2UH|^lZ|%*Ny#IAS7X0r3u}kcJ_dO!uDkR{MQt)H?Vy^n7yV68dzo52^ z<`I4*pqC_O?(>cTcs|-ciZ~r?Z6S$ByZK!JaTO*Y+V)zVmyhRrYuyhMA*z&}t2}`Q z$jP3*1U4p;5>{#CAtO=|-mrA902l~xlNrHZsGhg8qO{6n9AO#C()}x1+ z;w_4n8P?N6BwPRdy9{Yfi&-ByZVBlL zTT!kus54+w&)Yw73F867$Fn%N>T9993aO5R-r65l!NILw4(Q#BR4nOpWKqY(nqYIZ zD64q%6O`Ui$LNW4;g2*fC353%r6$paOq2_;^T+Y1G?JIHBW6*Iu|iyhd`thhf_+Z>w+k5gQeJ z=}L`=tPlzrt9JEWD2eOhdTInxw30;{UV4<|0L|5e@pXNl^k zdVj_H!sYJ!Qo)Z0R=!O_y_y*h8=J>(;h7Gq4)q!eb>un#^7yT+|R7kZH=&J`jQd$Xn=|l+r?kpD?ya^(*2etO?R%jw+$4HfR46> z=w5r&Kxv$>>lG=0jjzONg;+J$r$c(6V63S2E_}N)Z}ktK_(Q8C4mBZo5gnJ4>k8=2 z0d!~>+?6|KDxy7TLT`Fe4xdlx$FVR- z55cgyXDvTC(u_9o`$5n&(XNI~ zBl^FbUi`nD9`H$VvbHSvex3XF+Vj5jK_e~T_rVoBRkM{GD0lV)Er@N=J3}1&ibf#N zo|q4`99_%AW>CdIehv%4o(}C)))55N5~^yF%9vX^b;ytmyQQC@lB^jrG6X6$#3i5I=b=kSXJeAY4b|t32Zb>EsAQrMuTZ=F?#O zhQ-y16~Z>j-k01=P$dTASgX+4kGdBzw+VA_$#5`X0!H%@hY7;jK#TWjuY@1|dzC`C zg(XdM`3s-hdE(8DDR-N;&qeD-hHGjIy8ad@Go>`oqZDgI|0DV#4RVxlLJLjzNfpPc zmq92NII8M#vQ&S-RDi%m&`TF5B%;7`>oh6{=8|HDPIfddOcHRs3NJ>}0k6I@35{D! zF!V<^=j>$tN5FsGTCt6CB5eC=)b1si4+17Z?hbM+g8sA&toRJC`X@cEt?wOOnPMQ57I3rMQ zkHpCjVE*+$9!y%;F_AAOie1N}4n*@uzsS8j6cK#{X^~c~fAc?LRfyH)5eMV;#i?P8 zXHPM>ga|breNfy>!s~F@BuHPqpR1a6QHejJgmAMX>+mTm!I$xL#QtRcC!qZ-{+A^~ zT7=OXii>4329+9cYZ@7O_0cz6?t0d0sRTbYS)8eh-e+|ZiBKax4xylflsNCd-W)+r zw(I1>NfoXoxC`c^EpXP2Pj2=15e@E93YBToS&odc-&=S3`W6kgu?QUa@-w5oQHrnI z3dsX|X|_z2B4bWqgQ;^t@Q*Mf9}r@k)Z86Z%kAxG2xDR%Iwsc2gxXWtM2R%WA{)CZ zmA-FU9Fb^qYzbNxA57RE)iSLTPc;0=A!XdOeYRiqjDVp{y}xOK1Jz{qr%PZ|9oeut zXLjw(I#n>#I{KqhLfgJ>ghYfMX<=LmqG&YT6v5|XQf<018XGq)muZeF0ig)|yX)qt zI%T2`u)T8OQSfzV^L~sA%KOO{q%Ss74^e(;n3N;Mc zH$iFy|MbP<^q%1;l$*z}A|Mf)nxZ)L(yA=vIcY^jEXz;_%25MD8_UVv1MBvuI@pkV zBvtT*Lo;Rv#IR{lzVA?%oTaP{=1YSQn2I94Vl%hv5YkVaGD>(hI%~~-2jQUO=u81_r*bj``y1S zJ2fc|Id&^kd0mTffu;{=|LMujQVoBAC0B=nsNgCbRm27ok{2Zsb$`>HkpTC@*q=-f zoO#^=S@4?tPC0w67|Psi^MReeEthvM#u-{j!s2XTk?AM3X+Q}(kxCesenU>mfz;*} zYZx46C6tn>2~i7kx+QLMrxS)z7}amKS2E($#2b{3Y@IQNlz1{sEa#84dmY!1_(2OO z2{=Z8b~gI(TnHk{v)=6IQG>jaAZ2ILRxFd)D^I_4b<@a_p9K2>f)(w-;<3g6l3LuQ zTg}0C5ahn`2McNEk>~3RDfZZ8;$4e2BzJUQ^+%R#z=XDuCV5dLQ~RVv=!c{A33XM2 z<=@-|ZWMXbrxiKdEQ~%h`|3a}^iI+(znAE*5x(K&o+q8pupXkB{~EY{zXW`31^oQ{ zS^`*W_v}Q_qQ2N&yGl->+(bT+X*qfggwBW+`bg;E-ZmzN!G}b`hsmT=`6U;nk}U7u z+A)^1#oK9pFi^2}gf>dNdR_v^B_eTq^m)u_jMx^iP+P<$gzPopU;5$Y=jeas@Hx=q zvme4qdv?wmPZoT1p|&|q2!HK}#}r?6tFA~$5|)JzFA+Lm#JGV7Sf^lzUw<{Uex+ZLX8k;8B61ex&lUv2(vP7L9)g>JqwT2FaLK_aP$k9k}i~a5PNFvW5;0O_qk2X)4sUD zGV@0fp>RHx`dh0nbsx#}228S7)YjgeaLD(22 z?Zva>lZ-Eap#v6UT0p0mW(KFC8SUYr%U%8gJTKxLiX}@qZ6q;+_!;Qv@&QuLD(5Nv z6^f{PgkZNxOSL~nL~vL?bx2KoB;v*%2qef#!x`l}lN0X~9x}RAt~0ogaOi%#FnI4z zOCE_AT!_EdjK81PjR)VUCp;@{7vK^6XbpG{ct2P-*tg*_=;Hg5dpGx~_KXftruHzh z5TnE64TvG(v2t}dvJU#^0tPSpF7`4HiUNT2x%}O{D#Jh^<;Ay)z~XzZ`tDZJ94P<+ zQ$)}NnsHzvf}&I>1ib;j#1zgS%M&IQ18o#qfHYKL2hb(XOCF4+z^pAQMapFE4k08Ig@&~Wat_ueF>&QbWt}ivfO>6dPoRWHm+)_BQhPe8q zgi&wm!m-moN&B8=R1==!hxD5&5MndC?wxias{4?b;n@3iOC~iHYymK<{jx$mcIuK} zR0qHClFw7zizj-U$%F{g>Gz-s$ulNvD5J5Wxo=C@~TH+1}LDyo0w_aORu9Dm7bCp zO(|9A{OUrn`90WEQ1vZeWiDy=XP-yF-8FIms4?L3^JxC+3+Q@*p?fO8*VEtn?EH2+ z;F0zdRN3=BH$UQ*6E=<&p~#=83|wK4+e17tSEF_~vYOP%q+0CjA=~eyx{9;A5PpCV zjvz2uPlqL-=$NyR-8r^{#sHXb?xmWz9R!Irt*#`4VX@Z7b2#PiL)#`I-@SW8%p_%n6y!Fwq31BYZ4oTn1hQ z_HC*Uh>ENaMX-25c4*L~$xJp-b*pa+fJX>-nx&>!FuRAq%Lkw|0N?v-d*rC1^9lde z)6Z!ILiOa|Z+_XC!(V54I0*Ay#^c1f!LM&IJBu_whBNnH-G4n_4$L=L;)#iVwc4aO z!Q(xPmo<5eu0cLNn%q%KJQO7FvxUWFxMiJHQILA6JL0^0`Y-0la7BwY&g9tHpgB6b zG-N&V?>nLw)O^avk1KwKFZ?ie-$pH;m-spu=-Q4yesiy@zf`fy{; z%Q_T}03s|oP;2rJ3(&egcmnbY%va#yk|-Pz9o})jMT67dfT6U4gnV)0w832zi#j6o0H~Ju06gV(j%yT?ww=H z6*S-j4xBWqR8;jOtwGJzdqz)e$1QviLcj@s8jF!oQ==OVI}NK8T7c8L_(i?w?XsP+ zh**@#Yz--{lmzbQ$~=(-Ic{i0ejAYd)($fmnLA8~98CTaz3!|S5h>!pr1=yVT}#aB zC7MYTtZD_zC|+W9|I3H|*|X^0%7=~C(XjwMuVfOf2px#mmf>oYCb5cSrUL7!fnXC4 z*FeM@-a{A(^lmWm3H?HkuJ>hN>QX+nE3%TJXhg(Z2#q*58`Wl=)|6P9NXqc+s_tjm8dWGOo#xGf5R<(U zUTgnC*CLOdk{rCxZX&U16e{74`UX9DArl$2bd|=L-4Y-tH?376f)K)xpH}sTby*R> zqGC`+*mmOS(FM-@I>2vqVuB~1$P9YpV_{Daz7YBoMVl259y zDzk*egS2CbFH5NNF&H3~74$V_$=t#{u3;d#jWqry=zlhF`8kf&<2yJX0(tip+C5D9 z;R3%tgWP`7rw0A^-PVt%&dZ*kGp6^ID%oWn_s8aT*(_A2TP?Y%)Bv6}R%8#Z%H4m3 zhB_oURQhycIL=!T1M;C($z+b2!nhKNhjZESkQ`&-}Q`t^o z>^!!E;iRW6AxObzscp(Lgv@o9N9w1R^lX{i0tsPM&e<)|2390JD!*^{rsZ(szj7bP zs;+B0PB&It2gaG5KUtsm7{&|l)1E3#;3rHPcjV={m%bXiat;k7sb>>j(sdKY=+Z=v z?IO^d_|g25^t1B=k|D$$`frd{{x1+OK27Lc_5#pC-j2AlS~ zUs)u#gTL!x4O#G|PqG!+|AAHT@$*_PxRS!l2K5x74i;usj~PThsF!-Uvi#E82Q>#S>zEX=>KXMT9IkEa&Z zvd$B`+{zqV$t73A$;ylmzl(}M$M0x$<+*$6lj*Obsdo-g*(vwUZU&=ba!9o`+Y(V2 zEaosnjt-w6MC-8MvQ4y8+Qs0&3bXt31gA{c=2dijGtK2W)ugktO{*T-{ z0YGX8{JgHJnw6fiZEf4TCGQ{6NRY}!?^<2ak8mH2eH2kJXr?>l{8-)5Q+_~qtPc^rE6-acrHDv4rZN03LxXysxOkB zOJ1(_$MX$_6kK&r0mP}vB7L$t`fu;XAQc3^7w|?ISRNJV3qd^_u+wpP=4pB1K@V~8Uf7qp^oqy_eHil9`Xk|f9#Kl7CAa`J~^ zi+m*;SczC`j(*>EBlGr~S+5)fs>qIWd+7q+U=nOLf$Pr+L^p?Vt4sa-{wOlr(YW=yWtO^i9pS9`=MvK z_CR$=7FM}?tW(=;@F6llaq=r3F@Y5as@GoQef3S? zc`&}JL|ylNzkA*qds4532)_6nK)JN9e z)Ye4FNlp)oFR}!D@9Zu27APG3n=lK5iXb37?!vz3S{)g^cuLM-F#IA9FB;e=&9TM? zpf%aWpv!VYizz4{{}|8Za0cWdgc#EiMAY9P$P7SAxCNqe|GBZd$h1(39nBZWAkT#& z{q1l?w^GSJm@dF4=a01f9U}xTPtIy4Q1dTy4^j-s=p||K;k>V0 zMj@IaChkED6W?zVYAK+FeTY%xzAwP@VU4aC3>2V%=u;vh>~qgS89DCfYX4+Vju>p+bYV_*Hkn zh(rAw;v$90)3HYYaB)V)9f<4}sEX^rC(g}QJE*L6Ob}soT0kV)>~ zaw`{`limCBHFPL35a{w~XzOvywHJBozUII1IP$HzVe8{+O5^_YUJ~r;zszmIPdN0I zxqmp|{OP_1J^DY^ev}y$qU~2_NG^{dBrK8mqb7s-j zbZpzUZ5tCCPi#9A+qP{d6DJefw(VqMCtvP+?^kuI&JWnT)?VGKS2s{TMqwB~La%UH z{P@(!)WcaW0zJ@buOP%W$T@|gYE#%}EJUIl_r*BG(4ALxwQD0%M~-QYEm-Fg9WFe; zyw;Y@_+?EL>g4lnwFHSZV3&8>8W>gxRx)2x?P{AZE|Gw4owOpqS zE!bFDkv_#n0zIgzt3p*@2}QA1^t_u}#Qg!DPcainK@50K5{ug&%s>S4Bxtzy2e>rx zH7&Kyyl$AE{Ve;&=cQLt^kNi&S`|~fgPzK@BrZ1F@dbtlBGj^+uK)VWATV(CEUSi6 zu-#eB%mfHbGN$U7(J;?5$CET+Is-=N*RAmQ2l$}-S^weQr7pX}DSSPJ2{&QU0nfeh z`B=H>@xFO1oB0y-VG~&XAzRA2{#V9ZH^07&y2rT4(NlYL&8(XkC`-UWQm8KjKv6V=_8 zLZTy)zMn^mTEM@VTTcoHz$(Xjzyc3N%7?KUBY>_QX!7{j06qcXHAmlc8-tpTH43E% zuvxWm)@lQlBPIblYfoh7e=xBE;o%6(L!U;#)vA1k6ra~tDZUB6xPwpV1Y<2 z-{qsV;Ei87p0oBHr=+k``V?bYmc1jgdNG zEPq$GM(E7t!#oL#58UgGgWo|oWohiNA5c%bfg(lXnTfp~jV8^gXHU%4M@q|4<*yo& zh7<&yUn|VM-d*p4Nk&la=<@TrfxPj%IT8GpMa2FOskRgHU*s)Na#VVN==<=L*L8d1 ziOluJH*;`iVQ#+O)R2H<{&s!_JkgCOcfgWCRER-mm7~Sqp6k<*dFReEGI{;EQ=|O8 z0T3<^3V^;~;TEEo37FO7igk1DZXdxl!#R~27MChdqy%gEqxGdw-2=_dIDifGOB2K4 z1p3yKa-9-f<+u&3Ll-lQrQw^^#Y)~TQKwjacAcTLvc-PKpkHe4MM9C2bO|8@h4OQy zCJcpGb*X>dv*#RwO4J0L7yowUljNcYwtczO#W+qZ3>q{13X!5OmJZZzDE7E5Gs z4a4kTdko^3+-M@|I1pVC!ISQlQk-$Q3yD;jr(8m9B%B18k+kF$NdEx@kZza~5lJ@r z6&dWDYCE$sKKX>G^a=A;;x)Ku68Ho$iYlAI3B08d z54VHA;%0*Q9$@&d)%Z|`v<{XfeU02L8%SbFTjM5TPc`-@SK<`+-uIF_Ut#)hBY8fz zuRXVA$3F*d?4$f&1nwggD*Ne2r;9okg3O$Kd&`4)wAaBtVzkn5P#O!eh7z@Cg`gn%x>U;qH8aDxD{$({SC@l(>_Y-_fsb>g0t zf#>8OK=a_ebvEfTQ*Dy!9*tj9k1mh~f=gpLC=@GoMWh#^48U5Eu3d__bwXN~DKhrh zF&q-f4_8id+@%T+{URz!Zr{ktuDe*s3XevU9Xe`Odx;&jK`CG{N z&=wX4d0q{v6a}fQJaVI$NGk)G43|5y=%QuQw=%r(@i$lX3V-Z=a&Eu=tC{J3`DhmX zA=-Y()#m?S;I}{Kp1)6V{oEEj1{dnfLhM{!r~(b^Hy@C;f&+(-PxP$YIN;og0@Thb zFr+%5rK4v`iRQ+|$j=J{ zqrq0vGaY0xAd{Bu^uU$H_OI7rqHjs0&BPEvDf09j%5d=FWGqHcQGrJM*G2Bu)t%xm zZSRcSmVTLj(129B4zXi3W&T2O1_l(38>*DJ$?}pcZ$fe0WE?Y>Ds?l4mvMR5c$?Rn zwQSLx;nSEma3{UcQSJQ#YH!UME6A~3*kVL)X=>1?*ow%TOCZ~WBN^oblG=Kh9eW_S zprx>yFz1bNv?269Z!?IYr}T6ILRgKZF&qH}%0JgT^~Gl@{gW%5Q6Y9(o7ijMu0(M6 z<1aubgiRblRH16pLWNcilsWR74W7Cl5S!Ux&yp^eDsS#yW#1ug&?|ebw{z#`rZU_&cHQQ_?5!|6X?moBz@<3YJ^vPn0{a z&-T6_sd^(3e^$by4P6b1ygm?8T}=neB-;6)A;kx2TvdM@C&Vx)p>QZ-Dpy~os# zL}T^7ary)G8R~}LRzj(7VjO5uts%H|p=fM`poFYkqd|`nW10Q+OHxCpeMWu-%`zP2 zhjv|&=GmR`812t%8pV&~&g`~JlZa(2z@Q~<=VkYQ=vAj zzllL3(u8T6hteDLD&71($8)O@#4T<;V?l4;oG%9&!{HG)e+gg|QFD4uM zo89v_%F~I7Erc1TR(|*nHp!emKPbHodj2?n+yg7^v`9Iu{}u?Sa>WMY{QaJ{x&R5A zsc#BVg1B)JAV+|tZBEtSYM%?FLE^lK6PhWLPg9NLkW&@8lH2{>$NBwMxv;v zUFYi2jZI+|1t=B*`5;u5cSp(U7MjtArb&#HE5QlDJ>yr3Cf>(N4o3)Aw5RX{U6I~o-Isx7A5=Dm5bF{GHuLu zjX;HUeBg1Mg+BbkTi*Djfg1xCWJRn5ci?de6fZ+2gez}82{x3f@D>c_3g3o3Pe>~7 zTEnfOLulG6pc|*OwH()qAxv`1rZ<{q)q0`ujh59Gzs@3J=INsjWS7PrXia1s5UDg@-$;Oy`rUtLA+QK8tLFp+t2TN1w)Ie>^4Slj#syZT|4Nel+zq4gr>zl_2w z=n8e=%m-@;=bUtrXqUHeO;XOppG!XnP%;uvy*_&DYQu)4rT$lM^3b_3x*S+pLN8{E zvqI#hETqQCH-Z(*J91hYXtxL>dJ?+v3K2!dBEqp`*{s@iR@(Q zK)~?AgJkQ)18r`a14OX|A3K?uw8I3(`Z#7TtpZjK#4G|H1?=fN)>6hV=sKG==7VT#UqZZ3U1mG+z9wjqoa=UsZrc3%y>rf2!W5Biz82s@w;*)P~YKNtk5_4m`ZK zr(ptPw0o)E{kDwQ^E9FVa#!cGNBDImC`0(4N1n3fzmGg+HoUqdrl8z&hwOsT z$Y0h_e}loBNMm&1Kf>c&jSkL`p%X?(3lgG8o>(&0N+v!HLM&5RlyW6$RN$bfl;FDd z7O^eJv_L8hT<}ZeGE@3|ore(h{59?d>uJ6HcYQWne*6QxYXPtJ!5`l*4n0FH*@K*UPE=Y>xY4YDTI1gU;c zrL_tt89HNp>P*Ux4SJ=OR=SAL`vy4@A`6h_>-v3Lbe|*m*?03fX)Sm^H1omFU)1~4 z+5e#S_WzxC=f_W`;tyjNySHbC`&PgBuYhE#kSfV_; zV`B@3Wu$8G5Qnq~`vLJ!+(oAw9AVj5mr~!svK@s-9f8Bx=CN74Itu4dc>Tp!IEZoa zQ0bg3>1#=i9JXG?p*7V8|iNN)2JWisB|&78Q&8%u3LGH@5$ zHo1WxNuCNHlsdQX)fNAR%{9>EkVbMtW1}}dWl^2>Rj6Z4hLi0y2E~LGZX@NIXlyU~ zOVj>pFCZ9NhWYoGmD-P|n$Rn2K4NB{kzq)Mlr>3n>*%U}QsoBNG{4g&bs%p>V!vkR zb94||QguU@Z4`Z}=PwEU;9_22+f6CSG{cob^V0lnOQ^&U%W-qCu|_*olTg`Fqty6` zP&bV0Q%k0}Nkwn5Nzv+2DPA>rGsuj=^e^){dMcH?}DOmh&yLyMi6_uBKK*z8OS!tVD&H=Nc8uydpT??05^r)16g%VYQiF}N_ z_`2@7>WB$G0py?Um)(n=_Z-ac&$=HwGyV7ZohQ`C{|wN5N&W}w{zREneeZYbdwoCk zEkymzpjVQafDeEZ476Df1|-^Hk+3872m2Xl zkFsE%2L7}2mYw0+)TEeaZSH+w<6q{Q35mX#Kby9FUR_wV7I4NslNcUWaJHL*SkQ|u z(VOfk%@tA0kTHUR@~_2+@5|I0a#=RrcRdCwMJCvsz!us%sezk=Dds0+_h=erBOe_K zr%&N)`v<|~eHlG=Mov5&Ks@(vysx>B7Q0l3d8ADbM4lx*Opcqsf{R_R>Auui2Gpt* zVl^8wD|o%QZ%qt1ePlRBA?#MQp z*}u+(>mq^BUVHLEb>=>oe9$0%uD|r@>3d;>NkaQl=0vTdq|PufAP4Rhyql22u2z*8 z2B?F(F=U+cRm@&AX2nCY!dDk(%tA0wm6e?nqB%H)m@_=0PBh|bYMlGj^X&$z2n~`} zXaVugeY5{YWBwis6(tiG*6Vh(WV3IZ2NgsYH>2L~wjDpVz%>o~Q!{RmC;S^x8kom5 z#q$`{;_OyEMjna!F=|QKxLWdiw(fbo&-)b58W4~G{?EMa`2Rolo*vyF=UINYk&M<< z9M*XX=s5X9rO($E@F}1ZP=QLIPM7pY1%CRYKJ^j63l6LJ$&0{Puf6k1+x$C_e^{w; zq@~nLd-XDL0lSB!r8gAkGPm}jtBn)$ zkF0-o(#}+(W)D(GtC<%UNVgW8^6!j)gb;j1FhbTuUsjpp7tylMoQc&NuSp0n3NH?dUF&d&t&(2d+#Ma-MC%70{$*rePswdAZ zfo#A7{!Kp-9ag?_gmPY5?osfA$_GNZb;2Fv z+>I6mV9d*1_nP>|1QzN9jt4p!c-=7||K(94uL|hP-XDedPK#4A*o>l(DCh)w zOeXS;F6#{_uDx!@N<@Cg`2EF&CCtfYPmAQ3-f$9DQbig{b+kTg8VzY}TM&iic&NJP z_PRI^&TLjMFWDAqL#x~|qOQmDLdX8^_h+oHhgS)+EgO^@u_93=Igu&h zjG^62)@-jb3wrl)6@K+oj{+)7lY?hg{xLed8g_oi)~jojTWnubOu^&3j-Leqgo#_V zNeGpBy?m^uOy%{Y(6Cm>ACC#6a4AZ}Xm{8KFoO3Oy&}-2+5;RO7Gk4x772VJrE_9+ zIENlCS^mG|fZaauaMKJOSU%o5Lo9CfOOn1$ef#+sCZSQx!ChCK0l>ACiq8Wht5JQ z(2_~0KgJA~WTyxLVH$pfz4tP`mc4{0>WVk%yIpXA!p&q&eFdR7Isnp4iCv~+ zcj3RhtSmh~iWtK;BOw1m2PHtuyNPd%IuTdIJT3F>T;)I3st}c|E^7ZVrGxN@KE9G;W0~Ygr z782pH6MPlyaOI5)!RFx2ZPA;TRpY6QLhVQ^zd2mV|qj1?_{OtAMW=<+SkyG-_woY5Gmn* z#BwUy{}s(80qV{_x_aIYS_Qm01=8S$994b(qHsf1HcA(I1$6L&Q8|v`H~;elV$4qs9MtSz%?aaR>3b;2S|+3W<{d>+&^d=|Fz{UW|DFDHQCxf` zPN}Hopuf{i7aq+8nQPBX+Mr_*y;Je+0HhQeA!Pbaz)%C_97w+!c6X`2V!~m0_JP!4~7ku+94=v=PQMK^t z0WoVr!cuHk{H*?KwYIOSxc1MMTs*di+ihSCGDycDbab~B@zpRWR_aPr;`%@hR?IDp zXmRss9!Z}wsVujY^xom+BN+fGZZKu!cyC+6h;#6cz}SY1IU{55l)o1TuC{#{ilmeX zl+Dn*ypqiAexs_1nUfJK+<?STE)Fylv~ z-^cU!8fI>HG9Q8Sf3Tl|=l>`G2WZcOt(&j4=dX{m$0D1E&vSB6>Hw+~3b+fS#AuS4 zHKCbPplM5#ITP1+Yag68*`kUI{jwSUePFLO)s+g%jyS0mFc4C&Mmadzi`Q2BKGVRS zwWNq^l^kZs3Z%|XwHl0Ayd~34XDX;uE0AMrMEEc|=SS?cp3<|MD-Hb8`TDB}94WNJ zVM2SYfNiVZF1P*tEPW4ift12ty`aNS-LmR&`H`tUK+Lpf+1cS<9oLXd2^Upy|b|AH-E zLHOYLU-q2CLocd&q|LxHqggfAnPO|mjLrtGqAQ?r}+6$EB5`k74z>_|M^%Ger1(@C)g5nHM#R~V$Y z@6+hfVMjspNx^l~+qN98&{irB-pSvb<2=H-4lo>B^tK=F;(DHc3dBWK&sz!O_Zz0) z!HM2?$V%J{f#YTD-KclGao&B0=#%f-getvL~VIn61{^WmA%K<1P6s9yrr??3bt zK5gWpUjhESa;W`8v1NDV(ZUQbAfls-cWIwkeKYHl{1Khf%Tc0R*Y!-=HVnW$zP3Od zx^*h9TLB*{BO;rW(=AwE>4~vph%RU1yfehJR1J>-eFZ(eJ2apV&x&7TAoe>EdX`hb zI)j>FW1CsV2yVfqA&J2Pj3L?7DMk5l3{DfI#WuB=oSVJT`^5^#9T+#Q8;9XrkDEUI zfCgn|eP6qAed-pdSPe-Glk)3J%(kX{Cdc)~&h!Ih4ourQs&xrFhcI{5W8qtcPdu7!nPQS*J$mu?VmS)(#5PTw*)1_6d8Eg z{{VYQXFwgoo+*h<3$~gaXoCx{@Z+t=MCIkPpHGEnq)7|Y7;P1L<|_6Q@T5~hb?C2I z$Gt>4-H0eXa8z41Z#eIf687BItXMhoHyw{cnHQLr{zRxjxKAh|P&cwaV z;=cb6DH#0^DJVxBA_{tb)9$=mc~i)^^4hait@#fnIVK4dhr5|~NG5TmCIF#5&A6A@ zKXe+5w7lWn8(R!FJl#*!XBx+nK*Z$+`|GJP4?qB2PjDKX5WePE$UMSm6%ODL98L<82}-c9@y@gNE0JP+GW++yV>af>k=-D zLf^QSXs-+isDY*Fpk(k*6L>~A>yO6K4RpQ6*dbLMZfS~Jl@|nF%w0QLndY}@ z-D%b687$?chG@HA ztm!uot9B*!@N2X_n67bRU2Wil1lOQ-L{}6pvxx;I^*GZTbt@QcAUV#1@AvN_Ssm+U z+{0Wix{X}mf)4Ov@1RZCvP$_yvxs(ZLBRbhU$}!EQ|I5$JipZ610DV|QuEOz)es}i z&0`PDytbsaJ-n#~{y84A$Q2UJy^+xws$gJ^tK}#tdx?`uBgih@Y3q;c7+oqnR*gNz zR^!|?SWamQMjXYB!gj41%yA`&bAVj7{0mc`m+RN3R=$Ghff8P+;Xq zo{WG{iGf=Osr2$ig(q0>zOKD)jGR!e*k`E`^2(L?mB74QwW;T1IQ?k#D$_goh+sK7 z`K%5PLYOk@RwP$@PEEKD_Zz5b2idY;(On~DqW(6$Slbo?9v@M|_Py9e4oDt?;g;^i zn)fw5_4DCN*p)sXgFmU=dUR_34m@Yb+>R|!_Fxa(RZB}?Uh`Yby-H(RSUKGCG|2=8 zoS>FqWy)EKZDlhUJKsi^t-=;9J~L?p7`@U~IXPj@r(@Yp$ea*6m+&$m7)Oo0&*YvC z669dXFN4&wlDFU>zeWas5;-^sCDtImEG8>BGZvI$-9D!1NhtzaSaU}Ya}`L&mq4gP zc|+))hdk$(Sm9g%uAt{lO>587Q{MM_+E*;E!zEWVs0F_s@M1 z!x$s%`q)M6c{L}zFiq2YoSIcd#?GBaoSkf=+QFI+<2_m+4eRI}$5#<<9)x|-d9u>7 z7H=@*@Qab%t%vh(ws$9NVs<$(R7yTW(M%F$E4E0?@Bn6={fA@3hU6fRQRbO%8gPIQ zm6YJ^5-_1;VabK&xiNQtFA8zT4hJC`pf-ad+wN$LZcNU7h?-ZbC4U{g_reBU0 zwL`77uX@Zxv!AD=-v)RZYtW{~@K4rX6?i5J$^G@D4MxIfgoO&Y2`G73tRNj%YPi&i zxx3;sy=oiHUZe|;Z!dF#I?hS}aafQE#H)B^`#Ak~dhL2|Fq3Y{(qFusrf@m;EzB7~^S;Ge$bupAOckOxPV@vZ+_CTsMpY63>7 zM#ds%nx2RR9yS75x=4~ki39u}x^{dZdUQn`&P6-JEf5)c=IBfC$Cm&@l3;Y?z>ev5 zd$>nFdx7f{g+dj$r;5elZ03(KAUWVO8H<+qC~}9R(X><3F_D!h#A}znrO>1;bvBzvuU{=ITDG(Lrtw&Zd z2_bXmEZ) zRBZN~uclzKEsSDrqUElJx4LEmrjf>p!UVx-6o-7R4J$$tOSBw^DK#(<73M}aG<-6{ z0YU`==6)Qr1(102SDbg_-2ljc|0Wf>FtsY6XXy79)3ovLb!@9OK8O_tUVEs(x9QZ> z7Y)n%TTyyR8e}Qg>UfQ_PBest4BRE=^qadbOVl@+Yjo*YG0b2+PmQQn6tw zhDZpx3;aeMvDN}G;wjr^Z?_mFJQqD`_v7NRmV$${E3+3s+~>SsU10EZSpX3z^Ykz= zwh0r?JA8YAl1u)%h?hpxI4~35bKniTN?F>K=`Bc=T_-H(?t3= zON52JO`Y5v9u6fTN%Gev39@9e%kmgALzRx7Zb|e3wVqe&&eu|CovFi6bBowUP2q{*E1n~`7lF?m*#iT1FLb{ zI8!YD_Bol(Xu}pEizG?{Rl^dXYw@Dt_E~)OxJ4pj-SClyfiwf(akULOfT?3)h_f%9 z4=2!!jCJsuMwKbn`_FuPXUBT6HK#E=+jONRdR&W=S_u*x$3L%fQi<~OXr<~*TJZ9j zP?N8&thKbS<|;Jq=vamc+`Rd}=7qR5zs)2nsxCq9yA)esk5&NnMsz_%svACs_6EBdw32=PEzE>9QFm--r!G~Jt-?I0W2P-6Np@+ z&fCAzc_bXh0LWpffSb8!c#%5cu;ya{!ZK!yt|lhD{JzV3x`%jgwnoAc^vRe9{0x3k zmi^QC3yvwK>i55DCSb;kz|zP*90yDbW7{v7;zP!MY3`q=AnHDlt-!QR1j9o-7+} z8qtq)i3?s_f*D5M>oowiH0~fN;@M1MkRY$$8Rf^EDrs=|kQ0<> z=bEYKhIRGL zS4%59iG~1~(4R|Jk9ovk^O`Thk0~5#WE{o3Y-ek4?mt@l{eDTP0BuIqqSpNvfAVHN zZN%xYgcFcWA$zg}Hx%qf|89kVLj3M3ccw9*AacqFDrv-Q#bJPS zRGXCO(>461$sWmaJ5YuWOyxrC2!qRIh^mDf8F4;M0H5B=e9w8KB2uuy!^k5|a$nhg zobPvxpZkTz_Yvb~Yo6EJ2f*axKU*YzS^w23`u1OB-T0j7f1iCnQQG@Hs%2ZCzl|cL zOCI-aOu(>^z-%oM&jSl@=6*Yjjqvu2L_rEO+oB!j>*kYHssSr}6?rX4>0lD-R2kYn zao8+~4>p}{5z~7euk^w*GdlH~B+v{u7GLE+FHAHW^$_FrNln~p4^sp8!1+fZAsP-N zGy9cYD>IQ8Ks-EXv+w4}+Cn83sK*eRw<&^LM>P5mhc=$=*4g%U4pdz*rRoS$ZNl4% zQVkpv^YGH!6t`XvFf=6$GbV#60_631_nEfx`0qje&78|)8tUoY;IncUE6GHHs;4p- zQAozS+#L$1iX{qI2BB=R{t!Ei6=Unohd@WexJCJ6@wupIY5AfXKSH`?i0ZKPG z7K5#RPpMM2xjLz7YDCw2JVNRyrNN8RF}i8g+(u>1jMr9s`amBG{uC=qby(wIr4r`J zF$`$ewS62~WUTSBR+nAXoRc31K{$!>!lTjo(^aYOw(by!LtHlcyH5EXx?U93??`z3wDvEW)8f! zIZi+fZmN4yl!AP$FpN5Zl^zfw=t8BL`LhWQ%9wQTJ!@t4-15Pab+ zEtGp3VV7`xgUe_ge9+QX7hPBEVhge-O8{Spbu}g^3YnKvO-v73viQ+(YnAi0T;+}J z2=p9why)<`9}ps!cJ&F8{%*aO26`|t=ZP*RwPVTRr2hMNVPDADd&8F94+iv zvbcmw6TOcNBJmPA9JfNJf-WZ#jMe6Cp*(}i4mgaq-TytJuod%Q@3V^WTOp!z{_{U%$XDop$&gR5)cj|C-+z!hZ%KZi zBj4{jb=!T`lV!VF1zZTZWuNdvcATG9z=rNCFRl9g-IgLd(&!7gWV5Wm@yW9sn>Lky z{w0%)^oglM5mlp`wsu$nEi{qV2~b1i0F54Ss&rFU|j*Y2&tODjI&)N^B?vpq55TW2ruPgs@QawQ?}Hi zUQ~h@2w)Wny=`e&K!k9Mmz1$BxAo!jy`-~ftE1qC)-=uTr+fZvqaahGAI~*GmJPxR z-kSRF;m;Mx;BqmR;d@DfnAe&SoKcuVZVKV0r5d;mw4pJp$H&%2DV_KJ`KUZcD==T%Oxhv^K0S~p@UJT#US*y^w=|{ zP1i$Netpc=cYI!utLbM)5`*(PjE6pWY0D_qThIQR5r2O`8> zb1Rx|S%_}Xc5Sx$XHIJy@f**>y`kS5cW}8AGo(Kqxj3p8x49jd#EfnJR?By+)NGliG#35Bo7{F zRL)4c^s?OQuF_&z!a9=4(w?8xhI%4aG!z4 z`X|qW#x{TuW*SCd^V;`i`yuEX-w#7e#!oevFBMrlNDBl4CWJyHjD)2N@ngJOndHSN zSlxAxhMo_m6zhQHECA;DrtC_iaGd6`yzK=*ioLeN)lG^$oJ&eI&I^Gwh=NRDgZIaar)!G)Z*TaOkrGv@y|^Vn#Itmo7VN(C>V9yQ%NB{^(gAbWZv1G* znWU=FG_$SJ{Nh(rg4?35;gO*B7fOPm5@?Y8i#z3>(SQRi4$ykL;+DZQMT~d(ff*n)QdH zgZ+;Lrv$&h=VmWVXlDUfLprLtHH&kJ79LOchJ7LZqt3ZDWHU*KEza(rqEqo3e*MBOkre3g0O=yJNWMzWAmlBYzpi$5_?_o?1v z4(q7~BP-kUOVBvMNXk(Z+Dt&eup_%h=gYOczgV17h4bomGq)u*X3;XhKw?+bNi2@u zlYXPj>;PIf!U(^22hw&>og5Q3pM7l0nzg~f?LzJ`yFa~V>~p`z-@iq5!TVNR0fJ8* z7(hRNzdUg!wVn4vub!upowt~}uP5B?y0S9XN=0k>wZb8?ob%yg03V5bmGT`KegKk+ zOfLk(wL3(RY&;Elsaiq8rA)z52C@VTxh1BC$+9*bee0VWbj>U+r*H+5;+ed=8V0Fq zMkNqtYq)C%cg+T_&YinIP=orYlhMT;9lDS+f$~x6pRA3-4ro}QscO4v(E8WI8id7PGb3K|IikLrupqME1q#AZ=lL2P2u66KoRBn zE)yg;#~QUMyFDZTNk1JCy#(GV5=lLM0ov7H5ZQ9-%JLxgCI&2CsWp3XpWi2mUMI*& zAV5l*VCI#`kUfWUvz9ZV?uK2UTZFcwm)3i?$f>Lu5?{Iu?!$y9{f+irHl#24$|CQD zAXL+tCE-Jo?hN*{Q~9?5#&2u!UZe=Er5n43 zL7M$TL1(8-A=9z$f>BgOg4^{OEM+7lE&wItoBgE`D+z5Q4M+C6lC_qr(Ss86mrYSq zquZZt);y_l2yYpWd@~Ord z#Y2h4Mle#3HD{$`lhfXDxu3mSRRqJ9=MBa2Y21mVBv|H%@{h3s4B8*JCdR|Q$kVwn zzQT5*`7=sH>&+x6fUmR5>r^UtaLytj&H6Q`_(tZ6+o zS2$AN0k8LNiBwOOM8Lp-_6=NdXz2LQ?yLd`o}a})0J8;%+1Bdt|vh=UfWM(d?6^~6mMhcBgYN= z)ON+p&8`kbV$!*vet!h@d^|FKEMp41jLUYfkw$sF8OK|QUhH)m z8wdpj%MiogeAH=G7UO1@z|gxuH_X!4-N0fsjkQTc!!KAL0 zue==fx(vt@fAB=tk&1o@jpUeXvt9^A+hp}vZw?c8ncq0kM~jouMoy&+IUEUx=-3TU zX)*UOql!ivy$Nv8G1@6|(u@wZ1%Fd(JXr;XMp^>=Ca+Na&0^HCKZ8~kl9f6_vOgi) z2$xhgA=hGOE}%(u#AC60Nn~&|Jd4B(`(6q1@T0j3lerhl^y)n~W6Y@Fc|9E(JaDOX zF6r^1%RCR(hOq1Pbm>)%9x?rrI$59w*ynSg&i6|v?_E;xJI;Rp_~zE8r}pBlsm2=k zKkZJQzGT|Nrf9|!d>`#dDy-)T656etx3fQRTv6{PDa6q;9;jGbsRLjH7sP?$ilPVmk zDg1Q=ma_|Zy2bWe;2$>>+RNW)MPN05GW&r_J0IaECUdyx79r!)cN>LRB`XmETovDA zoBLccm8*v|D_v4dk-TfGUBXx@pF~5>)cmn^DQY^a2;|((Yp-<4@^8<1ct$OhT~~BL zxoCCR;Nl#mPEU89X4KRGFktA!sI4~5!Ds1Cu>Bf4OHg1$_&=LN5$i9*ClPebh~KJXtsU*+KoTghS6&j;wmJhC${RcVn5!lESatEr7-8rPYP!5m5Cr2WOX56H?_i=sOuHkPch0ZG(Kpltz?7@}}( zV2jcCboCrnl{c7xw~Jidmtw8H?A~>$L0yDEC7{BzQQ}rz#jteVf)05h2$|tK;9bB9J zvU^a{o6}GjJl0)cpQHc)42%%XzcG|I{C)~*_5^zNP)HFx8b37%q+M`xGdK8M@ZGBaiua-OT{3z zVsyupOE5RwSzrg1_WES)zrL{P4#vjWaC3Hc_Clq_8z;BWh~dION|uwLrrFv!*k~HtHcy;1wj0}KW1AX)KpjRDz;7)xox`vPRAN!N0AM8E_+Pls(PmC zBfTm3;<}Ns0SyOHLJh@yFqfI=>%G) z=us4W;=62F3?M71AFEl4ws8bQ6_m>uI}Mf|_!q}!*%I(F&i+heMC2Y$bvHSz1iO4j z)I`HJFdEhCwJ8X&!EuBG%5w6Z8;_Ll z`nyFn7@)A=qT#~dAi)^v6U-I^rFVpnr`nefR3#wKhgj`IVWV>}Xjg%zTF2}PMRd3d zUZ9@3VNBu2==IKFpKN(LJYo}<9uHZtUNA3tq2PVB3@Fb*T*D|lu;JN6SHw936JBvC z`}*CV6;lU1^OT4|@o|=q`|ymCV`e&RA!xXMK8;zEwWuzd8jBjN-KtZ8R$W%#DBX;> z_M*203I$OkJy_om^)M1x?g%DKI`MG8 zN-97xIDJTa_G>88%;w4SN9>m4sQgmKd0R!advH9&$b<+`^)F3*syGg^#-ORlA?2$! zriue3DE0;-oWK9L&ir$|^9|bEV`wCPY3o2;vH&sm5>#7J6U&z_k|$rfZvtSL7S_V zs>_0*SDnSMAwv25ljhy6YzgWL{y~E`U=(BIBGNzgaJP93^*oS9g7K;-m`ws^8kD`Qu(+o&oL-+o6yx8Bel-Fkxf@^4R@HZ`qe>{sBb4+; zLvBecO)dllEN#gQv%E~#UK)0aLA!sJ5bq13v5}MBiT-A5J*t0oCQu}p|Ek=wBODI@ zxa@OCD=2M+1Een)`N6s5w=mOL5&-Q1CdQf47`qW1gKGgfsCBsw2c=c2d`^vu=;s>? zFV=YWK+P1!J;Qpl3K_=?O+SFIm`iU{NideCD`s2ADC^)EZ}2g&cLkmUzj%KGDi_JL z5v)^R(qvZhU(IM#eSh|w5d$_FK`0aV6XwZ3IpiRsoJgp;MA2$&NN+nkzG?$ZAWWu? z$et_*!*}SG8KJ%n2B|{BF{`FUl?4gpUQPy%+3zcpr46Xdu8#G!k=xo=cEaYUr+6<= z&}!O=P2WVlQ{HoySn;cv)KFY226ADb@5?DFMqWUMST?=jG<^YQsI99HOd4X{pN?SF5*gU)8D{*ZLgC`-7?t-e9lZTG_yB4S@N7+kNdLO7$8|AZ5hYi%=X# zJK*C z;^yCQZN{{$6p#NM@Y&fWzTQ&!7ZIfe-ZGv*y`^2MfS7Ape~OFkzXiP045w28126Ta{FlR3=Jk&<^6< z-XlofAr)Nk@NEBW%;ipKODPmH`wdttcxdE8Wms_WbsMsYQ*2!M^T|=1jjtnZN56O< zg{KN#cSP;*Bf$FCy!h7pcMjS83zMD<&fuj<(^PK=N33mtd-rmOUyzmn;~73Y7b?vS z?Hv1=%O_e$$VO6MXv7?|0i2L^4`VB6SyWv;s>D;U2@U!kKmbC~C0Erpjh6TF=A8@- zG$RRNdZmg`mvtuzyI&T|%rAk^v6)!#RC4t8det!B^uG}Zuhc6hkRyDWdzb2YjNSIU z0$L_UTS@u9@GY_Q!sMuIV8Cqi!J^AM6eemI+*T?ec7Yt>Af4 z@)*o>dIQZ|g74Nkv|U#*q|Q#x#bg~UIXXg0rAyF^x*02TXd8t?;U15VB1W~9?~f^) z|5}K}0$PlxQk?Yc+%%|N5z#1oyqVFO)03{le_c3PfQZx*zTsE|UFv;k|DZj&?xxr} zzv^++&6YQa$=xc;yHRZv?7Z%RJ)cpSwrzfsbZeO%ZIPlb!hT;2uLqyCA2SWZ@HL-4 zPY>5(kwUP=Q~Cbqi5RBs)xOPvL!okCcR@@Y;hoMg0$AlLdrD&{7!xW0ia5P#?S^8z0h23t7)`az_mY?-uU&ji0~&b`0~ zNKE+uF&M|J(^7p1TjSg}p2bV5YiJ#VMacbh_z1M@E)RpLEr@GEf!4GHT8q|a5LYber47xhRu*A0EznF|y^8ny zl{v-{kF7}CqD(-9-7L6Dxu7F~5v6k$1)N;|XgZ~R`QtvUwOxTU4{l^KEl8^b8&tn) zX;aTSU$Kpj*|*50URkruE9)HRP#(W3cectsZU3;EF{vZ!g--cj% z>G{!k^?o|j{T?m!xtGRFt@2-Wckl6k`l_cHj{o4}-2cVkILB`oFr$aXdI6S33)ECBG3g>8e9O_Usrhjv>k;@#0T3SZ} zR=1wC%Dm8{uXR|gPCH}91dZGH;HsmPC*b+((WP^9x;-l}K3N5I(!??0`d>8e!XcE2ETZFJRA46CsRJ-4wbcSLJ0&)8gc71gaKip3uU$i zW-MWRo>hqf_{1Vg36>k4{{Z-)c)QHOwbv2XBK4`8-UMOV7Wu!IYh;1y5(PNM&@bKu zui(r)Vnz4xi(Q@Lzo~)DLZprzg?tQWU*{7(cY;4`((TqDJLSAyASu zylSOWL)gfvNJDhu&Bdnd!rfH|#J!XHP`xd)>631CZWeq-R4A@Yqr!;}1L zfj~RO9Hy=#2><>TVR*0`IBpMf=Om+N6CJx~_sHhM*~eo0^mSme1W$AnhD6j1fyZ=wx0zz#m}ef`6Gj-S>&tZ3h`kJ+vZ9GiD) z^opAwo2)&Xc!_eSn^u7u+=dc%(H2+SusXnBwuElz+i>GC;*?wgDO5Wi>r(BJiWrfg zj$f!K6q%{Ix#YP5A8jQ&b-Y>+5`y=gjo7jIq!a@LQi%tcIHmd*+9Gr=mh_TQn5X9tnnC?LV-havLuwD~Uu{gH3Bi7RJjp0`@(br1H-9E5Z z_R%j+*{cO-AxjneCdTP$gbXFW#nqDI%l)dVJ0bwmICT36P&t7c!Fy}wKzI0!?f1Xn z?#|Yyb7>|Vp7+X+Qj3{LKYaVC7Yp)YVl1`4X$N%<-IrH31J^1LNH3~n=|#ug8hDR1 zqTnp-SY9c1HQ@n;q=Vkrnf8t7O<&!>3!Y=1mm@ z!w4<>{-<2!7dc*Ppc{35W&&o?uLy}|F$|AN*fhOA-S zMF%)!q9TOAu%nny0L)*A2eZoV>@^TK@h{Pqx$Ez|uq?r`0l^oBP+bKhsi?H-N*iLS z*NL$_LO728VvV~vBPPq-;weLDa5RI}zf}id#U$g{Ndi!8!n)G1s6)BN%bf%z@Ze!b zW{6`Q5Espl{ybd*Qp^x2*H7O>R|SQ$j!6UH+0Pi8CDbBl#{zXkUpcDb{#G=F=kR{R zvTN`fB-dIina*Q=K#*hx{yk9wu1!$|hV$$&tlzgskepmY6bavA}fb zDJu~#EJkB5VvP6WF~agUK~plKI-V}VLck+b#Coqm!HS5-#M(~u5^^1xMVPLorBp}` zir~D)Sa60NaPe|sU&aAy zlg0ngqa6K2#5V9&WuCj9Hl@Q-Cr&smJwiRriC*`4gI#wDc zi}AKTCA2?$O?kOzYAT^0_bUekzwIA~%S|NfvWJX)9G)dEKi(Jg{05AN#)^b|M^1Dh za7mO?UR?E|N>ID1{e?;}9A9 zjbp-~SDbo{gvl=Z-0a+*`->wGPNFKMgef&e)hy#{+_(HvWW-flB=yVKVb#@A$&PDb z7@r8S>6L1TqrgOVeaA_k!(kYfF3+%HyKE6tKhmEK+!_HXfsGX_pl?iZ{#9YZJPT5nrQMc7is5o@__n!=FN z^750`k_;52{h|hSAU0Wju`8U1z*60*jSD3NO0xu^MrNl!5_l=(R2@cZH5U~N#ZnPx zedfUTluppKJPbHDw;bnT&&1cEuUzs*CNUK4b04hu$1(8`%ODnbmin7l(FPy##A;yV~o*wXco z;&;*F5q@6Qd+X&)6dj&qm;kjWPs!w;t-yXL!G$DO&f7OC-3Lt%BD z)pESxvn?4*c!StKXrh9wblHG}WuG^pj#i+4L1{Am`{tjWd9c17**u}7DdZ@80X05< z=AcSh$)hr4J0yCcTd2p%q!vN7>{6_&@3D?mxP* ztV5sk$ab+az023s7iSz2J4O68P+UAN-&$2^II2r!ks&##1J*DOux$T?by9fM6Xj7z z9o7y(fTOEsVrQBgVyNGD zqCL0Q12TP;m+AZq9k)7T9dy_9vACGr+m%vwA`S8Fddxx|=sd`cKqsf*0xsT=s=v#E zH?W^}q(vbNO)d=ZVsd;ssN=nf)RrN~DW~NdgSE;iJJl21Dm8$Gx0bP|s^Q#I2_024 zbjS#-c=w)XZtM3svFC3Mk6d4ASmw)Y_#2!mX(>o>Pb@C1C4B3+!iG}hJo2=70xJ}ao3=0)^iZ19dBHxY6N(Q5N0E?)?$iQn{UdoI>030T>*cWH{ zVW9u5f#aHz<3zs~o`N_v>(+-_WNIYJ;Y{!?B|RErKb4qRgXNEDojl+9cax>ZTNYQ}f8LG55_#)1;g5SWk*EZ(D}Z;5#IlvM@OS zd$i6-g+=jeoco-K=^^bwPE+Y8n(dKEES2t!g8pjSznW4&5Nw0 zWt57HPj>Qm4|I^8>SYSf3nsr^>rC#S`?JOK$xH#|2-;IJ_r0k+|If;+o~P5#k9Yqz z4`}``HJ;|$Me>yd!3;G&)`i|t4W1q}9k#rF<)L~XT_)*5KfK2fgVN}BET5v_$1;EB zW$tE;1JB$J4$x$5^^jUfyi$p*3dbhr66EPoF?ZbDAAin?xmiuKl+&8fk6 zoYdi)4OFyl`<{b_*d(*J|J1w83G#=bDNH#a_g2}fw1Ue#hI0HVMJJWVO+K%apd5EA z;shmo1SW4*x@I~?liR%;{Q~FVhzabD4r&ougZ)v{e9?zx`fb%_aY*Trbq;`_RqKg` z`mV9dGWdH`k7{L{ojqz`I3I#l4QTlh4xqTUt;IPbjZKywMnG4$QXW1GYbtKCU63NB z1J7!oH0`@IHg?%L+!uFt7nf_9D*iDdqmV9^L7Krc&gnHe+bz&UoT|^FUv4z@G%{D(Vq(Sjhcl7}!j}L&8m;jj z;bAmcicB@g2E?2d>-PCdTPeN=x5~#^)6<~@Q&~$$k4b@7zO;ziDg|G)$~V5jlAJrn zc)~YA^lRHi|3`sYmE<{b{n-6TU_^b-fKWJ$OQGm zOPq=fRe>#g`tUU{a*Ix2P@d{!6{ zUG-2a$LjlgRxHg1zkpMtG+CIzX&2LJgfX6Vid3YIsacLwcW0OPYqdMEAioJ2ME5p1 z+;EE>>hWu>-}Pah*T?PEwl8SpQ~ba57isXn)*T|)$JxNu+e@0?W9&57y27T#09&gh zILxnKLUgtt^6aqsdZcs1?5Cu;aew)XcE$k zc=s*5H-s^#L*hqTe{J5UUc*~JIu0^suhbKPw0y2!pP9R=xj-cG%@_MeVsghR!Jbms z;ch=2&sR$TA^&J0P`1NqXBcfi1vh6KUJc7QKPMTjJT-j{M@raK?{9|aE2h?e_qJoW zS7$QF&kCkZ$2J?RvUxCKDxL&;U;a6tVXIB93u0!+gY3-I?my!Kxwp1LEA89Sa=Xs3 zj})nO`hYhmaSW)7SAmf?s|jOk94o=LNhBA3R|nSvc4iDL$Tx9I#KLRo$A`#~3>?N) zo=XZ=3w6=@#O`;FvojN|#qwRn#%ms@Hm|2#sZMNcLO*iKS*m|J3x$s7K!)tL&2}4k zo{|R~;LK&h%k;KKI@2oA`}DMnBnT*9oJL^|u-{f&@&yIq=m#}a(ZMl=RJ_;W^+N}Z z06-&^qtf9~3U`5rH`5i3M)=gr5CU}k=GfyvSb>F!`03;K)1jf!RSx1G3oe_js!{Pln7_7yP%MpbvezN{NO?l^|9~&S{>hmsnnY0AV_Ci?JAgu81;R0baGOML734va4`Ux()x!a zKpm8lk+?~TJx94#)MyW;l0N~3V~KbRBOh!^@RPjKvdgmR=*NYj0nTLP#~V}1I65@d zwId?smeZPKZ>ZRxFrng_XmaVoe3~0ve-i|(aWWL zn@<3hl`+dtqD*0a&|o{0!XA2_F6meI=-Re{M&9!BZNhA zXhwnr0yZn&Ki3m&owNnYj9N9e+;?z-(56D^h5eU4cn0cWV65~$*C3Zy;i*d6pm33B z?Z?e1iz?yI{4)YLf;~yT-hS3=uxv?X1X@uJwp5mVSTG#?>X3kmB1_e=S@j@2;3fnh z(SCm1v$5!3L|9>5Rxg5kP9^JvPi;gh=46J8Yr4AOgQXPsV{<&oc36u7DUa8dnu(Zb zrWE_88*{*6t#zUx7>q52`gfO;YgTY++(COS$mzi06Gv+WT!E~IAr2Qd2Mp&0El~>h z3b|mscuMj4c5Y}@)L9Sk)kDu&V=%@NO7iS{^XmA_YIZ6d<&J>T+TPGy55>!;pG$upcME0bZES9C<;-&V$)K-U zqVDmY0K{kV8|d6sIu)I6?;cJkUek!1!O53fzSBV)EgJYdG6=oy;eKR2e)Rbd!47?? zFQtY582i1IK~VSGRPFmwn%@t5&hJ}77wc0pB8(GP{JIvs|L&vpxiwOt~P!OnP*EiOiF49F9*$IeS0!u@!4?~X{ndDsF#IEYbNo6q~n z?9<{~mBKIr&DSTU$RznJzuo{1PL?tgx zid@~E_zH;V5WA%w9IHcW5n%#uAow1}EGxSO!LtbSa3Z%bWHTOc>AcN$bB7fTU{P@ZFUW>Do0uoe%hixTFAMym zXo0N?y>*AV_>o85L=8+u_Of}&yl_$L;v2>aot05kw~;HN+mGWgBXX6L)NPTD7#(uf z&K06pYWbqv422GnnN($US@O)UCYA8AHEiV z{1W5MZNmeVQV4?X6}&er5NmQQ)ku_cD0~m4Hxth?n3xhVo;nYd@p9#>M%_%{t7Nnk zh2+Y7eOhFPzTJX7yh8jNLhYU}cMqT@MKIZo*PpbgG_NcJSRqkE667|h!<(0lWo2o{ zKEmQ&hN5OEvSqQgUtO8M+ddzgoil&l4=zd0ga311()j*|L0vCgP}x{f{o^_13)0^d3P9r<+5ru->E%RLb0v zNO>-C<){`gnO-d}w^|l_-pPIeg_ePR^W45$w#4VL>oK?I-AAd@zQgP)egtkOAUPfn z#hTq( zubX@`9(iOtQ`^vD@dD30h!{Mkp^rnOYD`Z35ufF+JtX(-@hs*Ya_BuhIZKwnB5o*GItu$`IuWfZHW3R(-%x)gw+=)-nFom*nb@IHuh#tp5x04Vh|u5h{JhU;5Wx_}2*@1x!?m_t--(;L$i0I& zE@(d6I5P_!APjkTYu*vWp{1ED%#YFXbsnmXovJxHqUB^(=2_qYNX9UwnNV?K2(Q+6 zRr%*^9m_wvW%y*=+2{RCpSFk=!x5mPs%ebHJ@#;OY-roMpMZV33gId@QLO8|H@ z?HfW4S+Wj?;>hI>r`(($4~9laJ_E>c+1PC2;Z#c4aJyfR9J(Jih2Ba0AN+ZTrs6;G zoxad|QpbPIW#10m=c%jrDH6ZSXeXU|Xtx@z+(n0V(Oc||LK`q|7i8q$z=q;U&MW6K z^^$K85HK6b)*COJCOC_DrWke3hkIzuz^DY#5;qdcHcL0?upmm)l&leM`B_B0mhW?J z-d@LSSGo2}?|vi~D?jHSAi+dAu!47|)?A&cg_~A9=&{|?FHZ`K{>ApOac3@7Uk86} z)Z6#@n|ARK_prczM22gec{jB*_Y-?)ryq|j)$OtbbXD|=GBOl86GuI2hjj#uyY1o~ zgf_Ly39N$nhGdwUfG<(GjrLl+PK=#u-f5oR{qDGF7lR_HNii~$Bt^M-u7N=B>cyz5 zH-{~Xns#?mMQ+>lO*|1J?}bCx4fx8z$F<-fm&PHRhe6 zoVR(x*A;nX^SbUoer=OHaNVVT&#{j<=+0}!M!i3?+5WhBo!R<)cK45LvYoa}DJ&EIV_)G5H1z1W{qPEeP}+ zJ*~6VL(zM3yT-Jv2sN>atNF6)FG9*hW#X)p>L$srJZqn!_#Q=7%hvyM7P?ilLG&)v zyhJ8`!M3GmlixF1O<GrAfO1pvxBRZZ@+ALpIX@0xnv&0Y$yJG`ht7G+;^*q% z>eQ_0EDC|-Ysfr;c)_6E)WIy6!iv3P_#|a{gp(9N1y&ukGZ$p1oBol;i{h95Q-J#9 zaX>C6Ili&8;XRa+cDO&;2X+?Qv6#}bP5nahAJxm<-0Hv_<2@=3(eQeS>i*F{i}_D9 zwdy}ylu?@ZxNbKdfIBQHBN|4aajGR2dDzek~ zH{3QRM1yZ-Ip^CI{-PzZHSayOuhl)_B|0{Q`WUei)d?+oj1)aQGinmaBo|7TqV1Wg zlJBgozXmB~$#2^%D){O*f$1Uk9{u%11n)P_y)rZ-GLh}M5F9aJ)nr3lyTNENx!WbC zAYv{BQC9Ks*QheS>U!5&i(EeftYwN9??PueN0%iAnO132Md?Ps&_6w!5?2UfZK~f9 z|GD9diYcQkSF5c>_q1YiL8AjmkEq-*dZz2sZdf~T!3f+3f9Ke6iW#NrLL|`ZgJ`>W zde#h<-n)BTU-#TP!8J5wjv-@>$L7o(t=a5(MVx!jM#!>fFC%Z!2g@j&wBH9@c2 z;IG=TB+u&~8`t_}{0HE_(sAK`>OOBv;^xEW6Ldc=SMX7I+B_!jMn`9=h#nbei4)DvH#l< z;;l}*J$EO1fTb@8NfMi)1e6f`d7wP_*D9GRyJ$+BsWl6UML`+t{wXuen$QJ30p1$} zKH-S#+0k37q`i(eU(MB~!sj4K5p!hE{BlPG1zdtC*GF8Ox+aLy6365*2Lv;UhkVbH z#qZ9QLNb{m9=1D<#i^wBzC>g4;H`)!5m#ISbO=U+&q zoMh5xxzhGc=^E(eE9>JmW|XWCkTI2{Tv=;jaSJLscnqS0Eoqu7(?_H-8N)c8GWUF1 zl7g_K)tB<6XtXRk@1?}eHCA0T&@bSjzzQ0y9R|wxTOLfyv;n@$^4JT1=!H_m4MWB9 z^%efMfUCDJnG^Zw1_s(ZN74q!24eh6czo_?I&P$y$=l+O?ibR8Wa(mmEiVqebl%A+% zbCV60W_+Z7;RubeO#+goRi#LQXKvQhiR|KwbUZvf4(UTd0^(Ku6!( z#;#_3T4O1+nm*rEe&F>q5taO22XKPIMvzW?Z~F6NjoN9*FXlC2jsI<eai}~l zTy7oACFm zUFNi3c+#VtTv(*YGC(xN^flcxx%qH&G*T;G>9GYX>+s|!YKXgXXS`>`4IY|HtO7~& zi|=X0`)h7;4cMuM_fg&b&Cn`wMgnY=X3r*Df-x7_7)_m`!^*nK9Fvt*hl)ReG(K_j zL4(lAzRlcfRmLsK%|{xB%Un#6NSj%5i+l@LT{*tv_MKBzYU=LgELWES2f1DMj0KgF z@S&&VqY+EfnKo6h?7VK-0bl+=o(iDZfesSn!=PfzZ$GK zd{HXessxCXHIj$kC}=)KB9MA@)g$B}wa^h|3xlRXOf+&pC`D6CcW~fxn zAa0wZ2%h|~{8&5v+ATD7tn`!7R!T-5hQp+{#u;7Mw#;HCH_uh@806b}WpB3Ay~W?& z?tOcGhUoted$r|z)HoyfYT~c_AC!Qq`439uo!wue_Pl;ghiWPjRb=Vf&SP00W95te zQ1Fm;_FRJ*DQ_<}Du*z>+eVx4w+~pZU4M}^A-+vYA;$XFJKO!E(&T%ka*m?uxZfi( z$JP^PK&`?=XfW%3Q`ha5E*Do8z@{Fk#y(ZA`5U49!ubT%JopE=IYw3-m~trn&&-3_ zg!VjFC_Uqz<-R&O~J=i+NtzRn?YLh~YiJQo^>zi{ENkrb>sX zw*{9A9NkLP7Cjqeq7@-#f#Dp)swBYo`jL^3i-xH10?Y*;OwbsVNNI3$x~;K?Bm&n6 z##^KSLpYs-DMEfRE1O$-T5gCUsBx+r_|}4#mtR^AhABCDS|tT0a#0YqWa6Nqn6R5f z0{1K0%|oEgQANM?%Ws51k==vNleefppH8o z{?+TJV_iDhVVO~02 zqW7uWa)n(Xl`IQA#~jQD+*qWLP1$B^m(~`2s1=YLGf+RVTJ;sGu!*S;Q zbl$>!29%vmz>vSj7Rq|>Q;u7Z%U3R2&&lTXHoSu0p|~=nf($Qq{lhb&F2q=#P!mz5 zVJfD2b^?vq&L2boje!^?JR!KE&80|r+K7q?e?SPJ_c51AMn(H@#UyrgPXl1zAU1Wz) z)}+*bl_g3QAZ@VH@;T^UkG)Di2&>8WW%o5C#%?PBp=*mLMB81d`N9pcS%;D6FUBAC zcyH}u_A*IXQP%Q%4~v3nIlhQ7qB5^dxXC&Z#iQ1#Drdq0==Ff-4_wf*3OUnX4mlScSCnR@pr}<38#J9Qf6bv4Cz5q%c-p zX+@=-D)H@7Vfx0l(ze=&Y8C%&5;Sx|x?+&Qmj9pX%hjpIAzHePSTe0>2}xH)=-$1{ z%cRcr&NPRgIL$%Y#@B9qJV?7PjlTP^2Cwbze!I9^&m|_Q|JkvubpJ#4g3Zxjf^P>! zL41v!d{HU*^-eGK=3Bjfu2PSkuSvXu;2b2AB{(&l4S0jF>HfEtSjrzK4%Cj=)(x!c zaD^3%nV?@BP%e$jZig%&@mvNxu?_xq-z&DO;i$<2oN7YbriJj@T=o?ts2$*gqLn3K zU&P`zM=@5@!%|xj4~sRkU5hxeJZ$SrW;L25--VmWjGCm1wWIYbcH+TSzpOl;E*n@t z6oiJBj?O-E+(aO8sU$i_E(i$-UV&~flETYfu}iSN@i*obkT_>GWY39URhxhP==^xH z6C`qZu|G(|H8D>tDCiw6fC5yd3)VA9>hFCjnY=cI?)})Ww}6e-l{+UlKEhB)IHrVf ziQn8HZVi<3p@T25YH@FXkOx}2T>60$x^`prauSh5M~i-;Q(naR^X6NL06s~Rm&xtodF`5$;Kr|oUXxlqY#&C9t^x%K&YVh`bGy5*NuuKF z{%X6b+CHM2)85MyRv2{tys59z&D&GZjG6%8V=xw27rC0RV_h&a^Y|^E>~rY{8-==6i(OXd-uJ4jE^sQhO^y+ zUX6JGQX7Sj@x1qC2cTrhiP_rRTG~c^^wLmhnA7D*GOBkyX1ITOXMUy}b!X35q%7&| z1NW{sdC>wp$hhnz#Iirk0=`40s9#RlONI%3j7+c?*AF)QT0_1FGm@H69WFRxqtmh* z9tcaD)&(oe@y=%9>^NbO3ms7E6c;vo302&n&u6#BiaDw5%V8-vxLCyL1*spYSBY>Y z;A4!_b|wJ|n9QKlB>|~ciim2V8w_~p-m11ocqRqqYxt74`gCyeky0z^SVwFXL?)^_ z)Q1)vtDT;tFtr)8VZwOD4OW=9oP7EDaRGri(oi}0;VT7bMLc?{5`lr^@O8)%hD{4y zfi@i-&iY;y9l%pBF;Qh3+MC*l2He_gk^;FE{_bIox5DkcVatZm<&5e957{iA)=sm znvmwTPJns_{I7ioF-f2Rl`T(5;7x=7@sRQzZTJ8UCnE>lJ>DR15InLwmRR$0BGE&$ zv?Zo;3i_Og&Z^qv!_8DKAk)p1lgwQuBDSS@t+9=(5l%q99KVEUthYA=caklB33~N( zA|x;O=Yl;0rItOQ?o+A$<*ibSiBqRsA1&i+4zM5*U8%(aOVqbePpmiuqdj}q*So{p ztt?#WX6j?-ATa#hZ!@+Ly?U2j=Nweo|)!^CN$LD{l>;D&v_b(rxMpx@@ zlw*nelW*ecu8)%!X41k~KN7)2XM+bnKhvQlR7pd-9NoX}Za=LVcjOXC&lxR+lJ;73 zA!Z^J(AUnuN2g)koVvU@ZS{W(o?x_<$qG&UkYjFtZzjYT%ZCeUqyi7*bA6N@G?zbN zrQBBSy_UMsbq6p8W_qGYPY*(}xJA_vYyk1>vva$jAiQ{U*<0`5-aaZ(*UV~p^|nE7 zD#kaLb?ia3Y4MVe(M}0twEZvvXn-z0vH21eZ~NZAWA(9CcQWdsq^ObmUsm z1)mmAf(Hp(n!Mn@kg`|?nQVJ-FEQIOTUg^b*!(`lv?Ov!wmydJQ_(=N$pg(+KGZMP-x#vgP0~3YP;-BBGKPn| zL|3aViZeGpG}Ju0#fQ5WWoFLVqIECW7^gfrom&#PPRa4{4iuf$hRB<2_o%Eon?Qku z{#NyMVHhj>Gs3-|7A}A8N->bje2eCDHOB{qy&sx7i!K{NEpb434y%}>JdD(*jMqW0 zGRBK*c{pMTmcOhBj~eclj!{YcwXX7V^8_c;aaxX(uSJdqj03N7xBTC09JW6Nb#>3? zrH1;Re;kv1g1CB~Zu344!$;6|>bpnpEy0M-C-n2SwgkJnju;jJ=thB%iCps{#WZba9V?x{1JGy~ zt(c@Ds$Y&QggifYyH5MlPX+avr@wC;&z9>uD16}drf|Z2jewQ>HOsupr^S_ivt?x_ z%hjUGikjsgiRDW7g6J@$Z@^XsV0Nxg6o8f*Lb_O z6J{_UF4y?X~J5+!sZ$dPI`Ebd5>0h>NlA-is<>=AYghl*7CQl9^Y zM}e2G#gP_44<|}4OW@GclYK=|d0T=4wQ2hvks4d5q)QOz*P{_}1j=3h&)27y_u;K1 zbpTh}bEB^Nb(NPx)X%5Zo|hs2kLaF?aEz}A`t!dClGFQmcyaN5e|nHDoPkOG=e67o zM`FPg#POvYWUBk1NF}=0F5u|L3<`@@ezyf&e3DG-RaH?fD*~IB(%yEg3E9K~bQ`LLiOzkSQSsJv>~;T` zAatTdvHMm__?0^LZkd!O@2~=-epBQKP%fz;FI;e1=|fSqenT4ao!idY0-&OaCYx#p>K#6U6nS#$C4aNA?$s^TThBo% z82KFW&)4~49Ous?kSTD0fA3)4wj!?S*v#litNU?j;n+iGV-Bk>PR62|LR)}GhW5Q> zO#j5qPhuiV?aV+w7=5Vd$WCxekf`bg2m3qw0qK-dUoN$yKoV9=6g>2z5}B_y6n0u>n!(WaQfdL{25Th=oP3pOKoFM{pyEL z7T(M(1fg=`5Wbvq2EMn8x3|vQ_G~SBn%_mwtkNhysAOng2seuUam=C+P|Oyovq1bK zY`0Je#fFr5<>`kgRUgW9ObY~&?Pn!FDI0^Ey3*$f=P|XPvBE4Kmc^|e6B)u+o*@PK za)|fUG-s7~zsr&!npYzo(|qJ%|CMbLWjTEX!loGN;);Rr55UI!-sTxh;`$YoC=mjm4l%Pdzk`7c?;{3(HC-c zA`ICHGj{Z^#7+N*J&I+(%iBVjXR6B!WgdiP*bN>KYuqf@9}V|c3;BLl_}%ykR4=mq zKLEu*I=_TlU7UR$Bv}pZDX5NheE9Iy`TgzeQ)>oOTy<1;7jFOj`m2BV-QWM0KmW<% zqa1pyFKaz^{9}EY%Zwkcr&Tz*#*i>nGa?1;%2cT*ViFR_-Z8ZsvogWk37e+BB*LrA z0YVre`C=}TTqMV=BUc?(f$s0|9l8XCxm&F9h5O2=yN$?_TjXN{< zSXJ1xAP_z6?qH1A-W#`FpU(8oBy|KRF@xJ5Rsd8; zO{Z}iYttibnvUNi*RJLjv~pPjGQ*<%f)n@z}n4y$HpMLa8Kl_%4!d`$2b&F z#We+LsxIrKkbqBvtygEX#J^X7+zwd8pjNi0X9 zDJRkoT$>2E`|iod9l0dj&bPDQdc9b*;yy#dxl~skoZ!y*!^^wd+xzc7{xP|MoK}ze z?ff*--~av>|L)s=|FaL@-aEI)daN&CJsN<=`Z8B&CK`d082VxRD-5)1lFt7DTpRfJ zpfIbVjo6&;V4ABo`MTYC z%`QZUQjZOg$`pROPY@tg<|5&3ag5%hDtPa`f!br7jtkJ13}-DEyca<%>z;eMn^g-- z2E{iddYwfx<_WGU6t=GPdR1PJ2Uy_R!ENroVBK-j!OVmXyLx&6A)8LXj$rZ`mx`5t z+wH0)od&VJ)BU=hgIIl%d&Td|xfU%8QrF$>hE`AqwRVDaL8B>0Uj>IZ)vIKJyL^xc ze4g+DV9*J)i>*|K0F`Dqa}`mcrHFDoghx>pBP6Lnd!olM>!{k71X4U;(-SJa9Hotg zbKg9s;^aj5lfh977MEjag4|LVW|`kyyS{*`$Oy22#KWaoOPyU-f@IP{Pn?>TOwYJq z@kiEWzpoAriQJ+bk3cfVBc!U*^o&f1^3AVKXbASaGz|yAviD{hE1@kN9?wX|U3?|L zMC$5t%DQKB)|B+Xp%Cd3Pz6oA&sc6|w|GF4Lh*=x%Bx)$DpkF?(H|f{KG%}?ESYFmFuw{>q}UV2H>&2+;!Ai ztUgE2tXf^fsZ|vcf|75~0k$g)#`FvqN*m+SU{ci2hGI!4Ky7C{<;xtvW#~C$>$-uH zRN-2xpjiUAA}bD)1R=#MZlV7F1vcjC>G=AaZ`QGH=S_@}x#tktETdi5q?e&t$F}nr z^=31`?u{fW&X={zRS8B-O}=2QnG@2gMO61G4OXpfc*#jz8(Amud6maXlc?X)wc6n;AR7{$O zAnF+GT!N+|rkpZCf|~&&EtYkmqv496G9a_|kt!f>x~iRjW#}|S3;2OXy^)^L%PNJ8 zWkhPB+*KIy&hPGHp9`rKTjM6vW1OuO-qfFTH$hc*ZLO5Lw5#KEa)SYYwq48#kC81c znRo4&)CygNB!tW0#g|S2l6Pao%{r5d|Dwhj+_b{pDQlbxp+cL}PJEqXB zf~avWL}#?N+|7Da1SN190rL!$xXf5?}hYcttWG9m3}y z2sZfT6nXaTwLcwv@*}9~Zb@|<+&)rhv*G&b`FKAc-hSv>X!Yyc+p2}aId32J$A9?U zfBow}{ZG%w%m2$4$74O#k7GR=fXDh0m&eU7&re@{_^{3;VeH6{d=F^`J@&l|G7q|n z+2}4d4Hl}yc-pv(23B6h-*~RFSexXJz@o0Ytf5SqRw}th(}XCD9W5XsWAzkXWp1!Q z^@p$DzyI)jg}?>|#74|^(b?Sw{0LcCc+|qCmutg=yu8x)uj?w}0j}D=00_l8YW3#Y zupM=qqHCWCgMsE6&1PwAB8~P;z^oI2vWh?wV__|x?eMMcSJ7#V!SAW&MVP>1uq#Los^qLW6 zhomPDI_(ZGR5X&@l3y%tTT_@;(>{JOf|uOd+6Cr*iEv1KW9XXI>@y=jX7&kl_pHkk zbE4tccS<@v-OucPu{0C+W|;tR+Tkjt@6x^-i`?>W0GUK#e<)>`5=L8?d~}SRS>{o;GR zSWz?y0Jfl6inX%m43#D{7vUt1;k?eL<9vFq_f>UD64rJY71e4_KDvkCyO(#LZf~z| zKXB<-B*bFB-n#gkPrv-l_rLy&pZygc>#@F=^=JSd>kC~3p4QXzdYT!brWF=~Q}1!b zCk0g%477!fqh0l21NSLUH^4knVT96Hg<1X@4AnNz#1)V}OcwvqZSc6#D~P*9zuvn` z990!Gwbsjrcc|6V_jG~_5_J);?GA%b8VdKa2C!qvz*0op+6-zfAF2rksca)|w&s^R z2RBUk%n)RR4`fwwh`F7uD-Jnd?aVpZtOkJOip$Sk`P?QRxdWL|^c0&+kOEIK-2&8w zkH({DB4|t-!^AE_RAIA0GE)i9gGIFKC%Y~xyslipNJ1)of}vAvoA;}l4>|s#+n9e7 zRx`FNSQFe{kiyRwOo-WTD!vSWstTD+Qr7PkaO3ZRVW-&M>t z(k8($y|pjf_V7%zx@QC+&6Y#1dyNwjD2=BL+uha;U?B{rIQ?dVF>^hA4te(Wo1s{w z10R4j>Y39eYq=w^JMdr&>Cq5@vb9qu)~`GY2;E52y7*ezF8RnI7u+JJO2+2DujNZ> zch11>G0hERN$n(^wtI7ff_gL#`UE67`RWw7# zmIIT!W>2ni((XM1dItOE)4p|_!R*KFV8Kc=Tjlf6Xrj*Z{B(O;FE7BF8ImGfL!9IE zx^kSbX1b2!<@x#b_5-A+W1Za*b65ZI_T7K`?r;C>Cx5=`Nnu|H#4wRIniT;_$qy7$Z|DLc5CJ&FJ8S3lngpnIdE6wbX7ApcA+RFmCOjBA`#|( zAizYxDujljM$_YYiZz<{FVzJPa~G{LK;NWFRjApu`sQBXszwMzKgz((8^%Mvk@&Cej%0Du37gxr5TsVcXJvPPt=gYfQ0o^lVTSK8;OU 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%0D?^L4zWQ|+qWYpnJBkDO}hYkSV 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 z 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

7f~da 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 z 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

)!$&wL0(H@#@j{e)B*6_V<5h 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 literal 0 HcmV?d00001 diff --git a/demo-vault-v2/event-team-sync-2025-01-13.md b/demo-vault-v2/event-team-sync-2025-01-13.md new file mode 100644 index 0000000..716cbe0 --- /dev/null +++ b/demo-vault-v2/event-team-sync-2025-01-13.md @@ -0,0 +1,13 @@ +--- +type: Event +belongs_to: "[[25q1-laputa-v1]]" +related_to: + - "[[person-luca-rossi]]" + - "[[person-matteo-cellini]]" +date: 2025-01-13 +--- + +# Team sync — 2025-01-13 + +Short checkpoint on V1 priorities: stabilize quick open, tighten keyboard navigation, and keep the inspector fast. + diff --git a/demo-vault-v2/laputa-qa-reference.md b/demo-vault-v2/laputa-qa-reference.md new file mode 100644 index 0000000..5c0e482 --- /dev/null +++ b/demo-vault-v2/laputa-qa-reference.md @@ -0,0 +1,16 @@ +--- +type: Note +aliases: + - "[[Laputa QA Reference]]" +related_to: + - "[[25q2-laputa-v2]]" +--- + +# Laputa QA Reference + +This note anchors the fixture's binary attachment scenario and gives native QA a simple note with an embedded image. + +![Fixture reference image](attachments/laputa-reference.png) + +Use this note to confirm that the editor renders an attached asset without dragging in a huge demo corpus. + diff --git a/demo-vault-v2/measure-close-rate.md b/demo-vault-v2/measure-close-rate.md new file mode 100644 index 0000000..b04a404 --- /dev/null +++ b/demo-vault-v2/measure-close-rate.md @@ -0,0 +1,12 @@ +--- +type: Measure +aliases: + - "[[Sponsorship Close Rate]]" +belongs_to: "[[responsibility-sponsorships]]" +unit: percent +--- + +# Sponsorship Close Rate + +Tracks how many qualified sponsor conversations become signed deals. + diff --git a/demo-vault-v2/measure-sponsorship-mrr.md b/demo-vault-v2/measure-sponsorship-mrr.md new file mode 100644 index 0000000..539ed40 --- /dev/null +++ b/demo-vault-v2/measure-sponsorship-mrr.md @@ -0,0 +1,12 @@ +--- +type: Measure +aliases: + - "[[Sponsorship MRR]]" +belongs_to: "[[responsibility-sponsorships]]" +unit: EUR/month +--- + +# Sponsorship MRR + +Tracks monthly recurring sponsorship revenue so the responsibility note has a real linked metric. + diff --git a/demo-vault-v2/note-on-clear-prose.md b/demo-vault-v2/note-on-clear-prose.md new file mode 100644 index 0000000..38791b1 --- /dev/null +++ b/demo-vault-v2/note-on-clear-prose.md @@ -0,0 +1,9 @@ +--- +type: Note +topics: + - "[[topic-writing]]" +--- + +# On Clear Prose + +Reference note for a book worth reopening whenever prose starts to feel bloated. diff --git a/demo-vault-v2/person-luca-rossi.md b/demo-vault-v2/person-luca-rossi.md new file mode 100644 index 0000000..52f7f59 --- /dev/null +++ b/demo-vault-v2/person-luca-rossi.md @@ -0,0 +1,11 @@ +--- +type: Person +aliases: + - "[[Luca Rossi]]" +tier: 1st +--- + +# Luca Rossi + +Owns the Laputa product work and remains the primary owner on the fixture's project notes. + diff --git a/demo-vault-v2/person-matteo-cellini.md b/demo-vault-v2/person-matteo-cellini.md new file mode 100644 index 0000000..e8cd716 --- /dev/null +++ b/demo-vault-v2/person-matteo-cellini.md @@ -0,0 +1,11 @@ +--- +type: Person +aliases: + - "[[Matteo Cellini]]" +tier: 1st +--- + +# Matteo Cellini + +Owns sponsor outreach and makes the responsibility/procedure relationships feel like real working notes. + diff --git a/demo-vault-v2/procedure-quarterly-sponsor-outreach.md b/demo-vault-v2/procedure-quarterly-sponsor-outreach.md new file mode 100644 index 0000000..7d612e8 --- /dev/null +++ b/demo-vault-v2/procedure-quarterly-sponsor-outreach.md @@ -0,0 +1,16 @@ +--- +type: Procedure +aliases: + - "[[Quarterly Sponsor Outreach]]" +belongs_to: "[[responsibility-sponsorships]]" +owner: "[[person-matteo-cellini]]" +cadence: Quarterly +--- + +# Quarterly Sponsor Outreach + +Review the pipeline, choose the next target companies, and send a fresh outreach batch each quarter. + +- Start from last quarter's warm leads. +- Share the shortlist with [[person-matteo-cellini]] before sending outreach. + diff --git a/demo-vault-v2/procedure-sponsor-onboarding.md b/demo-vault-v2/procedure-sponsor-onboarding.md new file mode 100644 index 0000000..2fa9f78 --- /dev/null +++ b/demo-vault-v2/procedure-sponsor-onboarding.md @@ -0,0 +1,17 @@ +--- +type: Procedure +aliases: + - "[[Sponsor Onboarding]]" +belongs_to: "[[responsibility-sponsorships]]" +owner: "[[person-luca-rossi]]" +cadence: "As needed" +--- + +# Sponsor Onboarding + +Turn a signed sponsor into a smooth first placement with minimal back-and-forth. + +- Confirm the publication date. +- Review copy and assets. +- Hand off recurring communication to [[person-matteo-cellini]]. + diff --git a/demo-vault-v2/refactoring-business-plan.md b/demo-vault-v2/refactoring-business-plan.md new file mode 100644 index 0000000..12ef38e --- /dev/null +++ b/demo-vault-v2/refactoring-business-plan.md @@ -0,0 +1,140 @@ +--- +type: Note +_display: sheet +tags: + - spreadsheet + - business-plan +_sheet: + frozen_rows: 14 + columns: + A: + width: 276.63636363636374 + B: + width: 185 + C: + width: 132 + D: + width: 132 + E: + width: 112 + F: + width: 132 + G: + width: 132 + H: + width: 132 + I: + width: 132 + J: + width: 128 + K: + width: 132 + L: + width: 120 + M: + width: 96 + N: + width: 120 + O: + width: 108 + P: + width: 124 + Q: + width: 124 + R: + width: 132 + S: + width: 360 + cells: + A1: + bold: true + italic: true + A14: + bold: true + B14: + bold: true + C14: + bold: true + D14: + bold: true + E14: + bold: true + F14: + bold: true + G14: + bold: true + H14: + bold: true + I14: + bold: true + J14: + bold: true + K14: + bold: true + L14: + bold: true + M14: + bold: true + N14: + bold: true + O14: + bold: true + P14: + bold: true + Q14: + bold: true + R14: + bold: true + S14: + bold: true +--- +Refactoring business plan,3-year monthly operating model,,,,,,,,,,,,,,,,, +Start free newsletter subscribers,2500,,,,,,,,,,,,,,,,, +Monthly free subscriber growth rate,0.07,,,,,,,,,,,,,,,,, +Monthly free subscriber churn,0.012,,,,,,,,,,,,,,,,, +Paid conversion rate on incremental audience,0.035,,,,,,,,,,,,,,,,, +Paid subscription price,12,,,,,,,,,,,,,,,,, +Paid subscriber monthly churn,0.035,,,,,,,,,,,,,,,,, +Sponsorship CPM,55,,,,,,,,,,,,,,,,, +Sponsorship slots per month,2,,,,,,,,,,,,,,,,, +Average open rate,0.46,,,,,,,,,,,,,,,,, +Consulting project price,6000,,,,,,,,,,,,,,,,, +Consulting projects per month at maturity,2,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,, +Month #,Month,Free subscribers,Paid subscribers,Paid conversion,Newsletter revenue,Sponsor impressions,Sponsorship revenue,Consulting revenue,Digital products,Total revenue,Content/Ops,Tools,Contractors,Marketing,Total expenses,Net profit,Cumulative cash,Key hypothesis +1,Jun 2026,=$B$2,=C15*$B$5,=D15/C15,=D15*$B$6,=C15*$B$10,=G15/1000*$B$8*$B$9,=1/36*$B$12*$B$11,=1*50,=F15+H15+I15+J15,=1500+A15*50,=350+A15*10,=0,=C15*0.12,=SUM(L15:O15),=K15-P15,=Q15,Validate positioning and grow high-signal audience +2,Jul 2026,=C15*(1+$B$3-$B$4),=D15*(1-$B$7)+(C16-C15)*$B$5,=D16/C16,=D16*$B$6,=C16*$B$10,=G16/1000*$B$8*$B$9,=2/36*$B$12*$B$11,=2*50,=F16+H16+I16+J16,=1500+A16*50,=350+A16*10,=0,=C16*0.12,=SUM(L16:O16),=K16-P16,=R15+Q16,Validate positioning and grow high-signal audience +3,Aug 2026,=C16*(1+$B$3-$B$4),=D16*(1-$B$7)+(C17-C16)*$B$5,=D17/C17,=D17*$B$6,=C17*$B$10,=G17/1000*$B$8*$B$9,=3/36*$B$12*$B$11,=3*50,=F17+H17+I17+J17,=1500+A17*50,=350+A17*10,=0,=C17*0.12,=SUM(L17:O17),=K17-P17,=R16+Q17,Validate positioning and grow high-signal audience +4,Sep 2026,=C17*(1+$B$3-$B$4),=D17*(1-$B$7)+(C18-C17)*$B$5,=D18/C18,=D18*$B$6,=C18*$B$10,=G18/1000*$B$8*$B$9,=4/36*$B$12*$B$11,=4*50,=F18+H18+I18+J18,=1500+A18*50,=350+A18*10,=0,=C18*0.12,=SUM(L18:O18),=K18-P18,=R17+Q18,Validate positioning and grow high-signal audience +5,Oct 2026,=C18*(1+$B$3-$B$4),=D18*(1-$B$7)+(C19-C18)*$B$5,=D19/C19,=D19*$B$6,=C19*$B$10,=G19/1000*$B$8*$B$9,=5/36*$B$12*$B$11,=5*50,=F19+H19+I19+J19,=1500+A19*50,=350+A19*10,=0,=C19*0.12,=SUM(L19:O19),=K19-P19,=R18+Q19,Validate positioning and grow high-signal audience +6,Nov 2026,=C19*(1+$B$3-$B$4),=D19*(1-$B$7)+(C20-C19)*$B$5,=D20/C20,=D20*$B$6,=C20*$B$10,=G20/1000*$B$8*$B$9,=6/36*$B$12*$B$11,=6*50,=F20+H20+I20+J20,=1500+A20*50,=350+A20*10,=0,=C20*0.12,=SUM(L20:O20),=K20-P20,=R19+Q20,Validate positioning and grow high-signal audience +7,Dec 2026,=C20*(1+$B$3-$B$4),=D20*(1-$B$7)+(C21-C20)*$B$5,=D21/C21,=D21*$B$6,=C21*$B$10,=G21/1000*$B$8*$B$9,=7/36*$B$12*$B$11,=7*50,=F21+H21+I21+J21,=1500+A21*50,=350+A21*10,=2000+A21*125,=C21*0.12,=SUM(L21:O21),=K21-P21,=R20+Q21,Package repeatable advisory offers and first sponsors +8,Jan 2027,=C21*(1+$B$3-$B$4),=D21*(1-$B$7)+(C22-C21)*$B$5,=D22/C22,=D22*$B$6,=C22*$B$10,=G22/1000*$B$8*$B$9,=8/36*$B$12*$B$11,=8*50,=F22+H22+I22+J22,=1500+A22*50,=350+A22*10,=2000+A22*125,=C22*0.12,=SUM(L22:O22),=K22-P22,=R21+Q22,Package repeatable advisory offers and first sponsors +9,Feb 2027,=C22*(1+$B$3-$B$4),=D22*(1-$B$7)+(C23-C22)*$B$5,=D23/C23,=D23*$B$6,=C23*$B$10,=G23/1000*$B$8*$B$9,=9/36*$B$12*$B$11,=9*50,=F23+H23+I23+J23,=1500+A23*50,=350+A23*10,=2000+A23*125,=C23*0.12,=SUM(L23:O23),=K23-P23,=R22+Q23,Package repeatable advisory offers and first sponsors +10,Mar 2027,=C23*(1+$B$3-$B$4),=D23*(1-$B$7)+(C24-C23)*$B$5,=D24/C24,=D24*$B$6,=C24*$B$10,=G24/1000*$B$8*$B$9,=10/36*$B$12*$B$11,=450+(10-9)*175,=F24+H24+I24+J24,=1500+A24*50,=350+A24*10,=2000+A24*125,=C24*0.12,=SUM(L24:O24),=K24-P24,=R23+Q24,Package repeatable advisory offers and first sponsors +11,Apr 2027,=C24*(1+$B$3-$B$4),=D24*(1-$B$7)+(C25-C24)*$B$5,=D25/C25,=D25*$B$6,=C25*$B$10,=G25/1000*$B$8*$B$9,=11/36*$B$12*$B$11,=450+(11-9)*175,=F25+H25+I25+J25,=1500+A25*50,=350+A25*10,=2000+A25*125,=C25*0.12,=SUM(L25:O25),=K25-P25,=R24+Q25,Package repeatable advisory offers and first sponsors +12,May 2027,=C25*(1+$B$3-$B$4),=D25*(1-$B$7)+(C26-C25)*$B$5,=D26/C26,=D26*$B$6,=C26*$B$10,=G26/1000*$B$8*$B$9,=12/36*$B$12*$B$11,=450+(12-9)*175,=F26+H26+I26+J26,=1500+A26*50,=350+A26*10,=2000+A26*125,=C26*0.12,=SUM(L26:O26),=K26-P26,=R25+Q26,Package repeatable advisory offers and first sponsors +13,Jun 2027,=C26*(1+$B$3-$B$4),=D26*(1-$B$7)+(C27-C26)*$B$5,=D27/C27,=D27*$B$6,=C27*$B$10,=G27/1000*$B$8*$B$9,=13/36*$B$12*$B$11,=450+(13-9)*175,=F27+H27+I27+J27,=1500+A27*50,=350+A27*10,=2000+A27*125,=C27*0.12,=SUM(L27:O27),=K27-P27,=R26+Q27,Scale paid membership and recurring sponsorship inventory +14,Jul 2027,=C27*(1+$B$3-$B$4),=D27*(1-$B$7)+(C28-C27)*$B$5,=D28/C28,=D28*$B$6,=C28*$B$10,=G28/1000*$B$8*$B$9,=14/36*$B$12*$B$11,=450+(14-9)*175,=F28+H28+I28+J28,=1500+A28*50,=350+A28*10,=2000+A28*125,=C28*0.12,=SUM(L28:O28),=K28-P28,=R27+Q28,Scale paid membership and recurring sponsorship inventory +15,Aug 2027,=C28*(1+$B$3-$B$4),=D28*(1-$B$7)+(C29-C28)*$B$5,=D29/C29,=D29*$B$6,=C29*$B$10,=G29/1000*$B$8*$B$9,=15/36*$B$12*$B$11,=450+(15-9)*175,=F29+H29+I29+J29,=1500+A29*50,=350+A29*10,=2000+A29*125,=C29*0.12,=SUM(L29:O29),=K29-P29,=R28+Q29,Scale paid membership and recurring sponsorship inventory +16,Sep 2027,=C29*(1+$B$3-$B$4),=D29*(1-$B$7)+(C30-C29)*$B$5,=D30/C30,=D30*$B$6,=C30*$B$10,=G30/1000*$B$8*$B$9,=16/36*$B$12*$B$11,=450+(16-9)*175,=F30+H30+I30+J30,=1500+A30*50,=350+A30*10,=2000+A30*125,=C30*0.12,=SUM(L30:O30),=K30-P30,=R29+Q30,Scale paid membership and recurring sponsorship inventory +17,Oct 2027,=C30*(1+$B$3-$B$4),=D30*(1-$B$7)+(C31-C30)*$B$5,=D31/C31,=D31*$B$6,=C31*$B$10,=G31/1000*$B$8*$B$9,=17/36*$B$12*$B$11,=450+(17-9)*175,=F31+H31+I31+J31,=1500+A31*50,=350+A31*10,=2000+A31*125,=C31*0.12,=SUM(L31:O31),=K31-P31,=R30+Q31,Scale paid membership and recurring sponsorship inventory +18,Nov 2027,=C31*(1+$B$3-$B$4),=D31*(1-$B$7)+(C32-C31)*$B$5,=D32/C32,=D32*$B$6,=C32*$B$10,=G32/1000*$B$8*$B$9,=18/36*$B$12*$B$11,=450+(18-9)*175,=F32+H32+I32+J32,=1500+A32*50,=350+A32*10,=2000+A32*125,=C32*0.12,=SUM(L32:O32),=K32-P32,=R31+Q32,Scale paid membership and recurring sponsorship inventory +19,Dec 2027,=C32*(1+$B$3-$B$4),=D32*(1-$B$7)+(C33-C32)*$B$5,=D33/C33,=D33*$B$6,=C33*$B$10,=G33/1000*$B$8*$B$9,=19/36*$B$12*$B$11,=450+(19-9)*175,=F33+H33+I33+J33,=1500+A33*50,=350+A33*10,=2000+A33*125,=C33*0.12,=SUM(L33:O33),=K33-P33,=R32+Q33,Scale paid membership and recurring sponsorship inventory +20,Jan 2028,=C33*(1+$B$3-$B$4),=D33*(1-$B$7)+(C34-C33)*$B$5,=D34/C34,=D34*$B$6,=C34*$B$10,=G34/1000*$B$8*$B$9,=20/36*$B$12*$B$11,=450+(20-9)*175,=F34+H34+I34+J34,=1500+A34*50,=350+A34*10,=2000+A34*125,=C34*0.12,=SUM(L34:O34),=K34-P34,=R33+Q34,Scale paid membership and recurring sponsorship inventory +21,Feb 2028,=C34*(1+$B$3-$B$4),=D34*(1-$B$7)+(C35-C34)*$B$5,=D35/C35,=D35*$B$6,=C35*$B$10,=G35/1000*$B$8*$B$9,=21/36*$B$12*$B$11,=450+(21-9)*175,=F35+H35+I35+J35,=1500+A35*50,=350+A35*10,=2000+A35*125,=C35*0.12,=SUM(L35:O35),=K35-P35,=R34+Q35,Scale paid membership and recurring sponsorship inventory +22,Mar 2028,=C35*(1+$B$3-$B$4),=D35*(1-$B$7)+(C36-C35)*$B$5,=D36/C36,=D36*$B$6,=C36*$B$10,=G36/1000*$B$8*$B$9,=22/36*$B$12*$B$11,=450+(22-9)*175,=F36+H36+I36+J36,=1500+A36*50,=350+A36*10,=2000+A36*125,=C36*0.12,=SUM(L36:O36),=K36-P36,=R35+Q36,Scale paid membership and recurring sponsorship inventory +23,Apr 2028,=C36*(1+$B$3-$B$4),=D36*(1-$B$7)+(C37-C36)*$B$5,=D37/C37,=D37*$B$6,=C37*$B$10,=G37/1000*$B$8*$B$9,=23/36*$B$12*$B$11,=450+(23-9)*175,=F37+H37+I37+J37,=1500+A37*50,=350+A37*10,=2000+A37*125,=C37*0.12,=SUM(L37:O37),=K37-P37,=R36+Q37,Scale paid membership and recurring sponsorship inventory +24,May 2028,=C37*(1+$B$3-$B$4),=D37*(1-$B$7)+(C38-C37)*$B$5,=D38/C38,=D38*$B$6,=C38*$B$10,=G38/1000*$B$8*$B$9,=24/36*$B$12*$B$11,=450+(24-9)*175,=F38+H38+I38+J38,=1500+A38*50,=350+A38*10,=2000+A38*125,=C38*0.12,=SUM(L38:O38),=K38-P38,=R37+Q38,Scale paid membership and recurring sponsorship inventory +25,Jun 2028,=C38*(1+$B$3-$B$4),=D38*(1-$B$7)+(C39-C38)*$B$5,=D39/C39,=D39*$B$6,=C39*$B$10,=G39/1000*$B$8*$B$9,=25/36*$B$12*$B$11,=450+(25-9)*175,=F39+H39+I39+J39,=1500+A39*50,=350+A39*10,=2000+A39*125,=C39*0.12,=SUM(L39:O39),=K39-P39,=R38+Q39,Expand products and reduce founder-delivery dependency +26,Jul 2028,=C39*(1+$B$3-$B$4),=D39*(1-$B$7)+(C40-C39)*$B$5,=D40/C40,=D40*$B$6,=C40*$B$10,=G40/1000*$B$8*$B$9,=26/36*$B$12*$B$11,=450+(26-9)*175,=F40+H40+I40+J40,=1500+A40*50,=350+A40*10,=2000+A40*125,=C40*0.12,=SUM(L40:O40),=K40-P40,=R39+Q40,Expand products and reduce founder-delivery dependency +27,Aug 2028,=C40*(1+$B$3-$B$4),=D40*(1-$B$7)+(C41-C40)*$B$5,=D41/C41,=D41*$B$6,=C41*$B$10,=G41/1000*$B$8*$B$9,=27/36*$B$12*$B$11,=450+(27-9)*175,=F41+H41+I41+J41,=1500+A41*50,=350+A41*10,=2000+A41*125,=C41*0.12,=SUM(L41:O41),=K41-P41,=R40+Q41,Expand products and reduce founder-delivery dependency +28,Sep 2028,=C41*(1+$B$3-$B$4),=D41*(1-$B$7)+(C42-C41)*$B$5,=D42/C42,=D42*$B$6,=C42*$B$10,=G42/1000*$B$8*$B$9,=28/36*$B$12*$B$11,=450+(28-9)*175,=F42+H42+I42+J42,=1500+A42*50,=350+A42*10,=2000+A42*125,=C42*0.12,=SUM(L42:O42),=K42-P42,=R41+Q42,Expand products and reduce founder-delivery dependency +29,Oct 2028,=C42*(1+$B$3-$B$4),=D42*(1-$B$7)+(C43-C42)*$B$5,=D43/C43,=D43*$B$6,=C43*$B$10,=G43/1000*$B$8*$B$9,=29/36*$B$12*$B$11,=450+(29-9)*175,=F43+H43+I43+J43,=1500+A43*50,=350+A43*10,=2000+A43*125,=C43*0.12,=SUM(L43:O43),=K43-P43,=R42+Q43,Expand products and reduce founder-delivery dependency +30,Nov 2028,=C43*(1+$B$3-$B$4),=D43*(1-$B$7)+(C44-C43)*$B$5,=D44/C44,=D44*$B$6,=C44*$B$10,=G44/1000*$B$8*$B$9,=30/36*$B$12*$B$11,=450+(30-9)*175,=F44+H44+I44+J44,=1500+A44*50,=350+A44*10,=2000+A44*125,=C44*0.12,=SUM(L44:O44),=K44-P44,=R43+Q44,Expand products and reduce founder-delivery dependency +31,Dec 2028,=C44*(1+$B$3-$B$4),=D44*(1-$B$7)+(C45-C44)*$B$5,=D45/C45,=D45*$B$6,=C45*$B$10,=G45/1000*$B$8*$B$9,=31/36*$B$12*$B$11,=450+(31-9)*175,=F45+H45+I45+J45,=1500+A45*50,=350+A45*10,=2000+A45*125,=C45*0.12,=SUM(L45:O45),=K45-P45,=R44+Q45,Expand products and reduce founder-delivery dependency +32,Jan 2029,=C45*(1+$B$3-$B$4),=D45*(1-$B$7)+(C46-C45)*$B$5,=D46/C46,=D46*$B$6,=C46*$B$10,=G46/1000*$B$8*$B$9,=32/36*$B$12*$B$11,=450+(32-9)*175,=F46+H46+I46+J46,=1500+A46*50,=350+A46*10,=2000+A46*125,=C46*0.12,=SUM(L46:O46),=K46-P46,=R45+Q46,Expand products and reduce founder-delivery dependency +33,Feb 2029,=C46*(1+$B$3-$B$4),=D46*(1-$B$7)+(C47-C46)*$B$5,=D47/C47,=D47*$B$6,=C47*$B$10,=G47/1000*$B$8*$B$9,=33/36*$B$12*$B$11,=450+(33-9)*175,=F47+H47+I47+J47,=1500+A47*50,=350+A47*10,=2000+A47*125,=C47*0.12,=SUM(L47:O47),=K47-P47,=R46+Q47,Expand products and reduce founder-delivery dependency +34,Mar 2029,=C47*(1+$B$3-$B$4),=D47*(1-$B$7)+(C48-C47)*$B$5,=D48/C48,=D48*$B$6,=C48*$B$10,=G48/1000*$B$8*$B$9,=34/36*$B$12*$B$11,=450+(34-9)*175,=F48+H48+I48+J48,=1500+A48*50,=350+A48*10,=2000+A48*125,=C48*0.12,=SUM(L48:O48),=K48-P48,=R47+Q48,Expand products and reduce founder-delivery dependency +35,Apr 2029,=C48*(1+$B$3-$B$4),=D48*(1-$B$7)+(C49-C48)*$B$5,=D49/C49,=D49*$B$6,=C49*$B$10,=G49/1000*$B$8*$B$9,=35/36*$B$12*$B$11,=450+(35-9)*175,=F49+H49+I49+J49,=1500+A49*50,=350+A49*10,=2000+A49*125,=C49*0.12,=SUM(L49:O49),=K49-P49,=R48+Q49,Expand products and reduce founder-delivery dependency +36,May 2029,=C49*(1+$B$3-$B$4),=D49*(1-$B$7)+(C50-C49)*$B$5,=D50/C50,=D50*$B$6,=C50*$B$10,=G50/1000*$B$8*$B$9,=36/36*$B$12*$B$11,=450+(36-9)*175,=F50+H50+I50+J50,=1500+A50*50,=350+A50*10,=2000+A50*125,=C50*0.12,=SUM(L50:O50),=K50-P50,=R49+Q50,Expand products and reduce founder-delivery dependency diff --git a/demo-vault-v2/responsibility-sponsorships.md b/demo-vault-v2/responsibility-sponsorships.md new file mode 100644 index 0000000..e112127 --- /dev/null +++ b/demo-vault-v2/responsibility-sponsorships.md @@ -0,0 +1,21 @@ +--- +type: Responsibility +aliases: + - "[[Sponsorships]]" +belongs_to: "[[area-building]]" +has_measures: + - "[[measure-sponsorship-mrr]]" + - "[[measure-close-rate]]" +has_procedures: + - "[[procedure-quarterly-sponsor-outreach]]" + - "[[procedure-sponsor-onboarding]]" +status: Open +--- + +# Sponsorships + +The compact responsibility used to verify that wikilink-valued fields render as relationships instead of plain text properties. + +- `status` stays a normal property. +- `has_measures` and `has_procedures` should render in the relationships area. + diff --git a/demo-vault-v2/rtl-mixed-direction-qa.md b/demo-vault-v2/rtl-mixed-direction-qa.md new file mode 100644 index 0000000..2dec819 --- /dev/null +++ b/demo-vault-v2/rtl-mixed-direction-qa.md @@ -0,0 +1,17 @@ +--- +type: Note +topics: + - "[[topic-writing]]" +--- + +# RTL Mixed Direction QA + +مرحبا بالعالم. هذه فقرة عربية لاختبار اتجاه النص من اليمين إلى اليسار داخل محرر تولاريا. + +English text should keep reading left to right when it appears next to Arabic content. + +English then مرحبا بالعالم keeps both scripts readable on one line. + +مرحبا بالعالم then English keeps the Arabic run anchored correctly while preserving the English words. + +Use this note when checking rich editor and raw Markdown editor behavior for automatic LTR/RTL direction. diff --git a/demo-vault-v2/tolaria-sheet-prototype-sample.md b/demo-vault-v2/tolaria-sheet-prototype-sample.md new file mode 100644 index 0000000..bfe6e59 --- /dev/null +++ b/demo-vault-v2/tolaria-sheet-prototype-sample.md @@ -0,0 +1,30 @@ +--- +type: Note +_display: sheet +tags: + - spreadsheet +_sheet: + frozen_rows: 1 + columns: + A: + width: 180 + B: + width: 140 + C: + width: 140 + D: + width: 140 + E: + width: 140 + cells: + A2: + bold: true + A5: + bold: true +--- +Metric,January,February,March,Quarter +Subscriptions,1200,1350,1500,=SUM(B2:D2) +Services,800,900,700,=SUM(B3:D3) +Expenses,650,700,760,=SUM(B4:D4) +Net,=B2+B3-B4,=C2+C3-C4,=D2+D3-D4,=SUM(B5:D5) +Growth,,=(C5-B5)/B5,=(D5-C5)/C5,=(E5-B5)/B5 diff --git a/demo-vault-v2/topic-writing.md b/demo-vault-v2/topic-writing.md new file mode 100644 index 0000000..35013c9 --- /dev/null +++ b/demo-vault-v2/topic-writing.md @@ -0,0 +1,13 @@ +--- +type: Topic +aliases: + - "[[Writing]]" +--- + +# Writing + +The exact-match Quick Open target for search ranking QA. + +- [[writing-for-clarity-vs-writing-for-credit]] +- [[writing-weekly-rhythm]] +- [[note-on-clear-prose]] diff --git a/demo-vault-v2/type/area.md b/demo-vault-v2/type/area.md new file mode 100644 index 0000000..a58b436 --- /dev/null +++ b/demo-vault-v2/type/area.md @@ -0,0 +1,11 @@ +--- +type: Type +icon: folders +color: amber +sidebar label: Areas +--- + +# Area + +Areas are ongoing domains of responsibility with no fixed end date. + diff --git a/demo-vault-v2/type/event.md b/demo-vault-v2/type/event.md new file mode 100644 index 0000000..901a5c3 --- /dev/null +++ b/demo-vault-v2/type/event.md @@ -0,0 +1,11 @@ +--- +type: Type +icon: calendar +color: orange +sidebar label: Events +--- + +# Event + +Events are time-bound occurrences tied to projects, people, or periods. + diff --git a/demo-vault-v2/type/measure.md b/demo-vault-v2/type/measure.md new file mode 100644 index 0000000..3e3e8a9 --- /dev/null +++ b/demo-vault-v2/type/measure.md @@ -0,0 +1,11 @@ +--- +type: Type +icon: chart-line-up +color: cyan +sidebar label: Measures +--- + +# Measure + +Measures track the numbers that matter for a responsibility or project. + diff --git a/demo-vault-v2/type/note.md b/demo-vault-v2/type/note.md new file mode 100644 index 0000000..347b4d0 --- /dev/null +++ b/demo-vault-v2/type/note.md @@ -0,0 +1,11 @@ +--- +type: Type +icon: note +color: slate +sidebar label: Notes +--- + +# Note + +Notes capture references, ideas, or QA artifacts that do not need a more specific type. + diff --git a/demo-vault-v2/type/person.md b/demo-vault-v2/type/person.md new file mode 100644 index 0000000..0bf537f --- /dev/null +++ b/demo-vault-v2/type/person.md @@ -0,0 +1,11 @@ +--- +type: Type +icon: user +color: rose +sidebar label: People +--- + +# Person + +People notes represent collaborators, owners, or recurring contacts. + diff --git a/demo-vault-v2/type/procedure.md b/demo-vault-v2/type/procedure.md new file mode 100644 index 0000000..9560c7a --- /dev/null +++ b/demo-vault-v2/type/procedure.md @@ -0,0 +1,11 @@ +--- +type: Type +icon: checklist +color: violet +sidebar label: Procedures +--- + +# Procedure + +Procedures describe repeatable workflows that support a responsibility. + diff --git a/demo-vault-v2/type/project.md b/demo-vault-v2/type/project.md new file mode 100644 index 0000000..0ea5ad4 --- /dev/null +++ b/demo-vault-v2/type/project.md @@ -0,0 +1,11 @@ +--- +type: Type +icon: rocket +color: blue +sidebar label: Projects +--- + +# Project + +Projects are time-bound efforts with an owner, a status, and a clear outcome. + diff --git a/demo-vault-v2/type/quarter.md b/demo-vault-v2/type/quarter.md new file mode 100644 index 0000000..d51d59d --- /dev/null +++ b/demo-vault-v2/type/quarter.md @@ -0,0 +1,11 @@ +--- +type: Type +icon: clock-countdown +color: emerald +sidebar label: Quarters +--- + +# Quarter + +Quarter notes group the initiatives and outcomes for a planning window. + diff --git a/demo-vault-v2/type/responsibility.md b/demo-vault-v2/type/responsibility.md new file mode 100644 index 0000000..70f179f --- /dev/null +++ b/demo-vault-v2/type/responsibility.md @@ -0,0 +1,11 @@ +--- +type: Type +icon: briefcase +color: green +sidebar label: Responsibilities +--- + +# Responsibility + +Responsibilities are long-lived ownership areas with linked procedures and measures. + diff --git a/demo-vault-v2/type/topic.md b/demo-vault-v2/type/topic.md new file mode 100644 index 0000000..fcabf9a --- /dev/null +++ b/demo-vault-v2/type/topic.md @@ -0,0 +1,11 @@ +--- +type: Type +icon: books +color: indigo +sidebar label: Topics +--- + +# Topic + +Topics collect related notes and make search/navigation scenarios easy to inspect. + diff --git a/demo-vault-v2/views/active-projects.yml b/demo-vault-v2/views/active-projects.yml new file mode 100644 index 0000000..793904f --- /dev/null +++ b/demo-vault-v2/views/active-projects.yml @@ -0,0 +1,9 @@ +name: Active Projects +icon: rocket +color: blue +sort: "modified:desc" +filters: + all: + - field: type + op: equals + value: Project diff --git a/demo-vault-v2/writing-for-clarity-vs-writing-for-credit.md b/demo-vault-v2/writing-for-clarity-vs-writing-for-credit.md new file mode 100644 index 0000000..8e7bee4 --- /dev/null +++ b/demo-vault-v2/writing-for-clarity-vs-writing-for-credit.md @@ -0,0 +1,11 @@ +--- +type: Note +topics: + - "[[topic-writing]]" +status: Published +--- + +# Writing for Clarity vs. Writing for Credit + +Write to be understood before you write to impress. This note exists so `Writing` has a strong prefix match underneath the exact title result. + diff --git a/demo-vault-v2/writing-weekly-rhythm.md b/demo-vault-v2/writing-weekly-rhythm.md new file mode 100644 index 0000000..298d58f --- /dev/null +++ b/demo-vault-v2/writing-weekly-rhythm.md @@ -0,0 +1,11 @@ +--- +type: Note +topics: + - "[[topic-writing]]" +status: Active +--- + +# Writing Weekly Rhythm + +A short operational note about keeping a weekly publishing cadence without overproducing drafts. + diff --git a/design/add-property-inline.pen b/design/add-property-inline.pen new file mode 100644 index 0000000..4109362 --- /dev/null +++ b/design/add-property-inline.pen @@ -0,0 +1,448 @@ +{ + "children": [ + { + "type": "frame", + "id": "apEmpty", + "x": 0, + "y": 0, + "name": "Add Property — Inline Form (Empty State)", + "theme": { "Mode": "Light" }, + "width": 320, + "height": 180, + "fill": "$--background", + "layout": "vertical", + "gap": 8, + "padding": 12, + "children": [ + { + "type": "text", + "id": "apEmptyDesc", + "name": "description", + "fill": "$--muted-foreground", + "content": "Inline add-property form: replaces the old grey popup. Fields are horizontal, matching existing property row layout. Shows after clicking + Add property.", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "apEmptyExisting1", + "name": "Existing Property Row — Status", + "width": "fill_container", + "cornerRadius": 4, + "padding": [4, 6], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "apEmptyLabel1", + "fill": "$--muted-foreground", + "content": "STATUS", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 1.2 + }, + { + "type": "frame", + "id": "apEmptyBadge1", + "fill": "$--accent-green-light", + "cornerRadius": 16, + "padding": [1, 6], + "children": [ + { + "type": "text", + "id": "apEmptyBadgeText1", + "fill": "$--accent-green", + "content": "ACTIVE", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 1.2 + } + ] + } + ] + }, + { + "type": "frame", + "id": "apEmptySep", + "name": "Separator", + "width": "fill_container", + "height": 1, + "fill": "$--border" + }, + { + "type": "frame", + "id": "apEmptyForm", + "name": "Inline Add Property Form — Empty", + "width": "fill_container", + "gap": 6, + "padding": [4, 6], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "apEmptyNameInput", + "name": "Name Input", + "width": 90, + "height": 26, + "fill": "$--muted", + "cornerRadius": 4, + "stroke": { "align": "inside", "thickness": 1, "fill": "$--border" }, + "padding": [4, 6], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "apEmptyNamePlaceholder", + "fill": "$--muted-foreground", + "content": "Name", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "apEmptyTypeSelect", + "name": "Type Selector", + "width": 72, + "height": 26, + "fill": "$--muted", + "cornerRadius": 4, + "stroke": { "align": "inside", "thickness": 1, "fill": "$--border" }, + "padding": [4, 6], + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "apEmptyTypeIcon", + "width": 12, + "height": 12, + "iconFontName": "type", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "apEmptyTypePlaceholder", + "fill": "$--muted-foreground", + "content": "Text", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "apEmptyTypeChevron", + "width": 10, + "height": 10, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "apEmptyValueInput", + "name": "Value Input", + "width": "fill_container", + "height": 26, + "fill": "$--muted", + "cornerRadius": 4, + "stroke": { "align": "inside", "thickness": 1, "fill": "$--border" }, + "padding": [4, 6], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "apEmptyValuePlaceholder", + "fill": "$--muted-foreground", + "content": "Value", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "apEmptyConfirmBtn", + "name": "Confirm Button", + "width": 24, + "height": 24, + "fill": "$--primary", + "cornerRadius": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "apEmptyConfirmIcon", + "width": 14, + "height": 14, + "iconFontName": "check", + "iconFontFamily": "lucide", + "fill": "#ffffff" + } + ] + }, + { + "type": "frame", + "id": "apEmptyCancelBtn", + "name": "Cancel Button", + "width": 24, + "height": 24, + "cornerRadius": 4, + "stroke": { "align": "inside", "thickness": 1, "fill": "$--border" }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "apEmptyCancelIcon", + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "apFilled", + "x": 400, + "y": 0, + "name": "Add Property — Inline Form (Filled State)", + "theme": { "Mode": "Light" }, + "width": 320, + "height": 180, + "fill": "$--background", + "layout": "vertical", + "gap": 8, + "padding": 12, + "children": [ + { + "type": "text", + "id": "apFilledDesc", + "name": "description", + "fill": "$--muted-foreground", + "content": "Filled state: user has typed a property name, selected a type (Date), and entered a value. Confirm button is enabled (blue). The form looks like the property row it will create.", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "apFilledExisting1", + "name": "Existing Property Row — Status", + "width": "fill_container", + "cornerRadius": 4, + "padding": [4, 6], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "apFilledLabel1", + "fill": "$--muted-foreground", + "content": "STATUS", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 1.2 + }, + { + "type": "frame", + "id": "apFilledBadge1", + "fill": "$--accent-green-light", + "cornerRadius": 16, + "padding": [1, 6], + "children": [ + { + "type": "text", + "id": "apFilledBadgeText1", + "fill": "$--accent-green", + "content": "ACTIVE", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 1.2 + } + ] + } + ] + }, + { + "type": "frame", + "id": "apFilledSep", + "name": "Separator", + "width": "fill_container", + "height": 1, + "fill": "$--border" + }, + { + "type": "frame", + "id": "apFilledForm", + "name": "Inline Add Property Form — Filled", + "width": "fill_container", + "gap": 6, + "padding": [4, 6], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "apFilledNameInput", + "name": "Name Input (filled)", + "width": 90, + "height": 26, + "fill": "$--muted", + "cornerRadius": 4, + "stroke": { "align": "inside", "thickness": 1, "fill": "$--primary" }, + "padding": [4, 6], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "apFilledNameText", + "fill": "$--foreground", + "content": "deadline", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "apFilledTypeSelect", + "name": "Type Selector (Date selected)", + "width": 72, + "height": 26, + "fill": "$--muted", + "cornerRadius": 4, + "stroke": { "align": "inside", "thickness": 1, "fill": "$--border" }, + "padding": [4, 6], + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "apFilledTypeIcon", + "width": 12, + "height": 12, + "iconFontName": "calendar", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "apFilledTypeText", + "fill": "$--foreground", + "content": "Date", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "apFilledTypeChevron", + "width": 10, + "height": 10, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "apFilledValueInput", + "name": "Value Input (filled)", + "width": "fill_container", + "height": 26, + "fill": "$--muted", + "cornerRadius": 4, + "stroke": { "align": "inside", "thickness": 1, "fill": "$--primary" }, + "padding": [4, 6], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "apFilledValueText", + "fill": "$--foreground", + "content": "2026-03-15", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "apFilledConfirmBtn", + "name": "Confirm Button (enabled)", + "width": 24, + "height": 24, + "fill": "$--primary", + "cornerRadius": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "apFilledConfirmIcon", + "width": 14, + "height": 14, + "iconFontName": "check", + "iconFontFamily": "lucide", + "fill": "#ffffff" + } + ] + }, + { + "type": "frame", + "id": "apFilledCancelBtn", + "name": "Cancel Button", + "width": 24, + "height": 24, + "cornerRadius": 4, + "stroke": { "align": "inside", "thickness": 1, "fill": "$--border" }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "apFilledCancelIcon", + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + } + ] + } + ], + "variables": { + "--background": { "type": "color", "value": [{ "value": "#ffffff" }] }, + "--foreground": { "type": "color", "value": [{ "value": "#37352F" }] }, + "--muted": { "type": "color", "value": [{ "value": "#F0F0EF" }] }, + "--muted-foreground": { "type": "color", "value": [{ "value": "#9B9A97" }] }, + "--border": { "type": "color", "value": [{ "value": "#E9E9E7" }] }, + "--primary": { "type": "color", "value": [{ "value": "#155DFF" }] }, + "--accent-green": { "type": "color", "value": [{ "value": "#38A169" }] }, + "--accent-green-light": { "type": "color", "value": [{ "value": "rgba(56, 161, 105, 0.1)" }] } + } +} \ No newline at end of file diff --git a/design/ai-agent-panel-ui.pen b/design/ai-agent-panel-ui.pen new file mode 100644 index 0000000..1014831 --- /dev/null +++ b/design/ai-agent-panel-ui.pen @@ -0,0 +1,340 @@ +{ + "children": [ + { + "type": "frame", + "id": "ai_panel_closed", + "name": "AI Agent Panel — Closed (trigger button in toolbar)", + "x": 0, + "y": 0, + "width": 1280, + "height": 800, + "fill": "$--background", + "layout": "horizontal", + "theme": { "Mode": "Light" }, + "children": [ + { + "type": "rectangle", + "id": "ai_closed_editor", + "width": "fill_container", + "height": "fill_container", + "fill": "$--muted", + "cornerRadius": 4 + }, + { + "type": "frame", + "id": "ai_closed_toolbar", + "name": "Right Toolbar", + "layout": "vertical", + "width": 36, + "height": "fill_container", + "fill": "$--background", + "padding": [8, 4], + "gap": 4, + "children": [ + { + "type": "frame", + "id": "ai_trigger_btn", + "name": "AI Trigger Button", + "width": 28, + "height": 28, + "cornerRadius": 6, + "fill": "$--muted", + "layout": "horizontal", + "padding": [6, 6], + "children": [ + { + "type": "text", + "id": "ai_trigger_icon", + "content": "✦", + "fill": "$--muted-foreground", + "fontSize": 14 + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "ai_panel_open_idle", + "name": "AI Agent Panel — Open, Idle (empty state)", + "x": 1320, + "y": 0, + "width": 1280, + "height": 800, + "fill": "$--background", + "layout": "horizontal", + "theme": { "Mode": "Light" }, + "children": [ + { + "type": "rectangle", + "id": "ai_open_editor", + "width": "fill_container", + "height": "fill_container", + "fill": "$--muted", + "cornerRadius": 4 + }, + { + "type": "frame", + "id": "ai_panel_sidebar", + "name": "AI Panel Sidebar", + "layout": "vertical", + "width": 320, + "height": "fill_container", + "fill": "$--background", + "children": [ + { + "type": "frame", + "id": "ai_panel_header", + "name": "Panel Header", + "layout": "horizontal", + "width": "fill_container", + "height": 45, + "padding": [0, 12], + "gap": 8, + "children": [ + { + "type": "text", + "id": "ai_header_label", + "content": "AI", + "fontSize": 13, + "fontWeight": "600", + "fill": "$--muted-foreground", + "width": "fill_container" + }, + { + "type": "text", + "id": "ai_close_icon", + "content": "✕", + "fontSize": 12, + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "ai_empty_state", + "name": "Empty State", + "layout": "vertical", + "width": "fill_container", + "height": "fill_container", + "padding": [24, 24], + "children": [ + { + "type": "text", + "id": "ai_empty_hint", + "content": "Ask the AI agent to work on your vault…", + "fontSize": 13, + "fill": "$--muted-foreground", + "textAlign": "center", + "width": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "ai_input_row", + "name": "Input Row", + "layout": "horizontal", + "width": "fill_container", + "padding": [8, 12], + "gap": 8, + "children": [ + { + "type": "rectangle", + "id": "ai_input_field", + "width": "fill_container", + "height": 32, + "cornerRadius": 6, + "fill": "$--muted" + }, + { + "type": "frame", + "id": "ai_send_btn", + "name": "Send Button", + "width": 28, + "height": 28, + "cornerRadius": 6, + "fill": "$--primary", + "layout": "horizontal", + "padding": [6, 6], + "children": [ + { + "type": "text", + "id": "ai_send_icon", + "content": "→", + "fontSize": 12, + "fill": "$--primary-foreground" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "ai_panel_active", + "name": "AI Agent Panel — Active (streaming, action cards visible)", + "x": 2640, + "y": 0, + "width": 1280, + "height": 800, + "fill": "$--background", + "layout": "horizontal", + "theme": { "Mode": "Light" }, + "children": [ + { + "type": "rectangle", + "id": "ai_active_editor", + "width": "fill_container", + "height": "fill_container", + "fill": "$--muted", + "cornerRadius": 4 + }, + { + "type": "frame", + "id": "ai_active_panel", + "name": "AI Panel — With Messages", + "layout": "vertical", + "width": 320, + "height": "fill_container", + "fill": "$--background", + "children": [ + { + "type": "frame", + "id": "ai_active_header", + "name": "Panel Header", + "layout": "horizontal", + "width": "fill_container", + "height": 45, + "padding": [0, 12], + "gap": 8, + "children": [ + { + "type": "text", + "id": "ai_active_label", + "content": "AI", + "fontSize": 13, + "fontWeight": "600", + "fill": "$--muted-foreground", + "width": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "ai_message_thread", + "name": "Message Thread", + "layout": "vertical", + "width": "fill_container", + "height": "fill_container", + "padding": [12, 12], + "gap": 16, + "children": [ + { + "type": "frame", + "id": "ai_user_msg", + "name": "User Message", + "layout": "vertical", + "width": "fill_container", + "gap": 4, + "children": [ + { + "type": "text", + "content": "You", + "fontSize": 11, + "fontWeight": "600", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "content": "Crea una nota evento per la riunione con Marco domani", + "fontSize": 13, + "fill": "$--foreground", + "width": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "ai_agent_msg", + "name": "Agent Message + Actions", + "layout": "vertical", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "ai_action_done", + "name": "Action Card — Done", + "layout": "horizontal", + "width": "fill_container", + "height": 28, + "padding": [0, 8], + "gap": 6, + "cornerRadius": 4, + "fill": "$--muted", + "children": [ + { + "type": "text", + "content": "✓", + "fontSize": 12, + "fill": "$--primary" + }, + { + "type": "text", + "content": "Loaded vault context", + "fontSize": 12, + "fill": "$--foreground", + "width": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "ai_action_progress", + "name": "Action Card — In Progress", + "layout": "horizontal", + "width": "fill_container", + "height": 28, + "padding": [0, 8], + "gap": 6, + "cornerRadius": 4, + "fill": "$--muted", + "children": [ + { + "type": "text", + "content": "◌", + "fontSize": 12, + "fill": "$--primary" + }, + { + "type": "text", + "content": "Creating: 2026-03-01-meeting-marco.md", + "fontSize": 12, + "fill": "$--foreground", + "width": "fill_container" + } + ] + }, + { + "type": "text", + "content": "Ho creato la nota evento e linkato Marco come partecipante. La trovi già aperta in un nuovo tab.", + "fontSize": 13, + "fill": "$--foreground", + "width": "fill_container" + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/design/ai-agent-wiring.pen b/design/ai-agent-wiring.pen new file mode 100644 index 0000000..576c27d --- /dev/null +++ b/design/ai-agent-wiring.pen @@ -0,0 +1 @@ +{"children":[{"type":"frame","id":"ai_wiring_state_machine","name":"AI Agent Wiring — State Machine (idle → thinking → tool-executing → response)","x":0,"y":0,"width":1280,"height":600,"fill":"$--background","layout":"horizontal","gap":32,"padding":[40,40],"alignItems":"flex-start","theme":{"Mode":"Light"},"children":[{"type":"frame","id":"state_idle","name":"State: Idle","width":240,"height":"fit_content(200)","fill":"$--muted","cornerRadius":12,"layout":"vertical","gap":12,"padding":[20,20],"children":[{"type":"frame","id":"idle_badge","width":80,"height":28,"cornerRadius":99,"fill":"$--accent-green-light","layout":"horizontal","padding":[4,12],"alignItems":"center","children":[{"type":"text","id":"idle_label","content":"Idle","fill":"$--foreground","fontSize":13,"fontWeight":"600"}]},{"type":"text","id":"idle_desc","content":"User types message in input bar.\nModel selector available.\nSend button enabled when input non-empty.","fill":"$--muted-foreground","fontSize":12,"lineHeight":1.5},{"type":"frame","id":"idle_input_mock","width":"fill_container","height":36,"cornerRadius":8,"fill":"$--background","strokeColor":"$--border","strokeThickness":1,"layout":"horizontal","padding":[8,10],"gap":8,"alignItems":"center","children":[{"type":"text","id":"idle_placeholder","content":"Ask the AI agent...","fill":"$--muted-foreground","fontSize":12}]}]},{"type":"frame","id":"state_arrow_1","width":40,"height":200,"layout":"vertical","alignItems":"center","padding":[80,0],"children":[{"type":"text","id":"arrow_1","content":"→","fill":"$--muted-foreground","fontSize":24}]},{"type":"frame","id":"state_thinking","name":"State: Thinking","width":240,"height":"fit_content(200)","fill":"$--muted","cornerRadius":12,"layout":"vertical","gap":12,"padding":[20,20],"children":[{"type":"frame","id":"thinking_badge","width":100,"height":28,"cornerRadius":99,"fill":"rgba(74,158,255,0.15)","layout":"horizontal","padding":[4,12],"alignItems":"center","children":[{"type":"text","id":"thinking_label","content":"Thinking","fill":"$--primary","fontSize":13,"fontWeight":"600"}]},{"type":"text","id":"thinking_desc","content":"Claude API called with:\n• System prompt (vault context)\n• Tool definitions (14 MCP tools)\n• Conversation history\n\nStreaming indicator shown.\nInput disabled.","fill":"$--muted-foreground","fontSize":12,"lineHeight":1.5},{"type":"frame","id":"thinking_indicator","width":"fill_container","height":24,"layout":"horizontal","gap":4,"alignItems":"center","padding":[0,4],"children":[{"type":"text","id":"thinking_dots","content":"● ● ●","fill":"$--muted-foreground","fontSize":12}]}]},{"type":"frame","id":"state_arrow_2","width":40,"height":200,"layout":"vertical","alignItems":"center","padding":[80,0],"children":[{"type":"text","id":"arrow_2","content":"→","fill":"$--muted-foreground","fontSize":24}]},{"type":"frame","id":"state_tool_exec","name":"State: Tool Executing","width":240,"height":"fit_content(200)","fill":"$--muted","cornerRadius":12,"layout":"vertical","gap":12,"padding":[20,20],"children":[{"type":"frame","id":"tool_badge","width":140,"height":28,"cornerRadius":99,"fill":"rgba(255,170,51,0.15)","layout":"horizontal","padding":[4,12],"alignItems":"center","children":[{"type":"text","id":"tool_label","content":"Tool Executing","fill":"#CC8800","fontSize":13,"fontWeight":"600"}]},{"type":"text","id":"tool_desc","content":"Claude returns tool_use block.\nFor each tool call:\n1. Send {id,tool,args} → WS:9710\n2. Wait for {id,result}\n3. Feed tool_result back to Claude\n4. Action card: spinner → ✓\n\nMay loop multiple times.","fill":"$--muted-foreground","fontSize":12,"lineHeight":1.5},{"type":"frame","id":"tool_card_mock","width":"fill_container","height":"fit_content(30)","cornerRadius":6,"fill":"rgba(74,158,255,0.1)","layout":"horizontal","gap":8,"padding":[6,10],"alignItems":"center","children":[{"type":"text","id":"tool_icon","content":"⟳","fill":"$--muted-foreground","fontSize":12},{"type":"text","id":"tool_name","content":"search_notes","fill":"$--foreground","fontSize":12},{"type":"text","id":"tool_status","content":"●","fill":"#CC8800","fontSize":10}]}]},{"type":"frame","id":"state_arrow_3","width":40,"height":200,"layout":"vertical","alignItems":"center","padding":[80,0],"children":[{"type":"text","id":"arrow_3","content":"→","fill":"$--muted-foreground","fontSize":24}]},{"type":"frame","id":"state_response","name":"State: Response","width":240,"height":"fit_content(200)","fill":"$--muted","cornerRadius":12,"layout":"vertical","gap":12,"padding":[20,20],"children":[{"type":"frame","id":"response_badge","width":110,"height":28,"cornerRadius":99,"fill":"$--accent-green-light","layout":"horizontal","padding":[4,12],"alignItems":"center","children":[{"type":"text","id":"response_label","content":"Response","fill":"$--foreground","fontSize":13,"fontWeight":"600"}]},{"type":"text","id":"response_desc","content":"Claude returns final text block.\nDisplayed as assistant message.\n\nAction cards show ✓ for all tools.\n[Undo ↩] button available.\nInput re-enabled for follow-up.","fill":"$--muted-foreground","fontSize":12,"lineHeight":1.5},{"type":"frame","id":"undo_btn_mock","width":80,"height":28,"cornerRadius":6,"fill":"$--background","strokeColor":"$--border","strokeThickness":1,"layout":"horizontal","padding":[4,12],"gap":4,"alignItems":"center","children":[{"type":"text","id":"undo_icon","content":"↩","fill":"$--muted-foreground","fontSize":12},{"type":"text","id":"undo_text","content":"Undo","fill":"$--muted-foreground","fontSize":12}]}]}]},{"type":"frame","id":"ai_wiring_data_flow","name":"AI Agent Wiring — Data Flow","x":0,"y":660,"width":1280,"height":400,"fill":"$--background","layout":"vertical","gap":20,"padding":[32,40],"theme":{"Mode":"Light"},"children":[{"type":"text","id":"flow_title","content":"Data Flow: useAiAgent Hook","fill":"$--foreground","fontSize":18,"fontWeight":"700"},{"type":"frame","id":"flow_diagram","width":"fill_container","height":"fit_content(200)","layout":"vertical","gap":8,"children":[{"type":"text","id":"flow_1","content":"1. User sends message → useAiAgent.sendMessage(text)","fill":"$--foreground","fontSize":13,"lineHeight":1.6},{"type":"text","id":"flow_2","content":"2. Build system prompt: vault context + selected notes","fill":"$--foreground","fontSize":13,"lineHeight":1.6},{"type":"text","id":"flow_3","content":"3. POST /api/ai/agent → Vite proxy → Anthropic API (with tools, stream:false)","fill":"$--foreground","fontSize":13,"lineHeight":1.6},{"type":"text","id":"flow_4","content":"4. If response has tool_use → execute via WS bridge (port 9710) → feed result back → goto 3","fill":"$--foreground","fontSize":13,"lineHeight":1.6},{"type":"text","id":"flow_5","content":"5. If response has text only → display as assistant message → back to idle","fill":"$--foreground","fontSize":13,"lineHeight":1.6},{"type":"text","id":"flow_6","content":"6. Before each run: snapshot touched files (read via WS). Undo restores snapshots.","fill":"$--muted-foreground","fontSize":12,"lineHeight":1.6}]},{"type":"frame","id":"flow_components","width":"fill_container","height":"fit_content(60)","layout":"horizontal","gap":24,"children":[{"type":"frame","id":"comp_hook","width":"fit_content(120)","height":"fit_content(40)","cornerRadius":8,"fill":"rgba(74,158,255,0.1)","padding":[8,16],"layout":"vertical","gap":4,"children":[{"type":"text","id":"comp_hook_name","content":"useAiAgent","fill":"$--primary","fontSize":13,"fontWeight":"600"},{"type":"text","id":"comp_hook_desc","content":"Hook: state, sendMessage, undo","fill":"$--muted-foreground","fontSize":11}]},{"type":"frame","id":"comp_proxy","width":"fit_content(120)","height":"fit_content(40)","cornerRadius":8,"fill":"rgba(74,158,255,0.1)","padding":[8,16],"layout":"vertical","gap":4,"children":[{"type":"text","id":"comp_proxy_name","content":"/api/ai/agent","fill":"$--primary","fontSize":13,"fontWeight":"600"},{"type":"text","id":"comp_proxy_desc","content":"Vite proxy → Anthropic","fill":"$--muted-foreground","fontSize":11}]},{"type":"frame","id":"comp_ws","width":"fit_content(120)","height":"fit_content(40)","cornerRadius":8,"fill":"rgba(74,158,255,0.1)","padding":[8,16],"layout":"vertical","gap":4,"children":[{"type":"text","id":"comp_ws_name","content":"WS Bridge :9710","fill":"$--primary","fontSize":13,"fontWeight":"600"},{"type":"text","id":"comp_ws_desc","content":"Tool execution","fill":"$--muted-foreground","fontSize":11}]},{"type":"frame","id":"comp_panel","width":"fit_content(120)","height":"fit_content(40)","cornerRadius":8,"fill":"rgba(74,158,255,0.1)","padding":[8,16],"layout":"vertical","gap":4,"children":[{"type":"text","id":"comp_panel_name","content":"AiPanel.tsx","fill":"$--primary","fontSize":13,"fontWeight":"600"},{"type":"text","id":"comp_panel_desc","content":"UI: messages, actions, undo","fill":"$--muted-foreground","fontSize":11}]}]}]}],"variables":{}} \ No newline at end of file diff --git a/design/align-property-dropdown.pen b/design/align-property-dropdown.pen new file mode 100644 index 0000000..e880b7c --- /dev/null +++ b/design/align-property-dropdown.pen @@ -0,0 +1 @@ +{"children":[{"children":[{"content":"BEFORE (mismatched styles)","fill":"#9CA3AF","fontFamily":"Inter","fontSize":11,"fontWeight":"600","id":"dFBnk","letterSpacing":1,"name":"titleBefore","type":"text"},{"alignItems":"center","children":[{"content":"Type","fill":"#9CA3AF","fontFamily":"JetBrains Mono","fontSize":10,"fontWeight":"500","id":"EC2s2","letterSpacing":0.5,"name":"typeLabelBefore","type":"text"},{"alignItems":"center","children":[{"content":"Project ▾","fill":"#7C3AED","fontFamily":"Inter","fontSize":12,"fontWeight":"500","id":"d8mDu","name":"typePillText","type":"text"}],"cornerRadius":6,"fill":"#EDE9FE","gap":4,"id":"Km8Xu","name":"typePillBefore","padding":[2,8],"type":"frame"}],"id":"ICZ33","justifyContent":"space_between","name":"Type Row — Old Style","type":"frame","width":"fill_container"},{"fill":"#E5E7EB","height":1,"id":"e0bqn","name":"divider1","type":"rectangle","width":"fill_container"},{"content":"Add Property form type selector:","fill":"#9CA3AF","fontFamily":"Inter","fontSize":10,"fontWeight":"normal","id":"EbI7c","name":"addLabel","type":"text"},{"alignItems":"center","children":[{"alignItems":"center","children":[{"content":"Property name","fill":"#9CA3AF","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"gNteI","name":"addInputText","type":"text"}],"cornerRadius":4,"fill":"#F3F4F6","height":26,"id":"0tXsa","name":"addInput","padding":[0,6],"stroke":{"align":"inside","fill":"#E5E7EB","thickness":1},"type":"frame","width":90},{"alignItems":"center","children":[{"fill":"#9CA3AF","height":14,"iconFontFamily":"lucide","iconFontName":"type","id":"ZUNFA","name":"addTypeIcon","type":"icon_font","width":14},{"content":"Text ▾","fill":"#374151","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"LJf42","name":"addTypeText","type":"text"}],"cornerRadius":4,"fill":"#F3F4F6","gap":4,"height":26,"id":"sQpJ5","name":"addTypeSelect","padding":[0,6],"stroke":{"align":"inside","fill":"#E5E7EB","thickness":1},"type":"frame","width":82}],"gap":6,"id":"MoHiU","name":"Add Property Row","type":"frame","width":"fill_container"}],"fill":"#FFFFFF","gap":16,"id":"07ZYk","layout":"vertical","name":"Align Property Dropdown — Before","padding":20,"type":"frame","width":280,"x":0,"y":0},{"children":[{"content":"AFTER (unified style)","fill":"#16A34A","fontFamily":"Inter","fontSize":11,"fontWeight":"600","id":"kfWbF","letterSpacing":1,"name":"titleAfter","type":"text"},{"alignItems":"center","children":[{"content":"Type","fill":"#9CA3AF","fontFamily":"JetBrains Mono","fontSize":10,"fontWeight":"500","id":"7ifiz","letterSpacing":0.5,"name":"typeLabelAfter","type":"text"},{"alignItems":"center","children":[{"content":"Project ▾","fill":"#374151","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"vbqJ1","name":"typeTextAfter","type":"text"}],"cornerRadius":4,"fill":"#F3F4F6","gap":4,"height":26,"id":"GrDBq","name":"typeSelectAfter","padding":[0,6],"stroke":{"align":"inside","fill":"#E5E7EB","thickness":1},"type":"frame"}],"id":"gSUcm","justifyContent":"space_between","name":"Type Row — Unified Style","type":"frame","width":"fill_container"},{"fill":"#E5E7EB","height":1,"id":"Qn9Ju","name":"divider2","type":"rectangle","width":"fill_container"},{"content":"Add Property form type selector:","fill":"#9CA3AF","fontFamily":"Inter","fontSize":10,"fontWeight":"normal","id":"L2xAX","name":"addLabel2","type":"text"},{"alignItems":"center","children":[{"alignItems":"center","children":[{"content":"Property name","fill":"#9CA3AF","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"hvpLW","name":"addInputText2","type":"text"}],"cornerRadius":4,"fill":"#F3F4F6","height":26,"id":"pH05L","name":"addInput2","padding":[0,6],"stroke":{"align":"inside","fill":"#E5E7EB","thickness":1},"type":"frame","width":90},{"alignItems":"center","children":[{"fill":"#9CA3AF","height":14,"iconFontFamily":"lucide","iconFontName":"type","id":"KTwLT","name":"addTypeIcon2","type":"icon_font","width":14},{"content":"Text ▾","fill":"#374151","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"R7qMP","name":"addTypeText2","type":"text"}],"cornerRadius":4,"fill":"#F3F4F6","gap":4,"height":26,"id":"8AbX1","name":"addTypeSelect2","padding":[0,6],"stroke":{"align":"inside","fill":"#E5E7EB","thickness":1},"type":"frame","width":82}],"gap":6,"id":"B3Xp2","name":"Add Property Row","type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"fill":"#16A34A","height":14,"iconFontFamily":"lucide","iconFontName":"check","id":"pjX8a","name":"matchIcon","type":"icon_font","width":14},{"content":"Both selectors now share the same visual style: bordered, muted bg, 4px radius, 26px height","fill":"#166534","fontFamily":"Inter","fontSize":10,"fontWeight":"normal","id":"Vbnnu","lineHeight":1.4,"name":"matchText","type":"text"}],"cornerRadius":6,"fill":"#F0FDF4","gap":6,"id":"ZUwuH","name":"matchNote","padding":[8,10],"type":"frame","width":"fill_container"}],"fill":"#FFFFFF","gap":16,"id":"ywjeP","layout":"vertical","name":"Align Property Dropdown — After (Unified)","padding":20,"type":"frame","width":280,"x":320,"y":0}],"variables":{}} \ No newline at end of file diff --git a/design/archived-note-indicator.pen b/design/archived-note-indicator.pen new file mode 100644 index 0000000..ebcc2d6 --- /dev/null +++ b/design/archived-note-indicator.pen @@ -0,0 +1 @@ +{"children":[],"variables":{}} \ No newline at end of file diff --git a/design/auto-pull-vault.pen b/design/auto-pull-vault.pen new file mode 100644 index 0000000..747b5ab --- /dev/null +++ b/design/auto-pull-vault.pen @@ -0,0 +1 @@ +{"children":[],"variables":{}} diff --git a/design/autocomplete-type-color.pen b/design/autocomplete-type-color.pen new file mode 100644 index 0000000..a3af113 --- /dev/null +++ b/design/autocomplete-type-color.pen @@ -0,0 +1 @@ +{"children":[{"type":"frame","id":"frame1","x":0,"y":0,"name":"Autocomplete — correct type colors","clip":true,"width":500,"height":400,"fill":"#FFFFFF","layout":"vertical","gap":0,"children":[{"type":"frame","id":"header","name":"Search Input","width":"fill_container","height":44,"fill":"#FFFFFF","stroke":{"align":"inside","thickness":{"bottom":1},"fill":"#E5E7EB"},"padding":[0,16],"alignItems":"center","children":[{"type":"text","id":"searchText","content":"[[ever","fontSize":15,"fontFamily":"Inter","fill":"#1F2937"}]},{"type":"frame","id":"results","name":"Results List","width":"fill_container","layout":"vertical","gap":0,"fill":"#FFFFFF","children":[{"type":"frame","id":"row1","name":"Result Row — Typed (Evergreen)","width":"fill_container","height":36,"padding":[0,12],"alignItems":"center","justifyContent":"spaceBetween","fill":"#F3F4F6","children":[{"type":"frame","id":"row1Left","gap":6,"alignItems":"center","children":[{"type":"text","id":"icon1","content":"🌿","fontSize":14},{"type":"text","id":"title1","content":"Evergreen: Writing Process","fontSize":14,"fontFamily":"Inter","fill":"#1F2937"}]},{"type":"frame","id":"badge1","padding":[2,8],"cornerRadius":4,"fill":"#ECFDF5","children":[{"type":"text","id":"badgeText1","content":"Evergreen","fontSize":11,"fontFamily":"Inter","fill":"#10B981","fontWeight":"500"}]}]},{"type":"frame","id":"row2","name":"Result Row — Typed (Project)","width":"fill_container","height":36,"padding":[0,12],"alignItems":"center","justifyContent":"spaceBetween","children":[{"type":"frame","id":"row2Left","gap":6,"alignItems":"center","children":[{"type":"text","id":"icon2","content":"🔧","fontSize":14},{"type":"text","id":"title2","content":"Evergreen Garden Project","fontSize":14,"fontFamily":"Inter","fill":"#1F2937"}]},{"type":"frame","id":"badge2","padding":[2,8],"cornerRadius":4,"fill":"#FEF2F2","children":[{"type":"text","id":"badgeText2","content":"Project","fontSize":11,"fontFamily":"Inter","fill":"#EF4444","fontWeight":"500"}]}]},{"type":"frame","id":"row3","name":"Result Row — Typed (Topic)","width":"fill_container","height":36,"padding":[0,12],"alignItems":"center","justifyContent":"spaceBetween","children":[{"type":"frame","id":"row3Left","gap":6,"alignItems":"center","children":[{"type":"text","id":"icon3","content":"🏷️","fontSize":14},{"type":"text","id":"title3","content":"Evergreen Notes Philosophy","fontSize":14,"fontFamily":"Inter","fill":"#1F2937"}]},{"type":"frame","id":"badge3","padding":[2,8],"cornerRadius":4,"fill":"#ECFDF5","children":[{"type":"text","id":"badgeText3","content":"Topic","fontSize":11,"fontFamily":"Inter","fill":"#10B981","fontWeight":"500"}]}]},{"type":"frame","id":"row4","name":"Result Row — Untyped (neutral grey)","width":"fill_container","height":36,"padding":[0,12],"alignItems":"center","justifyContent":"spaceBetween","children":[{"type":"frame","id":"row4Left","gap":6,"alignItems":"center","children":[{"type":"text","id":"title4","content":"My evergreen garden notes","fontSize":14,"fontFamily":"Inter","fill":"#1F2937"}]}]},{"type":"frame","id":"row5","name":"Result Row — Typed (Person)","width":"fill_container","height":36,"padding":[0,12],"alignItems":"center","justifyContent":"spaceBetween","children":[{"type":"frame","id":"row5Left","gap":6,"alignItems":"center","children":[{"type":"text","id":"icon5","content":"👤","fontSize":14},{"type":"text","id":"title5","content":"Evergreen Smith","fontSize":14,"fontFamily":"Inter","fill":"#1F2937"}]},{"type":"frame","id":"badge5","padding":[2,8],"cornerRadius":4,"fill":"#FFFBEB","children":[{"type":"text","id":"badgeText5","content":"Person","fontSize":11,"fontFamily":"Inter","fill":"#F59E0B","fontWeight":"500"}]}]}]}]},{"type":"frame","id":"frame2","x":0,"y":440,"name":"Legend — Type Color Mapping","width":500,"height":200,"fill":"#FFFFFF","layout":"vertical","gap":8,"padding":16,"children":[{"type":"text","id":"legendTitle","content":"Type → Color Mapping (Fixed)","fontSize":16,"fontFamily":"Inter","fill":"#1F2937","fontWeight":"700"},{"type":"text","id":"legend1","content":"• Project/Experiment → Red (--accent-red)","fontSize":12,"fontFamily":"Inter","fill":"#EF4444"},{"type":"text","id":"legend2","content":"• Responsibility/Procedure → Purple (--accent-purple)","fontSize":12,"fontFamily":"Inter","fill":"#8B5CF6"},{"type":"text","id":"legend3","content":"• Person/Event → Yellow (--accent-yellow)","fontSize":12,"fontFamily":"Inter","fill":"#F59E0B"},{"type":"text","id":"legend4","content":"• Topic → Green (--accent-green)","fontSize":12,"fontFamily":"Inter","fill":"#10B981"},{"type":"text","id":"legend5","content":"• Type → Blue (--accent-blue)","fontSize":12,"fontFamily":"Inter","fill":"#3B82F6"},{"type":"text","id":"legend6","content":"• Custom types → custom color from Type document","fontSize":12,"fontFamily":"Inter","fill":"#6B7280"},{"type":"text","id":"legend7","content":"• Untyped (Note/null) → Grey (--muted-foreground), no icon","fontSize":12,"fontFamily":"Inter","fill":"#9CA3AF"}]}],"variables":{}} \ No newline at end of file diff --git a/design/build-number-status-bar.pen b/design/build-number-status-bar.pen new file mode 100644 index 0000000..ea0475d --- /dev/null +++ b/design/build-number-status-bar.pen @@ -0,0 +1,96 @@ +{ + "children": [ + { + "type": "frame", + "id": "build_number_status_bar", + "name": "Build Number — Status Bar", + "x": 0, + "y": 0, + "width": 1200, + "height": 36, + "fill": "$--background", + "layout": "horizontal", + "gap": 12, + "padding": [ + 0, + 16 + ], + "theme": { + "Mode": "Light" + }, + "children": [ + { + "type": "text", + "id": "status_vault", + "content": "vault-name", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 12 + }, + { + "type": "text", + "id": "status_sep1", + "content": "|", + "fill": "$--border", + "fontFamily": "Inter", + "fontSize": 12 + }, + { + "type": "text", + "id": "status_build", + "content": "b223", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 12 + }, + { + "type": "text", + "id": "status_note", + "content": "← dynamic build number from package.json, replacing hardcoded v0.4.2", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 11, + "fontStyle": "italic" + } + ] + }, + { + "type": "frame", + "id": "build_number_fallback", + "name": "Build Number — Fallback (no build info)", + "x": 0, + "y": 60, + "width": 400, + "height": 36, + "fill": "$--background", + "layout": "horizontal", + "gap": 12, + "padding": [ + 0, + 16 + ], + "theme": { + "Mode": "Light" + }, + "children": [ + { + "type": "text", + "id": "fallback_build", + "content": "b?", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 12 + }, + { + "type": "text", + "id": "fallback_note", + "content": "← shows b? when build number unavailable", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 11, + "fontStyle": "italic" + } + ] + } + ] +} \ No newline at end of file diff --git a/design/command-palette-type-aware.pen b/design/command-palette-type-aware.pen new file mode 100644 index 0000000..9b2a4db --- /dev/null +++ b/design/command-palette-type-aware.pen @@ -0,0 +1,387 @@ +{ + "children": [ + { + "type": "frame", + "id": "cp_new_ev", + "name": "Command Palette — type \"new ev\" autocomplete", + "x": 0, + "y": 0, + "width": 620, + "height": "fit_content(500)", + "fill": "$--background", + "layout": "vertical", + "gap": 24, + "padding": [32, 32], + "theme": { "Mode": "Light" }, + "children": [ + { + "type": "text", + "id": "cp_new_title", + "content": "Command Palette — \"new ev\" autocomplete", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "cp_new_dialog", + "name": "Palette Dialog", + "width": 520, + "height": "fit_content(440)", + "fill": "$--popover", + "layout": "vertical", + "cornerRadius": [12, 12, 12, 12], + "stroke": "$--border", + "strokeThickness": 1, + "children": [ + { + "type": "frame", + "id": "cp_new_input_row", + "width": "fill_container", + "height": 48, + "padding": [0, 16], + "layout": "horizontal", + "verticalAlign": "center", + "stroke": "$--border", + "strokeSides": ["bottom"], + "strokeThickness": 1, + "children": [ + { + "type": "text", + "id": "cp_new_query", + "content": "new ev", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 15 + } + ] + }, + { + "type": "frame", + "id": "cp_new_results", + "name": "Results", + "width": "fill_container", + "layout": "vertical", + "padding": [4, 0], + "children": [ + { + "type": "text", + "id": "cp_new_group_label", + "content": "NOTE", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600", + "letterSpacing": 1, + "padding": [8, 16, 4, 16] + }, + { + "type": "frame", + "id": "cp_new_row1", + "name": "New Event (selected)", + "width": "fill_container", + "height": 36, + "padding": [0, 12], + "layout": "horizontal", + "verticalAlign": "center", + "horizontalAlign": "spaceBetween", + "cornerRadius": [6, 6, 6, 6], + "fill": "$--accent", + "margin": [0, 4], + "children": [ + { + "type": "text", + "id": "cp_new_row1_label", + "content": "New Event", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 14 + } + ] + }, + { + "type": "frame", + "id": "cp_new_row2", + "name": "New Event Log", + "width": "fill_container", + "height": 36, + "padding": [0, 12], + "layout": "horizontal", + "verticalAlign": "center", + "horizontalAlign": "spaceBetween", + "cornerRadius": [6, 6, 6, 6], + "margin": [0, 4], + "children": [ + { + "type": "text", + "id": "cp_new_row2_label", + "content": "New Event Log", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 14 + } + ] + }, + { + "type": "frame", + "id": "cp_new_row3", + "name": "New Experiment", + "width": "fill_container", + "height": 36, + "padding": [0, 12], + "layout": "horizontal", + "verticalAlign": "center", + "horizontalAlign": "spaceBetween", + "cornerRadius": [6, 6, 6, 6], + "margin": [0, 4], + "children": [ + { + "type": "text", + "id": "cp_new_row3_label", + "content": "New Experiment", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 14 + } + ] + } + ] + }, + { + "type": "frame", + "id": "cp_new_footer", + "width": "fill_container", + "height": 32, + "padding": [0, 16], + "layout": "horizontal", + "verticalAlign": "center", + "gap": 16, + "stroke": "$--border", + "strokeSides": ["top"], + "strokeThickness": 1, + "children": [ + { + "type": "text", + "id": "cp_new_hint1", + "content": "↑↓ navigate", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 11 + }, + { + "type": "text", + "id": "cp_new_hint2", + "content": "↵ select", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 11 + }, + { + "type": "text", + "id": "cp_new_hint3", + "content": "esc close", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 11 + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "cp_list_pe", + "name": "Command Palette — type \"list pe\" autocomplete", + "x": 720, + "y": 0, + "width": 620, + "height": "fit_content(500)", + "fill": "$--background", + "layout": "vertical", + "gap": 24, + "padding": [32, 32], + "theme": { "Mode": "Light" }, + "children": [ + { + "type": "text", + "id": "cp_list_title", + "content": "Command Palette — \"list pe\" autocomplete", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "cp_list_dialog", + "name": "Palette Dialog", + "width": 520, + "height": "fit_content(440)", + "fill": "$--popover", + "layout": "vertical", + "cornerRadius": [12, 12, 12, 12], + "stroke": "$--border", + "strokeThickness": 1, + "children": [ + { + "type": "frame", + "id": "cp_list_input_row", + "width": "fill_container", + "height": 48, + "padding": [0, 16], + "layout": "horizontal", + "verticalAlign": "center", + "stroke": "$--border", + "strokeSides": ["bottom"], + "strokeThickness": 1, + "children": [ + { + "type": "text", + "id": "cp_list_query", + "content": "list pe", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 15 + } + ] + }, + { + "type": "frame", + "id": "cp_list_results", + "name": "Results", + "width": "fill_container", + "layout": "vertical", + "padding": [4, 0], + "children": [ + { + "type": "text", + "id": "cp_list_group_label", + "content": "NAVIGATION", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600", + "letterSpacing": 1, + "padding": [8, 16, 4, 16] + }, + { + "type": "frame", + "id": "cp_list_row1", + "name": "List People (selected)", + "width": "fill_container", + "height": 36, + "padding": [0, 12], + "layout": "horizontal", + "verticalAlign": "center", + "horizontalAlign": "spaceBetween", + "cornerRadius": [6, 6, 6, 6], + "fill": "$--accent", + "margin": [0, 4], + "children": [ + { + "type": "text", + "id": "cp_list_row1_label", + "content": "List People", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 14 + } + ] + }, + { + "type": "frame", + "id": "cp_list_row2", + "name": "List Procedures", + "width": "fill_container", + "height": 36, + "padding": [0, 12], + "layout": "horizontal", + "verticalAlign": "center", + "horizontalAlign": "spaceBetween", + "cornerRadius": [6, 6, 6, 6], + "margin": [0, 4], + "children": [ + { + "type": "text", + "id": "cp_list_row2_label", + "content": "List Procedures", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 14 + } + ] + }, + { + "type": "frame", + "id": "cp_list_row3", + "name": "List Projects", + "width": "fill_container", + "height": 36, + "padding": [0, 12], + "layout": "horizontal", + "verticalAlign": "center", + "horizontalAlign": "spaceBetween", + "cornerRadius": [6, 6, 6, 6], + "margin": [0, 4], + "children": [ + { + "type": "text", + "id": "cp_list_row3_label", + "content": "List Projects", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 14 + } + ] + } + ] + }, + { + "type": "frame", + "id": "cp_list_footer", + "width": "fill_container", + "height": 32, + "padding": [0, 16], + "layout": "horizontal", + "verticalAlign": "center", + "gap": 16, + "stroke": "$--border", + "strokeSides": ["top"], + "strokeThickness": 1, + "children": [ + { + "type": "text", + "id": "cp_list_hint1", + "content": "↑↓ navigate", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 11 + }, + { + "type": "text", + "id": "cp_list_hint2", + "content": "↵ select", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 11 + }, + { + "type": "text", + "id": "cp_list_hint3", + "content": "esc close", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 11 + } + ] + } + ] + } + ] + } + ], + "variables": {} +} diff --git a/design/date-picker-shadcn.pen b/design/date-picker-shadcn.pen new file mode 100644 index 0000000..6f60223 --- /dev/null +++ b/design/date-picker-shadcn.pen @@ -0,0 +1,285 @@ +{ + "children": [ + { + "type": "frame", + "id": "datePickerClosed", + "x": 0, + "y": 0, + "name": "Date Picker — closed state (showing formatted date as button)", + "width": 320, + "height": 40, + "fill": "#ffffff", + "gap": 8, + "padding": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dateClosedLabel", + "name": "label", + "fill": "#6b7280", + "content": "deadline", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal", + "letterSpacing": 1.2, + "textTransform": "uppercase" + }, + { + "type": "frame", + "id": "dateClosedButton", + "name": "date-button", + "width": "fill_container", + "justifyContent": "flex-end", + "alignItems": "center", + "gap": 4, + "children": [ + { + "type": "text", + "id": "dateClosedIcon", + "name": "calendar-icon", + "fill": "#9ca3af", + "content": "📅", + "fontSize": 12 + }, + { + "type": "text", + "id": "dateClosedValue", + "name": "date-value", + "fill": "#374151", + "content": "Mar 31, 2026", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "datePickerOpen", + "x": 0, + "y": 160, + "name": "Date Picker — open state (calendar popup below button)", + "width": 320, + "height": 380, + "fill": "#ffffff", + "layout": "vertical", + "gap": 4, + "padding": 0, + "children": [ + { + "type": "frame", + "id": "dateOpenRow", + "name": "property-row", + "width": "fill_container", + "height": 40, + "gap": 8, + "padding": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dateOpenLabel", + "name": "label", + "fill": "#6b7280", + "content": "deadline", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal", + "letterSpacing": 1.2, + "textTransform": "uppercase" + }, + { + "type": "frame", + "id": "dateOpenButton", + "name": "date-button-active", + "width": "fill_container", + "justifyContent": "flex-end", + "alignItems": "center", + "gap": 4, + "children": [ + { + "type": "text", + "id": "dateOpenIcon", + "name": "calendar-icon", + "fill": "#6366f1", + "content": "📅", + "fontSize": 12 + }, + { + "type": "text", + "id": "dateOpenValue", + "name": "date-value", + "fill": "#111827", + "content": "Mar 31, 2026", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "calendarPopup", + "name": "calendar-popup", + "width": 280, + "height": 320, + "fill": "#ffffff", + "layout": "vertical", + "cornerRadius": 8, + "padding": 12, + "gap": 8, + "stroke": { + "align": "outside", + "thickness": 1, + "fill": "#e5e7eb" + }, + "children": [ + { + "type": "frame", + "id": "calendarHeader", + "name": "calendar-header", + "width": "fill_container", + "height": 32, + "alignItems": "center", + "justifyContent": "space-between", + "children": [ + { + "type": "text", + "id": "calPrevBtn", + "name": "prev-month", + "fill": "#6b7280", + "content": "◀", + "fontSize": 12 + }, + { + "type": "text", + "id": "calMonthYear", + "name": "month-year", + "fill": "#111827", + "content": "March 2026", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "calNextBtn", + "name": "next-month", + "fill": "#6b7280", + "content": "▶", + "fontSize": 12 + } + ] + }, + { + "type": "frame", + "id": "calWeekdayRow", + "name": "weekday-headers", + "width": "fill_container", + "height": 24, + "alignItems": "center", + "justifyContent": "space-between", + "children": [ + { "type": "text", "id": "wd1", "fill": "#9ca3af", "content": "Su", "fontSize": 11, "fontFamily": "Inter", "fontWeight": "500" }, + { "type": "text", "id": "wd2", "fill": "#9ca3af", "content": "Mo", "fontSize": 11, "fontFamily": "Inter", "fontWeight": "500" }, + { "type": "text", "id": "wd3", "fill": "#9ca3af", "content": "Tu", "fontSize": 11, "fontFamily": "Inter", "fontWeight": "500" }, + { "type": "text", "id": "wd4", "fill": "#9ca3af", "content": "We", "fontSize": 11, "fontFamily": "Inter", "fontWeight": "500" }, + { "type": "text", "id": "wd5", "fill": "#9ca3af", "content": "Th", "fontSize": 11, "fontFamily": "Inter", "fontWeight": "500" }, + { "type": "text", "id": "wd6", "fill": "#9ca3af", "content": "Fr", "fontSize": 11, "fontFamily": "Inter", "fontWeight": "500" }, + { "type": "text", "id": "wd7", "fill": "#9ca3af", "content": "Sa", "fontSize": 11, "fontFamily": "Inter", "fontWeight": "500" } + ] + }, + { + "type": "frame", + "id": "calGrid", + "name": "calendar-grid", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "frame", + "id": "calRow1", + "name": "week-1", + "width": "fill_container", + "height": 32, + "alignItems": "center", + "justifyContent": "space-between", + "children": [ + { "type": "text", "id": "d1", "fill": "#d1d5db", "content": " ", "fontSize": 12, "fontFamily": "Inter" }, + { "type": "text", "id": "d2", "fill": "#d1d5db", "content": " ", "fontSize": 12, "fontFamily": "Inter" }, + { "type": "text", "id": "d3", "fill": "#d1d5db", "content": " ", "fontSize": 12, "fontFamily": "Inter" }, + { "type": "text", "id": "d4", "fill": "#d1d5db", "content": " ", "fontSize": 12, "fontFamily": "Inter" }, + { "type": "text", "id": "d5", "fill": "#d1d5db", "content": " ", "fontSize": 12, "fontFamily": "Inter" }, + { "type": "text", "id": "d6", "fill": "#d1d5db", "content": " ", "fontSize": 12, "fontFamily": "Inter" }, + { "type": "text", "id": "d7", "fill": "#374151", "content": "1", "fontSize": 12, "fontFamily": "Inter" } + ] + }, + { + "type": "frame", + "id": "calRow5", + "name": "week-5-selected", + "width": "fill_container", + "height": 32, + "alignItems": "center", + "justifyContent": "space-between", + "children": [ + { "type": "text", "id": "d29", "fill": "#374151", "content": "29", "fontSize": 12, "fontFamily": "Inter" }, + { "type": "text", "id": "d30", "fill": "#374151", "content": "30", "fontSize": 12, "fontFamily": "Inter" }, + { + "type": "frame", + "id": "selectedDay", + "name": "selected-day-31", + "width": 28, + "height": 28, + "fill": "#111827", + "cornerRadius": 14, + "alignItems": "center", + "justifyContent": "center", + "children": [ + { "type": "text", "id": "d31", "fill": "#ffffff", "content": "31", "fontSize": 12, "fontFamily": "Inter", "fontWeight": "600" } + ] + }, + { "type": "text", "id": "d32", "fill": "#d1d5db", "content": " ", "fontSize": 12, "fontFamily": "Inter" }, + { "type": "text", "id": "d33", "fill": "#d1d5db", "content": " ", "fontSize": 12, "fontFamily": "Inter" }, + { "type": "text", "id": "d34", "fill": "#d1d5db", "content": " ", "fontSize": 12, "fontFamily": "Inter" }, + { "type": "text", "id": "d35", "fill": "#d1d5db", "content": " ", "fontSize": 12, "fontFamily": "Inter" } + ] + } + ] + }, + { + "type": "frame", + "id": "calFooter", + "name": "calendar-footer", + "width": "fill_container", + "height": 28, + "alignItems": "center", + "justifyContent": "flex-end", + "gap": 8, + "children": [ + { + "type": "text", + "id": "calClearBtn", + "name": "clear-button", + "fill": "#6b7280", + "content": "Clear", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + } + ] + } + ] + } + ], + "variables": {} +} \ No newline at end of file diff --git a/design/design-full-layouts.pen b/design/design-full-layouts.pen new file mode 100644 index 0000000..455a82f --- /dev/null +++ b/design/design-full-layouts.pen @@ -0,0 +1,13769 @@ +{ + "children": [ + { + "type": "frame", + "id": "X6DiX", + "x": 0, + "y": 0, + "name": "Full Layout — Editor Focused", + "theme": { + "Mode": "Light" + }, + "clip": true, + "width": 1440, + "height": 900, + "fill": "$--background", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "QXp_S", + "name": "macOS Title Bar", + "width": "fill_container", + "height": 38, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "gap": 12, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "a7ql_", + "name": "trafficLights", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "PIagL", + "name": "closeBtn", + "fill": "#FF5F57", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "UkrIX", + "name": "minimizeBtn", + "fill": "#FEBC2E", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "rpllF", + "name": "maximizeBtn", + "fill": "#28C840", + "width": 12, + "height": 12 + } + ] + } + ] + }, + { + "type": "frame", + "id": "YHu8M", + "name": "Content", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "AUMkh", + "name": "Panel: Sidebar", + "clip": true, + "width": 250, + "height": "fill_container", + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": 0, + "fill": "$--sidebar-border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "jIs8c", + "name": "Filters", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 1, + "padding": [ + 8, + 8, + 16, + 8 + ], + "children": [ + { + "type": "frame", + "id": "uDixt", + "name": "filterAll", + "width": "fill_container", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "0ZAuF", + "name": "leftAll", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "CvmcV", + "name": "iconAll", + "width": 18, + "height": 18, + "iconFontName": "tray-fill", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "_IXF5", + "name": "fAllTxt", + "fill": "$--foreground", + "content": "Untagged", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "cc5p7", + "name": "countAll", + "height": 20, + "fill": "$--secondary", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "wekdD", + "name": "badgeAllTxt", + "fill": "$--muted-foreground", + "content": "24", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "UzJob", + "name": "filterUntagged", + "width": "fill_container", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "UassE", + "name": "leftUntagged", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "94gjp", + "name": "untaggedIcon", + "width": 18, + "height": 18, + "iconFontName": "cardholder", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--primary" + }, + { + "type": "text", + "id": "Z4kx8", + "name": "untaggedTxt", + "fill": "$--primary", + "content": "All Notes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "mxu-s", + "name": "countUntagged", + "height": 20, + "fill": "$--primary", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "766If", + "name": "untaggedBadgeTxt", + "fill": "$--primary-foreground", + "content": "9247", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ], + "fill": "#4a9eff18" + }, + { + "type": "frame", + "id": "j6x6d", + "name": "filterTrash", + "width": "fill_container", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "2-DOi", + "name": "leftTrash", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "otR4H", + "name": "trashIcon", + "width": 18, + "height": 18, + "iconFontName": "trash", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "XnkPL", + "name": "trashTxt", + "fill": "$--foreground", + "content": "Archive", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "FVqx6", + "name": "countTrash", + "height": 20, + "fill": "$--secondary", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "3QQmO", + "name": "trashBadgeTxt", + "fill": "$--muted-foreground", + "content": "2", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "IGH4B", + "name": "filterChanges", + "width": "fill_container", + "cornerRadius": 6, + "gap": 6, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "RM0XF", + "name": "leftChanges", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "i-4iz", + "name": "iconChanges", + "width": 18, + "height": 18, + "iconFontName": "git-branch", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "Px5GR", + "name": "fChangesTxt", + "fill": "$--foreground", + "content": "Changes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "wO3WO", + "name": "countChanges", + "height": 20, + "fill": "$--secondary", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YJxJn", + "name": "badgeChangesTxt", + "fill": "$--muted-foreground", + "content": "3", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "jXROv", + "name": "Sections", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "padding": [ + 8, + 0 + ], + "children": [ + { + "type": "frame", + "id": "sO-XL", + "name": "projGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6, + 8, + 6 + ], + "children": [ + { + "type": "frame", + "id": "8e7Vt", + "name": "projHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "rcsfO", + "name": "leftProj", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "gytXN", + "name": "projIcon", + "width": 18, + "height": 18, + "iconFontName": "wrench", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-red" + }, + { + "type": "text", + "id": "-s_mS", + "name": "projLabel", + "fill": "$--foreground", + "content": "Projects", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "CwD_O", + "name": "projChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "52A6U", + "name": "projItem1", + "width": "fill_container", + "fill": "$--accent-red-light", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "BgNaS", + "name": "proj1Txt", + "fill": "$--accent-red", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "VdDh5", + "name": "projItem2", + "width": "fill_container", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "A0wXl", + "name": "proj2Txt", + "fill": "$--muted-foreground", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "fZ-0x", + "name": "projItem3", + "width": "fill_container", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "TtXDd", + "name": "proj3Txt", + "fill": "$--muted-foreground", + "content": "AI Habit Tracker", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "GZd6p", + "name": "respGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "E6xqR", + "name": "respHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "fa0Nk", + "name": "leftResp", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "S8AHA", + "name": "respIcon", + "width": 18, + "height": 18, + "iconFontName": "medal", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "xe_g8", + "name": "respLabel", + "fill": "$--foreground", + "content": "Responsibilities", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "XpuwI", + "name": "respChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "shgFZ", + "name": "procGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "8Ujcl", + "name": "respHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "J4tD5", + "name": "leftResp", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "jdHxB", + "name": "respIcon", + "width": 18, + "height": 18, + "iconFontName": "arrows-clockwise", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "Gt0UH", + "name": "Procedures", + "fill": "$--foreground", + "content": "Procedures", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "GFHmg", + "name": "procChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "hiql_", + "name": "peopleGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "hC4r1", + "name": "peopleHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "TrVIO", + "name": "leftPeople", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "Dr2Jm", + "name": "peopleIcon", + "width": 18, + "height": 18, + "iconFontName": "users", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-yellow" + }, + { + "type": "text", + "id": "lzFwD", + "name": "peopleLbl", + "fill": "$--foreground", + "content": "People", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "u7Y66", + "name": "peopleChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "v05Mj", + "name": "eventsGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "KVmYN", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "P0NRp", + "name": "leftEvents", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "NaHtD", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "calendar-blank", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-yellow" + }, + { + "type": "text", + "id": "YsEle", + "name": "eventsLbl", + "fill": "$--foreground", + "content": "Events", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "b0yk2", + "name": "eventsChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "oMYo2", + "name": "topicsGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "TUZI-", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "6uK9k", + "name": "leftEvents", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "WJIXx", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "tag", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-green" + }, + { + "type": "text", + "id": "3dn7l", + "name": "eventsLbl", + "fill": "$--foreground", + "content": "Topics", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "yPQuN", + "name": "topicsChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "N-qVJ", + "name": "topicsGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dsXnU", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "aW1pQ", + "name": "leftEvents", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "SQjcs", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "leaf", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-green" + }, + { + "type": "text", + "id": "Vv4ha", + "name": "eventsLbl", + "fill": "$--foreground", + "content": "Notes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "P1KUL", + "name": "topicsChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "MhWVM", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "zgDZ5", + "name": "leftEvents", + "gap": 8, + "padding": [ + 8, + 0 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "sMBNg", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "gFAJs", + "name": "eventsLbl", + "fill": "$--muted-foreground", + "content": "Create new type", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "EyZli", + "name": "Commit Area", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "top": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "padding": 12, + "children": [ + { + "type": "frame", + "id": "1JRzZ", + "name": "commitBtn", + "width": "fill_container", + "fill": "$--primary", + "cornerRadius": 6, + "gap": 6, + "padding": [ + 8, + 16 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "Ts-7a", + "name": "commitIcon", + "width": 14, + "height": 14, + "iconFontName": "git-commit-horizontal", + "iconFontFamily": "lucide", + "fill": "$--primary-foreground" + }, + { + "type": "text", + "id": "pEh20", + "name": "commitTxt", + "fill": "$--primary-foreground", + "content": "Commit & Push", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "ZC4Tp", + "name": "commitBadge", + "height": 18, + "fill": "#ffffff40", + "cornerRadius": 9, + "padding": [ + 0, + 5 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dH3M0", + "name": "commitBadgeTxt", + "fill": "$--white", + "content": "3", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "iayXd", + "name": "Resize Handle", + "fill": "$--border", + "width": 1, + "height": "fill_container" + }, + { + "type": "frame", + "id": "KYP3S", + "name": "Panel: NoteList", + "clip": true, + "width": 300, + "height": "fill_container", + "fill": "$--card", + "stroke": { + "align": "inside", + "thickness": 0, + "fill": "$--border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "udhLQ", + "name": "NoteList Header", + "width": "fill_container", + "height": 45, + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "padding": [ + 14, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ajmdp", + "name": "nlTitle", + "fill": "$--foreground", + "content": "All Notes", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "5SvsJ", + "name": "nlActions", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "qFJVP", + "name": "searchIcon", + "width": 16, + "height": 16, + "iconFontName": "magnifying-glass", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "cgafy", + "name": "plusIcon", + "width": 16, + "height": 16, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "koQ0J", + "name": "Note Items", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "1H3yZ", + "name": "Backlinks Group", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "3kSDX", + "name": "Note Item Selected", + "width": "fill_container", + "fill": "$--accent-red-light", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "#E9E9E7" + }, + "children": [ + { + "type": "rectangle", + "id": "Ha8HS", + "name": "leftAccent", + "fill": "$--accent-red", + "width": 3, + "height": "fill_container" + }, + { + "type": "frame", + "id": "pvIeZ", + "name": "noteSelContent", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "miGdK", + "name": "noteSelRow", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "XsFs9", + "name": "titleGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "qGt0S", + "name": "noteSelTitle", + "fill": "$--foreground", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "icon_font", + "id": "nbrro", + "name": "ico", + "width": 14, + "height": 14, + "iconFontName": "wrench", + "iconFontFamily": "lucide", + "fill": "$--accent-red" + } + ] + } + ] + }, + { + "type": "text", + "id": "2ONoL", + "name": "noteSelSnip", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Personal knowledge and life management desktop app...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "9An2M", + "name": "noteSelTime", + "fill": "$--accent-red", + "content": "2m ago", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "kZZVQ", + "name": "Related Notes Group", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "15OPP", + "name": "Related Notes Header", + "width": "fill_container", + "height": 32, + "fill": "$--muted", + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "VZfPV", + "name": "rnLeft", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "yt3sX", + "name": "rnLabel", + "fill": "$--muted-foreground", + "content": "RELATED NOTES", + "fontFamily": "IBM Plex Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "rMUGk", + "name": "rnCount", + "fill": "$--muted-foreground", + "content": "2", + "fontFamily": "IBM Plex Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "lxFxs", + "name": "rnRight", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "T9Tni", + "name": "rnFilter", + "width": 12, + "height": 12, + "iconFontName": "funnel", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "7zq2S", + "name": "rnPlus", + "width": 12, + "height": 12, + "iconFontName": "plus", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "k2dd-", + "name": "rnChev", + "width": 12, + "height": 12, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "64sl5", + "name": "Note Item", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "Jgqoc", + "name": "note2Row", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "u-lDr", + "name": "leafGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Sx4pi", + "name": "note2Title", + "fill": "$--foreground", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "FKgVd", + "name": "leafIco", + "width": 14, + "height": 14, + "iconFontName": "leaf", + "iconFontFamily": "phosphor", + "fill": "$--accent-green" + } + ] + } + ] + }, + { + "type": "text", + "id": "Or7kg", + "name": "note2Snip", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Redesigning portfolio site with modern stack and updated visual language...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "icctC", + "name": "note2Time", + "fill": "$--muted-foreground", + "content": "1h ago", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "lpkip", + "name": "Note Item", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "hV9DU", + "name": "note2Row", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "4MowJ", + "name": "leafGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "OqjO4", + "name": "note2Title", + "fill": "$--foreground", + "content": "Sample note 2", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "geEmd", + "name": "leafIco", + "width": 14, + "height": 14, + "iconFontName": "leaf", + "iconFontFamily": "phosphor", + "fill": "$--accent-green" + } + ] + } + ] + }, + { + "type": "text", + "id": "ya9zF", + "name": "note2Snip", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Lorem ipsum dolor sit amet consequetur with modern stack and updated language...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "yfwf5", + "name": "note2Time", + "fill": "$--muted-foreground", + "content": "1h ago", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "piEtY", + "name": "Events Group", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "fOL6M", + "name": "Events Header", + "width": "fill_container", + "height": 32, + "fill": "$--muted", + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "cx0o6", + "name": "evLeft", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "4073P", + "name": "evLabel", + "fill": "$--muted-foreground", + "content": "EVENTS", + "fontFamily": "IBM Plex Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "up3pe", + "name": "evCount", + "fill": "$--muted-foreground", + "content": "2", + "fontFamily": "IBM Plex Sans", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "QCl7r", + "name": "evRight", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "r-KNu", + "name": "evFilter", + "width": 12, + "height": 12, + "iconFontName": "funnel", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "4-yEN", + "name": "evPlus", + "width": 12, + "height": 12, + "iconFontName": "plus", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "is8Ce", + "name": "evChev", + "width": 12, + "height": 12, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "2Bw0D", + "name": "Note Item 2", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "bDrW1", + "name": "note3Row", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dIBZK", + "name": "calGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YXOiG", + "name": "note3Title", + "fill": "$--foreground", + "content": "Meeting with Grant", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "UHaDj", + "name": "calIco", + "width": 14, + "height": 14, + "iconFontName": "calendar", + "iconFontFamily": "lucide", + "fill": "$--accent-yellow" + } + ] + } + ] + }, + { + "type": "text", + "id": "wHYGL", + "name": "note3Snip", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Discussed Q1 goals and hiring priorities with the engineering team leads...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "zyqm_", + "name": "note3Time", + "fill": "$--muted-foreground", + "content": "Yesterday", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "BVMQi", + "name": "Note Item 2", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "Kj543", + "name": "note3Row", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "_csvb", + "name": "calGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "fsnPK", + "name": "note3Title", + "fill": "$--foreground", + "content": "Meeting with Matteo", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "O3Je-", + "name": "calIco", + "width": 14, + "height": 14, + "iconFontName": "calendar", + "iconFontFamily": "lucide", + "fill": "$--accent-yellow" + } + ] + } + ] + }, + { + "type": "text", + "id": "NGfVJ", + "name": "note3Snip", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Discussed Q1 goals and hiring priorities with the engineering team leads...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "dw3Za", + "name": "note3Time", + "fill": "$--muted-foreground", + "content": "Yesterday", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "fKf2N", + "name": "Resize Handle 2", + "fill": "$--border", + "width": 1, + "height": "fill_container" + }, + { + "type": "frame", + "id": "MLegO", + "name": "Panel: Editor", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$--background", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "2UACq", + "name": "Tab Bar", + "width": "fill_container", + "height": 45, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "RjysZ", + "name": "Tab Active", + "height": "fill_container", + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "right": 1 + }, + "fill": "$--border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ZWVUX", + "name": "tab1Txt", + "fill": "$--foreground", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "5xN9D", + "name": "tab1Close", + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "bqwJh", + "name": "Tab Inactive", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "right": 1, + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "jxV4d", + "name": "tab2Txt", + "fill": "$--muted-foreground", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "ZS9l6", + "name": "tab2Close", + "opacity": 0, + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "fqCqt", + "name": "tabSpacer", + "width": "fill_container", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + } + }, + { + "type": "frame", + "id": "KdlSR", + "name": "tabControls", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1, + "left": 1 + }, + "fill": "$--border" + }, + "gap": 12, + "padding": [ + 0, + 12 + ], + "justifyContent": "space_around", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "68hyI", + "name": "iconPlus", + "width": 16, + "height": 16, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "-LdMX", + "name": "iconSplit", + "width": 16, + "height": 16, + "iconFontName": "columns", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "tyTM3", + "name": "iconExpand", + "width": 16, + "height": 16, + "iconFontName": "arrows-out-simple", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "4w-0l", + "name": "Editor Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "NuCqT", + "name": "Editor Left", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "bkLPb", + "name": "Info Bar", + "width": "fill_container", + "height": 45, + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "htutC", + "name": "infoLeft", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "K65hN", + "name": "breadcrumbType", + "fill": "$--muted-foreground", + "content": "Project", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "3Xf0o", + "name": "breadcrumbSep", + "fill": "$--muted-foreground", + "content": "›", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "eNhLA", + "name": "breadcrumbName", + "fill": "$--foreground", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "TLxAY", + "name": "dotSep", + "fill": "$--muted-foreground", + "content": "·", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "5hBHI", + "name": "modifiedTxt", + "fill": "$--accent-yellow", + "content": "M", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "xfmHb", + "name": "infoRight", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "tzPx4", + "name": "iconSearch", + "width": 16, + "height": 16, + "iconFontName": "magnifying-glass", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "z9LJI", + "name": "iconSearch", + "width": 16, + "height": 16, + "iconFontName": "git-branch", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "19AWC", + "name": "iconSearch", + "width": 16, + "height": 16, + "iconFontName": "cursor-text", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "8TamW", + "name": "iconAI", + "width": 16, + "height": 16, + "iconFontName": "sparkle", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "a6NOM", + "name": "iconMore", + "width": 16, + "height": 16, + "iconFontName": "dots-three", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "uoKNo", + "name": "Editor Content", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 16, + "padding": [ + 32, + 64 + ], + "children": [ + { + "type": "text", + "id": "bhmHx", + "name": "editorP1", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Personal knowledge and life management desktop app, built with Tauri v2 + React + TypeScript + CodeMirror 6. It reads a vault of markdown files with YAML frontmatter and presents them in a four-panel UI inspired by Bear Notes.", + "lineHeight": 1.6, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "pmflZ", + "name": "editorH2", + "fill": "$--foreground", + "content": "Architecture", + "lineHeight": 1.3, + "fontFamily": "Inter", + "fontSize": 24, + "fontWeight": "600" + }, + { + "type": "text", + "id": "YPHOR", + "name": "editorP2", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "The app uses a four-panel layout: Sidebar for navigation, NoteList for browsing, Editor for content, and Inspector for metadata. All data lives in markdown files with YAML frontmatter, git-versioned.", + "lineHeight": 1.6, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "8m_Ts", + "name": "Inspector", + "clip": true, + "width": 260, + "height": "fill_container", + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "left": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "gr1Z5", + "name": "Inspector Header", + "width": "fill_container", + "height": 45, + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "gap": 8, + "padding": [ + 0, + 12 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "608WF", + "name": "leftGroup", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "Bpugj", + "name": "propIcon", + "width": 16, + "height": 16, + "iconFontName": "sliders-horizontal", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "hxMyO", + "name": "inspTitle", + "fill": "$--muted-foreground", + "content": "Properties", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "icon_font", + "id": "_i79H", + "name": "sidebarIcon", + "width": 16, + "height": 16, + "iconFontName": "x", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "VgKF5", + "name": "Inspector Body", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 16, + "padding": 12, + "children": [ + { + "type": "frame", + "id": "JeUUj", + "name": "Properties Section", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "LEpT-", + "name": "addPropBtn", + "width": "fill_container", + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "padding": [ + 6, + 12 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "1nsVc", + "name": "addPropTxt", + "fill": "$--muted-foreground", + "content": "+ Add property", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "AUN9O", + "name": "propType", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "EnTXQ", + "name": "propTypeLbl", + "fill": "$--muted-foreground", + "content": "TYPE", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vS0Af", + "name": "propTypeVal", + "fill": "$--foreground", + "content": "Project", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "s0Vdq", + "name": "propStatus", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "UUhF3", + "name": "propStatusLbl", + "fill": "$--muted-foreground", + "content": "STATUS", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "UMqtv", + "name": "propStatusBadge", + "fill": "$--accent-green-light", + "cornerRadius": 16, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "transparent" + }, + "gap": 8, + "padding": [ + 1, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "P2h15", + "name": "Badge Text", + "fill": "$--accent-green", + "content": "ACTIVE", + "lineHeight": 1.33, + "textAlign": "center", + "textAlignVertical": "middle", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 1.2 + } + ] + } + ] + }, + { + "type": "frame", + "id": "Ye0W8", + "name": "propOwner", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ZvN9n", + "name": "propOwnerLbl", + "fill": "$--muted-foreground", + "content": "OWNER", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "p18lp", + "name": "propOwnerVal", + "fill": "$--foreground", + "content": "Luca", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "YgQHc", + "name": "Relationships Section", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "MK0WC", + "name": "relGroup1", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "_rh_c", + "name": "relGrp1Title", + "fill": "$--muted-foreground", + "content": "BELONGS TO", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "wQayu", + "name": "relLaputaBtn", + "width": "fill_container", + "fill": "$--accent-green-light", + "cornerRadius": 6, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "5BZc-", + "name": "laputaTxt", + "fill": "$--accent-green", + "content": "Laputa", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "bj3hA", + "name": "topicIcon", + "opacity": 0.5, + "width": 14, + "height": 14, + "iconFontName": "tag", + "iconFontFamily": "phosphor", + "fill": "$--accent-green" + } + ] + }, + { + "type": "frame", + "id": "9NFxD", + "name": "relLaputaBtn", + "width": "fill_container", + "fill": "$--accent-green-light", + "cornerRadius": 6, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "HoFKQ", + "name": "laputaTxt", + "fill": "$--accent-green", + "content": "Building", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "U_GEF", + "name": "topicIcon", + "opacity": 0.5, + "width": 14, + "height": 14, + "iconFontName": "tag", + "iconFontFamily": "phosphor", + "fill": "$--accent-green" + } + ] + }, + { + "type": "frame", + "id": "Hf4E8", + "name": "linkExisting", + "width": "fill_container", + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "yVLK0", + "name": "laputaTxt", + "fill": "$--muted-foreground", + "content": "+ Link existing", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "EShyo", + "name": "relGroup2", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "CPHxH", + "name": "relGrp2Title", + "fill": "$--muted-foreground", + "content": "RELATED TO", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "F4ygG", + "name": "relMigrationBtn", + "width": "fill_container", + "fill": "$--accent-red-light", + "cornerRadius": 6, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "5xPef", + "name": "migrationTxt", + "fill": "$--accent-red", + "content": "Shadcn Migration", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "tLBGZ", + "name": "expIcon", + "opacity": 0.5, + "width": 14, + "height": 14, + "iconFontName": "flask", + "iconFontFamily": "phosphor", + "fill": "$--accent-red" + } + ] + }, + { + "type": "frame", + "id": "3UaSi", + "name": "linkExisting", + "width": "fill_container", + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "suN12", + "name": "laputaTxt", + "fill": "$--muted-foreground", + "content": "+ Link existing", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "zL5_Y", + "name": "Backlinks Section", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "JbVdn", + "name": "blTitle", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ob0bb", + "name": "blTitleTxt", + "rotation": -0.5285936385085725, + "fill": "$--muted-foreground", + "content": "BACKLINKS", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "3ZsuW", + "name": "blItem1", + "fill": "$--primary", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "wkQVV", + "name": "blItem2", + "fill": "$--primary", + "content": "Meeting with Grant", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "6s0lo", + "name": "blItem3", + "fill": "$--primary", + "content": "Q1 Planning", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "r7vXk", + "name": "History Section", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "text", + "id": "iw_bo", + "name": "histTitle", + "fill": "$--muted-foreground", + "content": "HISTORY", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "xzvFM", + "name": "histItem1", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "left": 2 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 0, + 0, + 0, + 10 + ], + "children": [ + { + "type": "text", + "id": "Ut4ez", + "name": "histItem1Hash", + "fill": "$--accent-blue", + "content": "a089f44 · feat: migrate to shadcn/ui", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "svS7D", + "name": "histItem1Date", + "fill": "$--muted-foreground", + "content": "Feb 16, 2026", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "rwIrR", + "name": "histItem2", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "left": 2 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 0, + 0, + 0, + 10 + ], + "children": [ + { + "type": "text", + "id": "l6CYP", + "name": "histItem2Hash", + "fill": "$--accent-blue", + "content": "5a4b4ac · feat: emoji favicon", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "httIc", + "name": "histItem2Date", + "fill": "$--muted-foreground", + "content": "Feb 15, 2026", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "Lo-el", + "name": "lightStatusBar", + "width": "fill_container", + "height": 30, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "top": 1 + }, + "fill": "$--border" + }, + "padding": [ + 0, + 8 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "b51Tr", + "name": "Status Left", + "height": "fill_container", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "ojWSJ", + "name": "versionIcon", + "width": 14, + "height": 14, + "iconFontName": "box", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "qB18p", + "name": "versionText", + "fill": "$--muted-foreground", + "content": "v0.4.2", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "ykdyj", + "name": "sep1", + "fill": "$--border", + "content": "|", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "3B0P6", + "name": "branchIcon", + "width": 14, + "height": 14, + "iconFontName": "git-branch", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "HpD4X", + "name": "branchText", + "fill": "$--muted-foreground", + "content": "main", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "6RVGg", + "name": "sep2", + "fill": "$--border", + "content": "|", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "h9Ul_", + "name": "syncIcon", + "width": 13, + "height": 13, + "iconFontName": "refresh-cw", + "iconFontFamily": "lucide", + "fill": "$--accent-green" + }, + { + "type": "text", + "id": "yer4n", + "name": "syncText", + "fill": "$--muted-foreground", + "content": "Synced 2m ago", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Ggt4q", + "name": "Status Right", + "height": "fill_container", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "YowOM", + "name": "aiIcon", + "width": 13, + "height": 13, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "bVd9g", + "name": "aiText", + "fill": "$--muted-foreground", + "content": "Claude Sonnet 4", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "8QSq1", + "name": "sep3", + "fill": "$--border", + "content": "|", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "z-rFh", + "name": "notesCount", + "width": 13, + "height": 13, + "iconFontName": "file-text", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "U_qqx", + "name": "notesText", + "fill": "$--muted-foreground", + "content": "1,247 notes", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "MQE9Z", + "name": "sep4", + "fill": "$--border", + "content": "|", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "XB_ZN", + "name": "bellIcon", + "width": 13, + "height": 13, + "iconFontName": "bell", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "gmulL", + "name": "settingsIcon", + "width": 13, + "height": 13, + "iconFontName": "settings", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "QR-Au", + "x": 0, + "y": 1050, + "name": "Full Layout — Search Panel Active", + "theme": { + "Mode": "Light" + }, + "clip": true, + "width": 1440, + "height": 900, + "fill": "$--background", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "D2byp", + "name": "macOS Title Bar", + "width": "fill_container", + "height": 38, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "gap": 12, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "EDFgx", + "name": "trafficLights", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "DfcH7", + "name": "closeBtn", + "fill": "#FF5F57", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "k4jwL", + "name": "minimizeBtn", + "fill": "#FEBC2E", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "4la7Z", + "name": "maximizeBtn", + "fill": "#28C840", + "width": 12, + "height": 12 + } + ] + } + ] + }, + { + "type": "frame", + "id": "md1g3", + "name": "Content", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "A_kBk", + "name": "Panel: Sidebar", + "clip": true, + "width": 250, + "height": "fill_container", + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": 0, + "fill": "$--sidebar-border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "GLS8B", + "name": "Filters", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 1, + "padding": [ + 8, + 8, + 16, + 8 + ], + "children": [ + { + "type": "frame", + "id": "5KmsU", + "name": "filterAll", + "width": "fill_container", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "CO7qs", + "name": "leftAll", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "UqiN-", + "name": "iconAll", + "width": 18, + "height": 18, + "iconFontName": "tray-fill", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "GQxwa", + "name": "fAllTxt", + "fill": "$--foreground", + "content": "Untagged", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "OszI0", + "name": "countAll", + "height": 20, + "fill": "$--secondary", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "aws3Z", + "name": "badgeAllTxt", + "fill": "$--muted-foreground", + "content": "24", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dwASK", + "name": "filterUntagged", + "width": "fill_container", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "RFfYP", + "name": "leftUntagged", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "b4vff", + "name": "untaggedIcon", + "width": 18, + "height": 18, + "iconFontName": "cardholder", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--primary" + }, + { + "type": "text", + "id": "t3WAm", + "name": "untaggedTxt", + "fill": "$--primary", + "content": "All Notes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "fHpat", + "name": "countUntagged", + "height": 20, + "fill": "$--primary", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ZOPgU", + "name": "untaggedBadgeTxt", + "fill": "$--primary-foreground", + "content": "9247", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ], + "fill": "#4a9eff18" + }, + { + "type": "frame", + "id": "xfTev", + "name": "filterTrash", + "width": "fill_container", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Y4E4C", + "name": "leftTrash", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "kK01T", + "name": "trashIcon", + "width": 18, + "height": 18, + "iconFontName": "trash", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "-LXnc", + "name": "trashTxt", + "fill": "$--foreground", + "content": "Archive", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "nt-Xb", + "name": "countTrash", + "height": 20, + "fill": "$--secondary", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rRo0q", + "name": "trashBadgeTxt", + "fill": "$--muted-foreground", + "content": "2", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "F3pkw", + "name": "filterChanges", + "width": "fill_container", + "cornerRadius": 6, + "gap": 6, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "wcNVU", + "name": "leftChanges", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "xhrmm", + "name": "iconChanges", + "width": 18, + "height": 18, + "iconFontName": "git-branch", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "prgnw", + "name": "fChangesTxt", + "fill": "$--foreground", + "content": "Changes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "PugNW", + "name": "countChanges", + "height": 20, + "fill": "$--secondary", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "5bfMY", + "name": "badgeChangesTxt", + "fill": "$--muted-foreground", + "content": "3", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "JUzOE", + "name": "Sections", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "padding": [ + 8, + 0 + ], + "children": [ + { + "type": "frame", + "id": "OXkKG", + "name": "projGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6, + 8, + 6 + ], + "children": [ + { + "type": "frame", + "id": "G9YEB", + "name": "projHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "jL0bz", + "name": "leftProj", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "1LJDb", + "name": "projIcon", + "width": 18, + "height": 18, + "iconFontName": "wrench", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-red" + }, + { + "type": "text", + "id": "fsRhn", + "name": "projLabel", + "fill": "$--foreground", + "content": "Projects", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "EK_iu", + "name": "projChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "_MfW7", + "name": "projItem1", + "width": "fill_container", + "fill": "$--accent-red-light", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "buYPs", + "name": "proj1Txt", + "fill": "$--accent-red", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "TsI_D", + "name": "projItem2", + "width": "fill_container", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "_h7S1", + "name": "proj2Txt", + "fill": "$--muted-foreground", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "bNfnr", + "name": "projItem3", + "width": "fill_container", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "9SY8F", + "name": "proj3Txt", + "fill": "$--muted-foreground", + "content": "AI Habit Tracker", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "CjIEL", + "name": "respGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "U5NLf", + "name": "respHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "5yyfh", + "name": "leftResp", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "hjjpp", + "name": "respIcon", + "width": 18, + "height": 18, + "iconFontName": "medal", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "8paVp", + "name": "respLabel", + "fill": "$--foreground", + "content": "Responsibilities", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "YKDWL", + "name": "respChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "lmqzR", + "name": "procGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "UOvjm", + "name": "respHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "j6Mzi", + "name": "leftResp", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "hTccp", + "name": "respIcon", + "width": 18, + "height": 18, + "iconFontName": "arrows-clockwise", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "0o3Rd", + "name": "Procedures", + "fill": "$--foreground", + "content": "Procedures", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "CuU3s", + "name": "procChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Lbn-0", + "name": "peopleGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "k4VBq", + "name": "peopleHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Ilgp-", + "name": "leftPeople", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "AK0H9", + "name": "peopleIcon", + "width": 18, + "height": 18, + "iconFontName": "users", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-yellow" + }, + { + "type": "text", + "id": "s9plC", + "name": "peopleLbl", + "fill": "$--foreground", + "content": "People", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "Ry1Km", + "name": "peopleChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Xo_LE", + "name": "eventsGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "x-sYh", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "XG0mM", + "name": "leftEvents", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "xinWJ", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "calendar-blank", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-yellow" + }, + { + "type": "text", + "id": "5YiYH", + "name": "eventsLbl", + "fill": "$--foreground", + "content": "Events", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "4kX5T", + "name": "eventsChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "N2Kl0", + "name": "topicsGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "OC_Wz", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "_COf_", + "name": "leftEvents", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "5ddUS", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "tag", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-green" + }, + { + "type": "text", + "id": "mYK3y", + "name": "eventsLbl", + "fill": "$--foreground", + "content": "Topics", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "CUHPs", + "name": "topicsChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "c-AgB", + "name": "topicsGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "updPo", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "kIAbE", + "name": "leftEvents", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "JQy54", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "leaf", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-green" + }, + { + "type": "text", + "id": "Hh9wq", + "name": "eventsLbl", + "fill": "$--foreground", + "content": "Notes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "27zSn", + "name": "topicsChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "FQdQQ", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "FWkK6", + "name": "leftEvents", + "gap": 8, + "padding": [ + 8, + 0 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "V0gxx", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "AM8SJ", + "name": "eventsLbl", + "fill": "$--muted-foreground", + "content": "Create new type", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "a4iCv", + "name": "Commit Area", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "top": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "padding": 12, + "children": [ + { + "type": "frame", + "id": "c6wjQ", + "name": "commitBtn", + "width": "fill_container", + "fill": "$--primary", + "cornerRadius": 6, + "gap": 6, + "padding": [ + 8, + 16 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "8YmeU", + "name": "commitIcon", + "width": 14, + "height": 14, + "iconFontName": "git-commit-horizontal", + "iconFontFamily": "lucide", + "fill": "$--primary-foreground" + }, + { + "type": "text", + "id": "3miZQ", + "name": "commitTxt", + "fill": "$--primary-foreground", + "content": "Commit & Push", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "YXy6F", + "name": "commitBadge", + "height": 18, + "fill": "#ffffff40", + "cornerRadius": 9, + "padding": [ + 0, + 5 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "9q2NX", + "name": "commitBadgeTxt", + "fill": "$--white", + "content": "3", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "S5zsk", + "name": "Resize Handle", + "fill": "$--border", + "width": 1, + "height": "fill_container" + }, + { + "type": "frame", + "id": "RL1ew", + "name": "Panel: NoteList", + "clip": true, + "width": 300, + "height": "fill_container", + "fill": "$--card", + "stroke": { + "align": "inside", + "thickness": 0, + "fill": "$--border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "AVROe", + "name": "NoteList Header", + "width": "fill_container", + "height": 45, + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "padding": [ + 14, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "3Dlx5", + "name": "nlTitle", + "fill": "$--foreground", + "content": "All Notes", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "B_hDb", + "name": "nlActions", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "1o_2j", + "name": "searchIcon", + "width": 16, + "height": 16, + "iconFontName": "magnifying-glass", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "wI4DJ", + "name": "plusIcon", + "width": 16, + "height": 16, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Rqh8Y", + "name": "Note Items", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "GsD3O", + "name": "Backlinks Group", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "O3ith", + "name": "Note Item Selected", + "width": "fill_container", + "fill": "$--accent-red-light", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "#E9E9E7" + }, + "children": [ + { + "type": "rectangle", + "id": "LmFnl", + "name": "leftAccent", + "fill": "$--accent-red", + "width": 3, + "height": "fill_container" + }, + { + "type": "frame", + "id": "6jJVC", + "name": "noteSelContent", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "3eUuB", + "name": "noteSelRow", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "HwQTu", + "name": "titleGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "oh0Nx", + "name": "noteSelTitle", + "fill": "$--foreground", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "icon_font", + "id": "92t6N", + "name": "ico", + "width": 14, + "height": 14, + "iconFontName": "wrench", + "iconFontFamily": "lucide", + "fill": "$--accent-red" + } + ] + } + ] + }, + { + "type": "text", + "id": "iapFF", + "name": "noteSelSnip", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Personal knowledge and life management desktop app...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "dbFN3", + "name": "noteSelTime", + "fill": "$--accent-red", + "content": "2m ago", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "-5PW8", + "name": "Related Notes Group", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "IkgDl", + "name": "Related Notes Header", + "width": "fill_container", + "height": 32, + "fill": "$--muted", + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "el34m", + "name": "rnLeft", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ffLM0", + "name": "rnLabel", + "fill": "$--muted-foreground", + "content": "RELATED NOTES", + "fontFamily": "IBM Plex Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "A7jbI", + "name": "rnCount", + "fill": "$--muted-foreground", + "content": "2", + "fontFamily": "IBM Plex Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "6Cw-D", + "name": "rnRight", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "EkBrm", + "name": "rnFilter", + "width": 12, + "height": 12, + "iconFontName": "funnel", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "OiAPI", + "name": "rnPlus", + "width": 12, + "height": 12, + "iconFontName": "plus", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "IFfVO", + "name": "rnChev", + "width": 12, + "height": 12, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "XG9Tg", + "name": "Note Item", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "ibW95", + "name": "note2Row", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "_MlWz", + "name": "leafGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "MZJ5r", + "name": "note2Title", + "fill": "$--foreground", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "_qdsw", + "name": "leafIco", + "width": 14, + "height": 14, + "iconFontName": "leaf", + "iconFontFamily": "phosphor", + "fill": "$--accent-green" + } + ] + } + ] + }, + { + "type": "text", + "id": "2liVx", + "name": "note2Snip", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Redesigning portfolio site with modern stack and updated visual language...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "TCQuI", + "name": "note2Time", + "fill": "$--muted-foreground", + "content": "1h ago", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "43UHB", + "name": "Note Item", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "DSC7W", + "name": "note2Row", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "fjSDQ", + "name": "leafGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "wTu8C", + "name": "note2Title", + "fill": "$--foreground", + "content": "Sample note 2", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "1QPiQ", + "name": "leafIco", + "width": 14, + "height": 14, + "iconFontName": "leaf", + "iconFontFamily": "phosphor", + "fill": "$--accent-green" + } + ] + } + ] + }, + { + "type": "text", + "id": "43L7m", + "name": "note2Snip", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Lorem ipsum dolor sit amet consequetur with modern stack and updated language...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "RcKQy", + "name": "note2Time", + "fill": "$--muted-foreground", + "content": "1h ago", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "GnvhS", + "name": "Events Group", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "D_Dxu", + "name": "Events Header", + "width": "fill_container", + "height": 32, + "fill": "$--muted", + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "vDIey", + "name": "evLeft", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "q7YSK", + "name": "evLabel", + "fill": "$--muted-foreground", + "content": "EVENTS", + "fontFamily": "IBM Plex Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "HrCep", + "name": "evCount", + "fill": "$--muted-foreground", + "content": "2", + "fontFamily": "IBM Plex Sans", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "KC4JY", + "name": "evRight", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "8Bqzc", + "name": "evFilter", + "width": 12, + "height": 12, + "iconFontName": "funnel", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "Ncjba", + "name": "evPlus", + "width": 12, + "height": 12, + "iconFontName": "plus", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "wVeQy", + "name": "evChev", + "width": 12, + "height": 12, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "LLaDT", + "name": "Note Item 2", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "dk5T8", + "name": "note3Row", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "yq1MX", + "name": "calGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "azv9n", + "name": "note3Title", + "fill": "$--foreground", + "content": "Meeting with Grant", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "bzPbO", + "name": "calIco", + "width": 14, + "height": 14, + "iconFontName": "calendar", + "iconFontFamily": "lucide", + "fill": "$--accent-yellow" + } + ] + } + ] + }, + { + "type": "text", + "id": "W2zQX", + "name": "note3Snip", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Discussed Q1 goals and hiring priorities with the engineering team leads...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "yiflh", + "name": "note3Time", + "fill": "$--muted-foreground", + "content": "Yesterday", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "qpVBh", + "name": "Note Item 2", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "H2GoU", + "name": "note3Row", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "jUxXL", + "name": "calGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "zlSrF", + "name": "note3Title", + "fill": "$--foreground", + "content": "Meeting with Matteo", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "QFA7g", + "name": "calIco", + "width": 14, + "height": 14, + "iconFontName": "calendar", + "iconFontFamily": "lucide", + "fill": "$--accent-yellow" + } + ] + } + ] + }, + { + "type": "text", + "id": "S7tfE", + "name": "note3Snip", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Discussed Q1 goals and hiring priorities with the engineering team leads...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "84akW", + "name": "note3Time", + "fill": "$--muted-foreground", + "content": "Yesterday", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "kqcSS", + "name": "Resize Handle 2", + "fill": "$--border", + "width": 1, + "height": "fill_container" + }, + { + "type": "frame", + "id": "yI94h", + "name": "Panel: Editor", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$--background", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "IbaLP", + "name": "Tab Bar", + "width": "fill_container", + "height": 45, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "WFTLH", + "name": "Tab Active", + "height": "fill_container", + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "right": 1 + }, + "fill": "$--border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "5qYgu", + "name": "tab1Txt", + "fill": "$--foreground", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "TG1oa", + "name": "tab1Close", + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "s3CbP", + "name": "Tab Inactive", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "right": 1, + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LJqW3", + "name": "tab2Txt", + "fill": "$--muted-foreground", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "9u2Lp", + "name": "tab2Close", + "opacity": 0, + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "beMtf", + "name": "tabSpacer", + "width": "fill_container", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + } + }, + { + "type": "frame", + "id": "2jnhM", + "name": "tabControls", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1, + "left": 1 + }, + "fill": "$--border" + }, + "gap": 12, + "padding": [ + 0, + 12 + ], + "justifyContent": "space_around", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "DB5oM", + "name": "iconPlus", + "width": 16, + "height": 16, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "aD6Zp", + "name": "iconSplit", + "width": 16, + "height": 16, + "iconFontName": "columns", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "jOY8N", + "name": "iconExpand", + "width": 16, + "height": 16, + "iconFontName": "arrows-out-simple", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "RIloA", + "name": "Editor Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "xQh-S", + "name": "Search Scrim", + "x": 0, + "y": 0, + "width": "fill_container", + "height": "fill_container", + "fill": "#00000008" + }, + { + "type": "frame", + "id": "O1H0q", + "name": "Editor Left", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "yVgkY", + "name": "Info Bar", + "width": "fill_container", + "height": 45, + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "6R_ol", + "name": "infoLeft", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "hqrF6", + "name": "breadcrumbType", + "fill": "$--muted-foreground", + "content": "Project", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "wri-y", + "name": "breadcrumbSep", + "fill": "$--muted-foreground", + "content": "›", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "aJ9Ij", + "name": "breadcrumbName", + "fill": "$--foreground", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "PXDzs", + "name": "dotSep", + "fill": "$--muted-foreground", + "content": "·", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Y7ORU", + "name": "modifiedTxt", + "fill": "$--accent-yellow", + "content": "M", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "3mw5a", + "name": "infoRight", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "TLrxL", + "name": "iconSearch", + "width": 16, + "height": 16, + "iconFontName": "magnifying-glass", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "3qqRK", + "name": "iconSearch", + "width": 16, + "height": 16, + "iconFontName": "git-branch", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "k_wJK", + "name": "iconSearch", + "width": 16, + "height": 16, + "iconFontName": "cursor-text", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "l1J1b", + "name": "iconAI", + "width": 16, + "height": 16, + "iconFontName": "sparkle", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "OYxYL", + "name": "iconMore", + "width": 16, + "height": 16, + "iconFontName": "dots-three", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "L5ga1", + "name": "Editor Content", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 16, + "padding": [ + 32, + 64 + ], + "children": [ + { + "type": "text", + "id": "31dfV", + "name": "editorP1", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Personal knowledge and life management desktop app, built with Tauri v2 + React + TypeScript + CodeMirror 6. It reads a vault of markdown files with YAML frontmatter and presents them in a four-panel UI inspired by Bear Notes.", + "lineHeight": 1.6, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "hT17f", + "name": "editorH2", + "fill": "$--foreground", + "content": "Architecture", + "lineHeight": 1.3, + "fontFamily": "Inter", + "fontSize": 24, + "fontWeight": "600" + }, + { + "type": "text", + "id": "rhk-8", + "name": "editorP2", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "The app uses a four-panel layout: Sidebar for navigation, NoteList for browsing, Editor for content, and Inspector for metadata. All data lives in markdown files with YAML frontmatter, git-versioned.", + "lineHeight": 1.6, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "GmAF6", + "name": "Inspector", + "clip": true, + "width": 260, + "height": "fill_container", + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "left": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "x_zWw", + "name": "Inspector Header", + "width": "fill_container", + "height": 45, + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "gap": 8, + "padding": [ + 0, + 12 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "jusHS", + "name": "leftGroup", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "jAvXS", + "name": "propIcon", + "width": 16, + "height": 16, + "iconFontName": "sliders-horizontal", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "2DFD2", + "name": "inspTitle", + "fill": "$--muted-foreground", + "content": "Properties", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "icon_font", + "id": "h3_LP", + "name": "sidebarIcon", + "width": 16, + "height": 16, + "iconFontName": "x", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "fAW8G", + "name": "Inspector Body", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 16, + "padding": 12, + "children": [ + { + "type": "frame", + "id": "6kMFv", + "name": "Properties Section", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "o0GPt", + "name": "addPropBtn", + "width": "fill_container", + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "padding": [ + 6, + 12 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "nFYSs", + "name": "addPropTxt", + "fill": "$--muted-foreground", + "content": "+ Add property", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Lix0z", + "name": "propType", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ui6BQ", + "name": "propTypeLbl", + "fill": "$--muted-foreground", + "content": "TYPE", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "xvb_A", + "name": "propTypeVal", + "fill": "$--foreground", + "content": "Project", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "EoxR3", + "name": "propStatus", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mhPbZ", + "name": "propStatusLbl", + "fill": "$--muted-foreground", + "content": "STATUS", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "V8dLb", + "name": "propStatusBadge", + "fill": "$--accent-green-light", + "cornerRadius": 16, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "transparent" + }, + "gap": 8, + "padding": [ + 1, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "HklId", + "name": "Badge Text", + "fill": "$--accent-green", + "content": "ACTIVE", + "lineHeight": 1.33, + "textAlign": "center", + "textAlignVertical": "middle", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 1.2 + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "xWWeV", + "name": "Relationships Section", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "faCRJ", + "name": "relGroup1", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "e4hku", + "name": "relGrp1Title", + "fill": "$--muted-foreground", + "content": "BELONGS TO", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Ucmgk", + "name": "relLaputaBtn", + "width": "fill_container", + "fill": "$--accent-green-light", + "cornerRadius": 6, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "HP94T", + "name": "laputaTxt", + "fill": "$--accent-green", + "content": "Laputa", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "VCM1s", + "name": "topicIcon", + "opacity": 0.5, + "width": 14, + "height": 14, + "iconFontName": "tag", + "iconFontFamily": "phosphor", + "fill": "$--accent-green" + } + ] + }, + { + "type": "frame", + "id": "NKNrl", + "name": "relLaputaBtn", + "width": "fill_container", + "fill": "$--accent-green-light", + "cornerRadius": 6, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "7Fgue", + "name": "laputaTxt", + "fill": "$--accent-green", + "content": "Building", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "ot-0i", + "name": "topicIcon", + "opacity": 0.5, + "width": 14, + "height": 14, + "iconFontName": "tag", + "iconFontFamily": "phosphor", + "fill": "$--accent-green" + } + ] + }, + { + "type": "frame", + "id": "p85Ij", + "name": "linkExisting", + "width": "fill_container", + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "twL0t", + "name": "laputaTxt", + "fill": "$--muted-foreground", + "content": "+ Link existing", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "L799V", + "name": "relGroup2", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "hT1h0", + "name": "relGrp2Title", + "fill": "$--muted-foreground", + "content": "RELATED TO", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "D_WgI", + "name": "relMigrationBtn", + "width": "fill_container", + "fill": "$--accent-red-light", + "cornerRadius": 6, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ec81_", + "name": "migrationTxt", + "fill": "$--accent-red", + "content": "Shadcn Migration", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "S-vxq", + "name": "expIcon", + "opacity": 0.5, + "width": 14, + "height": 14, + "iconFontName": "flask", + "iconFontFamily": "phosphor", + "fill": "$--accent-red" + } + ] + }, + { + "type": "frame", + "id": "qiAMM", + "name": "linkExisting", + "width": "fill_container", + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ny0XL", + "name": "laputaTxt", + "fill": "$--muted-foreground", + "content": "+ Link existing", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "EW804", + "name": "Backlinks Section", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "iakOI", + "name": "blTitle", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mIh0h", + "name": "blTitleTxt", + "rotation": -0.5285936385085725, + "fill": "$--muted-foreground", + "content": "BACKLINKS", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "qRWB3", + "name": "blItem1", + "fill": "$--primary", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "W5Kbo", + "name": "blItem2", + "fill": "$--primary", + "content": "Meeting with Grant", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "74gGV", + "name": "blItem3", + "fill": "$--primary", + "content": "Q1 Planning", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "MGyob", + "name": "History Section", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "text", + "id": "yQzzw", + "name": "histTitle", + "fill": "$--muted-foreground", + "content": "HISTORY", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "D4Tgg", + "name": "histItem1", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "left": 2 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 0, + 0, + 0, + 10 + ], + "children": [ + { + "type": "text", + "id": "WGiuL", + "name": "histItem1Hash", + "fill": "$--accent-blue", + "content": "a089f44 · feat: migrate to shadcn/ui", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "it7Kq", + "name": "histItem1Date", + "fill": "$--muted-foreground", + "content": "Feb 16, 2026", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "q_JXB", + "name": "histItem2", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "left": 2 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 0, + 0, + 0, + 10 + ], + "children": [ + { + "type": "text", + "id": "FvrLg", + "name": "histItem2Hash", + "fill": "$--accent-blue", + "content": "5a4b4ac · feat: emoji favicon", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "kDCXM", + "name": "histItem2Date", + "fill": "$--muted-foreground", + "content": "Feb 15, 2026", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "iCeGZ", + "name": "Search Panel", + "x": 80, + "y": 60, + "width": 480, + "fill": "$--card", + "cornerRadius": 12, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "shadow": [ + { + "x": 0, + "y": 8, + "blur": 32, + "spread": -4, + "fill": "#00000020" + } + ], + "layout": "vertical", + "clip": true, + "children": [ + { + "type": "frame", + "id": "CF2zr", + "name": "searchInputRow", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "gap": 8, + "padding": [ + 14, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "O_fof", + "name": "searchIcon", + "width": 18, + "height": 18, + "iconFontName": "magnifying-glass", + "iconFontFamily": "phosphor", + "fill": "$--primary" + }, + { + "type": "text", + "id": "iLTe7", + "name": "searchQuery", + "fill": "$--foreground", + "content": "time management", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Ouy78", + "name": "searchClearBtn", + "width": 20, + "height": 20, + "fill": "$--muted", + "cornerRadius": 10, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "mI1q5", + "name": "clearX", + "width": 12, + "height": 12, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "fGvPp", + "name": "resultsHeader", + "width": "fill_container", + "padding": [ + 8, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "h0VR7", + "name": "resultsCount", + "fill": "$--muted-foreground", + "content": "3 results", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "8NV5p", + "name": "searchResult1", + "width": "fill_container", + "fill": "$--accent-blue-light", + "layout": "vertical", + "gap": 4, + "padding": [ + 10, + 16 + ], + "children": [ + { + "type": "frame", + "id": "nGwMG", + "name": "sr1Row", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "fselJ", + "name": "sr1Title", + "fill": "$--foreground", + "content": "Time Management Strategies", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "icon_font", + "id": "6BhB1", + "name": "sr1Icon", + "width": 14, + "height": 14, + "iconFontName": "leaf", + "iconFontFamily": "phosphor", + "fill": "$--accent-green" + } + ] + }, + { + "type": "text", + "id": "mrzGz", + "name": "sr1Snippet", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Effective time management requires clear priorities and consistent review cycles...", + "lineHeight": 1.4, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "uGf_u", + "name": "searchResult2", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [ + 10, + 16 + ], + "children": [ + { + "type": "frame", + "id": "M3Z_b", + "name": "sr2Row", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "_afkK", + "name": "sr2Title", + "fill": "$--foreground", + "content": "Weekly Review Process", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "f9OL0", + "name": "sr2Icon", + "width": 14, + "height": 14, + "iconFontName": "arrows-clockwise", + "iconFontFamily": "phosphor", + "fill": "$--accent-purple" + } + ] + }, + { + "type": "text", + "id": "Rn89e", + "name": "sr2Snippet", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Every Sunday: review tasks, update time blocks, plan the week ahead...", + "lineHeight": 1.4, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "D4qLW", + "name": "searchResult3", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [ + 10, + 16, + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "dXBPc", + "name": "sr3Row", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "7eWQ_", + "name": "sr3Title", + "fill": "$--foreground", + "content": "Q1 Planning", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "0KeJ1", + "name": "sr3Icon", + "width": 14, + "height": 14, + "iconFontName": "calendar-blank", + "iconFontFamily": "phosphor", + "fill": "$--accent-yellow" + } + ] + }, + { + "type": "text", + "id": "UcByL", + "name": "sr3Snippet", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Allocated time management focus blocks in Q1 for deep work sessions...", + "lineHeight": 1.4, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "IMqpw", + "name": "searchFooter", + "width": "fill_container", + "fill": "$--muted", + "stroke": { + "align": "inside", + "thickness": { + "top": 1 + }, + "fill": "$--border" + }, + "gap": 16, + "padding": [ + 8, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "6gOIh", + "name": "footerHint1", + "fill": "$--muted-foreground", + "content": "↑↓ Navigate", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "LHkSK", + "name": "footerHint2", + "fill": "$--muted-foreground", + "content": "↵ Open", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "qqfIt", + "name": "footerHint3", + "fill": "$--muted-foreground", + "content": "Esc Close", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "L0KqX", + "name": "lightStatusBar", + "width": "fill_container", + "height": 30, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "top": 1 + }, + "fill": "$--border" + }, + "padding": [ + 0, + 8 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Np9m-", + "name": "Status Left", + "height": "fill_container", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "V3HFi", + "name": "versionIcon", + "width": 14, + "height": 14, + "iconFontName": "box", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "YkNbh", + "name": "versionText", + "fill": "$--muted-foreground", + "content": "v0.4.2", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "JH56P", + "name": "sep1", + "fill": "$--border", + "content": "|", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "qL-f2", + "name": "branchIcon", + "width": 14, + "height": 14, + "iconFontName": "git-branch", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "yro7z", + "name": "branchText", + "fill": "$--muted-foreground", + "content": "main", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "CXGWw", + "name": "sep2", + "fill": "$--border", + "content": "|", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "xOHNm", + "name": "syncIcon", + "width": 13, + "height": 13, + "iconFontName": "refresh-cw", + "iconFontFamily": "lucide", + "fill": "$--accent-green" + }, + { + "type": "text", + "id": "j4lBc", + "name": "syncText", + "fill": "$--muted-foreground", + "content": "Synced 2m ago", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ihq0v", + "name": "Status Right", + "height": "fill_container", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "kz722", + "name": "aiIcon", + "width": 13, + "height": 13, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "qzFAM", + "name": "aiText", + "fill": "$--muted-foreground", + "content": "Claude Sonnet 4", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "stI-S", + "name": "sep3", + "fill": "$--border", + "content": "|", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "DXxdw", + "name": "notesCount", + "width": 13, + "height": 13, + "iconFontName": "file-text", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "OehL-", + "name": "notesText", + "fill": "$--muted-foreground", + "content": "1,247 notes", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ZNpGu", + "name": "sep4", + "fill": "$--border", + "content": "|", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "g-Zfe", + "name": "bellIcon", + "width": 13, + "height": 13, + "iconFontName": "bell", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "nUudo", + "name": "settingsIcon", + "width": 13, + "height": 13, + "iconFontName": "settings", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "6DhJh", + "x": 0, + "y": 2100, + "name": "Full Layout — Rich Properties + Autocomplete", + "theme": { + "Mode": "Light" + }, + "clip": true, + "width": 1440, + "height": 900, + "fill": "$--background", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "U52CB", + "name": "macOS Title Bar", + "width": "fill_container", + "height": 38, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "gap": 12, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "9xPhv", + "name": "trafficLights", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "xK0i1", + "name": "closeBtn", + "fill": "#FF5F57", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "W-zEa", + "name": "minimizeBtn", + "fill": "#FEBC2E", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "Lz9Ly", + "name": "maximizeBtn", + "fill": "#28C840", + "width": 12, + "height": 12 + } + ] + } + ] + }, + { + "type": "frame", + "id": "-eZ2Q", + "name": "Content", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "qWB0S", + "name": "Panel: Sidebar", + "clip": true, + "width": 250, + "height": "fill_container", + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": 0, + "fill": "$--sidebar-border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "MJJ3h", + "name": "Filters", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 1, + "padding": [ + 8, + 8, + 16, + 8 + ], + "children": [ + { + "type": "frame", + "id": "Apugb", + "name": "filterAll", + "width": "fill_container", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "MRMwA", + "name": "leftAll", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "bF9xl", + "name": "iconAll", + "width": 18, + "height": 18, + "iconFontName": "tray-fill", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "vDABp", + "name": "fAllTxt", + "fill": "$--foreground", + "content": "Untagged", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "uMV2b", + "name": "countAll", + "height": 20, + "fill": "$--secondary", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "5CjlO", + "name": "badgeAllTxt", + "fill": "$--muted-foreground", + "content": "24", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "nYVMy", + "name": "filterUntagged", + "width": "fill_container", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "y2a7H", + "name": "leftUntagged", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "GTbCN", + "name": "untaggedIcon", + "width": 18, + "height": 18, + "iconFontName": "cardholder", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "MyPei", + "name": "untaggedTxt", + "fill": "$--foreground", + "content": "All Notes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "Jq-_7", + "name": "countUntagged", + "height": 20, + "fill": "$--secondary", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "sq-ni", + "name": "untaggedBadgeTxt", + "fill": "$--muted-foreground", + "content": "6", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "OwWRW", + "name": "filterTrash", + "width": "fill_container", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "nEDEe", + "name": "leftTrash", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "q71pa", + "name": "trashIcon", + "width": 18, + "height": 18, + "iconFontName": "trash", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "ycx_R", + "name": "trashTxt", + "fill": "$--foreground", + "content": "Archive", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "q1o3f", + "name": "countTrash", + "height": 20, + "fill": "$--secondary", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "JDhNE", + "name": "trashBadgeTxt", + "fill": "$--muted-foreground", + "content": "2", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "1cNey", + "name": "filterChanges", + "width": "fill_container", + "cornerRadius": 6, + "gap": 6, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "3Zjt9", + "name": "leftChanges", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "HwG_w", + "name": "iconChanges", + "width": 18, + "height": 18, + "iconFontName": "git-branch", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "hsvfN", + "name": "fChangesTxt", + "fill": "$--foreground", + "content": "Changes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "3iQa3", + "name": "countChanges", + "height": 20, + "fill": "$--secondary", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "IpnSM", + "name": "badgeChangesTxt", + "fill": "$--muted-foreground", + "content": "3", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "4zVQn", + "name": "Sections", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "padding": [ + 8, + 0 + ], + "children": [ + { + "type": "frame", + "id": "2KL00", + "name": "projGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6, + 8, + 6 + ], + "children": [ + { + "type": "frame", + "id": "60D7x", + "name": "projHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Ns0Uw", + "name": "leftProj", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "TlBFF", + "name": "projIcon", + "width": 18, + "height": 18, + "iconFontName": "wrench", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-red" + }, + { + "type": "text", + "id": "DU0f7", + "name": "projLabel", + "fill": "$--foreground", + "content": "Projects", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "LhRc1", + "name": "projChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "XpysB", + "name": "projItem1", + "width": "fill_container", + "fill": "$--accent-red-light", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "EHkKO", + "name": "proj1Txt", + "fill": "$--accent-red", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "PY1Qz", + "name": "projItem2", + "width": "fill_container", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "lklgj", + "name": "proj2Txt", + "fill": "$--muted-foreground", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "uDbn3", + "name": "projItem3", + "width": "fill_container", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "4p5Tw", + "name": "proj3Txt", + "fill": "$--muted-foreground", + "content": "AI Habit Tracker", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "lTCT0", + "name": "respGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "lNMXl", + "name": "respHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Sifl6", + "name": "leftResp", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "Lzww_", + "name": "respIcon", + "width": 18, + "height": 18, + "iconFontName": "medal", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "MDzfV", + "name": "respLabel", + "fill": "$--foreground", + "content": "Responsibilities", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "nIT14", + "name": "respChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "jxdL-", + "name": "procGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "K6ZYE", + "name": "respHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "0yx1-", + "name": "leftResp", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "q8gWU", + "name": "respIcon", + "width": 18, + "height": 18, + "iconFontName": "arrows-clockwise", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "UFUbg", + "name": "Procedures", + "fill": "$--foreground", + "content": "Procedures", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "znhle", + "name": "procChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "sVqwB", + "name": "peopleGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "x5bql", + "name": "peopleHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "rT7Sw", + "name": "leftPeople", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "EdWKN", + "name": "peopleIcon", + "width": 18, + "height": 18, + "iconFontName": "users", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-yellow" + }, + { + "type": "text", + "id": "IeV1X", + "name": "peopleLbl", + "fill": "$--foreground", + "content": "People", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "ijXZt", + "name": "peopleChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "jJrjZ", + "name": "personItem1", + "width": "fill_container", + "fill": "$--accent-yellow-light", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "2qK41", + "name": "person1Txt", + "fill": "$--accent-yellow", + "content": "Grant", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "RhLex", + "name": "personItem2", + "width": "fill_container", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "o6wg_", + "name": "person2Txt", + "fill": "$--muted-foreground", + "content": "Matteo", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "_tP3q", + "name": "personItem3", + "width": "fill_container", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "tLGQc", + "name": "person3Txt", + "fill": "$--muted-foreground", + "content": "Sarah", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "HNWez", + "name": "eventsGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "4TSY6", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "esbbT", + "name": "leftEvents", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "6v9eG", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "calendar-blank", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-yellow" + }, + { + "type": "text", + "id": "Gbmba", + "name": "eventsLbl", + "fill": "$--foreground", + "content": "Events", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "4HZQC", + "name": "eventsChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "k8Qek", + "name": "topicsGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "ZJKIA", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "aOgWv", + "name": "leftEvents", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "4uMpd", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "tag", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-green" + }, + { + "type": "text", + "id": "55-Jj", + "name": "eventsLbl", + "fill": "$--foreground", + "content": "Topics", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "C5q2c", + "name": "topicsChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Dl7QT", + "name": "topicsGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "zssYh", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "XuI_M", + "name": "leftEvents", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "hKKJ2", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "leaf", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-green" + }, + { + "type": "text", + "id": "HJ6Sg", + "name": "eventsLbl", + "fill": "$--foreground", + "content": "Notes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "R9E3Y", + "name": "topicsChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "KyUdZ", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "7E72I", + "name": "leftEvents", + "gap": 8, + "padding": [ + 8, + 0 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "akfbn", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "NUGRe", + "name": "eventsLbl", + "fill": "$--muted-foreground", + "content": "Create new type", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "uAgKJ", + "name": "Commit Area", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "top": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "padding": 12, + "children": [ + { + "type": "frame", + "id": "0zA0z", + "name": "commitBtn", + "width": "fill_container", + "fill": "$--primary", + "cornerRadius": 6, + "gap": 6, + "padding": [ + 8, + 16 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "w07m8", + "name": "commitIcon", + "width": 14, + "height": 14, + "iconFontName": "git-commit-horizontal", + "iconFontFamily": "lucide", + "fill": "$--primary-foreground" + }, + { + "type": "text", + "id": "4D-Y_", + "name": "commitTxt", + "fill": "$--primary-foreground", + "content": "Commit & Push", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "vWH8M", + "name": "commitBadge", + "height": 18, + "fill": "#ffffff40", + "cornerRadius": 9, + "padding": [ + 0, + 5 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "969L9", + "name": "commitBadgeTxt", + "fill": "$--white", + "content": "3", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "UYNhO", + "name": "Resize Handle", + "fill": "$--border", + "width": 1, + "height": "fill_container" + }, + { + "type": "frame", + "id": "m93C0", + "name": "Panel: NoteList", + "clip": true, + "width": 300, + "height": "fill_container", + "fill": "$--card", + "stroke": { + "align": "inside", + "thickness": 0, + "fill": "$--border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "978jd", + "name": "NoteList Header", + "width": "fill_container", + "height": 45, + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "padding": [ + 14, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "hSxft", + "name": "nlTitle", + "fill": "$--foreground", + "content": "People", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "vOlfM", + "name": "nlActions", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "8oc-r", + "name": "searchIcon", + "width": 16, + "height": 16, + "iconFontName": "magnifying-glass", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "oF9bA", + "name": "plusIcon", + "width": 16, + "height": 16, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "e99_i", + "name": "Note Items", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "3lYpU", + "name": "Backlinks Group", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "lmika", + "name": "Note Item Selected", + "width": "fill_container", + "fill": "$--accent-yellow-light", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "#E9E9E7" + }, + "children": [ + { + "type": "rectangle", + "id": "B3XPh", + "name": "leftAccent", + "fill": "$--accent-yellow", + "width": 3, + "height": "fill_container" + }, + { + "type": "frame", + "id": "_6k8_", + "name": "noteSelContent", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "3aPer", + "name": "noteSelRow", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "CKtmP", + "name": "titleGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "4sDfy", + "name": "noteSelTitle", + "fill": "$--foreground", + "content": "Grant", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "icon_font", + "id": "C_5rR", + "name": "ico", + "width": 14, + "height": 14, + "iconFontName": "users", + "iconFontFamily": "lucide", + "fill": "$--accent-yellow" + } + ] + } + ] + }, + { + "type": "text", + "id": "2j16A", + "name": "noteSelSnip", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Engineering lead at Acme Corp, focused on infrastructure and platform...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "4aybL", + "name": "noteSelTime", + "fill": "$--accent-yellow", + "content": "3h ago", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "-2gWn", + "name": "Related Notes Group", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "ZDyjn", + "name": "Related Notes Header", + "width": "fill_container", + "height": 32, + "fill": "$--muted", + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "C3498", + "name": "rnLeft", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "L6f44", + "name": "rnLabel", + "fill": "$--muted-foreground", + "content": "RELATED NOTES", + "fontFamily": "IBM Plex Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "SP2mP", + "name": "rnCount", + "fill": "$--muted-foreground", + "content": "2", + "fontFamily": "IBM Plex Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "VUxRC", + "name": "rnRight", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "K0u69", + "name": "rnFilter", + "width": 12, + "height": 12, + "iconFontName": "funnel", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "5I6Q6", + "name": "rnPlus", + "width": 12, + "height": 12, + "iconFontName": "plus", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "nWc9j", + "name": "rnChev", + "width": 12, + "height": 12, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "0g78J", + "name": "Note Item", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "UYt-3", + "name": "note2Row", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "UHgKQ", + "name": "leafGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "I3Kzu", + "name": "note2Title", + "fill": "$--foreground", + "content": "Matteo", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "2SrYa", + "name": "leafIco", + "width": 14, + "height": 14, + "iconFontName": "users", + "iconFontFamily": "phosphor", + "fill": "$--accent-yellow" + } + ] + } + ] + }, + { + "type": "text", + "id": "u9IDo", + "name": "note2Snip", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Product manager working on growth initiatives and user research...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "bIgVf", + "name": "note2Time", + "fill": "$--muted-foreground", + "content": "1h ago", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ZCkrg", + "name": "Note Item", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "2HTTQ", + "name": "note2Row", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "lpW_m", + "name": "leafGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Yjt0e", + "name": "note2Title", + "fill": "$--foreground", + "content": "Sarah", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "n8Bdp", + "name": "leafIco", + "width": 14, + "height": 14, + "iconFontName": "users", + "iconFontFamily": "phosphor", + "fill": "$--accent-yellow" + } + ] + } + ] + }, + { + "type": "text", + "id": "sBpy_", + "name": "note2Snip", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Designer specializing in design systems and component libraries...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "SfEiI", + "name": "note2Time", + "fill": "$--muted-foreground", + "content": "1h ago", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "UjKyN", + "name": "Events Group", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "1ieBk", + "name": "Events Header", + "width": "fill_container", + "height": 32, + "fill": "$--muted", + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "jL0Cn", + "name": "evLeft", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "WPdoU", + "name": "evLabel", + "fill": "$--muted-foreground", + "content": "EVENTS", + "fontFamily": "IBM Plex Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "begIt", + "name": "evCount", + "fill": "$--muted-foreground", + "content": "2", + "fontFamily": "IBM Plex Sans", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "unj5n", + "name": "evRight", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "96nxy", + "name": "evFilter", + "width": 12, + "height": 12, + "iconFontName": "funnel", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "anHN8", + "name": "evPlus", + "width": 12, + "height": 12, + "iconFontName": "plus", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "51Def", + "name": "evChev", + "width": 12, + "height": 12, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "1Y6r5", + "name": "Note Item 2", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "NtQjv", + "name": "note3Row", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "AGbrd", + "name": "calGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "xla4h", + "name": "note3Title", + "fill": "$--foreground", + "content": "Meeting with Grant", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "_FbgU", + "name": "calIco", + "width": 14, + "height": 14, + "iconFontName": "calendar", + "iconFontFamily": "lucide", + "fill": "$--accent-yellow" + } + ] + } + ] + }, + { + "type": "text", + "id": "0VoCK", + "name": "note3Snip", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Discussed Q1 goals and hiring priorities with the engineering team leads...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "6Xbdy", + "name": "note3Time", + "fill": "$--muted-foreground", + "content": "Yesterday", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "KazQF", + "name": "Note Item 2", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "1_Cz1", + "name": "note3Row", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "s4GwN", + "name": "calGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "U3H8E", + "name": "note3Title", + "fill": "$--foreground", + "content": "Meeting with Matteo", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "gqOjx", + "name": "calIco", + "width": 14, + "height": 14, + "iconFontName": "calendar", + "iconFontFamily": "lucide", + "fill": "$--accent-yellow" + } + ] + } + ] + }, + { + "type": "text", + "id": "-o6br", + "name": "note3Snip", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Discussed Q1 goals and hiring priorities with the engineering team leads...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "N4dE4", + "name": "note3Time", + "fill": "$--muted-foreground", + "content": "Yesterday", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "hli5_", + "name": "Resize Handle 2", + "fill": "$--border", + "width": 1, + "height": "fill_container" + }, + { + "type": "frame", + "id": "OSNm1", + "name": "Panel: Editor", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$--background", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "0bMNp", + "name": "Tab Bar", + "width": "fill_container", + "height": 45, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "MIU7l", + "name": "Tab Active", + "height": "fill_container", + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "right": 1 + }, + "fill": "$--border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "08oJY", + "name": "tab1Txt", + "fill": "$--foreground", + "content": "Grant", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "Nb4TP", + "name": "tab1Close", + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "x41zt", + "name": "Tab Inactive", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "right": 1, + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "OeJUm", + "name": "tab2Txt", + "fill": "$--muted-foreground", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "j0rhL", + "name": "tab2Close", + "opacity": 0, + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "JSjRx", + "name": "tabSpacer", + "width": "fill_container", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + } + }, + { + "type": "frame", + "id": "K33DX", + "name": "tabControls", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1, + "left": 1 + }, + "fill": "$--border" + }, + "gap": 12, + "padding": [ + 0, + 12 + ], + "justifyContent": "space_around", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "cvEPw", + "name": "iconPlus", + "width": 16, + "height": 16, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "l33JZ", + "name": "iconSplit", + "width": 16, + "height": 16, + "iconFontName": "columns", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "CnxJJ", + "name": "iconExpand", + "width": 16, + "height": 16, + "iconFontName": "arrows-out-simple", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "2tTb3", + "name": "Editor Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "VvcZt", + "name": "Editor Left", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "FS1KF", + "name": "Info Bar", + "width": "fill_container", + "height": 45, + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "IRSz3", + "name": "infoLeft", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "CPWzB", + "name": "breadcrumbType", + "fill": "$--muted-foreground", + "content": "Person", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vGAbj", + "name": "breadcrumbSep", + "fill": "$--muted-foreground", + "content": "›", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "3UmsD", + "name": "breadcrumbName", + "fill": "$--foreground", + "content": "Grant", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "DN8so", + "name": "dotSep", + "fill": "$--muted-foreground", + "content": "·", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "kXgl0", + "name": "modifiedTxt", + "fill": "$--accent-yellow", + "content": "M", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "3wpHS", + "name": "infoRight", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "MDVCo", + "name": "iconSearch", + "width": 16, + "height": 16, + "iconFontName": "magnifying-glass", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "N_aAh", + "name": "iconSearch", + "width": 16, + "height": 16, + "iconFontName": "git-branch", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "3BBMe", + "name": "iconSearch", + "width": 16, + "height": 16, + "iconFontName": "cursor-text", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "tKvI2", + "name": "iconAI", + "width": 16, + "height": 16, + "iconFontName": "sparkle", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "D88o2", + "name": "iconMore", + "width": 16, + "height": 16, + "iconFontName": "dots-three", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "vlazI", + "name": "Editor Content", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 16, + "padding": [ + 32, + 64 + ], + "children": [ + { + "type": "text", + "id": "gabKY", + "name": "editorP1", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Grant is an engineering lead at Acme Corp, focused on infrastructure, Kubernetes, and platform reliability. He drives the technical roadmap for backend systems and mentors junior engineers.", + "lineHeight": 1.6, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "rZOkl", + "name": "editorH2", + "fill": "$--foreground", + "content": "Key Responsibilities", + "lineHeight": 1.3, + "fontFamily": "Inter", + "fontSize": 24, + "fontWeight": "600" + }, + { + "type": "text", + "id": "VqiRL", + "name": "editorP2", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Leads the infrastructure team. Oversees Kubernetes cluster management, CI/CD pipelines, and service mesh. Reports to CTO. Weekly 1:1 on Thursdays.", + "lineHeight": 1.6, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "B_y3t", + "name": "Inspector", + "clip": true, + "width": 260, + "height": "fill_container", + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "left": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "IY9fD", + "name": "Inspector Header", + "width": "fill_container", + "height": 45, + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "gap": 8, + "padding": [ + 0, + 12 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "w2rRY", + "name": "leftGroup", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "rCy6H", + "name": "propIcon", + "width": 16, + "height": 16, + "iconFontName": "sliders-horizontal", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "74Es2", + "name": "inspTitle", + "fill": "$--muted-foreground", + "content": "Properties", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "icon_font", + "id": "pRO4e", + "name": "sidebarIcon", + "width": 16, + "height": 16, + "iconFontName": "x", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "iSZtX", + "name": "Inspector Body", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 16, + "padding": 12, + "children": [ + { + "type": "frame", + "id": "FGspn", + "name": "Properties Section", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "9Cl0d", + "name": "addPropBtn", + "width": "fill_container", + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "padding": [ + 6, + 12 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "qqk-x", + "name": "addPropTxt", + "fill": "$--muted-foreground", + "content": "+ Add property", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "10Rpz", + "name": "propType", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "BCqzK", + "name": "propTypeLbl", + "fill": "$--muted-foreground", + "content": "TYPE", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "l7rGQ", + "name": "propTypeVal", + "fill": "$--foreground", + "content": "Person", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "X-DIV", + "name": "propCompany", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "_jYqg", + "name": "propCompanyLbl", + "fill": "$--muted-foreground", + "content": "COMPANY", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "hBEEy", + "name": "propCompanyVal", + "fill": "$--foreground", + "content": "Acme Corp", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "DplyA", + "name": "propRole", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Artv7", + "name": "propRoleLbl", + "fill": "$--muted-foreground", + "content": "ROLE", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ZtdR4", + "name": "propRoleVal", + "fill": "$--foreground", + "content": "Eng Lead", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "kr9yE", + "name": "propStatus", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "lCQ2c", + "name": "propStatusLbl", + "fill": "$--muted-foreground", + "content": "STATUS", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "UktKt", + "name": "propStatusBadge", + "fill": "$--accent-green-light", + "cornerRadius": 16, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "transparent" + }, + "gap": 8, + "padding": [ + 1, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "PZMuf", + "name": "Badge Text", + "fill": "$--accent-green", + "content": "ACTIVE", + "lineHeight": 1.33, + "textAlign": "center", + "textAlignVertical": "middle", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 1.2 + } + ] + } + ] + }, + { + "type": "frame", + "id": "uq-Ch", + "name": "propTags", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "9-GAi", + "name": "propTagsLbl", + "fill": "$--muted-foreground", + "content": "TAGS", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "um9TH", + "name": "tagsList", + "width": "fill_container", + "gap": 4, + "flexWrap": "wrap", + "children": [ + { + "type": "frame", + "id": "pAZPG", + "name": "tag1", + "fill": "$--accent-green-light", + "cornerRadius": 4, + "padding": [ + 2, + 6 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LxWxl", + "name": "tag1Txt", + "fill": "$--accent-green", + "content": "infrastructure", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "uu7oj", + "name": "tag2", + "fill": "$--accent-blue-light", + "cornerRadius": 4, + "padding": [ + 2, + 6 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "F3Ono", + "name": "tag2Txt", + "fill": "$--accent-blue", + "content": "kubernetes", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "500" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "oII4d", + "name": "Relationships Section", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "LxgzI", + "name": "relGroup1", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "M1hqt", + "name": "relGrp1Title", + "fill": "$--muted-foreground", + "content": "WORKS AT", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "1axQn", + "name": "relAcmeBtn", + "width": "fill_container", + "fill": "$--accent-red-light", + "cornerRadius": 6, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VKyg3", + "name": "acmeTxt", + "fill": "$--accent-red", + "content": "Acme Corp", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "GG63z", + "name": "companyIcon", + "opacity": 0.5, + "width": 14, + "height": 14, + "iconFontName": "wrench", + "iconFontFamily": "phosphor", + "fill": "$--accent-red" + } + ] + }, + { + "type": "frame", + "id": "kpHDy", + "name": "linkExistingActive", + "width": "fill_container", + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--primary" + }, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Q7UgS", + "name": "linkTxt", + "fill": "$--foreground", + "content": "Infra", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "RBm2-", + "name": "searchIco", + "width": 14, + "height": 14, + "iconFontName": "magnifying-glass", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "X_w5s", + "name": "autocompleteDropdown", + "width": "fill_container", + "fill": "$--card", + "cornerRadius": 8, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "shadow": [ + { + "x": 0, + "y": 4, + "blur": 16, + "spread": -2, + "fill": "#00000015" + } + ], + "layout": "vertical", + "clip": true, + "children": [ + { + "type": "frame", + "id": "3Pxkn", + "name": "acItem1", + "width": "fill_container", + "fill": "$--accent-blue-light", + "gap": 8, + "padding": [ + 8, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "BgVut", + "name": "acIcon1", + "width": 14, + "height": 14, + "iconFontName": "medal", + "iconFontFamily": "phosphor", + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "RPfuk", + "name": "acTxt1", + "fill": "$--foreground", + "content": "Infrastructure Team", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "sxoEV", + "name": "acItem2", + "width": "fill_container", + "gap": 8, + "padding": [ + 8, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "7ikv0", + "name": "acIcon2", + "width": 14, + "height": 14, + "iconFontName": "tag", + "iconFontFamily": "phosphor", + "fill": "$--accent-green" + }, + { + "type": "text", + "id": "gVVyY", + "name": "acTxt2", + "fill": "$--foreground", + "content": "Infrastructure", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "r85eA", + "name": "acItem3", + "width": "fill_container", + "gap": 8, + "padding": [ + 8, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "_BaES", + "name": "acIcon3", + "width": 14, + "height": 14, + "iconFontName": "wrench", + "iconFontFamily": "phosphor", + "fill": "$--accent-red" + }, + { + "type": "text", + "id": "x-wlM", + "name": "acTxt3", + "fill": "$--foreground", + "content": "Infra Migration", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "N5Sll", + "name": "Backlinks Section", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "imLWJ", + "name": "blTitle", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Y-UG1", + "name": "blTitleTxt", + "fill": "$--muted-foreground", + "content": "BACKLINKS", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "6K6Bd", + "name": "blItem1", + "fill": "$--primary", + "content": "Meeting with Grant", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "6EGhq", + "name": "blItem2", + "fill": "$--primary", + "content": "Q1 Planning", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "FCfBL", + "name": "blItem3", + "fill": "$--primary", + "content": "Hiring Pipeline", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "WAxYr", + "name": "lightStatusBar", + "width": "fill_container", + "height": 30, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "top": 1 + }, + "fill": "$--border" + }, + "padding": [ + 0, + 8 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "q_a9K", + "name": "Status Left", + "height": "fill_container", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "Mlziz", + "name": "versionIcon", + "width": 14, + "height": 14, + "iconFontName": "box", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "gzUtx", + "name": "versionText", + "fill": "$--muted-foreground", + "content": "v0.4.2", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "3x0kF", + "name": "sep1", + "fill": "$--border", + "content": "|", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "hY-Is", + "name": "branchIcon", + "width": 14, + "height": 14, + "iconFontName": "git-branch", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "k9Hay", + "name": "branchText", + "fill": "$--muted-foreground", + "content": "main", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "WKBM-", + "name": "sep2", + "fill": "$--border", + "content": "|", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "lhY_1", + "name": "syncIcon", + "width": 13, + "height": 13, + "iconFontName": "refresh-cw", + "iconFontFamily": "lucide", + "fill": "$--accent-green" + }, + { + "type": "text", + "id": "uELg8", + "name": "syncText", + "fill": "$--muted-foreground", + "content": "Synced 2m ago", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "c38hL", + "name": "Status Right", + "height": "fill_container", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "leRDx", + "name": "aiIcon", + "width": 13, + "height": 13, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "W6In1", + "name": "aiText", + "fill": "$--muted-foreground", + "content": "Claude Sonnet 4", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "rxqIt", + "name": "sep3", + "fill": "$--border", + "content": "|", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "B0jTr", + "name": "notesCount", + "width": 13, + "height": 13, + "iconFontName": "file-text", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "01ygw", + "name": "notesText", + "fill": "$--muted-foreground", + "content": "1,247 notes", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "BeA6T", + "name": "sep4", + "fill": "$--border", + "content": "|", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "yNSZ0", + "name": "bellIcon", + "width": 13, + "height": 13, + "iconFontName": "bell", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "RbzrU", + "name": "settingsIcon", + "width": 13, + "height": 13, + "iconFontName": "settings", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "cDqLP", + "x": 0, + "y": 3150, + "name": "Full Layout — Changes View", + "theme": { + "Mode": "Light" + }, + "clip": true, + "width": 1440, + "height": 900, + "fill": "$--background", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "2HK9s", + "name": "macOS Title Bar", + "width": "fill_container", + "height": 38, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "gap": 12, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "AiPEP", + "name": "trafficLights", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "X3X_h", + "name": "closeBtn", + "fill": "#FF5F57", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "P1J0D", + "name": "minimizeBtn", + "fill": "#FEBC2E", + "width": 12, + "height": 12 + }, + { + "type": "ellipse", + "id": "3zuNz", + "name": "maximizeBtn", + "fill": "#28C840", + "width": 12, + "height": 12 + } + ] + } + ] + }, + { + "type": "frame", + "id": "-ekXq", + "name": "Content", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "M2KC-", + "name": "Panel: Sidebar", + "clip": true, + "width": 250, + "height": "fill_container", + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": 0, + "fill": "$--sidebar-border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "ZulZE", + "name": "Filters", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 1, + "padding": [ + 8, + 8, + 16, + 8 + ], + "children": [ + { + "type": "frame", + "id": "IS4Fm", + "name": "filterAll", + "width": "fill_container", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "vPb76", + "name": "leftAll", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "yuOG7", + "name": "iconAll", + "width": 18, + "height": 18, + "iconFontName": "tray-fill", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "XsyTZ", + "name": "fAllTxt", + "fill": "$--foreground", + "content": "Untagged", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "XP413", + "name": "countAll", + "height": 20, + "fill": "$--secondary", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "jwZW-", + "name": "badgeAllTxt", + "fill": "$--muted-foreground", + "content": "24", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "nJQFj", + "name": "filterUntagged", + "width": "fill_container", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "rYzZ5", + "name": "leftUntagged", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "rOyw2", + "name": "untaggedIcon", + "width": 18, + "height": 18, + "iconFontName": "cardholder", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "y-u3E", + "name": "untaggedTxt", + "fill": "$--foreground", + "content": "All Notes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "kQQSy", + "name": "countUntagged", + "height": 20, + "fill": "$--secondary", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "-He5A", + "name": "untaggedBadgeTxt", + "fill": "$--muted-foreground", + "content": "6", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "z-rwa", + "name": "filterTrash", + "width": "fill_container", + "cornerRadius": 6, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "EZw96", + "name": "leftTrash", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "yFQYH", + "name": "trashIcon", + "width": 18, + "height": 18, + "iconFontName": "trash", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--foreground" + }, + { + "type": "text", + "id": "zgVFp", + "name": "trashTxt", + "fill": "$--foreground", + "content": "Archive", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "2ZVsp", + "name": "countTrash", + "height": 20, + "fill": "$--secondary", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "6KjzA", + "name": "trashBadgeTxt", + "fill": "$--muted-foreground", + "content": "2", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Jz98b", + "name": "filterChanges", + "width": "fill_container", + "cornerRadius": 6, + "gap": 6, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "hCAhH", + "name": "leftChanges", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "zut43", + "name": "iconChanges", + "width": 18, + "height": 18, + "iconFontName": "git-branch", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-orange" + }, + { + "type": "text", + "id": "eNZE_", + "name": "fChangesTxt", + "fill": "$--accent-orange", + "content": "Changes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "Vd1IP", + "name": "countChanges", + "height": 20, + "fill": "$--accent-orange", + "cornerRadius": 9999, + "padding": [ + 0, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "WQevI", + "name": "badgeChangesTxt", + "fill": "$--primary-foreground", + "content": "3", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ], + "fill": "#D9730D18" + } + ] + }, + { + "type": "frame", + "id": "iZGMX", + "name": "Sections", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "padding": [ + 8, + 0 + ], + "children": [ + { + "type": "frame", + "id": "K9kps", + "name": "projGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6, + 8, + 6 + ], + "children": [ + { + "type": "frame", + "id": "NqU-g", + "name": "projHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "l4Db7", + "name": "leftProj", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "UH57P", + "name": "projIcon", + "width": 18, + "height": 18, + "iconFontName": "wrench", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-red" + }, + { + "type": "text", + "id": "qhWRv", + "name": "projLabel", + "fill": "$--foreground", + "content": "Projects", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "2z0Fc", + "name": "projChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "hZaR_", + "name": "projItem1", + "width": "fill_container", + "fill": "$--accent-red-light", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "KMKlm", + "name": "proj1Txt", + "fill": "$--accent-red", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "4auYC", + "name": "projItem2", + "width": "fill_container", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "-h_S-", + "name": "proj2Txt", + "fill": "$--muted-foreground", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Xby4k", + "name": "projItem3", + "width": "fill_container", + "cornerRadius": 6, + "padding": [ + 6, + 16, + 6, + 28 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "zflkU", + "name": "proj3Txt", + "fill": "$--muted-foreground", + "content": "AI Habit Tracker", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "yOvdh", + "name": "respGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "9NVPP", + "name": "respHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "KLgB7", + "name": "leftResp", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "_RJw9", + "name": "respIcon", + "width": 18, + "height": 18, + "iconFontName": "medal", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "P2TLS", + "name": "respLabel", + "fill": "$--foreground", + "content": "Responsibilities", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "FMkvu", + "name": "respChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "jdhEx", + "name": "procGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "yNxrB", + "name": "respHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ZbG_K", + "name": "leftResp", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "Lrz9B", + "name": "respIcon", + "width": 18, + "height": 18, + "iconFontName": "arrows-clockwise", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "JYP7R", + "name": "Procedures", + "fill": "$--foreground", + "content": "Procedures", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "cuKa1", + "name": "procChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "9ZtV7", + "name": "peopleGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "bqUk7", + "name": "peopleHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "GDmNS", + "name": "leftPeople", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "4N-jT", + "name": "peopleIcon", + "width": 18, + "height": 18, + "iconFontName": "users", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-yellow" + }, + { + "type": "text", + "id": "eYxow", + "name": "peopleLbl", + "fill": "$--foreground", + "content": "People", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "zxTf-", + "name": "peopleChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "oO2nv", + "name": "eventsGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "01fhT", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "BpjDl", + "name": "leftEvents", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "WNSv4", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "calendar-blank", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-yellow" + }, + { + "type": "text", + "id": "Z-BdI", + "name": "eventsLbl", + "fill": "$--foreground", + "content": "Events", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "VtYYq", + "name": "eventsChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "QB60u", + "name": "topicsGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "dr3Dy", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "gip-z", + "name": "leftEvents", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "YWJHr", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "tag", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-green" + }, + { + "type": "text", + "id": "M4fMh", + "name": "eventsLbl", + "fill": "$--foreground", + "content": "Topics", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "be_vU", + "name": "topicsChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "jtFdo", + "name": "topicsGroup", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": { + "type": "color", + "color": "$--border", + "enabled": false + } + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 4, + 6 + ], + "children": [ + { + "type": "frame", + "id": "t051Q", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "CSC3E", + "name": "leftEvents", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "wt2dG", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "leaf", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--accent-green" + }, + { + "type": "text", + "id": "qqxUQ", + "name": "eventsLbl", + "fill": "$--foreground", + "content": "Notes", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "OyEhu", + "name": "topicsChev", + "width": 14, + "height": 14, + "iconFontName": "chevron-right", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "b0y4m", + "name": "eventsHeader", + "width": "fill_container", + "cornerRadius": 4, + "gap": 8, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "XKefw", + "name": "leftEvents", + "gap": 8, + "padding": [ + 8, + 0 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "5MMXe", + "name": "eventsIcon", + "width": 18, + "height": 18, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "weight": 700, + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "yJWsr", + "name": "eventsLbl", + "fill": "$--muted-foreground", + "content": "Create new type", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "wTdWJ", + "name": "Commit Area", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "top": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "padding": 12, + "children": [ + { + "type": "frame", + "id": "alvR1", + "name": "commitBtn", + "width": "fill_container", + "fill": "$--primary", + "cornerRadius": 6, + "gap": 6, + "padding": [ + 8, + 16 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "OZpvZ", + "name": "commitIcon", + "width": 14, + "height": 14, + "iconFontName": "git-commit-horizontal", + "iconFontFamily": "lucide", + "fill": "$--primary-foreground" + }, + { + "type": "text", + "id": "vye-W", + "name": "commitTxt", + "fill": "$--primary-foreground", + "content": "Commit & Push", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "M_lH9", + "name": "commitBadge", + "height": 18, + "fill": "#ffffff40", + "cornerRadius": 9, + "padding": [ + 0, + 5 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kTQAa", + "name": "commitBadgeTxt", + "fill": "$--white", + "content": "3", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "aBuSJ", + "name": "Resize Handle", + "fill": "$--border", + "width": 1, + "height": "fill_container" + }, + { + "type": "frame", + "id": "5CXKO", + "name": "Panel: NoteList", + "clip": true, + "width": 300, + "height": "fill_container", + "fill": "$--card", + "stroke": { + "align": "inside", + "thickness": 0, + "fill": "$--border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "UhLWn", + "name": "NoteList Header", + "width": "fill_container", + "height": 45, + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "padding": [ + 14, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "jyxqb", + "name": "nlTitle", + "fill": "$--foreground", + "content": "Changes", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "0eL1W", + "name": "nlActions", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "XsWjG", + "name": "searchIcon", + "width": 16, + "height": 16, + "iconFontName": "magnifying-glass", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "bBObW", + "name": "plusIcon", + "width": 16, + "height": 16, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "TK7b7", + "name": "Note Items", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "dri89", + "name": "Backlinks Group", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "9mLoL", + "name": "Note Item Selected", + "width": "fill_container", + "fill": "$--accent-yellow-light", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "#E9E9E7" + }, + "children": [ + { + "type": "rectangle", + "id": "lmpZx", + "name": "leftAccent", + "fill": "$--accent-yellow", + "width": 3, + "height": "fill_container" + }, + { + "type": "frame", + "id": "CkBEU", + "name": "noteSelContent", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "0KzHG", + "name": "noteSelRow", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ScgZY", + "name": "titleGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "3mYfY", + "name": "noteSelTitle", + "fill": "$--foreground", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "jqZDh", + "name": "modBadge", + "width": 18, + "height": 18, + "fill": "$--accent-yellow-light", + "cornerRadius": 4, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "v8EXM", + "name": "modBadgeTxt", + "fill": "$--accent-yellow", + "content": "M", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "700" + } + ] + } + ] + } + ] + }, + { + "type": "text", + "id": "mCaj6", + "name": "noteSelSnip", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Personal knowledge and life management desktop app...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "mi4c4", + "name": "noteSelTime", + "fill": "$--accent-yellow", + "content": "Modified", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "MYwXd", + "name": "Related Notes Group", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "fKL4E", + "name": "Related Notes Header", + "width": "fill_container", + "height": 32, + "fill": "$--muted", + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "eCRui", + "name": "rnLeft", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "UEtOs", + "name": "rnLabel", + "fill": "$--muted-foreground", + "content": "MODIFIED", + "fontFamily": "IBM Plex Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "geohb", + "name": "rnCount", + "fill": "$--muted-foreground", + "content": "2", + "fontFamily": "IBM Plex Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "vW3Ux", + "name": "rnRight", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "8-7YX", + "name": "rnFilter", + "width": 12, + "height": 12, + "iconFontName": "funnel", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "rJ2Qt", + "name": "rnPlus", + "width": 12, + "height": 12, + "iconFontName": "plus", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "Agecx", + "name": "rnChev", + "width": 12, + "height": 12, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Ef8xK", + "name": "Note Item", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "ikmO0", + "name": "note2Row", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "DgJzU", + "name": "leafGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "0SmlX", + "name": "note2Title", + "fill": "$--foreground", + "content": "Weekly Review Process", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "Ipmvo", + "name": "leafIco", + "width": 14, + "height": 14, + "iconFontName": "pencil-simple", + "iconFontFamily": "phosphor", + "fill": "$--accent-yellow" + } + ] + } + ] + }, + { + "type": "text", + "id": "6-aLC", + "name": "note2Snip", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Every Sunday: review tasks, update time blocks, plan the week ahead...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "35sd_", + "name": "note2Time", + "fill": "$--muted-foreground", + "content": "Modified", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "D1PDV", + "name": "Note Item", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "m3Yy9", + "name": "note2Row", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "G7YLm", + "name": "leafGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VeAEm", + "name": "note2Title", + "fill": "$--foreground", + "content": "Q1 Planning", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "rXUE_", + "name": "leafIco", + "width": 14, + "height": 14, + "iconFontName": "pencil-simple", + "iconFontFamily": "phosphor", + "fill": "$--accent-yellow" + } + ] + } + ] + }, + { + "type": "text", + "id": "1G-GA", + "name": "note2Snip", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Allocated focus blocks in Q1 for deep work sessions on infrastructure...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "p_rqw", + "name": "note2Time", + "fill": "$--muted-foreground", + "content": "Modified", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "3o8WQ", + "name": "Events Group", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "G7UWp", + "name": "Events Header", + "width": "fill_container", + "height": 32, + "fill": "$--muted", + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "msMtG", + "name": "evLeft", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "SGt7e", + "name": "evLabel", + "fill": "$--muted-foreground", + "content": "MODIFIED", + "fontFamily": "IBM Plex Mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "1PVP7", + "name": "evCount", + "fill": "$--muted-foreground", + "content": "2", + "fontFamily": "IBM Plex Sans", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "FCzPr", + "name": "evRight", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "UYR0D", + "name": "evFilter", + "width": 12, + "height": 12, + "iconFontName": "funnel", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "4jbCj", + "name": "evPlus", + "width": 12, + "height": 12, + "iconFontName": "plus", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "WRN9r", + "name": "evChev", + "width": 12, + "height": 12, + "iconFontName": "chevron-down", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "RiDwJ", + "name": "Note Item 2", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "Rvp4A", + "name": "note3Row", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "bnvDp", + "name": "calGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "QjRDJ", + "name": "note3Title", + "fill": "$--foreground", + "content": "Time Management Strategies", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "LeQCT", + "name": "calIco", + "width": 14, + "height": 14, + "iconFontName": "pencil-simple", + "iconFontFamily": "phosphor", + "fill": "$--accent-yellow" + } + ] + } + ] + }, + { + "type": "text", + "id": "vwDsc", + "name": "note3Snip", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Effective time management requires clear priorities and consistent review...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "OPKF_", + "name": "note3Time", + "fill": "$--muted-foreground", + "content": "Modified", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "PsBeq", + "name": "Note Item 2", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 4, + "padding": [ + 14, + 16 + ], + "children": [ + { + "type": "frame", + "id": "sPOHy", + "name": "note3Row", + "width": "fill_container", + "gap": 8, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "1Ecj7", + "name": "calGroup", + "width": "fill_container", + "gap": 6, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "DlZRH", + "name": "note3Title", + "fill": "$--foreground", + "content": "Time Management Strategies", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "yi-xp", + "name": "calIco", + "width": 14, + "height": 14, + "iconFontName": "pencil-simple", + "iconFontFamily": "phosphor", + "fill": "$--accent-yellow" + } + ] + } + ] + }, + { + "type": "text", + "id": "CJPuv", + "name": "note3Snip", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Effective time management requires clear priorities and consistent review...", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "6MtFY", + "name": "note3Time", + "fill": "$--muted-foreground", + "content": "Modified", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "0kPjR", + "name": "Resize Handle 2", + "fill": "$--border", + "width": 1, + "height": "fill_container" + }, + { + "type": "frame", + "id": "XRb-G", + "name": "Panel: Editor", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$--background", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "GMRdK", + "name": "Tab Bar", + "width": "fill_container", + "height": 45, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "iXash", + "name": "Tab Active", + "height": "fill_container", + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "right": 1 + }, + "fill": "$--border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "eo9dK", + "name": "tab1Txt", + "fill": "$--foreground", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "6MLT7", + "name": "tab1Close", + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "0UmLQ", + "name": "Tab Inactive", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "right": 1, + "bottom": 1 + }, + "fill": "$--sidebar-border" + }, + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "1JK8S", + "name": "tab2Txt", + "fill": "$--muted-foreground", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "k2PZa", + "name": "tab2Close", + "opacity": 0, + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "bM7NZ", + "name": "tabSpacer", + "width": "fill_container", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + } + }, + { + "type": "frame", + "id": "G6gUe", + "name": "tabControls", + "height": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1, + "left": 1 + }, + "fill": "$--border" + }, + "gap": 12, + "padding": [ + 0, + 12 + ], + "justifyContent": "space_around", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "oVC29", + "name": "iconPlus", + "width": 16, + "height": 16, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "XwYxq", + "name": "iconSplit", + "width": 16, + "height": 16, + "iconFontName": "columns", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "8z4xd", + "name": "iconExpand", + "width": 16, + "height": 16, + "iconFontName": "arrows-out-simple", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ZIoKq", + "name": "Editor Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "tJCEQ", + "name": "Editor Left", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "L9_bR", + "name": "Info Bar", + "width": "fill_container", + "height": 45, + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "padding": [ + 6, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "7YAl6", + "name": "infoLeft", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "8RsE6", + "name": "breadcrumbType", + "fill": "$--muted-foreground", + "content": "Project", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "aPIWW", + "name": "breadcrumbSep", + "fill": "$--muted-foreground", + "content": "›", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Hko61", + "name": "breadcrumbName", + "fill": "$--foreground", + "content": "Laputa App", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "UgLju", + "name": "dotSep", + "fill": "$--muted-foreground", + "content": "·", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "atZy3", + "name": "modifiedTxt", + "fill": "$--accent-yellow", + "content": "M", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "i_O9Q", + "name": "infoRight", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "mATmR", + "name": "iconSearch", + "width": 16, + "height": 16, + "iconFontName": "magnifying-glass", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "7-BlR", + "name": "iconSearch", + "width": 16, + "height": 16, + "iconFontName": "git-branch", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "hggOM", + "name": "iconSearch", + "width": 16, + "height": 16, + "iconFontName": "cursor-text", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "lKS8-", + "name": "iconAI", + "width": 16, + "height": 16, + "iconFontName": "sparkle", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "etlhz", + "name": "iconMore", + "width": 16, + "height": 16, + "iconFontName": "dots-three", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "n-cpK", + "name": "Editor Content", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 16, + "padding": [ + 32, + 64 + ], + "children": [ + { + "type": "text", + "id": "jnGoo", + "name": "editorP1", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Personal knowledge and life management desktop app, built with Tauri v2 + React + TypeScript + CodeMirror 6. It reads a vault of markdown files with YAML frontmatter and presents them in a four-panel UI inspired by Bear Notes.", + "lineHeight": 1.6, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "fwFRq", + "name": "editorH2", + "fill": "$--foreground", + "content": "Architecture", + "lineHeight": 1.3, + "fontFamily": "Inter", + "fontSize": 24, + "fontWeight": "600" + }, + { + "type": "text", + "id": "qK0e0", + "name": "editorP2", + "fill": "$--foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "The app uses a four-panel layout: Sidebar for navigation, NoteList for browsing, Editor for content, and Inspector for metadata. All data lives in markdown files with YAML frontmatter, git-versioned.", + "lineHeight": 1.6, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "RkIhR", + "name": "Inspector", + "clip": true, + "width": 260, + "height": "fill_container", + "fill": "$--background", + "stroke": { + "align": "inside", + "thickness": { + "left": 1 + }, + "fill": "$--border" + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "0CJaZ", + "name": "Inspector Header", + "width": "fill_container", + "height": 45, + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "gap": 8, + "padding": [ + 0, + 12 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "TPquS", + "name": "leftGroup", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "AVtUy", + "name": "propIcon", + "width": 16, + "height": 16, + "iconFontName": "sliders-horizontal", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "Lgi69", + "name": "inspTitle", + "fill": "$--muted-foreground", + "content": "Properties", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "icon_font", + "id": "uWuxn", + "name": "sidebarIcon", + "width": 16, + "height": 16, + "iconFontName": "x", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "u6jJ2", + "name": "Inspector Body", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 16, + "padding": 12, + "children": [ + { + "type": "frame", + "id": "MRZfg", + "name": "Properties Section", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "qC-P7", + "name": "addPropBtn", + "width": "fill_container", + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "padding": [ + 6, + 12 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "HBg9i", + "name": "addPropTxt", + "fill": "$--muted-foreground", + "content": "+ Add property", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "oHFVh", + "name": "propType", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Mk9GH", + "name": "propTypeLbl", + "fill": "$--muted-foreground", + "content": "TYPE", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "NUS5u", + "name": "propTypeVal", + "fill": "$--foreground", + "content": "Project", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "EAnqI", + "name": "propStatus", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YZlCv", + "name": "propStatusLbl", + "fill": "$--muted-foreground", + "content": "STATUS", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "khF0Y", + "name": "propStatusBadge", + "fill": "$--accent-green-light", + "cornerRadius": 16, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "transparent" + }, + "gap": 8, + "padding": [ + 1, + 6 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "H3X2o", + "name": "Badge Text", + "fill": "$--accent-green", + "content": "ACTIVE", + "lineHeight": 1.33, + "textAlign": "center", + "textAlignVertical": "middle", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 1.2 + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "F0I-7", + "name": "Relationships Section", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "10XXp", + "name": "relGroup1", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "qh7dI", + "name": "relGrp1Title", + "fill": "$--muted-foreground", + "content": "BELONGS TO", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ovNo_", + "name": "relLaputaBtn", + "width": "fill_container", + "fill": "$--accent-green-light", + "cornerRadius": 6, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "u6cth", + "name": "laputaTxt", + "fill": "$--accent-green", + "content": "Laputa", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "911p5", + "name": "topicIcon", + "opacity": 0.5, + "width": 14, + "height": 14, + "iconFontName": "tag", + "iconFontFamily": "phosphor", + "fill": "$--accent-green" + } + ] + }, + { + "type": "frame", + "id": "TwrqD", + "name": "relLaputaBtn", + "width": "fill_container", + "fill": "$--accent-green-light", + "cornerRadius": 6, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "R02qX", + "name": "laputaTxt", + "fill": "$--accent-green", + "content": "Building", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "URmQn", + "name": "topicIcon", + "opacity": 0.5, + "width": 14, + "height": 14, + "iconFontName": "tag", + "iconFontFamily": "phosphor", + "fill": "$--accent-green" + } + ] + }, + { + "type": "frame", + "id": "y-RCD", + "name": "linkExisting", + "width": "fill_container", + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Sohlo", + "name": "laputaTxt", + "fill": "$--muted-foreground", + "content": "+ Link existing", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "NON3K", + "name": "relGroup2", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "PRmP1", + "name": "relGrp2Title", + "fill": "$--muted-foreground", + "content": "RELATED TO", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ZORAR", + "name": "relMigrationBtn", + "width": "fill_container", + "fill": "$--accent-red-light", + "cornerRadius": 6, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "zBinS", + "name": "migrationTxt", + "fill": "$--accent-red", + "content": "Shadcn Migration", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "fmvl7", + "name": "expIcon", + "opacity": 0.5, + "width": 14, + "height": 14, + "iconFontName": "flask", + "iconFontFamily": "phosphor", + "fill": "$--accent-red" + } + ] + }, + { + "type": "frame", + "id": "rk_6m", + "name": "linkExisting", + "width": "fill_container", + "cornerRadius": 6, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "padding": [ + 6, + 10 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "0BiPL", + "name": "laputaTxt", + "fill": "$--muted-foreground", + "content": "+ Link existing", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "4CbGG", + "name": "Backlinks Section", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "bYwPw", + "name": "blTitle", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "FXmIm", + "name": "blTitleTxt", + "rotation": -0.5285936385085725, + "fill": "$--muted-foreground", + "content": "BACKLINKS", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "QkxSV", + "name": "blItem1", + "fill": "$--primary", + "content": "Portfolio Rewrite", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "5GEwY", + "name": "blItem2", + "fill": "$--primary", + "content": "Meeting with Grant", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "JjS06", + "name": "blItem3", + "fill": "$--primary", + "content": "Q1 Planning", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "-AVRp", + "name": "History Section", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "text", + "id": "s2sCa", + "name": "histTitle", + "fill": "$--muted-foreground", + "content": "HISTORY", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Wh7sL", + "name": "histItem1", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "left": 2 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 0, + 0, + 0, + 10 + ], + "children": [ + { + "type": "text", + "id": "WJkad", + "name": "histItem1Hash", + "fill": "$--accent-blue", + "content": "a089f44 · feat: migrate to shadcn/ui", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "tdi_I", + "name": "histItem1Date", + "fill": "$--muted-foreground", + "content": "Feb 16, 2026", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "LM_vp", + "name": "histItem2", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "left": 2 + }, + "fill": "$--border" + }, + "layout": "vertical", + "gap": 2, + "padding": [ + 0, + 0, + 0, + 10 + ], + "children": [ + { + "type": "text", + "id": "FAnJW", + "name": "histItem2Hash", + "fill": "$--accent-blue", + "content": "5a4b4ac · feat: emoji favicon", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "9bpmf", + "name": "histItem2Date", + "fill": "$--muted-foreground", + "content": "Feb 15, 2026", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "gH1Q6", + "name": "lightStatusBar", + "width": "fill_container", + "height": 30, + "fill": "$--sidebar", + "stroke": { + "align": "inside", + "thickness": { + "top": 1 + }, + "fill": "$--border" + }, + "padding": [ + 0, + 8 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "8q9j3", + "name": "Status Left", + "height": "fill_container", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "HoLpj", + "name": "versionIcon", + "width": 14, + "height": 14, + "iconFontName": "box", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "k-eWA", + "name": "versionText", + "fill": "$--muted-foreground", + "content": "v0.4.2", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "Y-wAn", + "name": "sep1", + "fill": "$--border", + "content": "|", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "gtv6Y", + "name": "branchIcon", + "width": 14, + "height": 14, + "iconFontName": "git-branch", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "xnDxG", + "name": "branchText", + "fill": "$--muted-foreground", + "content": "main", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "qpDn7", + "name": "sep2", + "fill": "$--border", + "content": "|", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "NUI5N", + "name": "syncIcon", + "width": 13, + "height": 13, + "iconFontName": "pencil-simple", + "iconFontFamily": "phosphor", + "fill": "$--accent-yellow" + }, + { + "type": "text", + "id": "sDa8O", + "name": "syncText", + "fill": "$--muted-foreground", + "content": "3 modified", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "4RcrW", + "name": "Status Right", + "height": "fill_container", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "5g3ye", + "name": "aiIcon", + "width": 13, + "height": 13, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "$--accent-purple" + }, + { + "type": "text", + "id": "D1hG8", + "name": "aiText", + "fill": "$--muted-foreground", + "content": "Claude Sonnet 4", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "l4igr", + "name": "sep3", + "fill": "$--border", + "content": "|", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "2uctj", + "name": "notesCount", + "width": 13, + "height": 13, + "iconFontName": "file-text", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "uv6xG", + "name": "notesText", + "fill": "$--muted-foreground", + "content": "1,247 notes", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "tViOI", + "name": "sep4", + "fill": "$--border", + "content": "|", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "i4ZVl", + "name": "bellIcon", + "width": 13, + "height": 13, + "iconFontName": "bell", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "icon_font", + "id": "-T7ch", + "name": "settingsIcon", + "width": 13, + "height": 13, + "iconFontName": "settings", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + } + ] + } + ] + } + ], + "variables": { + "--accent": { + "type": "color", + "value": [ + { + "value": "#EBEBEA" + }, + { + "value": "#252545", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-blue": { + "type": "color", + "value": [ + { + "value": "#2383E2" + }, + { + "value": "#4a9eff", + "theme": { + "Mode": "Dark" + } + }, + { + "value": "#155DFF" + }, + { + "value": "#155DFF", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-foreground": { + "type": "color", + "value": [ + { + "value": "#37352F" + }, + { + "value": "#ffffff", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-green": { + "type": "color", + "value": [ + { + "value": "#0F7B6C" + }, + { + "value": "#4caf50", + "theme": { + "Mode": "Dark" + } + }, + { + "value": "#00B38B" + }, + { + "value": "#00B38B", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-orange": { + "type": "color", + "value": [ + { + "value": "#D9730D" + }, + { + "value": "#ff9800", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-purple": { + "type": "color", + "value": [ + { + "value": "#9065B0" + }, + { + "value": "#9c72ff", + "theme": { + "Mode": "Dark" + } + }, + { + "value": "#A932FF" + }, + { + "value": "#A932FF", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-red": { + "type": "color", + "value": [ + { + "value": "#E03E3E" + }, + { + "value": "#f44336", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--background": { + "type": "color", + "value": [ + { + "value": "#FFFFFF" + }, + { + "value": "#0f0f1a", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--black": { + "type": "color", + "value": "#000000" + }, + "--border": { + "type": "color", + "value": [ + { + "value": "#E9E9E7" + }, + { + "value": "#2a2a4a", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--card": { + "type": "color", + "value": [ + { + "value": "#FFFFFF" + }, + { + "value": "#16162a", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--card-foreground": { + "type": "color", + "value": [ + { + "value": "#37352F" + }, + { + "value": "#e0e0e0", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--destructive": { + "type": "color", + "value": [ + { + "value": "#E03E3E" + }, + { + "value": "#f44336", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--destructive-foreground": { + "type": "color", + "value": [ + { + "value": "#FFFFFF" + }, + { + "value": "#ffffff", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--font-primary": { + "type": "string", + "value": [ + { + "value": "Inter" + }, + { + "value": "-apple-system, BlinkMacSystemFont, Inter, sans-serif" + }, + { + "value": "Inter" + } + ] + }, + "--font-system": { + "type": "string", + "value": "-apple-system, BlinkMacSystemFont, sans-serif" + }, + "--foreground": { + "type": "color", + "value": [ + { + "value": "#37352F" + }, + { + "value": "#e0e0e0", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--input": { + "type": "color", + "value": [ + { + "value": "#E9E9E7" + }, + { + "value": "#2a2a4a", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--muted": { + "type": "color", + "value": [ + { + "value": "#F0F0EF" + }, + { + "value": "#1e1e3a", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--muted-foreground": { + "type": "color", + "value": [ + { + "value": "#787774" + }, + { + "value": "#888888", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--popover": { + "type": "color", + "value": [ + { + "value": "#FFFFFF" + }, + { + "value": "#1e1e3a", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--popover-foreground": { + "type": "color", + "value": [ + { + "value": "#37352F" + }, + { + "value": "#e0e0e0", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--primary": { + "type": "color", + "value": [ + { + "value": "#2383E2" + }, + { + "value": "#4a9eff", + "theme": { + "Mode": "Dark" + } + }, + { + "value": "#155DFF" + }, + { + "value": "#155DFF", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--primary-foreground": { + "type": "color", + "value": [ + { + "value": "#FFFFFF" + }, + { + "value": "#ffffff", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--radius-lg": { + "type": "number", + "value": 8 + }, + "--radius-md": { + "type": "number", + "value": 6 + }, + "--radius-sm": { + "type": "number", + "value": 4 + }, + "--ring": { + "type": "color", + "value": [ + { + "value": "#2383E2" + }, + { + "value": "#4a9eff", + "theme": { + "Mode": "Dark" + } + }, + { + "value": "#155DFF" + }, + { + "value": "#155DFF", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--secondary": { + "type": "color", + "value": [ + { + "value": "#EBEBEA" + }, + { + "value": "#2a2a4a", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--secondary-foreground": { + "type": "color", + "value": [ + { + "value": "#37352F" + }, + { + "value": "#e0e0e0", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--sidebar": { + "type": "color", + "value": [ + { + "value": "#F7F6F3" + }, + { + "value": "#1a1a2e", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--sidebar-accent": { + "type": "color", + "value": [ + { + "value": "#EBEBEA" + }, + { + "value": "#252545", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--sidebar-accent-foreground": { + "type": "color", + "value": [ + { + "value": "#37352F" + }, + { + "value": "#ffffff", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--sidebar-border": { + "type": "color", + "value": [ + { + "value": "#E9E9E7" + }, + { + "value": "#2a2a4a", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--sidebar-foreground": { + "type": "color", + "value": [ + { + "value": "#37352F" + }, + { + "value": "#e0e0e0", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--sidebar-primary": { + "type": "color", + "value": [ + { + "value": "#2383E2" + }, + { + "value": "#4a9eff", + "theme": { + "Mode": "Dark" + } + }, + { + "value": "#155DFF" + }, + { + "value": "#155DFF", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--sidebar-primary-foreground": { + "type": "color", + "value": [ + { + "value": "#FFFFFF" + }, + { + "value": "#ffffff", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--sidebar-ring": { + "type": "color", + "value": [ + { + "value": "#2383E2" + }, + { + "value": "#4a9eff", + "theme": { + "Mode": "Dark" + } + }, + { + "value": "#155DFF" + }, + { + "value": "#155DFF", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--white": { + "type": "color", + "value": "#ffffff" + }, + "--accent-yellow": { + "type": "color", + "value": [ + { + "value": "#F0B100" + }, + { + "value": "#F0B100", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-blue-light": { + "type": "color", + "value": [ + { + "value": "#155DFF14" + }, + { + "value": "#155DFF20", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-green-light": { + "type": "color", + "value": [ + { + "value": "#00B38B14" + }, + { + "value": "#00B38B20", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-purple-light": { + "type": "color", + "value": [ + { + "value": "#A932FF14" + }, + { + "value": "#A932FF20", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-red-light": { + "type": "color", + "value": [ + { + "value": "#E03E3E14" + }, + { + "value": "#f4433620", + "theme": { + "Mode": "Dark" + } + } + ] + }, + "--accent-yellow-light": { + "type": "color", + "value": [ + { + "value": "#F0B10014" + }, + { + "value": "#F0B10020", + "theme": { + "Mode": "Dark" + } + } + ] + } + } +} \ No newline at end of file diff --git a/design/drag-drop-images.pen b/design/drag-drop-images.pen new file mode 100644 index 0000000..1f92f39 --- /dev/null +++ b/design/drag-drop-images.pen @@ -0,0 +1,78 @@ +{ + "children": [ + { + "type": "frame", + "id": "drag_drop_idle", + "name": "Drag & Drop Images — Idle (no drag)", + "x": 0, + "y": 0, + "width": 900, + "height": 600, + "fill": "$--background", + "layout": "vertical", + "theme": { "Mode": "Light" }, + "children": [ + { + "type": "rectangle", + "id": "drag_idle_editor", + "width": "fill_container", + "height": "fill_container", + "fill": "$--background", + "cornerRadius": 0 + } + ] + }, + { + "type": "frame", + "id": "drag_drop_active", + "name": "Drag & Drop Images — Drag Over (overlay shown)", + "x": 940, + "y": 0, + "width": 900, + "height": 600, + "fill": "$--background", + "layout": "vertical", + "theme": { "Mode": "Light" }, + "children": [ + { + "type": "frame", + "id": "drag_active_container", + "name": "Editor with Drag Overlay", + "layout": "vertical", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "rectangle", + "id": "drag_active_editor_bg", + "width": "fill_container", + "height": "fill_container", + "fill": "$--background" + }, + { + "type": "frame", + "id": "drag_overlay", + "name": "Drop Overlay", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "fill": "rgba(0,0,0,0.4)", + "cornerRadius": 8, + "children": [ + { + "type": "text", + "id": "drag_overlay_label", + "content": "Drop image to insert", + "fontSize": 18, + "fontWeight": "600", + "fill": "#ffffff", + "textAlign": "center" + } + ] + } + ] + } + ] + } + ] +} diff --git a/design/fix-property-dropdown-narrow.pen b/design/fix-property-dropdown-narrow.pen new file mode 100644 index 0000000..deff115 --- /dev/null +++ b/design/fix-property-dropdown-narrow.pen @@ -0,0 +1,56 @@ +{ + "children": [ + { + "type": "frame", + "id": "fpdn_before", + "name": "Property Dropdown Narrow — Before (clipped)", + "x": 0, + "y": 0, + "width": 280, + "height": 200, + "fill": "#1a1a1a", + "layout": "vertical", + "gap": 8, + "padding": 12, + "theme": "dark", + "children": [ + { + "type": "text", + "id": "fpdn_before_label", + "content": "Bug: StatusDropdown clipped by overflow:hidden container at 200-280px panel width", + "fill": "#ff4444", + "fontFamily": "SF Pro Text", + "fontSize": 12, + "fontWeight": "regular", + "letterSpacing": 0 + } + ] + }, + { + "type": "frame", + "id": "fpdn_after", + "name": "Property Dropdown Narrow — After (portal, no clip)", + "x": 320, + "y": 0, + "width": 280, + "height": 200, + "fill": "#1a1a1a", + "layout": "vertical", + "gap": 8, + "padding": 12, + "theme": "dark", + "children": [ + { + "type": "text", + "id": "fpdn_after_label", + "content": "Fix: StatusDropdown + DisplayModeSelector use createPortal with fixed positioning — renders above overflow:hidden, visible at any panel width", + "fill": "#44bb44", + "fontFamily": "SF Pro Text", + "fontSize": 12, + "fontWeight": "regular", + "letterSpacing": 0 + } + ] + } + ] +} diff --git a/design/getting-started-vault.pen b/design/getting-started-vault.pen new file mode 100644 index 0000000..4ac91aa --- /dev/null +++ b/design/getting-started-vault.pen @@ -0,0 +1,381 @@ +{ + "children": [ + { + "type": "frame", + "id": "welcome_screen", + "name": "Getting Started — Welcome Screen", + "x": 0, + "y": 0, + "width": 1440, + "height": 900, + "fill": "#F7F6F3", + "layout": "vertical", + "alignItems": "center", + "justifyContent": "center", + "gap": 0, + "theme": { "Mode": "Light" }, + "children": [ + { + "type": "frame", + "id": "welcome_card", + "name": "Welcome Card", + "width": 520, + "height": "fit_content", + "fill": "#FFFFFF", + "cornerRadius": [12, 12, 12, 12], + "layout": "vertical", + "alignItems": "center", + "gap": 24, + "padding": [48, 48, 48, 48], + "stroke": "#E9E9E7", + "strokeThickness": 1, + "children": [ + { + "type": "frame", + "id": "welcome_icon_wrap", + "name": "Icon", + "width": 64, + "height": 64, + "fill": "#EBF4FF", + "cornerRadius": [16, 16, 16, 16], + "layout": "vertical", + "alignItems": "center", + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "welcome_icon_text", + "content": "✦", + "fontSize": 28, + "fill": "#2383E2", + "fontFamily": "Inter" + } + ] + }, + { + "type": "frame", + "id": "welcome_text_group", + "name": "Text Group", + "layout": "vertical", + "alignItems": "center", + "gap": 8, + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "welcome_title", + "content": "Welcome to Laputa", + "fill": "#37352F", + "fontFamily": "Inter", + "fontSize": 28, + "fontWeight": "700", + "letterSpacing": -0.5, + "textAlign": "center" + }, + { + "type": "text", + "id": "welcome_subtitle", + "content": "Wiki-linked knowledge management for deep thinkers.\nChoose how to get started.", + "fill": "#787774", + "fontFamily": "Inter", + "fontSize": 14, + "lineHeight": 1.6, + "textAlign": "center" + } + ] + }, + { + "type": "rectangle", + "id": "welcome_divider", + "width": "fill_container", + "height": 1, + "fill": "#E9E9E7" + }, + { + "type": "frame", + "id": "welcome_actions", + "name": "Actions", + "layout": "vertical", + "gap": 12, + "width": "fill_container", + "children": [ + { + "type": "frame", + "id": "btn_create_vault", + "name": "Create Getting Started Vault Button", + "width": "fill_container", + "height": 44, + "fill": "#2383E2", + "cornerRadius": [8, 8, 8, 8], + "layout": "horizontal", + "alignItems": "center", + "justifyContent": "center", + "gap": 8, + "children": [ + { + "type": "text", + "id": "btn_create_icon", + "content": "+", + "fill": "#FFFFFF", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600" + }, + { + "type": "text", + "id": "btn_create_label", + "content": "Create Getting Started vault", + "fill": "#FFFFFF", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "btn_open_folder", + "name": "Open Existing Folder Button", + "width": "fill_container", + "height": 44, + "fill": "#FFFFFF", + "stroke": "#E9E9E7", + "strokeThickness": 1, + "cornerRadius": [8, 8, 8, 8], + "layout": "horizontal", + "alignItems": "center", + "justifyContent": "center", + "gap": 8, + "children": [ + { + "type": "text", + "id": "btn_open_icon", + "content": "📂", + "fill": "#37352F", + "fontFamily": "Inter", + "fontSize": 14 + }, + { + "type": "text", + "id": "btn_open_label", + "content": "Open an existing folder", + "fill": "#37352F", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "text", + "id": "welcome_hint", + "content": "The Getting Started vault will be created in ~/Documents/Laputa", + "fill": "#787774", + "fontFamily": "Inter", + "fontSize": 12, + "textAlign": "center" + } + ] + } + ] + }, + { + "type": "frame", + "id": "vault_missing_screen", + "name": "Getting Started — Vault Missing Error", + "x": 1540, + "y": 0, + "width": 1440, + "height": 900, + "fill": "#F7F6F3", + "layout": "vertical", + "alignItems": "center", + "justifyContent": "center", + "gap": 0, + "theme": { "Mode": "Light" }, + "children": [ + { + "type": "frame", + "id": "missing_card", + "name": "Missing Vault Card", + "width": 520, + "height": "fit_content", + "fill": "#FFFFFF", + "cornerRadius": [12, 12, 12, 12], + "layout": "vertical", + "alignItems": "center", + "gap": 24, + "padding": [48, 48, 48, 48], + "stroke": "#E9E9E7", + "strokeThickness": 1, + "children": [ + { + "type": "frame", + "id": "missing_icon_wrap", + "name": "Warning Icon", + "width": 64, + "height": 64, + "fill": "#FFF3E0", + "cornerRadius": [16, 16, 16, 16], + "layout": "vertical", + "alignItems": "center", + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "missing_icon_text", + "content": "⚠", + "fontSize": 28, + "fill": "#E8890C", + "fontFamily": "Inter" + } + ] + }, + { + "type": "frame", + "id": "missing_text_group", + "name": "Text Group", + "layout": "vertical", + "alignItems": "center", + "gap": 8, + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "missing_title", + "content": "Vault not found", + "fill": "#37352F", + "fontFamily": "Inter", + "fontSize": 28, + "fontWeight": "700", + "letterSpacing": -0.5, + "textAlign": "center" + }, + { + "type": "text", + "id": "missing_subtitle", + "content": "The vault folder could not be found on disk.\nIt may have been moved or deleted.", + "fill": "#787774", + "fontFamily": "Inter", + "fontSize": 14, + "lineHeight": 1.6, + "textAlign": "center" + } + ] + }, + { + "type": "frame", + "id": "missing_path_badge", + "name": "Path Badge", + "width": "fill_container", + "height": "fit_content", + "fill": "#F7F6F3", + "cornerRadius": [6, 6, 6, 6], + "layout": "horizontal", + "alignItems": "center", + "justifyContent": "center", + "padding": [8, 12, 8, 12], + "children": [ + { + "type": "text", + "id": "missing_path_text", + "content": "~/Laputa", + "fill": "#787774", + "fontFamily": "SF Mono, monospace", + "fontSize": 12 + } + ] + }, + { + "type": "rectangle", + "id": "missing_divider", + "width": "fill_container", + "height": 1, + "fill": "#E9E9E7" + }, + { + "type": "frame", + "id": "missing_actions", + "name": "Actions", + "layout": "vertical", + "gap": 12, + "width": "fill_container", + "children": [ + { + "type": "frame", + "id": "btn_recreate_vault", + "name": "Create Getting Started Vault Button", + "width": "fill_container", + "height": 44, + "fill": "#2383E2", + "cornerRadius": [8, 8, 8, 8], + "layout": "horizontal", + "alignItems": "center", + "justifyContent": "center", + "gap": 8, + "children": [ + { + "type": "text", + "id": "btn_recreate_icon", + "content": "+", + "fill": "#FFFFFF", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600" + }, + { + "type": "text", + "id": "btn_recreate_label", + "content": "Create Getting Started vault", + "fill": "#FFFFFF", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "btn_choose_folder", + "name": "Choose Different Folder Button", + "width": "fill_container", + "height": 44, + "fill": "#FFFFFF", + "stroke": "#E9E9E7", + "strokeThickness": 1, + "cornerRadius": [8, 8, 8, 8], + "layout": "horizontal", + "alignItems": "center", + "justifyContent": "center", + "gap": 8, + "children": [ + { + "type": "text", + "id": "btn_choose_icon", + "content": "📂", + "fill": "#37352F", + "fontFamily": "Inter", + "fontSize": 14 + }, + { + "type": "text", + "id": "btn_choose_label", + "content": "Choose a different folder", + "fill": "#37352F", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "500" + } + ] + } + ] + } + ] + } + ] + } + ], + "variables": {} +} diff --git a/design/git-status-bar.pen b/design/git-status-bar.pen new file mode 100644 index 0000000..747b5ab --- /dev/null +++ b/design/git-status-bar.pen @@ -0,0 +1 @@ +{"children":[],"variables":{}} diff --git a/design/github-oauth-fix.pen b/design/github-oauth-fix.pen new file mode 100644 index 0000000..7dbabdc --- /dev/null +++ b/design/github-oauth-fix.pen @@ -0,0 +1,258 @@ +{ + "children": [ + { + "type": "frame", + "id": "gofix01", + "x": 0, + "y": 0, + "name": "GitHub OAuth Fix — Modal with inline device flow", + "clip": true, + "width": 560, + "height": 420, + "fill": "$--background", + "cornerRadius": 12, + "layout": "vertical", + "stroke": { "align": "inside", "thickness": 1, "fill": "$--border" }, + "children": [ + { + "type": "frame", + "id": "gofix01-hdr", + "name": "Header", + "width": "fill_container", + "height": 56, + "fill": "$--background", + "padding": [0, 24], + "alignItems": "center", + "justifyContent": "space-between", + "stroke": { "align": "inside", "thickness": { "bottom": 1 }, "fill": "$--border" }, + "children": [ + { "type": "text", "id": "gofix01-title", "content": "Connect GitHub Repo", "fontSize": 16, "fontWeight": 600, "fill": "$--foreground" }, + { "type": "frame", "id": "gofix01-close", "width": 24, "height": 24, "cornerRadius": 4, "alignItems": "center", "justifyContent": "center", "children": [{ "type": "text", "id": "gofix01-x", "content": "X", "fontSize": 14, "fill": "$--muted-foreground" }] } + ] + }, + { + "type": "frame", + "id": "gofix01-desc", + "name": "Description", + "width": "fill_container", + "padding": [16, 24], + "children": [ + { "type": "text", "id": "gofix01-desc-t", "content": "Connect your GitHub account to clone or create vaults backed by GitHub repos.", "fontSize": 13, "fill": "$--muted-foreground", "lineHeight": 1.5 } + ] + }, + { + "type": "frame", + "id": "gofix01-body", + "name": "DeviceFlowLogin", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "padding": [24, 24], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "gofix01-login-btn", + "name": "LoginButton", + "width": 200, + "height": 36, + "fill": "$--foreground", + "cornerRadius": 6, + "alignItems": "center", + "justifyContent": "center", + "gap": 8, + "children": [ + { "type": "text", "id": "gofix01-gh-icon", "content": "G", "fontSize": 14, "fontWeight": 700, "fill": "$--background" }, + { "type": "text", "id": "gofix01-login-label", "content": "Login with GitHub", "fontSize": 13, "fontWeight": 500, "fill": "$--background" } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "gofix02", + "x": 660, + "y": 0, + "name": "GitHub OAuth Fix — Device code waiting state", + "clip": true, + "width": 560, + "height": 420, + "fill": "$--background", + "cornerRadius": 12, + "layout": "vertical", + "stroke": { "align": "inside", "thickness": 1, "fill": "$--border" }, + "children": [ + { + "type": "frame", + "id": "gofix02-hdr", + "name": "Header", + "width": "fill_container", + "height": 56, + "fill": "$--background", + "padding": [0, 24], + "alignItems": "center", + "justifyContent": "space-between", + "stroke": { "align": "inside", "thickness": { "bottom": 1 }, "fill": "$--border" }, + "children": [ + { "type": "text", "id": "gofix02-title", "content": "Connect GitHub Repo", "fontSize": 16, "fontWeight": 600, "fill": "$--foreground" }, + { "type": "frame", "id": "gofix02-close", "width": 24, "height": 24, "cornerRadius": 4, "alignItems": "center", "justifyContent": "center", "children": [{ "type": "text", "id": "gofix02-x", "content": "X", "fontSize": 14, "fill": "$--muted-foreground" }] } + ] + }, + { + "type": "frame", + "id": "gofix02-desc", + "name": "Description", + "width": "fill_container", + "padding": [16, 24], + "children": [ + { "type": "text", "id": "gofix02-desc-t", "content": "Connect your GitHub account to clone or create vaults backed by GitHub repos.", "fontSize": 13, "fill": "$--muted-foreground", "lineHeight": 1.5 } + ] + }, + { + "type": "frame", + "id": "gofix02-body", + "name": "DeviceCodeCard", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": [24, 24], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "gofix02-card", + "name": "CodeCard", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "padding": [16, 24], + "cornerRadius": 8, + "alignItems": "center", + "stroke": { "align": "inside", "thickness": 1, "fill": "$--border" }, + "children": [ + { "type": "text", "id": "gofix02-hint", "content": "Enter this code on GitHub:", "fontSize": 12, "fill": "$--muted-foreground" }, + { + "type": "frame", + "id": "gofix02-code-row", + "gap": 8, + "alignItems": "center", + "children": [ + { "type": "text", "id": "gofix02-code", "content": "ABCD-1234", "fontSize": 24, "fontWeight": 700, "letterSpacing": 4, "fontFamily": "monospace", "fill": "$--foreground" }, + { "type": "text", "id": "gofix02-copy", "content": "[copy]", "fontSize": 12, "fill": "$--muted-foreground" } + ] + }, + { "type": "text", "id": "gofix02-url", "content": "https://github.com/login/device", "fontSize": 12, "fill": "$--muted-foreground", "textDecoration": "underline" }, + { + "type": "frame", + "id": "gofix02-spinner-row", + "gap": 6, + "alignItems": "center", + "children": [ + { "type": "text", "id": "gofix02-waiting", "content": "Waiting for authorization...", "fontSize": 12, "fill": "$--muted-foreground" } + ] + } + ] + }, + { + "type": "frame", + "id": "gofix02-cancel-btn", + "name": "CancelButton", + "width": 80, + "height": 32, + "cornerRadius": 6, + "alignItems": "center", + "justifyContent": "center", + "stroke": { "align": "inside", "thickness": 1, "fill": "$--border" }, + "children": [ + { "type": "text", "id": "gofix02-cancel-label", "content": "Cancel", "fontSize": 12, "fill": "$--muted-foreground" } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "gofix03", + "x": 1320, + "y": 0, + "name": "GitHub OAuth Fix — Expired/Error state with retry", + "clip": true, + "width": 560, + "height": 420, + "fill": "$--background", + "cornerRadius": 12, + "layout": "vertical", + "stroke": { "align": "inside", "thickness": 1, "fill": "$--border" }, + "children": [ + { + "type": "frame", + "id": "gofix03-hdr", + "name": "Header", + "width": "fill_container", + "height": 56, + "fill": "$--background", + "padding": [0, 24], + "alignItems": "center", + "justifyContent": "space-between", + "stroke": { "align": "inside", "thickness": { "bottom": 1 }, "fill": "$--border" }, + "children": [ + { "type": "text", "id": "gofix03-title", "content": "Connect GitHub Repo", "fontSize": 16, "fontWeight": 600, "fill": "$--foreground" }, + { "type": "frame", "id": "gofix03-close", "width": 24, "height": 24, "cornerRadius": 4, "alignItems": "center", "justifyContent": "center", "children": [{ "type": "text", "id": "gofix03-x", "content": "X", "fontSize": 14, "fill": "$--muted-foreground" }] } + ] + }, + { + "type": "frame", + "id": "gofix03-desc", + "name": "Description", + "width": "fill_container", + "padding": [16, 24], + "children": [ + { "type": "text", "id": "gofix03-desc-t", "content": "Connect your GitHub account to clone or create vaults backed by GitHub repos.", "fontSize": 13, "fill": "$--muted-foreground", "lineHeight": 1.5 } + ] + }, + { + "type": "frame", + "id": "gofix03-body", + "name": "ErrorState", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "padding": [24, 24], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "gofix03-login-btn", + "name": "LoginButton", + "width": 200, + "height": 36, + "fill": "$--foreground", + "cornerRadius": 6, + "alignItems": "center", + "justifyContent": "center", + "gap": 8, + "children": [ + { "type": "text", "id": "gofix03-gh-icon", "content": "G", "fontSize": 14, "fontWeight": 700, "fill": "$--background" }, + { "type": "text", "id": "gofix03-login-label", "content": "Login with GitHub", "fontSize": 13, "fontWeight": 500, "fill": "$--background" } + ] + }, + { + "type": "frame", + "id": "gofix03-error-row", + "gap": 8, + "alignItems": "center", + "children": [ + { "type": "text", "id": "gofix03-error-msg", "content": "Authorization expired. Please try again.", "fontSize": 12, "fill": "$--destructive" }, + { "type": "text", "id": "gofix03-retry-label", "content": "Retry", "fontSize": 12, "fill": "$--destructive" } + ] + } + ] + } + ] + } + ], + "variables": {} +} diff --git a/design/gs-git-init.pen b/design/gs-git-init.pen new file mode 100644 index 0000000..ddf305f --- /dev/null +++ b/design/gs-git-init.pen @@ -0,0 +1 @@ +{"children":[{"type":"frame","id":"gs_git_init_after","name":"Getting Started Vault — Git Initialized","x":0,"y":0,"width":600,"height":"fit_content(200)","fill":"#FFFFFF","layout":"vertical","gap":16,"padding":[32,32],"children":[{"type":"text","id":"gs_title","content":"After: Getting Started vault created","fontFamily":"Inter","fontSize":20,"fontWeight":"700","fill":"#111111"},{"type":"text","id":"gs_desc","content":"When a new Getting Started vault is created, it is automatically initialized as a git repo with an initial commit containing all sample files.","fontFamily":"Inter","fontSize":14,"lineHeight":1.5,"fill":"#555555"},{"type":"frame","id":"gs_terminal","name":"Git Log Output","width":"fill_container","height":"fit_content(80)","fill":"#1E1E1E","cornerRadius":[8,8,8,8],"padding":[16,16],"layout":"vertical","gap":4,"children":[{"type":"text","id":"gs_prompt","content":"$ git log --oneline","fontFamily":"JetBrains Mono, monospace","fontSize":12,"fill":"#AAAAAA"},{"type":"text","id":"gs_log","content":"a1b2c3d Initial vault setup","fontFamily":"JetBrains Mono, monospace","fontSize":12,"fill":"#66FF66"}]},{"type":"frame","id":"gs_details","name":"Details","width":"fill_container","layout":"vertical","gap":8,"children":[{"type":"text","id":"gs_detail1","content":"git init + git add . + git commit","fontFamily":"Inter","fontSize":13,"fill":"#333333"},{"type":"text","id":"gs_detail2","content":"Fallback author: Laputa (if no global git config)","fontFamily":"Inter","fontSize":13,"fill":"#333333"},{"type":"text","id":"gs_detail3","content":"No UI changes — backend only (git.rs + getting_started.rs)","fontFamily":"Inter","fontSize":13,"fill":"#333333"}]}]}],"variables":{}} \ No newline at end of file diff --git a/design/gs-wikilinks-fix.pen b/design/gs-wikilinks-fix.pen new file mode 100644 index 0000000..448ebce --- /dev/null +++ b/design/gs-wikilinks-fix.pen @@ -0,0 +1,17 @@ +{ + "children": [ + { + "name": "Getting Started Wikilinks — Fixed (styled with type color)", + "type": "FRAME", + "width": 800, + "height": 400, + "children": [ + { + "name": "Description", + "type": "TEXT", + "characters": "Wikilinks in Getting Started vault now resolve correctly:\n- path/to/note targets match entry paths ending in /path/to/note.md\n- Display text shows entry title (not raw path)\n- Color reflects the linked note's type (e.g. blue for Note type)\n- Broken links show muted color as expected\n\nBefore: [[note/welcome-to-laputa]] showed raw path, unstyled\nAfter: [[note/welcome-to-laputa]] shows 'Welcome to Laputa' in Note blue" + } + ] + } + ] +} diff --git a/design/hide-backlinks-empty.pen b/design/hide-backlinks-empty.pen new file mode 100644 index 0000000..ae27a8d --- /dev/null +++ b/design/hide-backlinks-empty.pen @@ -0,0 +1,65 @@ +{ + "children": [ + { + "type": "frame", + "id": "hideBacklinksEmpty_before", + "x": 0, + "y": 0, + "name": "Hide Empty Backlinks Label — Before (shows No backlinks label)", + "width": 280, + "height": 80, + "fill": "#ffffff", + "gap": 4, + "padding": 12, + "alignItems": "flex-start", + "children": [ + { + "type": "text", + "id": "bl_header_before", + "name": "section-header", + "fill": "#6b7280", + "content": "Backlinks", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 1.2 + }, + { + "type": "text", + "id": "bl_empty_before", + "name": "empty-label", + "fill": "#9ca3af", + "content": "No backlinks", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "hideBacklinksEmpty_after", + "x": 320, + "y": 0, + "name": "Hide Empty Backlinks Label — After (section hidden when empty)", + "width": 280, + "height": 80, + "fill": "#ffffff", + "gap": 4, + "padding": 12, + "alignItems": "flex-start", + "children": [ + { + "type": "text", + "id": "bl_note_after", + "name": "note", + "fill": "#9ca3af", + "content": "(Backlinks, Referenced By, and Relations sections are hidden when empty — no placeholder label shown)", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] +} \ No newline at end of file diff --git a/design/keyboard-first-nav.pen b/design/keyboard-first-nav.pen new file mode 100644 index 0000000..3a6bf14 --- /dev/null +++ b/design/keyboard-first-nav.pen @@ -0,0 +1,156 @@ +{ + "children": [ + { + "type": "frame", + "id": "kfn_notelist_nav", + "name": "Keyboard-first — Note list keyboard navigation", + "x": 0, + "y": 0, + "width": 280, + "height": "fit_content(400)", + "fill": "$--background", + "layout": "vertical", + "gap": 0, + "padding": [8, 0], + "children": [ + { + "type": "text", + "id": "kfn_notelist_label", + "content": "Note list — ↑↓ highlight, Enter opens", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "500", + "padding": [0, 12, 8, 12] + }, + { + "type": "frame", + "id": "kfn_note_item_selected", + "layout": "horizontal", + "width": "fill_container", + "height": 48, + "fill": "$--accent", + "padding": [0, 12], + "gap": 8, + "children": [ + { + "type": "text", + "id": "kfn_note_title_active", + "content": "My selected note (keyboard highlight)", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "kfn_note_item_normal", + "layout": "horizontal", + "width": "fill_container", + "height": 48, + "fill": "transparent", + "padding": [0, 12], + "gap": 8, + "children": [ + { + "type": "text", + "id": "kfn_note_title_normal", + "content": "Another note", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 13 + } + ] + } + ] + }, + { + "type": "frame", + "id": "kfn_menu_bar", + "name": "Keyboard-first — Menu bar shortcuts added", + "x": 320, + "y": 0, + "width": 400, + "height": "fit_content(200)", + "fill": "$--background", + "layout": "vertical", + "gap": 4, + "padding": [16, 16], + "children": [ + { + "type": "text", + "id": "kfn_menu_title", + "content": "New menu bar shortcuts", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "kfn_s1", + "content": "Archive Note — ⌘E\nTrash Note — ⌘⌫\nFind in Vault — ⌘⇧F\nGo Back — ⌘[\nGo Forward — ⌘]", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 13, + "lineHeight": 1.8 + } + ] + }, + { + "type": "frame", + "id": "kfn_inspector_tab", + "name": "Keyboard-first — Inspector Tab+Enter navigation", + "x": 760, + "y": 0, + "width": 300, + "height": "fit_content(200)", + "fill": "$--background", + "layout": "vertical", + "gap": 8, + "padding": [16, 16], + "children": [ + { + "type": "text", + "id": "kfn_inspector_title", + "content": "Inspector — Tab focuses rows, Enter starts editing", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "kfn_prop_focused", + "layout": "horizontal", + "width": "fill_container", + "height": 36, + "fill": "$--muted", + "cornerRadius": 4, + "padding": [0, 12], + "gap": 8, + "children": [ + { + "type": "text", + "id": "kfn_prop_key", + "content": "Status", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 12 + }, + { + "type": "text", + "id": "kfn_prop_val", + "content": "In Progress (focused — ring visible)", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 12 + } + ] + } + ] + } + ] +} diff --git a/design/mcp-autodetect-status-bar.pen b/design/mcp-autodetect-status-bar.pen new file mode 100644 index 0000000..747b5ab --- /dev/null +++ b/design/mcp-autodetect-status-bar.pen @@ -0,0 +1 @@ +{"children":[],"variables":{}} diff --git a/design/new-note-creation.pen b/design/new-note-creation.pen new file mode 100644 index 0000000..cddd569 --- /dev/null +++ b/design/new-note-creation.pen @@ -0,0 +1,185 @@ +{ + "children": [ + { + "type": "frame", + "id": "nnc_pending_save", + "name": "New Note Creation — Unsaved State (Cmd+N)", + "x": 0, + "y": 0, + "width": 800, + "height": 120, + "fill": "$--background", + "layout": "horizontal", + "gap": 0, + "padding": [ + 0, + 0 + ], + "theme": { + "Mode": "Light" + }, + "children": [ + { + "type": "frame", + "id": "nnc_tab_bar", + "name": "Tab Bar", + "layout": "horizontal", + "width": 800, + "height": 40, + "fill": "$--card", + "gap": 0, + "padding": [ + 0, + 8 + ], + "children": [ + { + "type": "frame", + "id": "nnc_tab_active", + "name": "Tab — Unsaved Note (blue dot + italic)", + "layout": "horizontal", + "width": 180, + "height": 40, + "fill": "$--background", + "gap": 6, + "padding": [ + 0, + 12 + ], + "children": [ + { + "type": "rectangle", + "id": "nnc_dot_pending", + "width": 8, + "height": 8, + "cornerRadius": 4, + "fill": "#3b82f6" + }, + { + "type": "text", + "id": "nnc_tab_label", + "content": "Untitled Note", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "400", + "fontStyle": "italic" + } + ] + } + ] + }, + { + "type": "frame", + "id": "nnc_editor_area", + "name": "Editor — Empty New Note", + "layout": "vertical", + "width": 800, + "height": 80, + "fill": "$--background", + "gap": 8, + "padding": [ + 16, + 24 + ], + "children": [ + { + "type": "text", + "id": "nnc_placeholder", + "content": "Start typing...", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 14 + } + ] + } + ] + }, + { + "type": "frame", + "id": "nnc_after_first_save", + "name": "New Note Creation — After First Save", + "x": 820, + "y": 0, + "width": 800, + "height": 120, + "fill": "$--background", + "layout": "vertical", + "gap": 0, + "padding": [ + 0, + 0 + ], + "theme": { + "Mode": "Light" + }, + "children": [ + { + "type": "frame", + "id": "nnc_tab_bar_saved", + "name": "Tab Bar — Saved", + "layout": "horizontal", + "width": 800, + "height": 40, + "fill": "$--card", + "gap": 0, + "padding": [ + 0, + 8 + ], + "children": [ + { + "type": "frame", + "id": "nnc_tab_saved", + "name": "Tab — Saved Note", + "layout": "horizontal", + "width": 180, + "height": 40, + "fill": "$--background", + "gap": 6, + "padding": [ + 0, + 12 + ], + "children": [ + { + "type": "text", + "id": "nnc_tab_label_saved", + "content": "My New Note", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "400" + } + ] + } + ] + }, + { + "type": "frame", + "id": "nnc_editor_saved", + "name": "Editor — Note with Content", + "layout": "vertical", + "width": 800, + "height": 80, + "fill": "$--background", + "gap": 8, + "padding": [ + 16, + 24 + ], + "children": [ + { + "type": "text", + "id": "nnc_content", + "content": "My New Note content...", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 14 + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/design/note-list-title-position.pen b/design/note-list-title-position.pen new file mode 100644 index 0000000..9cc46bf --- /dev/null +++ b/design/note-list-title-position.pen @@ -0,0 +1 @@ +{"children":[{"type":"frame","id":"AeVO5","name":"Sidebar Collapsed — NoteList Title Position","x":0,"y":0,"width":600,"fill":"$--background","layout":"vertical","gap":12,"padding":16,"children":[{"type":"text","id":"ryDyy","name":"label","content":"Sidebar Collapsed — NoteList with traffic light clearance","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"600","letterSpacing":1},{"type":"frame","id":"xZMtO","name":"App Window (sidebar collapsed)","clip":true,"width":"fill_container","cornerRadius":8,"stroke":{"align":"inside","thickness":1,"fill":"$--border"},"layout":"vertical","children":[{"type":"frame","id":"ymeIo","name":"Content Row","width":"fill_container","height":320,"layout":"none","children":[{"type":"frame","id":"VOhi9","name":"Panel: NoteList","x":0,"y":0,"width":300,"height":320,"fill":"$--card","stroke":{"align":"inside","fill":"$--border","thickness":{"right":1}},"layout":"vertical","children":[{"type":"frame","id":"Q884J","name":"NoteList Header (padded for traffic lights)","width":"fill_container","height":52,"alignItems":"center","justifyContent":"space_between","padding":[0,16,0,80],"stroke":{"align":"inside","fill":"$--border","thickness":{"bottom":1}},"children":[{"type":"text","id":"Ra8AY","content":"All Notes","fill":"$--foreground","fontFamily":"Inter","fontSize":14,"fontWeight":"600"},{"type":"frame","id":"0TYpG","name":"actions","alignItems":"center","gap":12,"children":[{"type":"icon_font","id":"v4VUg","iconFontFamily":"phosphor","iconFontName":"magnifying-glass","width":16,"height":16,"fill":"$--muted-foreground"},{"type":"icon_font","id":"y4CYF","iconFontFamily":"phosphor","iconFontName":"plus","width":16,"height":16,"fill":"$--muted-foreground"}]}]},{"type":"frame","id":"OCJxT","name":"Note Item 1","layout":"vertical","width":"fill_container","gap":4,"padding":[14,16],"stroke":{"align":"inside","fill":"$--border","thickness":{"bottom":1}},"children":[{"type":"text","id":"OThF7","content":"Build Laputa App","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"600"},{"type":"text","id":"rwqIZ","content":"Personal knowledge management app built with Tauri.","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":12,"lineHeight":1.5,"textGrowth":"fixed-width","width":"fill_container"},{"type":"text","id":"0phHO","content":"2m ago","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11}]},{"type":"frame","id":"YmjMb","name":"Note Item 2","layout":"vertical","width":"fill_container","gap":4,"padding":[14,16],"stroke":{"align":"inside","fill":"$--border","thickness":{"bottom":1}},"children":[{"type":"text","id":"gh2fT","content":"Facebook Ads Strategy","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"600"},{"type":"text","id":"Yn5dO","content":"Lookalike audiences convert 3x better than cold.","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":12,"lineHeight":1.5,"textGrowth":"fixed-width","width":"fill_container"},{"type":"text","id":"5AKHZ","content":"1h ago","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11}]}]},{"type":"frame","id":"V4qF5","name":"Panel: Editor","x":300,"y":0,"width":300,"height":320,"fill":"$--background","layout":"vertical","children":[{"type":"frame","id":"ztw3q","name":"Tab Bar","width":"fill_container","height":45,"fill":"$--sidebar","alignItems":"center","stroke":{"align":"inside","fill":"$--border","thickness":{"bottom":1}},"children":[{"type":"text","id":"NGfeH","content":" Build Laputa App","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"500"}]},{"type":"frame","id":"DXXof","name":"Editor Body","width":"fill_container","height":"fill_container","fill":"$--background","padding":[24,32],"children":[{"type":"text","id":"Hgsg7","content":"Build Laputa App","fill":"$--foreground","fontFamily":"Inter","fontSize":28,"fontWeight":"700"}]}]},{"type":"frame","id":"aSWvV","name":"Traffic Lights","x":0,"y":0,"gap":8,"padding":[18,0,0,14],"children":[{"type":"ellipse","id":"wwhm9","width":12,"height":12,"fill":"$--traffic-red"},{"type":"ellipse","id":"vYx2N","width":12,"height":12,"fill":"$--traffic-yellow"},{"type":"ellipse","id":"eRAiC","width":12,"height":12,"fill":"$--traffic-green"}]}]}]},{"type":"frame","id":"u5rgz","name":"Annotation","layout":"horizontal","gap":8,"alignItems":"center","padding":[8,12],"fill":"#E8F4FE","cornerRadius":6,"children":[{"type":"text","id":"hDyxd","content":"→","fill":"$--primary","fontFamily":"Inter","fontSize":14,"fontWeight":"700"},{"type":"text","id":"EtBGq","content":"paddingLeft: 80px clears traffic lights when sidebar is hidden (matches sidebar title bar padding)","fill":"$--foreground","fontFamily":"Inter","fontSize":12,"lineHeight":1.4,"textGrowth":"fixed-width","width":520}]}]}],"variables":{"--background":{"type":"color","value":"#FFFFFF"},"--foreground":{"type":"color","value":"#37352F"},"--card":{"type":"color","value":"#FFFFFF"},"--border":{"type":"color","value":"#E9E9E7"},"--muted":{"type":"color","value":"#F0F0EF"},"--muted-foreground":{"type":"color","value":"#787774"},"--sidebar":{"type":"color","value":"#F7F6F3"},"--sidebar-border":{"type":"color","value":"#E9E9E7"},"--primary":{"type":"color","value":"#155DFF"},"--destructive":{"type":"color","value":"#E03E3E"},"--accent-red-light":{"type":"color","value":"#E03E3E18"},"--traffic-red":{"type":"color","value":"#FF5F57"},"--traffic-yellow":{"type":"color","value":"#FEBC2E"},"--traffic-green":{"type":"color","value":"#28C840"}}} \ No newline at end of file diff --git a/design/note-subtitle-metadata.pen b/design/note-subtitle-metadata.pen new file mode 100644 index 0000000..747b5ab --- /dev/null +++ b/design/note-subtitle-metadata.pen @@ -0,0 +1 @@ +{"children":[],"variables":{}} diff --git a/design/note-templates.pen b/design/note-templates.pen new file mode 100644 index 0000000..ebcc2d6 --- /dev/null +++ b/design/note-templates.pen @@ -0,0 +1 @@ +{"children":[],"variables":{}} \ No newline at end of file diff --git a/design/note-type-labels.pen b/design/note-type-labels.pen new file mode 100644 index 0000000..747b5ab --- /dev/null +++ b/design/note-type-labels.pen @@ -0,0 +1 @@ +{"children":[],"variables":{}} diff --git a/design/notelist-multiselect.pen b/design/notelist-multiselect.pen new file mode 100644 index 0000000..8109f8c --- /dev/null +++ b/design/notelist-multiselect.pen @@ -0,0 +1,422 @@ +{ + "children": [ + { + "type": "frame", + "id": "ms_selected", + "name": "Multi-select — 2 notes selected (bulk action bar visible)", + "width": 340, + "height": "fit_content(600)", + "x": 0, + "y": 0, + "fill": "#FFFFFF", + "layout": "vertical", + "theme": { "Mode": "Light" }, + "children": [ + { + "type": "frame", + "id": "ms_hdr1", + "name": "header", + "width": "fill_container", + "height": 52, + "padding": [0, 16], + "alignItems": "center", + "justifyContent": "space_between", + "stroke": { "align": "inside", "fill": "#E5E5E5", "thickness": { "bottom": 1 } }, + "children": [ + { "type": "text", "id": "ms_h1t", "content": "Notes", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 14, "fontWeight": "600" }, + { "type": "frame", "id": "ms_h1icons", "gap": 12, "alignItems": "center", "children": [ + { "type": "text", "id": "ms_h1s", "content": "\uD83D\uDD0D", "fontSize": 14 }, + { "type": "text", "id": "ms_h1p", "content": "+", "fontSize": 16, "fill": "#9B9A97" } + ]} + ] + }, + { + "type": "frame", + "id": "ms_item1", + "name": "item-selected-1", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [14, 16], + "fill": "#EBF5FF", + "stroke": { "align": "inside", "fill": "#E5E5E5", "thickness": { "bottom": 1 } }, + "children": [ + { + "type": "frame", + "id": "ms_i1row", + "width": "fill_container", + "alignItems": "center", + "gap": 8, + "children": [ + { "type": "frame", "id": "ms_i1chk", "width": 16, "height": 16, "cornerRadius": 3, "fill": "#2383E2", "stroke": { "fill": "#2383E2", "thickness": 1 } }, + { "type": "text", "id": "ms_i1t", "content": "Build Laputa App", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 13, "fontWeight": "600" } + ] + }, + { "type": "text", "id": "ms_i1s", "content": "Build a personal knowledge management app.", "fill": "#9B9A97", "fontFamily": "Inter", "fontSize": 12 } + ] + }, + { + "type": "frame", + "id": "ms_item2", + "name": "item-normal", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [14, 16], + "stroke": { "align": "inside", "fill": "#E5E5E5", "thickness": { "bottom": 1 } }, + "children": [ + { + "type": "frame", + "id": "ms_i2row", + "width": "fill_container", + "alignItems": "center", + "gap": 8, + "children": [ + { "type": "frame", "id": "ms_i2chk", "width": 16, "height": 16, "cornerRadius": 3, "stroke": { "fill": "#D4D4D4", "thickness": 1 } }, + { "type": "text", "id": "ms_i2t", "content": "Facebook Ads Strategy", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 13, "fontWeight": "500" } + ] + }, + { "type": "text", "id": "ms_i2s", "content": "Lookalike audiences convert 3x better.", "fill": "#9B9A97", "fontFamily": "Inter", "fontSize": 12 } + ] + }, + { + "type": "frame", + "id": "ms_item3", + "name": "item-selected-2", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [14, 16], + "fill": "#EBF5FF", + "stroke": { "align": "inside", "fill": "#E5E5E5", "thickness": { "bottom": 1 } }, + "children": [ + { + "type": "frame", + "id": "ms_i3row", + "width": "fill_container", + "alignItems": "center", + "gap": 8, + "children": [ + { "type": "frame", "id": "ms_i3chk", "width": 16, "height": 16, "cornerRadius": 3, "fill": "#2383E2", "stroke": { "fill": "#2383E2", "thickness": 1 } }, + { "type": "text", "id": "ms_i3t", "content": "Matteo Cellini", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 13, "fontWeight": "600" } + ] + }, + { "type": "text", "id": "ms_i3s", "content": "Sponsorship manager.", "fill": "#9B9A97", "fontFamily": "Inter", "fontSize": 12 } + ] + }, + { + "type": "frame", + "id": "ms_item4", + "name": "item-normal-2", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [14, 16], + "stroke": { "align": "inside", "fill": "#E5E5E5", "thickness": { "bottom": 1 } }, + "children": [ + { "type": "text", "id": "ms_i4t", "content": "Kickoff Meeting", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 13, "fontWeight": "500" }, + { "type": "text", "id": "ms_i4s", "content": "Project kickoff meeting notes.", "fill": "#9B9A97", "fontFamily": "Inter", "fontSize": 12 } + ] + }, + { "type": "frame", "id": "ms_spacer1", "width": "fill_container", "height": 120 }, + { + "type": "frame", + "id": "ms_bulkbar", + "name": "bulk-action-bar", + "width": "fill_container", + "height": 48, + "padding": [0, 16], + "fill": "#37352F", + "alignItems": "center", + "justifyContent": "space_between", + "children": [ + { "type": "text", "id": "ms_bcount", "content": "2 selected", "fill": "#FFFFFF", "fontFamily": "Inter", "fontSize": 13, "fontWeight": "500" }, + { + "type": "frame", + "id": "ms_bactions", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ms_barchive", + "padding": [6, 12], + "cornerRadius": 6, + "fill": "#FFFFFF20", + "children": [ + { "type": "text", "id": "ms_bat1", "content": "Archive", "fill": "#FFFFFF", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "500" } + ] + }, + { + "type": "frame", + "id": "ms_btrash", + "padding": [6, 12], + "cornerRadius": 6, + "fill": "#E03E3E30", + "children": [ + { "type": "text", "id": "ms_bat2", "content": "Move to Trash", "fill": "#E03E3E", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "500" } + ] + }, + { + "type": "frame", + "id": "ms_bclose", + "padding": [6, 8], + "children": [ + { "type": "text", "id": "ms_bx", "content": "\u2715", "fill": "#FFFFFF80", "fontSize": 14 } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "ms_range", + "name": "Multi-select — range select Shift+click", + "width": 340, + "height": "fit_content(600)", + "x": 400, + "y": 0, + "fill": "#FFFFFF", + "layout": "vertical", + "theme": { "Mode": "Light" }, + "children": [ + { + "type": "frame", + "id": "ms_hdr2", + "name": "header", + "width": "fill_container", + "height": 52, + "padding": [0, 16], + "alignItems": "center", + "justifyContent": "space_between", + "stroke": { "align": "inside", "fill": "#E5E5E5", "thickness": { "bottom": 1 } }, + "children": [ + { "type": "text", "id": "ms_h2t", "content": "Notes", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 14, "fontWeight": "600" }, + { "type": "frame", "id": "ms_h2icons", "gap": 12, "alignItems": "center", "children": [ + { "type": "text", "id": "ms_h2s", "content": "\uD83D\uDD0D", "fontSize": 14 }, + { "type": "text", "id": "ms_h2p", "content": "+", "fontSize": 16, "fill": "#9B9A97" } + ]} + ] + }, + { + "type": "frame", + "id": "ms_r_annotation", + "name": "annotation", + "width": "fill_container", + "padding": [8, 16], + "fill": "#FFF8E1", + "children": [ + { "type": "text", "id": "ms_ra_t", "content": "Shift+Click: range from first-clicked to here", "fill": "#D9730D", "fontFamily": "Inter", "fontSize": 11, "fontWeight": "500" } + ] + }, + { + "type": "frame", + "id": "ms_r1", + "name": "range-anchor", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [14, 16], + "fill": "#EBF5FF", + "stroke": { "align": "inside", "fill": "#2383E2", "thickness": { "left": 3, "bottom": 1 } }, + "children": [ + { "type": "text", "id": "ms_r1t", "content": "Build Laputa App \u2190 anchor (first Cmd+click)", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 13, "fontWeight": "600" }, + { "type": "text", "id": "ms_r1s", "content": "Build a personal knowledge management app.", "fill": "#9B9A97", "fontFamily": "Inter", "fontSize": 12 } + ] + }, + { + "type": "frame", + "id": "ms_r2", + "name": "range-included", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [14, 16], + "fill": "#EBF5FF", + "stroke": { "align": "inside", "fill": "#E5E5E5", "thickness": { "bottom": 1 } }, + "children": [ + { "type": "text", "id": "ms_r2t", "content": "Facebook Ads Strategy (auto-included in range)", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 13, "fontWeight": "500" }, + { "type": "text", "id": "ms_r2s", "content": "Lookalike audiences convert 3x better.", "fill": "#9B9A97", "fontFamily": "Inter", "fontSize": 12 } + ] + }, + { + "type": "frame", + "id": "ms_r3", + "name": "range-end", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [14, 16], + "fill": "#EBF5FF", + "stroke": { "align": "inside", "fill": "#2383E2", "thickness": { "left": 3, "bottom": 1 } }, + "children": [ + { "type": "text", "id": "ms_r3t", "content": "Matteo Cellini \u2190 Shift+click target", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 13, "fontWeight": "600" }, + { "type": "text", "id": "ms_r3s", "content": "Sponsorship manager.", "fill": "#9B9A97", "fontFamily": "Inter", "fontSize": 12 } + ] + }, + { + "type": "frame", + "id": "ms_r4", + "name": "range-outside", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [14, 16], + "stroke": { "align": "inside", "fill": "#E5E5E5", "thickness": { "bottom": 1 } }, + "children": [ + { "type": "text", "id": "ms_r4t", "content": "Kickoff Meeting (outside range)", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 13, "fontWeight": "500" }, + { "type": "text", "id": "ms_r4s", "content": "Project kickoff meeting notes.", "fill": "#9B9A97", "fontFamily": "Inter", "fontSize": 12 } + ] + }, + { "type": "frame", "id": "ms_rspacer", "width": "fill_container", "height": 80 }, + { + "type": "frame", + "id": "ms_rbulkbar", + "name": "bulk-action-bar", + "width": "fill_container", + "height": 48, + "padding": [0, 16], + "fill": "#37352F", + "alignItems": "center", + "justifyContent": "space_between", + "children": [ + { "type": "text", "id": "ms_rbcount", "content": "3 selected", "fill": "#FFFFFF", "fontFamily": "Inter", "fontSize": 13, "fontWeight": "500" }, + { + "type": "frame", + "id": "ms_rbactions", + "gap": 8, + "alignItems": "center", + "children": [ + { "type": "frame", "id": "ms_rbarchive", "padding": [6, 12], "cornerRadius": 6, "fill": "#FFFFFF20", "children": [ + { "type": "text", "id": "ms_rba1", "content": "Archive", "fill": "#FFFFFF", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "500" } + ]}, + { "type": "frame", "id": "ms_rbtrash", "padding": [6, 12], "cornerRadius": 6, "fill": "#E03E3E30", "children": [ + { "type": "text", "id": "ms_rba2", "content": "Move to Trash", "fill": "#E03E3E", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "500" } + ]}, + { "type": "frame", "id": "ms_rbclose", "padding": [6, 8], "children": [ + { "type": "text", "id": "ms_rbx", "content": "\u2715", "fill": "#FFFFFF80", "fontSize": 14 } + ]} + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "ms_empty", + "name": "Multi-select — empty selection / hover state", + "width": 340, + "height": "fit_content(600)", + "x": 800, + "y": 0, + "fill": "#FFFFFF", + "layout": "vertical", + "theme": { "Mode": "Light" }, + "children": [ + { + "type": "frame", + "id": "ms_hdr3", + "name": "header", + "width": "fill_container", + "height": 52, + "padding": [0, 16], + "alignItems": "center", + "justifyContent": "space_between", + "stroke": { "align": "inside", "fill": "#E5E5E5", "thickness": { "bottom": 1 } }, + "children": [ + { "type": "text", "id": "ms_h3t", "content": "Notes", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 14, "fontWeight": "600" }, + { "type": "frame", "id": "ms_h3icons", "gap": 12, "alignItems": "center", "children": [ + { "type": "text", "id": "ms_h3s", "content": "\uD83D\uDD0D", "fontSize": 14 }, + { "type": "text", "id": "ms_h3p", "content": "+", "fontSize": 16, "fill": "#9B9A97" } + ]} + ] + }, + { + "type": "frame", + "id": "ms_e_annot", + "name": "annotation", + "width": "fill_container", + "padding": [8, 16], + "fill": "#F5F5F5", + "children": [ + { "type": "text", "id": "ms_ea_t", "content": "No selection \u2014 Cmd+Click to select, Shift+Click for range", "fill": "#9B9A97", "fontFamily": "Inter", "fontSize": 11 } + ] + }, + { + "type": "frame", + "id": "ms_e1", + "name": "item-active", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [14, 13, 14, 16], + "stroke": { "align": "inside", "fill": "#2383E2", "thickness": { "left": 3, "bottom": 1 } }, + "fill": "#EBF5FF", + "children": [ + { "type": "text", "id": "ms_e1t", "content": "Build Laputa App (current note \u2014 active)", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 13, "fontWeight": "600" }, + { "type": "text", "id": "ms_e1s", "content": "Build a personal knowledge management app.", "fill": "#9B9A97", "fontFamily": "Inter", "fontSize": 12 } + ] + }, + { + "type": "frame", + "id": "ms_e2", + "name": "item-hover", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [14, 16], + "fill": "#F5F5F4", + "stroke": { "align": "inside", "fill": "#E5E5E5", "thickness": { "bottom": 1 } }, + "children": [ + { "type": "text", "id": "ms_e2t", "content": "Facebook Ads Strategy (hover state)", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 13, "fontWeight": "500" }, + { "type": "text", "id": "ms_e2s", "content": "Lookalike audiences convert 3x better.", "fill": "#9B9A97", "fontFamily": "Inter", "fontSize": 12 } + ] + }, + { + "type": "frame", + "id": "ms_e3", + "name": "item-default", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [14, 16], + "stroke": { "align": "inside", "fill": "#E5E5E5", "thickness": { "bottom": 1 } }, + "children": [ + { "type": "text", "id": "ms_e3t", "content": "Matteo Cellini", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 13, "fontWeight": "500" }, + { "type": "text", "id": "ms_e3s", "content": "Sponsorship manager.", "fill": "#9B9A97", "fontFamily": "Inter", "fontSize": 12 } + ] + }, + { + "type": "frame", + "id": "ms_e4", + "name": "item-default-2", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [14, 16], + "stroke": { "align": "inside", "fill": "#E5E5E5", "thickness": { "bottom": 1 } }, + "children": [ + { "type": "text", "id": "ms_e4t", "content": "Kickoff Meeting", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 13, "fontWeight": "500" }, + { "type": "text", "id": "ms_e4s", "content": "Project kickoff meeting notes.", "fill": "#9B9A97", "fontFamily": "Inter", "fontSize": 12 } + ] + }, + { + "type": "frame", + "id": "ms_e_nobar", + "name": "no-bar-annotation", + "width": "fill_container", + "padding": [12, 16], + "fill": "#F5F5F5", + "children": [ + { "type": "text", "id": "ms_ena_t", "content": "\u2191 No bulk action bar when nothing is selected", "fill": "#9B9A97", "fontFamily": "Inter", "fontSize": 11 } + ] + } + ] + } + ], + "variables": {} +} \ No newline at end of file diff --git a/design/notes-type-icon-search.pen b/design/notes-type-icon-search.pen new file mode 100644 index 0000000..ebcc2d6 --- /dev/null +++ b/design/notes-type-icon-search.pen @@ -0,0 +1 @@ +{"children":[],"variables":{}} \ No newline at end of file diff --git a/design/properties-panel-header.pen b/design/properties-panel-header.pen new file mode 100644 index 0000000..ebcc2d6 --- /dev/null +++ b/design/properties-panel-header.pen @@ -0,0 +1 @@ +{"children":[],"variables":{}} \ No newline at end of file diff --git a/design/property-value-input.pen b/design/property-value-input.pen new file mode 100644 index 0000000..3f78a1e --- /dev/null +++ b/design/property-value-input.pen @@ -0,0 +1 @@ +{"children":[{"children":[{"content":"Text Input (default editing mode)","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"normal","id":"gfFsr","name":"lbl1","type":"text"},{"alignItems":"center","children":[{"content":"OWNER","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","id":"U7qrk","letterSpacing":1.2,"name":"k1","type":"text"},{"content":"Luca","fill":"$--secondary-foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"xus3c","name":"v1","type":"text"}],"cornerRadius":4,"fill":"$--bg-hover-subtle","id":"y19Vl","name":"Property Row — owner","padding":[4,6],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"content":"CADENCE","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","id":"iXGSk","letterSpacing":1.2,"name":"k2","type":"text"},{"children":[{"content":"Weekly","fill":"$--foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"Dp7NE","name":"inpTxt","type":"text"}],"cornerRadius":4,"fill":"$--muted","id":"jDfek","name":"Text Input Active","padding":[4,8],"stroke":{"align":"inside","fill":"$--ring","thickness":1},"type":"frame","width":"fill_container"}],"cornerRadius":4,"id":"lBJ6j","name":"Property Row — cadence (editing)","padding":[4,6],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"content":"DESCRIPTION","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","id":"wNhQR","letterSpacing":1.2,"name":"k3","type":"text"},{"content":"My project notes","fill":"$--secondary-foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"TDgEG","name":"v3","type":"text"}],"cornerRadius":4,"id":"rQySL","name":"Property Row — description","padding":[4,6],"type":"frame","width":"fill_container"}],"fill":"$--background","gap":8,"id":"ue2qN","layout":"vertical","name":"Property Value — Text Input (default)","padding":12,"type":"frame","width":300,"x":0,"y":0},{"children":[{"content":"Date Picker (calendar popover on click)","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"normal","id":"U8pRy","name":"lbl2","type":"text"},{"alignItems":"center","children":[{"content":"DEADLINE","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","id":"jVJZw","letterSpacing":1.2,"name":"kD1","type":"text"},{"alignItems":"center","children":[{"fill":"$--muted-foreground","height":12,"iconFontFamily":"lucide","iconFontName":"calendar","id":"9LGQq","name":"calIco","type":"icon_font","width":12},{"content":"Mar 31, 2026","fill":"$--secondary-foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"huIJ2","name":"dateTxt","type":"text"}],"cornerRadius":4,"gap":4,"id":"2Nx8W","name":"Date Button","padding":[2,4],"type":"frame"}],"cornerRadius":4,"fill":"$--bg-hover-subtle","id":"gAwzs","name":"Property Row — deadline (date display)","padding":[4,6],"type":"frame","width":"fill_container"},{"children":[{"alignItems":"center","children":[{"fill":"$--muted-foreground","height":16,"iconFontFamily":"lucide","iconFontName":"chevron-left","id":"jGsuq","name":"calNav1","type":"icon_font","width":16},{"content":"March 2026","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"600","id":"Q0Hbu","name":"calMo","type":"text"},{"fill":"$--muted-foreground","height":16,"iconFontFamily":"lucide","iconFontName":"chevron-right","id":"fo6jZ","name":"calNav2","type":"icon_font","width":16}],"id":"NTfdy","name":"Calendar Header","type":"frame","width":"fill_container"},{"content":"Su Mo Tu We Th Fr Sa","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"normal","id":"5XDoM","letterSpacing":0.5,"name":"calDays","type":"text"},{"children":[{"content":" 1 2 3 4 5 6 7","fill":"$--foreground","fontFamily":"IBM Plex Mono","fontSize":11,"fontWeight":"normal","id":"phwKq","name":"calW1","type":"text"},{"content":" 8 9 10 11 12 13 14","fill":"$--foreground","fontFamily":"IBM Plex Mono","fontSize":11,"fontWeight":"normal","id":"hIyef","name":"calW2","type":"text"},{"content":"15 16 17 18 19 20 21","fill":"$--foreground","fontFamily":"IBM Plex Mono","fontSize":11,"fontWeight":"normal","id":"N9zUH","name":"calW3","type":"text"},{"content":"22 23 24 25 26 27 28","fill":"$--foreground","fontFamily":"IBM Plex Mono","fontSize":11,"fontWeight":"normal","id":"02wVP","name":"calW4","type":"text"},{"content":"29 30 [31]","fill":"$--primary","fontFamily":"IBM Plex Mono","fontSize":11,"fontWeight":"normal","id":"pm7vP","name":"calW5","type":"text"}],"gap":2,"id":"obkXY","layout":"vertical","name":"Day Grid (representation)","type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"fill":"$--muted-foreground","height":12,"iconFontFamily":"lucide","iconFontName":"x","id":"cPTHQ","name":"clearIco","type":"icon_font","width":12},{"content":"Clear date","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"z5fZw","name":"clearTxt","type":"text"}],"gap":4,"id":"T2eel","name":"Clear Date Row","padding":[8,0,0,0],"stroke":{"align":"inside","fill":"$--border","thickness":{"top":1}},"type":"frame","width":"fill_container"}],"cornerRadius":8,"effect":{"blur":12,"color":"#00000015","offset":{"x":0,"y":4},"shadowType":"outer","type":"shadow"},"fill":"$--popover","gap":8,"id":"Ft17I","layout":"vertical","name":"Calendar Popover (representation)","padding":12,"stroke":{"align":"inside","fill":"$--border","thickness":1},"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"content":"START DATE","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","id":"HNo7P","letterSpacing":1.2,"name":"kD2","type":"text"},{"alignItems":"center","children":[{"fill":"$--muted-foreground","height":12,"iconFontFamily":"lucide","iconFontName":"calendar","id":"kKyym","name":"calIco2","type":"icon_font","width":12},{"content":"Pick a date...","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"w5Yim","name":"dateTxt2","type":"text"}],"cornerRadius":4,"gap":4,"id":"FShL0","name":"Date Button — empty","padding":[2,4],"type":"frame"}],"cornerRadius":4,"id":"6y8Ld","name":"Property Row — date (empty value with picker)","padding":[4,6],"type":"frame","width":"fill_container"},{"content":"Calendar opens for any value when display mode is date","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":10,"fontWeight":"normal","id":"AOnAh","name":"noteD","textGrowth":"fixed-width","type":"text","width":"fill_container"}],"fill":"$--background","gap":8,"id":"3kOd1","layout":"vertical","name":"Property Value — Date Picker (calendar open)","padding":12,"type":"frame","width":300,"x":400,"y":0},{"children":[{"content":"Boolean Toggle (yes/no click-to-flip)","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"normal","id":"Vi8uD","name":"lbl3","type":"text"},{"alignItems":"center","children":[{"content":"ARCHIVED","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","id":"5zOMO","letterSpacing":1.2,"name":"kB1","type":"text"},{"children":[{"content":"✗ No","fill":"$--secondary-foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"2jdhQ","name":"vB1t","type":"text"}],"cornerRadius":4,"fill":"transparent","id":"8aj9u","name":"Boolean Button — No","padding":[2,8],"stroke":{"align":"inside","fill":"$--border","thickness":1},"type":"frame"}],"cornerRadius":4,"id":"EPVN2","name":"Property Row — archived (false)","padding":[4,6],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"content":"PUBLISHED","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","id":"J4kqn","letterSpacing":1.2,"name":"kB2","type":"text"},{"children":[{"content":"✓ Yes","fill":"$--accent-green","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"PTt7N","name":"vB2t","type":"text"}],"cornerRadius":4,"fill":"transparent","id":"2Kc2p","name":"Boolean Button — Yes","padding":[2,8],"stroke":{"align":"inside","fill":"$--border","thickness":1},"type":"frame"}],"cornerRadius":4,"fill":"$--bg-hover-subtle","id":"Mx1EU","name":"Property Row — published (true)","padding":[4,6],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"content":"DRAFT","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","id":"y9lFO","letterSpacing":1.2,"name":"kB3","type":"text"},{"children":[{"content":"✓ Yes","fill":"$--accent-green","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"MMOH6","name":"vB3t","type":"text"}],"cornerRadius":4,"fill":"transparent","id":"KEvKa","name":"Boolean Button — Yes (from string)","padding":[2,8],"stroke":{"align":"inside","fill":"$--border","thickness":1},"type":"frame"}],"cornerRadius":4,"id":"O2K4k","name":"Property Row — draft (true, string)","padding":[4,6],"type":"frame","width":"fill_container"},{"content":"Handles both native boolean and string true/false values","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":10,"fontWeight":"normal","id":"9hRyB","name":"note3","textGrowth":"fixed-width","type":"text","width":"fill_container"}],"fill":"$--background","gap":8,"id":"MYYnw","layout":"vertical","name":"Property Value — Boolean Toggle","padding":12,"type":"frame","width":300,"x":0,"y":420},{"children":[{"content":"Status Dropdown (searchable with vault + suggested)","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"normal","id":"4wWho","name":"lbl4","type":"text"},{"alignItems":"center","children":[{"content":"STATUS","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","id":"G0vke","letterSpacing":1.2,"name":"kS1","type":"text"},{"children":[{"content":"ACTIVE","fill":"$--accent-green","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","id":"wZ0aD","letterSpacing":1.2,"name":"statusTxt","type":"text"}],"cornerRadius":16,"fill":"$--accent-green-light","id":"lmfgA","name":"Status Pill — Active","padding":[1,6],"type":"frame"}],"cornerRadius":4,"fill":"$--bg-hover-subtle","id":"EkmCc","name":"Property Row — status","padding":[4,6],"type":"frame","width":"fill_container"},{"children":[{"children":[{"content":"Type a status...","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"EBbCZ","name":"searchTxt","type":"text"}],"id":"lo69T","name":"Search Input","padding":[6,8],"stroke":{"align":"inside","fill":"$--border","thickness":{"bottom":1}},"type":"frame","width":"fill_container"},{"children":[{"content":"FROM VAULT","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":9,"fontWeight":"500","id":"WD9o4","letterSpacing":1.2,"name":"vaultTxt","type":"text"}],"id":"aU6V8","name":"Vault Section","padding":[4,8],"type":"frame","width":"fill_container"},{"children":[{"children":[{"content":"ACTIVE","fill":"$--accent-green","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","id":"ORffY","letterSpacing":1.2,"name":"pill1t","type":"text"}],"cornerRadius":16,"fill":"$--accent-green-light","id":"6X6T5","name":"pill1","padding":[1,6],"type":"frame"}],"cornerRadius":4,"fill":"$--muted","id":"aCzSa","name":"Option — Active (highlighted)","padding":[4,8],"type":"frame","width":"fill_container"},{"children":[{"children":[{"content":"DONE","fill":"$--accent-blue","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","id":"BNI2j","letterSpacing":1.2,"name":"pill2t","type":"text"}],"cornerRadius":16,"fill":"$--accent-blue-light","id":"dCU52","name":"pill2","padding":[1,6],"type":"frame"}],"id":"RMOKs","name":"Option — Done","padding":[4,8],"type":"frame","width":"fill_container"},{"children":[{"children":[{"content":"PAUSED","fill":"$--accent-orange","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","id":"JCZHo","letterSpacing":1.2,"name":"pill3t","type":"text"}],"cornerRadius":16,"fill":"$--accent-orange-light","id":"t820y","name":"pill3","padding":[1,6],"type":"frame"}],"id":"xGNr9","name":"Option — Paused","padding":[4,8],"type":"frame","width":"fill_container"},{"fill":"$--border","height":1,"id":"tvMv5","name":"sep1","type":"frame","width":"fill_container"},{"children":[{"content":"SUGGESTED","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":9,"fontWeight":"500","id":"nG95l","letterSpacing":1.2,"name":"sugTxt","type":"text"}],"id":"taNZ2","name":"Suggested Section","padding":[4,8],"type":"frame","width":"fill_container"},{"children":[{"children":[{"content":"IN PROGRESS","fill":"$--accent-yellow","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","id":"cNzSe","letterSpacing":1.2,"name":"pill4t","type":"text"}],"cornerRadius":16,"fill":"$--accent-yellow-light","id":"LPqKJ","name":"pill4","padding":[1,6],"type":"frame"}],"id":"YZUM8","name":"Option — In Progress","padding":[4,8],"type":"frame","width":"fill_container"},{"children":[{"children":[{"content":"BLOCKED","fill":"$--accent-red","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","id":"9drsD","letterSpacing":1.2,"name":"pill5t","type":"text"}],"cornerRadius":16,"fill":"$--accent-red-light","id":"ixyZV","name":"pill5","padding":[1,6],"type":"frame"}],"id":"snwbA","name":"Option — Blocked","padding":[4,8],"type":"frame","width":"fill_container"},{"height":4,"id":"nKzOO","name":"pad","type":"frame","width":"fill_container"}],"clip":true,"cornerRadius":8,"effect":{"blur":12,"color":"#00000015","offset":{"x":0,"y":4},"shadowType":"outer","type":"shadow"},"fill":"$--popover","id":"oHRUi","layout":"vertical","name":"Status Dropdown Popover","stroke":{"align":"inside","fill":"$--border","thickness":1},"type":"frame","width":"fill_container"}],"fill":"$--background","gap":8,"id":"aKgqZ","layout":"vertical","name":"Property Value — Status Dropdown (open)","padding":12,"type":"frame","width":300,"x":400,"y":420}],"variables":{"--accent-blue":{"type":"color","value":"#2383E2"},"--accent-blue-light":{"type":"color","value":"#D3E5EF"},"--accent-green":{"type":"color","value":"#0F7B6C"},"--accent-green-light":{"type":"color","value":"#DBEDDB"},"--accent-orange":{"type":"color","value":"#D9730D"},"--accent-orange-light":{"type":"color","value":"#FADEC9"},"--accent-purple":{"type":"color","value":"#9065B0"},"--accent-purple-light":{"type":"color","value":"#E8DEEE"},"--accent-red":{"type":"color","value":"#E03E3E"},"--accent-red-light":{"type":"color","value":"#FFE2DD"},"--accent-yellow":{"type":"color","value":"#DFAB01"},"--accent-yellow-light":{"type":"color","value":"#FDE68A"},"--background":{"type":"color","value":"#FFFFFF"},"--bg-hover-subtle":{"type":"color","value":"#F5F5F4"},"--border":{"type":"color","value":"#E5E5E2"},"--foreground":{"type":"color","value":"#37352F"},"--muted":{"type":"color","value":"#F5F5F4"},"--muted-foreground":{"type":"color","value":"#91918E"},"--popover":{"type":"color","value":"#FFFFFF"},"--popover-foreground":{"type":"color","value":"#37352F"},"--primary":{"type":"color","value":"#2383E2"},"--ring":{"type":"color","value":"#2383E2"},"--secondary-foreground":{"type":"color","value":"#37352F"}}} \ No newline at end of file diff --git a/design/raw-editor-mode.pen b/design/raw-editor-mode.pen new file mode 100644 index 0000000..747b5ab --- /dev/null +++ b/design/raw-editor-mode.pen @@ -0,0 +1 @@ +{"children":[],"variables":{}} diff --git a/design/relationship-x-cosmetic.pen b/design/relationship-x-cosmetic.pen new file mode 100644 index 0000000..b6abb9d --- /dev/null +++ b/design/relationship-x-cosmetic.pen @@ -0,0 +1,165 @@ +{ + "children": [ + { + "type": "frame", + "id": "rel_x_default", + "name": "Relationship Pill — Default (X hidden)", + "x": 0, + "y": 0, + "width": 280, + "height": "fit_content(40)", + "fill": "$--background", + "layout": "vertical", + "gap": 8, + "padding": [ + 16, + 16 + ], + "theme": { + "Mode": "Light" + }, + "children": [ + { + "type": "text", + "id": "rel_x_default_label", + "content": "Relationship Pill — Default State", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "rel_x_pill_default", + "layout": "horizontal", + "gap": 4, + "padding": [ + 4, + 8 + ], + "fill": "$--accent", + "borderRadius": 6, + "width": "fit_content", + "height": "fit_content", + "children": [ + { + "type": "text", + "id": "rel_x_pill_label", + "content": "Note Title", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "rectangle", + "id": "rel_x_type_icon", + "width": 14, + "height": 14, + "fill": "$--muted-foreground", + "opacity": 0.5, + "borderRadius": 2 + } + ] + }, + { + "type": "text", + "id": "rel_x_note_default", + "content": "X button not visible in default state", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 10 + } + ] + }, + { + "type": "frame", + "id": "rel_x_hover", + "name": "Relationship Pill — Hover (X visible)", + "x": 320, + "y": 0, + "width": 280, + "height": "fit_content(40)", + "fill": "$--background", + "layout": "vertical", + "gap": 8, + "padding": [ + 16, + 16 + ], + "theme": { + "Mode": "Light" + }, + "children": [ + { + "type": "text", + "id": "rel_x_hover_label", + "content": "Relationship Pill — Hover State", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "rel_x_pill_hover", + "layout": "horizontal", + "gap": 6, + "padding": [ + 4, + 8 + ], + "fill": "$--accent", + "borderRadius": 6, + "width": "fit_content", + "height": "fit_content", + "children": [ + { + "type": "text", + "id": "rel_x_pill_label_hover", + "content": "Note Title", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "rel_x_trailing", + "layout": "horizontal", + "gap": 6, + "children": [ + { + "type": "text", + "id": "rel_x_icon", + "content": "×", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "400" + }, + { + "type": "rectangle", + "id": "rel_x_type_icon_hover", + "width": 14, + "height": 14, + "fill": "$--muted-foreground", + "opacity": 0.5, + "borderRadius": 2 + } + ] + } + ] + }, + { + "type": "text", + "id": "rel_x_note_hover", + "content": "X appears inline as flex child, before type icon. Ring border on hover.", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 10 + } + ] + } + ] +} \ No newline at end of file diff --git a/design/search-bundle-qmd.pen b/design/search-bundle-qmd.pen new file mode 100644 index 0000000..759adb9 --- /dev/null +++ b/design/search-bundle-qmd.pen @@ -0,0 +1 @@ +{"children":[{"id":"status-bar-indexing","type":"frame","name":"StatusBar - Indexing Progress States","layout":"vertical","x":0,"y":0,"width":1400,"height":400,"gap":24,"padding":24,"fill":"#F7F6F3","children":[{"id":"title","type":"text","content":"StatusBar Indexing Progress - search-bundle-qmd","fontSize":20,"fontWeight":"600","fill":"#37352F"},{"id":"desc","type":"text","content":"The indexing badge appears in the StatusBar left section, after the pending changes badge. It shows the current indexing phase with a spinner icon (Loader2) during active phases and a Search icon when complete.","fontSize":14,"fill":"#9B9A97","width":900},{"id":"states","type":"frame","name":"States","layout":"vertical","gap":16,"width":1350,"children":[{"id":"state-idle","type":"frame","layout":"horizontal","gap":8,"alignItems":"center","children":[{"id":"label-idle","type":"text","content":"idle:","fontSize":12,"fontWeight":"600","fill":"#9B9A97","width":100},{"id":"val-idle","type":"text","content":"(hidden - no badge shown)","fontSize":12,"fill":"#9B9A97"}]},{"id":"state-installing","type":"frame","layout":"horizontal","gap":8,"alignItems":"center","children":[{"id":"label-install","type":"text","content":"installing:","fontSize":12,"fontWeight":"600","fill":"#3b82f6","width":100},{"id":"val-install","type":"text","content":"| [spinner] Installing search\u2026","fontSize":12,"fill":"#3b82f6"}]},{"id":"state-scanning","type":"frame","layout":"horizontal","gap":8,"alignItems":"center","children":[{"id":"label-scan","type":"text","content":"scanning:","fontSize":12,"fontWeight":"600","fill":"#3b82f6","width":100},{"id":"val-scan","type":"text","content":"| [spinner] Indexing\u2026 342/1,057","fontSize":12,"fill":"#3b82f6"}]},{"id":"state-embedding","type":"frame","layout":"horizontal","gap":8,"alignItems":"center","children":[{"id":"label-embed","type":"text","content":"embedding:","fontSize":12,"fontWeight":"600","fill":"#3b82f6","width":100},{"id":"val-embed","type":"text","content":"| [spinner] Embedding\u2026 50/200","fontSize":12,"fill":"#3b82f6"}]},{"id":"state-complete","type":"frame","layout":"horizontal","gap":8,"alignItems":"center","children":[{"id":"label-done","type":"text","content":"complete:","fontSize":12,"fontWeight":"600","fill":"#22c55e","width":100},{"id":"val-done","type":"text","content":"| [search icon] Index ready (auto-dismisses after 5s)","fontSize":12,"fill":"#22c55e"}]},{"id":"state-error","type":"frame","layout":"horizontal","gap":8,"alignItems":"center","children":[{"id":"label-err","type":"text","content":"error:","fontSize":12,"fontWeight":"600","fill":"#f97316","width":100},{"id":"val-err","type":"text","content":"| [search icon] Index error (orange accent)","fontSize":12,"fill":"#f97316"}]}]},{"id":"flow","type":"frame","name":"Flow","layout":"vertical","gap":8,"width":1350,"children":[{"id":"flow-title","type":"text","content":"Auto-indexing Flow","fontSize":16,"fontWeight":"600","fill":"#37352F"},{"id":"flow-desc","type":"text","content":"1. Vault opens \u2192 useIndexing checks index status via get_index_status\n2. If qmd missing or collection missing or pending embeds \u2192 triggers start_indexing\n3. Rust emits indexing-progress events (installing \u2192 scanning \u2192 embedding \u2192 complete)\n4. StatusBar shows IndexingBadge with spinner + phase label + progress counts\n5. On file save \u2192 trigger_incremental_index runs qmd update in background\n6. Complete phase auto-dismisses after 5 seconds","fontSize":13,"fill":"#37352F","width":900}]}]}],"variables":{}} \ No newline at end of file diff --git a/design/search-results-subtitle.pen b/design/search-results-subtitle.pen new file mode 100644 index 0000000..8c33764 --- /dev/null +++ b/design/search-results-subtitle.pen @@ -0,0 +1,665 @@ +{ + "children": [ + { + "type": "frame", + "id": "srs_main", + "name": "Search Results — subtitle with metadata", + "x": 0, + "y": 0, + "width": 540, + "fill": "$--card", + "cornerRadius": 12, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "shadow": [ + { + "x": 0, + "y": 8, + "blur": 32, + "spread": -4, + "fill": "#00000020" + } + ], + "layout": "vertical", + "clip": true, + "theme": { + "Mode": "Light" + }, + "children": [ + { + "type": "frame", + "id": "srs_inputRow", + "name": "searchInputRow", + "width": "fill_container", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "gap": 8, + "padding": [14, 16], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "srs_searchIcon", + "width": 18, + "height": 18, + "iconFontName": "magnifying-glass", + "iconFontFamily": "phosphor", + "fill": "$--primary" + }, + { + "type": "text", + "id": "srs_query", + "fill": "$--foreground", + "content": "time management", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "srs_resultsHeader", + "name": "resultsHeader", + "width": "fill_container", + "padding": [8, 16], + "alignItems": "center", + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "srs_count", + "fill": "$--muted-foreground", + "content": "3 results \u00b7 45ms", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "srs_result1", + "name": "searchResult1 (selected)", + "width": "fill_container", + "fill": "$--accent-blue-light", + "layout": "vertical", + "gap": 4, + "padding": [10, 16], + "children": [ + { + "type": "frame", + "id": "srs_r1_titleRow", + "name": "titleRow", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "srs_r1_title", + "fill": "$--foreground", + "content": "Time Management Strategies", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "srs_r1_badge", + "name": "typeBadge", + "fill": "$--accent-green-light", + "cornerRadius": 4, + "padding": [2, 6], + "children": [ + { + "type": "text", + "id": "srs_r1_badgeTxt", + "fill": "$--accent-green", + "content": "Evergreen", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "srs_r1_meta", + "name": "metadataRow", + "width": "fill_container", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "srs_r1_date", + "fill": "$--muted-foreground", + "content": "2d ago", + "fontFamily": "Inter", + "fontSize": 11 + }, + { + "type": "text", + "id": "srs_r1_sep1", + "fill": "$--muted-foreground", + "content": "\u00b7", + "fontFamily": "Inter", + "fontSize": 11 + }, + { + "type": "text", + "id": "srs_r1_words", + "fill": "$--muted-foreground", + "content": "1,247 words", + "fontFamily": "Inter", + "fontSize": 11 + }, + { + "type": "text", + "id": "srs_r1_sep2", + "fill": "$--muted-foreground", + "content": "\u00b7", + "fontFamily": "Inter", + "fontSize": 11 + }, + { + "type": "text", + "id": "srs_r1_links", + "fill": "$--muted-foreground", + "content": "5 links", + "fontFamily": "Inter", + "fontSize": 11 + } + ] + } + ] + }, + { + "type": "frame", + "id": "srs_result2", + "name": "searchResult2", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [10, 16], + "children": [ + { + "type": "frame", + "id": "srs_r2_titleRow", + "name": "titleRow", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "srs_r2_title", + "fill": "$--foreground", + "content": "Weekly Review Process", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "srs_r2_badge", + "name": "typeBadge", + "fill": "$--accent-purple-light", + "cornerRadius": 4, + "padding": [2, 6], + "children": [ + { + "type": "text", + "id": "srs_r2_badgeTxt", + "fill": "$--accent-purple", + "content": "Procedure", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "srs_r2_meta", + "name": "metadataRow", + "width": "fill_container", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "srs_r2_date", + "fill": "$--muted-foreground", + "content": "5h ago", + "fontFamily": "Inter", + "fontSize": 11 + }, + { + "type": "text", + "id": "srs_r2_sep1", + "fill": "$--muted-foreground", + "content": "\u00b7", + "fontFamily": "Inter", + "fontSize": 11 + }, + { + "type": "text", + "id": "srs_r2_created", + "fill": "$--muted-foreground", + "content": "Created Jan 15", + "fontFamily": "Inter", + "fontSize": 11 + }, + { + "type": "text", + "id": "srs_r2_sep2", + "fill": "$--muted-foreground", + "content": "\u00b7", + "fontFamily": "Inter", + "fontSize": 11 + }, + { + "type": "text", + "id": "srs_r2_words", + "fill": "$--muted-foreground", + "content": "856 words", + "fontFamily": "Inter", + "fontSize": 11 + }, + { + "type": "text", + "id": "srs_r2_sep3", + "fill": "$--muted-foreground", + "content": "\u00b7", + "fontFamily": "Inter", + "fontSize": 11 + }, + { + "type": "text", + "id": "srs_r2_links", + "fill": "$--muted-foreground", + "content": "12 links", + "fontFamily": "Inter", + "fontSize": 11 + } + ] + } + ] + }, + { + "type": "frame", + "id": "srs_result3", + "name": "searchResult3", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [10, 16, 14, 16], + "children": [ + { + "type": "frame", + "id": "srs_r3_titleRow", + "name": "titleRow", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "srs_r3_title", + "fill": "$--foreground", + "content": "Q1 Planning", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "srs_r3_meta", + "name": "metadataRow", + "width": "fill_container", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "srs_r3_date", + "fill": "$--muted-foreground", + "content": "Jan 3", + "fontFamily": "Inter", + "fontSize": 11 + }, + { + "type": "text", + "id": "srs_r3_sep1", + "fill": "$--muted-foreground", + "content": "\u00b7", + "fontFamily": "Inter", + "fontSize": 11 + }, + { + "type": "text", + "id": "srs_r3_words", + "fill": "$--muted-foreground", + "content": "2,103 words", + "fontFamily": "Inter", + "fontSize": 11 + } + ] + } + ] + }, + { + "type": "frame", + "id": "srs_footer", + "name": "searchFooter", + "width": "fill_container", + "fill": "$--muted", + "stroke": { + "align": "inside", + "thickness": { + "top": 1 + }, + "fill": "$--border" + }, + "gap": 16, + "padding": [8, 16], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "srs_hint1", + "fill": "$--muted-foreground", + "content": "\u2191\u2193 Navigate", + "fontFamily": "Inter", + "fontSize": 11 + }, + { + "type": "text", + "id": "srs_hint2", + "fill": "$--muted-foreground", + "content": "\u21b5 Open", + "fontFamily": "Inter", + "fontSize": 11 + }, + { + "type": "text", + "id": "srs_hint3", + "fill": "$--muted-foreground", + "content": "Esc Close", + "fontFamily": "Inter", + "fontSize": 11 + } + ] + } + ] + }, + { + "type": "frame", + "id": "srs_states", + "name": "Search Results — subtitle states", + "x": 600, + "y": 0, + "width": 540, + "layout": "vertical", + "gap": 24, + "padding": [24, 24], + "fill": "$--background", + "cornerRadius": 12, + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "$--border" + }, + "theme": { + "Mode": "Light" + }, + "children": [ + { + "type": "text", + "id": "srs_states_title", + "fill": "$--foreground", + "content": "Metadata Subtitle — States", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "srs_state1", + "name": "State: all metadata present", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [10, 16], + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "srs_s1_label", + "fill": "$--accent-blue", + "content": "All metadata present (modified \u2260 created)", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + }, + { + "type": "text", + "id": "srs_s1_title", + "fill": "$--foreground", + "content": "Time Management Strategies", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "text", + "id": "srs_s1_meta", + "fill": "$--muted-foreground", + "content": "2d ago \u00b7 Created Jan 15 \u00b7 1,247 words \u00b7 5 links", + "fontFamily": "Inter", + "fontSize": 11 + } + ] + }, + { + "type": "frame", + "id": "srs_state2", + "name": "State: no created date (same as modified)", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [10, 16], + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "srs_s2_label", + "fill": "$--accent-blue", + "content": "Modified = Created (skip 'Created' field)", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + }, + { + "type": "text", + "id": "srs_s2_title", + "fill": "$--foreground", + "content": "Quick Note", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "text", + "id": "srs_s2_meta", + "fill": "$--muted-foreground", + "content": "just now \u00b7 342 words \u00b7 2 links", + "fontFamily": "Inter", + "fontSize": 11 + } + ] + }, + { + "type": "frame", + "id": "srs_state3", + "name": "State: no links (zero outgoingLinks)", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [10, 16], + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "srs_s3_label", + "fill": "$--accent-blue", + "content": "No links (omitted from metadata)", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + }, + { + "type": "text", + "id": "srs_s3_title", + "fill": "$--foreground", + "content": "Random Thought", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "text", + "id": "srs_s3_meta", + "fill": "$--muted-foreground", + "content": "3h ago \u00b7 128 words", + "fontFamily": "Inter", + "fontSize": 11 + } + ] + }, + { + "type": "frame", + "id": "srs_state4", + "name": "State: empty note (zero words)", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [10, 16], + "stroke": { + "align": "inside", + "thickness": { + "bottom": 1 + }, + "fill": "$--border" + }, + "children": [ + { + "type": "text", + "id": "srs_s4_label", + "fill": "$--accent-blue", + "content": "Empty note (word count shows 'Empty')", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + }, + { + "type": "text", + "id": "srs_s4_title", + "fill": "$--foreground", + "content": "Untitled Draft", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "text", + "id": "srs_s4_meta", + "fill": "$--muted-foreground", + "content": "5d ago \u00b7 Empty", + "fontFamily": "Inter", + "fontSize": 11 + } + ] + }, + { + "type": "frame", + "id": "srs_state5", + "name": "State: no date at all", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [10, 16], + "children": [ + { + "type": "text", + "id": "srs_s5_label", + "fill": "$--accent-blue", + "content": "No date available (graceful degradation)", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600" + }, + { + "type": "text", + "id": "srs_s5_title", + "fill": "$--foreground", + "content": "Legacy Import", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "text", + "id": "srs_s5_meta", + "fill": "$--muted-foreground", + "content": "89 words \u00b7 1 link", + "fontFamily": "Inter", + "fontSize": 11 + } + ] + } + ] + } + ], + "variables": {} +} \ No newline at end of file diff --git a/design/search-subtitle-metadata.pen b/design/search-subtitle-metadata.pen new file mode 100644 index 0000000..fe4ea0c --- /dev/null +++ b/design/search-subtitle-metadata.pen @@ -0,0 +1,436 @@ +{"children":[ + { + "type": "frame", + "id": "f_search_meta", + "name": "Search Result — with metadata subtitle", + "x": 0, + "y": 0, + "width": 540, + "height": "fit_content", + "fill": "$--search-bg", + "layout": "vertical", + "padding": [24, 0], + "gap": 0, + "theme": { "Mode": "Light" }, + "children": [ + { + "type": "text", + "id": "f1_heading", + "content": "Search Result Item — with metadata subtitle", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "700", + "width": "fill_container", + "padding": [0, 16, 12, 16] + }, + { + "type": "text", + "id": "f1_desc", + "content": "Each search result now shows a metadata line below the snippet: modified date, word count, and link count. The type badge remains next to the title.", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 11, + "lineHeight": 1.5, + "width": "fill_container", + "padding": [0, 16, 16, 16] + }, + { + "type": "frame", + "id": "f1_countbar", + "width": "fill_container", + "padding": [6, 16], + "stroke": { "align": "inside", "fill": "$--search-border", "thickness": { "bottom": 1 } }, + "children": [ + { + "type": "text", + "id": "f1_count", + "content": "12 results · 148ms", + "fill": "$--search-highlight", + "fontFamily": "Inter", + "fontSize": 11 + } + ] + }, + { + "type": "frame", + "id": "f1_result1", + "name": "Result 1 — Selected", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [10, 16], + "fill": "$--accent", + "children": [ + { + "type": "frame", + "id": "f1_r1_titlerow", + "width": "fill_container", + "alignItems": "center", + "gap": 8, + "children": [ + { + "type": "text", + "id": "f1_r1_title", + "content": "How to Manage your Time", + "fill": "$--search-heading", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "f1_r1_badge", + "cornerRadius": 3, + "fill": "$--secondary", + "padding": [2, 6], + "children": [ + { + "type": "text", + "id": "f1_r1_badgeText", + "content": "Essay", + "fill": "$--search-text-muted", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "text", + "id": "f1_r1_snippet", + "content": "...how people can improve their time management skills. They gathered results in this article...", + "fill": "$--search-snippet", + "fontFamily": "Inter", + "fontSize": 12, + "lineHeight": 1.4, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "frame", + "id": "f1_r1_meta", + "name": "metadata line", + "width": "fill_container", + "gap": 0, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "f1_r1_m1", + "content": "Modified 2h ago · 1,240 words · 7 links", + "fill": "$--search-text-dim", + "fontFamily": "Inter", + "fontSize": 11 + } + ] + } + ] + }, + { + "type": "frame", + "id": "f1_result2", + "name": "Result 2 — Normal", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [10, 16], + "stroke": { "align": "inside", "fill": "$--search-border", "thickness": { "top": 1 } }, + "children": [ + { + "type": "frame", + "id": "f1_r2_titlerow", + "width": "fill_container", + "alignItems": "center", + "gap": 8, + "children": [ + { + "type": "text", + "id": "f1_r2_title", + "content": "Weekly Review Process", + "fill": "$--search-heading", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "f1_r2_badge", + "cornerRadius": 3, + "fill": "$--secondary", + "padding": [2, 6], + "children": [ + { + "type": "text", + "id": "f1_r2_badgeText", + "content": "Procedure", + "fill": "$--search-text-muted", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "text", + "id": "f1_r2_snippet", + "content": "Every Sunday morning, review the week's progress and plan the next week...", + "fill": "$--search-snippet", + "fontFamily": "Inter", + "fontSize": 12, + "lineHeight": 1.4, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "frame", + "id": "f1_r2_meta", + "name": "metadata line", + "width": "fill_container", + "gap": 0, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "f1_r2_m1", + "content": "Modified 3d ago · 520 words · 12 links", + "fill": "$--search-text-dim", + "fontFamily": "Inter", + "fontSize": 11 + } + ] + } + ] + }, + { + "type": "frame", + "id": "f1_result3", + "name": "Result 3 — No links", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [10, 16], + "stroke": { "align": "inside", "fill": "$--search-border", "thickness": { "top": 1 } }, + "children": [ + { + "type": "frame", + "id": "f1_r3_titlerow", + "width": "fill_container", + "alignItems": "center", + "gap": 8, + "children": [ + { + "type": "text", + "id": "f1_r3_title", + "content": "Morning Routine", + "fill": "$--search-heading", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "f1_r3_snippet", + "content": "A simple checklist for starting the day with intention...", + "fill": "$--search-snippet", + "fontFamily": "Inter", + "fontSize": 12, + "lineHeight": 1.4, + "textGrowth": "fixed-width", + "width": "fill_container" + }, + { + "type": "frame", + "id": "f1_r3_meta", + "name": "metadata line", + "width": "fill_container", + "gap": 0, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "f1_r3_m1", + "content": "Modified Jan 15 · 89 words", + "fill": "$--search-text-dim", + "fontFamily": "Inter", + "fontSize": 11 + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "f_notelist_meta", + "name": "NoteList Item — subtitle with link count", + "x": 600, + "y": 0, + "width": 340, + "height": "fit_content", + "fill": "$--card", + "layout": "vertical", + "padding": [24, 0], + "gap": 0, + "theme": { "Mode": "Light" }, + "children": [ + { + "type": "text", + "id": "f2_heading", + "content": "NoteList Item — enhanced subtitle", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "700", + "width": "fill_container", + "padding": [0, 16, 12, 16] + }, + { + "type": "text", + "id": "f2_desc", + "content": "Normal NoteList items add a link count after date and word count. The separator is a middle dot (·). When link count is 0, it's omitted.", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 11, + "lineHeight": 1.5, + "width": "fill_container", + "padding": [0, 16, 16, 16] + }, + { + "type": "frame", + "id": "f2_item1", + "name": "NoteListItem — with links", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [14, 16], + "stroke": { "align": "inside", "fill": "$--border", "thickness": { "bottom": 1 } }, + "children": [ + { + "type": "frame", + "id": "f2_i1_titlerow", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "f2_i1_title", + "content": "Build Laputa App", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "f2_i1_sub", + "content": "2h ago · 342 words · 5 links", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 11 + } + ] + }, + { + "type": "frame", + "id": "f2_item2", + "name": "NoteListItem — no links", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [14, 16], + "stroke": { "align": "inside", "fill": "$--border", "thickness": { "bottom": 1 } }, + "children": [ + { + "type": "frame", + "id": "f2_i2_titlerow", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "f2_i2_title", + "content": "Quick Meeting Notes", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "text", + "id": "f2_i2_sub", + "content": "5d ago · 120 words", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 11 + } + ] + }, + { + "type": "frame", + "id": "f2_item3", + "name": "NoteListItem — empty note", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "padding": [14, 16], + "stroke": { "align": "inside", "fill": "$--border", "thickness": { "bottom": 1 } }, + "children": [ + { + "type": "frame", + "id": "f2_i3_titlerow", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "f2_i3_title", + "content": "New Untitled Note", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "text", + "id": "f2_i3_sub", + "content": "just now · Empty", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 11 + } + ] + } + ] + } +],"variables":{ + "--search-bg": {"type":"color","value":[{"value":"#FFFFFF"},{"theme":{"Mode":"Dark"},"value":"#1E1E2E"}]}, + "--search-heading": {"type":"color","value":[{"value":"#37352F"},{"theme":{"Mode":"Dark"},"value":"#e0e0f0"}]}, + "--search-snippet": {"type":"color","value":[{"value":"#787774"},{"theme":{"Mode":"Dark"},"value":"#888899"}]}, + "--search-text-dim": {"type":"color","value":[{"value":"#787774"},{"theme":{"Mode":"Dark"},"value":"#666680"}]}, + "--search-text-muted": {"type":"color","value":[{"value":"#787774"},{"theme":{"Mode":"Dark"},"value":"#8888aa"}]}, + "--search-highlight": {"type":"color","value":[{"value":"#37352F"},{"theme":{"Mode":"Dark"},"value":"#555570"}]}, + "--search-border": {"type":"color","value":[{"value":"#E9E9E7"},{"theme":{"Mode":"Dark"},"value":"#4c4c6d"}]}, + "--foreground": {"type":"color","value":[{"value":"#1C1C1C"},{"theme":{"Mode":"Dark"},"value":"#F5F5F5"}]}, + "--muted-foreground": {"type":"color","value":[{"value":"#737373"},{"theme":{"Mode":"Dark"},"value":"#A3A3A3"}]}, + "--border": {"type":"color","value":[{"value":"#E5E5E5"},{"theme":{"Mode":"Dark"},"value":"#333333"}]}, + "--card": {"type":"color","value":[{"value":"#FFFFFF"},{"theme":{"Mode":"Dark"},"value":"#1C1C1C"}]}, + "--accent": {"type":"color","value":[{"value":"#F5F5F5"},{"theme":{"Mode":"Dark"},"value":"#262626"}]}, + "--secondary": {"type":"color","value":[{"value":"#F5F5F5"},{"theme":{"Mode":"Dark"},"value":"#262626"}]} +}} \ No newline at end of file diff --git a/design/smart-property-display.pen b/design/smart-property-display.pen new file mode 100644 index 0000000..482e365 --- /dev/null +++ b/design/smart-property-display.pen @@ -0,0 +1,524 @@ +{ + "children": [ + { + "type": "frame", + "id": "spd-date", + "x": 0, + "y": 0, + "name": "Smart Properties — Date picker open", + "width": 280, + "height": 300, + "fill": "#FFFFFF", + "layout": "vertical", + "padding": 12, + "gap": 8, + "children": [ + { + "type": "text", + "id": "spd-date-title", + "content": "Date Property Display", + "fontSize": 13, + "fontWeight": "600", + "fill": "#1A1A1A" + }, + { + "type": "frame", + "id": "spd-date-row", + "name": "DateRow", + "width": "fill_container", + "height": 28, + "alignItems": "center", + "justifyContent": "space-between", + "padding": [0, 6], + "children": [ + { + "type": "text", + "id": "spd-date-label", + "content": "DEADLINE", + "fontSize": 10, + "fontWeight": "500", + "fill": "#888888", + "letterSpacing": 1.2, + "textTransform": "uppercase" + }, + { + "type": "text", + "id": "spd-date-value", + "content": "Feb 25, 2026", + "fontSize": 12, + "fill": "#1A1A1A" + } + ] + }, + { + "type": "frame", + "id": "spd-date-picker", + "name": "DatePickerOpen", + "width": "fill_container", + "height": 200, + "fill": "#F5F5F5", + "cornerRadius": 8, + "stroke": { "fill": "#E0E0E0", "thickness": 1 }, + "layout": "vertical", + "padding": 12, + "gap": 8, + "children": [ + { + "type": "text", + "id": "spd-picker-month", + "content": "February 2026", + "fontSize": 13, + "fontWeight": "600", + "fill": "#1A1A1A" + }, + { + "type": "text", + "id": "spd-picker-hint", + "content": "Native opens on click.\nDisplays as 'Feb 25, 2026' when closed.", + "fontSize": 11, + "fill": "#888888" + } + ] + } + ] + }, + { + "type": "frame", + "id": "spd-bool", + "x": 300, + "y": 0, + "name": "Smart Properties — Boolean toggle", + "width": 280, + "height": 200, + "fill": "#FFFFFF", + "layout": "vertical", + "padding": 12, + "gap": 8, + "children": [ + { + "type": "text", + "id": "spd-bool-title", + "content": "Boolean Property Display", + "fontSize": 13, + "fontWeight": "600", + "fill": "#1A1A1A" + }, + { + "type": "frame", + "id": "spd-bool-row-on", + "name": "BooleanRowTrue", + "width": "fill_container", + "height": 28, + "alignItems": "center", + "justifyContent": "space-between", + "padding": [0, 6], + "children": [ + { + "type": "text", + "id": "spd-bool-label-on", + "content": "PUBLISHED", + "fontSize": 10, + "fontWeight": "500", + "fill": "#888888", + "letterSpacing": 1.2 + }, + { + "type": "frame", + "id": "spd-bool-toggle-on", + "name": "ToggleOn", + "height": 22, + "cornerRadius": 4, + "fill": "#E8F5E9", + "padding": [2, 8], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "spd-bool-val-on", + "content": "\u2713 Yes", + "fontSize": 12, + "fill": "#2E7D32" + } + ] + } + ] + }, + { + "type": "frame", + "id": "spd-bool-row-off", + "name": "BooleanRowFalse", + "width": "fill_container", + "height": 28, + "alignItems": "center", + "justifyContent": "space-between", + "padding": [0, 6], + "children": [ + { + "type": "text", + "id": "spd-bool-label-off", + "content": "ARCHIVED", + "fontSize": 10, + "fontWeight": "500", + "fill": "#888888", + "letterSpacing": 1.2 + }, + { + "type": "frame", + "id": "spd-bool-toggle-off", + "name": "ToggleOff", + "height": 22, + "cornerRadius": 4, + "stroke": { "fill": "#E0E0E0", "thickness": 1 }, + "padding": [2, 8], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "spd-bool-val-off", + "content": "\u2717 No", + "fontSize": 12, + "fill": "#888888" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "spd-status", + "x": 600, + "y": 0, + "name": "Smart Properties — Status badge", + "width": 280, + "height": 260, + "fill": "#FFFFFF", + "layout": "vertical", + "padding": 12, + "gap": 8, + "children": [ + { + "type": "text", + "id": "spd-status-title", + "content": "Status Property Display", + "fontSize": 13, + "fontWeight": "600", + "fill": "#1A1A1A" + }, + { + "type": "frame", + "id": "spd-status-row-active", + "name": "StatusActive", + "width": "fill_container", + "height": 28, + "alignItems": "center", + "justifyContent": "space-between", + "padding": [0, 6], + "children": [ + { + "type": "text", + "id": "spd-status-label-a", + "content": "STATUS", + "fontSize": 10, + "fontWeight": "500", + "fill": "#888888", + "letterSpacing": 1.2 + }, + { + "type": "frame", + "id": "spd-badge-active", + "name": "BadgeActive", + "cornerRadius": 16, + "fill": "#E8F5E9", + "padding": [1, 6], + "children": [ + { + "type": "text", + "id": "spd-badge-active-text", + "content": "ACTIVE", + "fontSize": 10, + "fontWeight": "600", + "fill": "#2E7D32", + "letterSpacing": 1.2 + } + ] + } + ] + }, + { + "type": "frame", + "id": "spd-status-row-draft", + "name": "StatusDraft", + "width": "fill_container", + "height": 28, + "alignItems": "center", + "justifyContent": "space-between", + "padding": [0, 6], + "children": [ + { + "type": "text", + "id": "spd-status-label-d", + "content": "STATUS", + "fontSize": 10, + "fontWeight": "500", + "fill": "#888888", + "letterSpacing": 1.2 + }, + { + "type": "frame", + "id": "spd-badge-draft", + "name": "BadgeDraft", + "cornerRadius": 16, + "fill": "#FFF8E1", + "padding": [1, 6], + "children": [ + { + "type": "text", + "id": "spd-badge-draft-text", + "content": "DRAFT", + "fontSize": 10, + "fontWeight": "600", + "fill": "#F9A825", + "letterSpacing": 1.2 + } + ] + } + ] + }, + { + "type": "frame", + "id": "spd-status-row-done", + "name": "StatusDone", + "width": "fill_container", + "height": 28, + "alignItems": "center", + "justifyContent": "space-between", + "padding": [0, 6], + "children": [ + { + "type": "text", + "id": "spd-status-label-dn", + "content": "STATUS", + "fontSize": 10, + "fontWeight": "500", + "fill": "#888888", + "letterSpacing": 1.2 + }, + { + "type": "frame", + "id": "spd-badge-done", + "name": "BadgeDone", + "cornerRadius": 16, + "fill": "#E3F2FD", + "padding": [1, 6], + "children": [ + { + "type": "text", + "id": "spd-badge-done-text", + "content": "DONE", + "fontSize": 10, + "fontWeight": "600", + "fill": "#1565C0", + "letterSpacing": 1.2 + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "spd-override", + "x": 900, + "y": 0, + "name": "Smart Properties — Display mode override dropdown", + "width": 280, + "height": 300, + "fill": "#FFFFFF", + "layout": "vertical", + "padding": 12, + "gap": 8, + "children": [ + { + "type": "text", + "id": "spd-override-title", + "content": "Display Mode Override", + "fontSize": 13, + "fontWeight": "600", + "fill": "#1A1A1A" + }, + { + "type": "frame", + "id": "spd-override-row", + "name": "PropertyWithOverrideHandle", + "width": "fill_container", + "height": 28, + "alignItems": "center", + "justifyContent": "space-between", + "padding": [0, 6], + "fill": "#F5F5F5", + "cornerRadius": 4, + "children": [ + { + "type": "text", + "id": "spd-override-label", + "content": "DEADLINE", + "fontSize": 10, + "fontWeight": "500", + "fill": "#888888", + "letterSpacing": 1.2 + }, + { + "type": "frame", + "id": "spd-override-actions", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "spd-override-val", + "content": "Feb 25, 2026", + "fontSize": 12, + "fill": "#1A1A1A" + }, + { + "type": "frame", + "id": "spd-override-handle", + "name": "OverrideHandle", + "width": 16, + "height": 16, + "cornerRadius": 3, + "fill": "#E0E0E0", + "alignItems": "center", + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "spd-override-icon", + "content": "\u25BE", + "fontSize": 10, + "fill": "#666666" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "spd-dropdown", + "name": "DisplayModeDropdown", + "width": 160, + "fill": "#FFFFFF", + "cornerRadius": 8, + "stroke": { "fill": "#E0E0E0", "thickness": 1 }, + "layout": "vertical", + "padding": 4, + "gap": 2, + "children": [ + { + "type": "frame", + "id": "spd-opt-text", + "name": "OptionText", + "width": "fill_container", + "height": 28, + "padding": [4, 8], + "cornerRadius": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "spd-opt-text-label", + "content": "Text", + "fontSize": 12, + "fill": "#1A1A1A" + } + ] + }, + { + "type": "frame", + "id": "spd-opt-date", + "name": "OptionDate", + "width": "fill_container", + "height": 28, + "padding": [4, 8], + "cornerRadius": 4, + "fill": "#E3F2FD", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "spd-opt-date-label", + "content": "\u2713 Date", + "fontSize": 12, + "fill": "#1565C0" + } + ] + }, + { + "type": "frame", + "id": "spd-opt-bool", + "name": "OptionBoolean", + "width": "fill_container", + "height": 28, + "padding": [4, 8], + "cornerRadius": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "spd-opt-bool-label", + "content": "Boolean", + "fontSize": 12, + "fill": "#1A1A1A" + } + ] + }, + { + "type": "frame", + "id": "spd-opt-status", + "name": "OptionStatus", + "width": "fill_container", + "height": 28, + "padding": [4, 8], + "cornerRadius": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "spd-opt-status-label", + "content": "Status", + "fontSize": 12, + "fill": "#1A1A1A" + } + ] + }, + { + "type": "frame", + "id": "spd-opt-url", + "name": "OptionURL", + "width": "fill_container", + "height": 28, + "padding": [4, 8], + "cornerRadius": 4, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "spd-opt-url-label", + "content": "URL", + "fontSize": 12, + "fill": "#1A1A1A" + } + ] + } + ] + } + ] + } + ], + "variables": {} +} diff --git a/design/sort-picker-properties.pen b/design/sort-picker-properties.pen new file mode 100644 index 0000000..ebcc2d6 --- /dev/null +++ b/design/sort-picker-properties.pen @@ -0,0 +1 @@ +{"children":[],"variables":{}} \ No newline at end of file diff --git a/design/status-color-picker.pen b/design/status-color-picker.pen new file mode 100644 index 0000000..e4ec159 --- /dev/null +++ b/design/status-color-picker.pen @@ -0,0 +1,619 @@ +{ + "children": [ + { + "children": [ + { + "content": "Status Dropdown — Color Dots", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600", + "id": "1wdqT", + "name": "title1", + "type": "text" + }, + { + "content": "Each status option shows a color dot on the left. Clicking the dot opens the color palette picker.", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal", + "id": "PsCXR", + "lineHeight": 1.5, + "name": "desc1", + "textGrowth": "fixed-width", + "type": "text", + "width": 272 + }, + { + "children": [ + { + "children": [ + { + "content": "Type a status...", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal", + "id": "VyDsY", + "name": "searchText", + "type": "text" + } + ], + "id": "tim0g", + "name": "searchBar", + "padding": [ + 8, + 10 + ], + "stroke": { + "fill": "$--border", + "thickness": { + "bottom": 1 + } + }, + "type": "frame", + "width": "fill_container" + }, + { + "children": [ + { + "content": "FROM VAULT", + "fill": "$--muted-foreground", + "fontFamily": "IBM Plex Mono", + "fontSize": 9, + "fontWeight": "500", + "id": "Bn3lU", + "letterSpacing": 1.2, + "name": "labelText", + "type": "text" + } + ], + "id": "CB90c", + "name": "sectionLabel", + "padding": [ + 4, + 10 + ], + "type": "frame", + "width": "fill_container" + }, + { + "alignItems": "center", + "children": [ + { + "fill": "$--accent-green", + "height": 10, + "id": "8uSm6", + "name": "dot1", + "type": "ellipse", + "width": 10 + }, + { + "children": [ + { + "content": "ACTIVE", + "fill": "$--accent-green", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "600", + "id": "h4OER", + "letterSpacing": 1.2, + "name": "pill1text", + "type": "text" + } + ], + "cornerRadius": 16, + "fill": "$--accent-green-light", + "id": "k7sSo", + "name": "pill1", + "padding": [ + 1, + 6 + ], + "type": "frame" + } + ], + "cornerRadius": 4, + "gap": 8, + "id": "IZrMP", + "name": "Status Option - Active", + "padding": [ + 5, + 10 + ], + "type": "frame", + "width": "fill_container" + }, + { + "alignItems": "center", + "children": [ + { + "fill": "$--accent-red", + "height": 10, + "id": "sSEfx", + "name": "dot2", + "type": "ellipse", + "width": 10 + }, + { + "children": [ + { + "content": "BLOCKED", + "fill": "$--accent-red", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "600", + "id": "61jBJ", + "letterSpacing": 1.2, + "name": "pill2text", + "type": "text" + } + ], + "cornerRadius": 16, + "fill": "$--accent-red-light", + "id": "ozLQM", + "name": "pill2", + "padding": [ + 1, + 6 + ], + "type": "frame" + } + ], + "cornerRadius": 4, + "gap": 8, + "id": "B3Im8", + "name": "Status Option - Blocked", + "padding": [ + 5, + 10 + ], + "type": "frame", + "width": "fill_container" + }, + { + "alignItems": "center", + "children": [ + { + "fill": "$--accent-yellow", + "height": 10, + "id": "2mTmp", + "name": "dot3", + "type": "ellipse", + "width": 10 + }, + { + "children": [ + { + "content": "DRAFT", + "fill": "$--accent-yellow", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "600", + "id": "evl7l", + "letterSpacing": 1.2, + "name": "pill3text", + "type": "text" + } + ], + "cornerRadius": 16, + "fill": "$--accent-yellow-light", + "id": "6Fuxw", + "name": "pill3", + "padding": [ + 1, + 6 + ], + "type": "frame" + } + ], + "cornerRadius": 4, + "fill": "$--muted", + "gap": 8, + "id": "TwUBA", + "name": "Status Option - Draft (hover)", + "padding": [ + 5, + 10 + ], + "type": "frame", + "width": "fill_container" + } + ], + "cornerRadius": 8, + "fill": "$--background", + "id": "DT2gn", + "layout": "vertical", + "name": "Dropdown Container", + "stroke": { + "fill": "$--border", + "thickness": 1 + }, + "type": "frame", + "width": "fill_container" + } + ], + "fill": "$--background", + "gap": 16, + "id": "2N2BP", + "layout": "vertical", + "name": "1 — Status Dropdown with Color Dots", + "padding": 24, + "type": "frame", + "width": 320, + "x": 0, + "y": 0 + }, + { + "children": [ + { + "content": "Color Picker — Expanded", + "fill": "$--foreground", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600", + "id": "zPCnI", + "name": "title2", + "type": "text" + }, + { + "content": "Clicking a color dot expands the palette inline. The 8 accent colors from the design system are shown as clickable swatches. A 'Default' option resets to the built-in color.", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal", + "id": "TtaTt", + "lineHeight": 1.5, + "name": "desc2", + "textGrowth": "fixed-width", + "type": "text", + "width": 272 + }, + { + "children": [ + { + "alignItems": "center", + "children": [ + { + "fill": "$--accent-yellow", + "height": 10, + "id": "FRKg5", + "name": "dotActive", + "stroke": { + "fill": "$--foreground", + "thickness": 2 + }, + "type": "ellipse", + "width": 10 + }, + { + "children": [ + { + "content": "DRAFT", + "fill": "$--accent-yellow", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "600", + "id": "rA60a", + "letterSpacing": 1.2, + "name": "pillActiveText", + "type": "text" + } + ], + "cornerRadius": 16, + "fill": "$--accent-yellow-light", + "id": "15D2Q", + "name": "pillActive", + "padding": [ + 1, + 6 + ], + "type": "frame" + } + ], + "cornerRadius": 4, + "fill": "$--muted", + "gap": 8, + "id": "3QplJ", + "name": "Draft Option Row", + "padding": [ + 5, + 10 + ], + "type": "frame", + "width": "fill_container" + }, + { + "children": [ + { + "content": "ASSIGN COLOR", + "fill": "$--muted-foreground", + "fontFamily": "IBM Plex Mono", + "fontSize": 9, + "fontWeight": "500", + "id": "5yhS8", + "letterSpacing": 1.2, + "name": "paletteLabel", + "type": "text" + }, + { + "alignItems": "center", + "children": [ + { + "children": [ + { + "content": "Default", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "normal", + "id": "Q2ZwX", + "name": "defText", + "type": "text" + } + ], + "cornerRadius": 10, + "id": "Sgzjz", + "name": "defBtn", + "padding": [ + 2, + 6 + ], + "stroke": { + "fill": "$--border", + "thickness": 1 + }, + "type": "frame" + }, + { + "fill": "$--accent-red", + "height": 16, + "id": "sOgl3", + "name": "sw1", + "type": "ellipse", + "width": 16 + }, + { + "fill": "$--accent-orange", + "height": 16, + "id": "kqj6G", + "name": "sw2", + "type": "ellipse", + "width": 16 + }, + { + "fill": "$--accent-yellow", + "height": 16, + "id": "luCEG", + "name": "sw3", + "type": "ellipse", + "width": 16 + }, + { + "fill": "$--accent-green", + "height": 16, + "id": "KmBPE", + "name": "sw4", + "type": "ellipse", + "width": 16 + }, + { + "fill": "$--accent-blue", + "height": 16, + "id": "IsLDd", + "name": "sw5", + "type": "ellipse", + "width": 16 + }, + { + "fill": "$--accent-purple", + "height": 16, + "id": "M6bwU", + "name": "sw6", + "type": "ellipse", + "width": 16 + }, + { + "fill": "$--accent-teal", + "height": 16, + "id": "LKPIO", + "name": "sw7", + "type": "ellipse", + "width": 16 + }, + { + "fill": "$--accent-pink", + "height": 16, + "id": "bFQCa", + "name": "sw8", + "type": "ellipse", + "width": 16 + } + ], + "gap": 6, + "id": "zwiKV", + "name": "Swatches Row", + "type": "frame" + } + ], + "fill": "$--muted", + "gap": 8, + "id": "ZUL7a", + "layout": "vertical", + "name": "Color Palette", + "padding": [ + 8, + 10 + ], + "type": "frame", + "width": "fill_container" + }, + { + "fill": "$--border", + "height": 1, + "id": "hsnvv", + "name": "divider", + "type": "rectangle", + "width": "fill_container" + }, + { + "alignItems": "center", + "children": [ + { + "fill": "$--accent-green", + "height": 10, + "id": "Mddih", + "name": "otherDot", + "type": "ellipse", + "width": 10 + }, + { + "children": [ + { + "content": "ACTIVE", + "fill": "$--accent-green", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "600", + "id": "pVJ5Z", + "letterSpacing": 1.2, + "name": "otherText", + "type": "text" + } + ], + "cornerRadius": 16, + "fill": "$--accent-green-light", + "id": "bHWx0", + "name": "otherPill", + "padding": [ + 1, + 6 + ], + "type": "frame" + } + ], + "cornerRadius": 4, + "gap": 8, + "id": "EpnwC", + "name": "Other Option", + "padding": [ + 5, + 10 + ], + "type": "frame", + "width": "fill_container" + } + ], + "cornerRadius": 8, + "fill": "$--background", + "id": "f2Adq", + "layout": "vertical", + "name": "Dropdown with Picker", + "stroke": { + "fill": "$--border", + "thickness": 1 + }, + "type": "frame", + "width": "fill_container" + } + ], + "fill": "$--background", + "gap": 16, + "id": "jbCuz", + "layout": "vertical", + "name": "2 — Color Picker Expanded", + "padding": 24, + "type": "frame", + "width": 320, + "x": 360, + "y": 0 + } + ], + "variables": { + "--accent-blue": { + "type": "color", + "value": "#155DFF" + }, + "--accent-blue-light": { + "type": "color", + "value": "#155DFF14" + }, + "--accent-green": { + "type": "color", + "value": "#38A169" + }, + "--accent-green-light": { + "type": "color", + "value": "#38A16919" + }, + "--accent-orange": { + "type": "color", + "value": "#D9730D" + }, + "--accent-orange-light": { + "type": "color", + "value": "#D9730D19" + }, + "--accent-pink": { + "type": "color", + "value": "#D53F8C" + }, + "--accent-pink-light": { + "type": "color", + "value": "#D53F8C19" + }, + "--accent-purple": { + "type": "color", + "value": "#805AD5" + }, + "--accent-purple-light": { + "type": "color", + "value": "#805AD519" + }, + "--accent-red": { + "type": "color", + "value": "#E53E3E" + }, + "--accent-red-light": { + "type": "color", + "value": "#E53E3E19" + }, + "--accent-teal": { + "type": "color", + "value": "#319795" + }, + "--accent-teal-light": { + "type": "color", + "value": "#31979519" + }, + "--accent-yellow": { + "type": "color", + "value": "#D69E2E" + }, + "--accent-yellow-light": { + "type": "color", + "value": "#D69E2E19" + }, + "--background": { + "type": "color", + "value": "#FFFFFF" + }, + "--border": { + "type": "color", + "value": "#E9E9E7" + }, + "--foreground": { + "type": "color", + "value": "#37352F" + }, + "--muted": { + "type": "color", + "value": "#F5F5F4" + }, + "--muted-foreground": { + "type": "color", + "value": "#787774" + } + } +} \ No newline at end of file diff --git a/design/status-property-dropdown.pen b/design/status-property-dropdown.pen new file mode 100644 index 0000000..a0a8a67 --- /dev/null +++ b/design/status-property-dropdown.pen @@ -0,0 +1 @@ +{"children":[{"type":"frame","id":"sdClosed","x":0,"y":0,"name":"Status Property — Dropdown closed (pill display)","width":320,"height":48,"fill":"#ffffff","layout":"vertical","gap":0,"padding":[4,6],"children":[{"type":"text","id":"sdClDesc","fill":"#6b7280","content":"Closed state: status shown as colored pill. Click to open dropdown.","fontFamily":"Inter","fontSize":10,"fontWeight":"normal"},{"type":"frame","id":"sdClRow","name":"Status Row","width":"fill_container","cornerRadius":4,"padding":[4,6],"alignItems":"center","children":[{"type":"text","id":"sdClLabel","fill":"#6b7280","content":"STATUS","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","letterSpacing":1.2},{"type":"frame","id":"sdClPill","name":"Status Pill — Active","fill":"#dcfce7","cornerRadius":16,"padding":[1,6],"children":[{"type":"text","id":"sdClPillText","fill":"#16a34a","content":"ACTIVE","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","letterSpacing":1.2}]}]}]},{"type":"frame","id":"sdOpen","x":0,"y":80,"name":"Status Property — Dropdown open with options","width":320,"height":360,"fill":"#ffffff","layout":"vertical","gap":0,"padding":[4,6],"children":[{"type":"text","id":"sdOpDesc","fill":"#6b7280","content":"Open state: dropdown popover with colored status chips, search input, vault statuses, and defaults.","fontFamily":"Inter","fontSize":10,"fontWeight":"normal"},{"type":"frame","id":"sdOpRow","name":"Status Row (with open dropdown)","width":"fill_container","layout":"vertical","gap":0,"children":[{"type":"frame","id":"sdOpRowInner","name":"Row","width":"fill_container","cornerRadius":4,"padding":[4,6],"alignItems":"center","fill":"#f3f4f6","children":[{"type":"text","id":"sdOpLabel","fill":"#6b7280","content":"STATUS","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","letterSpacing":1.2},{"type":"frame","id":"sdOpPill","name":"Status Pill — Active (selected)","fill":"#dcfce7","cornerRadius":16,"padding":[1,6],"children":[{"type":"text","id":"sdOpPillText","fill":"#16a34a","content":"ACTIVE","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","letterSpacing":1.2}]}]},{"type":"frame","id":"sdOpDropdown","name":"Dropdown Popover","width":"fill_container","fill":"#ffffff","cornerRadius":8,"stroke":{"align":"inside","thickness":1,"fill":"#e5e7eb"},"layout":"vertical","gap":0,"padding":[4,0],"children":[{"type":"frame","id":"sdOpSearch","name":"Search Input","width":"fill_container","height":32,"padding":[4,8],"alignItems":"center","children":[{"type":"text","id":"sdOpSearchText","fill":"#9ca3af","content":"Type a status...","fontFamily":"Inter","fontSize":12,"fontWeight":"normal"}]},{"type":"frame","id":"sdOpSep1","name":"Separator","width":"fill_container","height":1,"fill":"#e5e7eb"},{"type":"frame","id":"sdOpDefaults","name":"Suggested Statuses","width":"fill_container","layout":"vertical","gap":0,"padding":[4,0],"children":[{"type":"text","id":"sdOpDefLabel","fill":"#9ca3af","content":"SUGGESTED","fontFamily":"IBM Plex Mono","fontSize":9,"fontWeight":"500","letterSpacing":1.2,"padding":[4,8]},{"type":"frame","id":"sdOpOpt1","name":"Option — Not started","width":"fill_container","height":28,"padding":[4,8],"alignItems":"center","cornerRadius":4,"children":[{"type":"frame","id":"sdOpOpt1Pill","fill":"#e0e7ff","cornerRadius":16,"padding":[1,6],"children":[{"type":"text","id":"sdOpOpt1Text","fill":"#6b7280","content":"NOT STARTED","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","letterSpacing":1.2}]}]},{"type":"frame","id":"sdOpOpt2","name":"Option — In progress","width":"fill_container","height":28,"padding":[4,8],"alignItems":"center","cornerRadius":4,"children":[{"type":"frame","id":"sdOpOpt2Pill","fill":"#f3e8ff","cornerRadius":16,"padding":[1,6],"children":[{"type":"text","id":"sdOpOpt2Text","fill":"#9333ea","content":"IN PROGRESS","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","letterSpacing":1.2}]}]},{"type":"frame","id":"sdOpOpt3","name":"Option — Active (hover)","width":"fill_container","height":28,"padding":[4,8],"alignItems":"center","cornerRadius":4,"fill":"#f3f4f6","children":[{"type":"frame","id":"sdOpOpt3Pill","fill":"#dcfce7","cornerRadius":16,"padding":[1,6],"children":[{"type":"text","id":"sdOpOpt3Text","fill":"#16a34a","content":"ACTIVE","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","letterSpacing":1.2}]}]},{"type":"frame","id":"sdOpOpt4","name":"Option — Done","width":"fill_container","height":28,"padding":[4,8],"alignItems":"center","cornerRadius":4,"children":[{"type":"frame","id":"sdOpOpt4Pill","fill":"#dbeafe","cornerRadius":16,"padding":[1,6],"children":[{"type":"text","id":"sdOpOpt4Text","fill":"#2563eb","content":"DONE","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","letterSpacing":1.2}]}]},{"type":"frame","id":"sdOpOpt5","name":"Option — Blocked","width":"fill_container","height":28,"padding":[4,8],"alignItems":"center","cornerRadius":4,"children":[{"type":"frame","id":"sdOpOpt5Pill","fill":"#fee2e2","cornerRadius":16,"padding":[1,6],"children":[{"type":"text","id":"sdOpOpt5Text","fill":"#dc2626","content":"BLOCKED","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","letterSpacing":1.2}]}]}]},{"type":"frame","id":"sdOpSep2","name":"Separator","width":"fill_container","height":1,"fill":"#e5e7eb"},{"type":"frame","id":"sdOpVault","name":"From Vault","width":"fill_container","layout":"vertical","gap":0,"padding":[4,0],"children":[{"type":"text","id":"sdOpVaultLabel","fill":"#9ca3af","content":"FROM VAULT","fontFamily":"IBM Plex Mono","fontSize":9,"fontWeight":"500","letterSpacing":1.2,"padding":[4,8]},{"type":"frame","id":"sdOpVOpt1","name":"Option — Draft (from vault)","width":"fill_container","height":28,"padding":[4,8],"alignItems":"center","cornerRadius":4,"children":[{"type":"frame","id":"sdOpVOpt1Pill","fill":"#fef9c3","cornerRadius":16,"padding":[1,6],"children":[{"type":"text","id":"sdOpVOpt1Text","fill":"#ca8a04","content":"DRAFT","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","letterSpacing":1.2}]}]},{"type":"frame","id":"sdOpVOpt2","name":"Option — Published (from vault)","width":"fill_container","height":28,"padding":[4,8],"alignItems":"center","cornerRadius":4,"children":[{"type":"frame","id":"sdOpVOpt2Pill","fill":"#dcfce7","cornerRadius":16,"padding":[1,6],"children":[{"type":"text","id":"sdOpVOpt2Text","fill":"#16a34a","content":"PUBLISHED","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","letterSpacing":1.2}]}]}]}]}]}]},{"type":"frame","id":"sdCustom","x":350,"y":0,"name":"Status Property — Custom status typed","width":320,"height":180,"fill":"#ffffff","layout":"vertical","gap":0,"padding":[4,6],"children":[{"type":"text","id":"sdCuDesc","fill":"#6b7280","content":"User types a custom status name not in suggestions. Press Enter to create it.","fontFamily":"Inter","fontSize":10,"fontWeight":"normal"},{"type":"frame","id":"sdCuRow","name":"Status Row with custom input","width":"fill_container","layout":"vertical","gap":0,"children":[{"type":"frame","id":"sdCuRowInner","name":"Row","width":"fill_container","cornerRadius":4,"padding":[4,6],"alignItems":"center","fill":"#f3f4f6","children":[{"type":"text","id":"sdCuLabel","fill":"#6b7280","content":"STATUS","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","letterSpacing":1.2},{"type":"frame","id":"sdCuPill","name":"Status Pill — Active","fill":"#dcfce7","cornerRadius":16,"padding":[1,6],"children":[{"type":"text","id":"sdCuPillText","fill":"#16a34a","content":"ACTIVE","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","letterSpacing":1.2}]}]},{"type":"frame","id":"sdCuDropdown","name":"Dropdown — with typed input","width":"fill_container","fill":"#ffffff","cornerRadius":8,"stroke":{"align":"inside","thickness":1,"fill":"#e5e7eb"},"layout":"vertical","gap":0,"padding":[4,0],"children":[{"type":"frame","id":"sdCuSearch","name":"Search Input (typed)","width":"fill_container","height":32,"padding":[4,8],"alignItems":"center","stroke":{"align":"inside","thickness":{"bottom":1},"fill":"#e5e7eb"},"children":[{"type":"text","id":"sdCuSearchText","fill":"#111827","content":"Needs Review","fontFamily":"Inter","fontSize":12,"fontWeight":"normal"}]},{"type":"frame","id":"sdCuCreate","name":"Create new option","width":"fill_container","height":32,"padding":[4,8],"alignItems":"center","cornerRadius":4,"children":[{"type":"text","id":"sdCuCreateLabel","fill":"#6b7280","content":"Create","fontFamily":"Inter","fontSize":11,"fontWeight":"normal"},{"type":"frame","id":"sdCuCreatePill","fill":"#e0e7ff","cornerRadius":16,"padding":[1,6],"children":[{"type":"text","id":"sdCuCreateText","fill":"#6b7280","content":"NEEDS REVIEW","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","letterSpacing":1.2}]}]}]}]}]}],"variables":{}} \ No newline at end of file diff --git a/design/sync-conflict-resolution.pen b/design/sync-conflict-resolution.pen new file mode 100644 index 0000000..ebcc2d6 --- /dev/null +++ b/design/sync-conflict-resolution.pen @@ -0,0 +1 @@ +{"children":[],"variables":{}} \ No newline at end of file diff --git a/design/tab-responsive-width.pen b/design/tab-responsive-width.pen new file mode 100644 index 0000000..747b5ab --- /dev/null +++ b/design/tab-responsive-width.pen @@ -0,0 +1 @@ +{"children":[],"variables":{}} diff --git a/design/tags-property-type.pen b/design/tags-property-type.pen new file mode 100644 index 0000000..c85941b --- /dev/null +++ b/design/tags-property-type.pen @@ -0,0 +1 @@ +{"children":[{"children":[{"content":"Tags Property — Display","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"600","id":"HHQbx","letterSpacing":0.5,"name":"title1","type":"text"},{"alignItems":"center","children":[{"content":"TAGS","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","id":"9BDgi","letterSpacing":1.2,"name":"label1","type":"text"},{"alignItems":"center","children":[{"alignItems":"center","children":[{"content":"React","fill":"$--accent-blue","fontFamily":"Inter","fontSize":11,"fontWeight":"500","id":"V1Dfn","name":"pillText1","type":"text"},{"content":"×","fill":"$--accent-blue","fontFamily":"Inter","fontSize":10,"fontWeight":"500","id":"urcjo","name":"pillX1","type":"text"}],"cornerRadius":12,"fill":"$--accent-blue-light","gap":4,"id":"70AK4","name":"pill1","padding":[2,8],"type":"frame"},{"alignItems":"center","children":[{"content":"TypeScript","fill":"$--accent-green","fontFamily":"Inter","fontSize":11,"fontWeight":"500","id":"VtpTf","name":"pillText2","type":"text"},{"content":"×","fill":"$--accent-green","fontFamily":"Inter","fontSize":10,"fontWeight":"500","id":"sw4gt","name":"pillX2","type":"text"}],"cornerRadius":12,"fill":"$--accent-green-light","gap":4,"id":"rCHzS","name":"pill2","padding":[2,8],"type":"frame"},{"alignItems":"center","children":[{"content":"Tauri","fill":"$--accent-purple","fontFamily":"Inter","fontSize":11,"fontWeight":"500","id":"OTVWd","name":"pillText3","type":"text"},{"content":"×","fill":"$--accent-purple","fontFamily":"Inter","fontSize":10,"fontWeight":"500","id":"j8w8E","name":"pillX3","type":"text"}],"cornerRadius":12,"fill":"$--accent-purple-light","gap":4,"id":"U4Dfr","name":"pill3","padding":[2,8],"type":"frame"},{"alignItems":"center","children":[{"content":"+","fill":"$--accent-blue","fontFamily":"Inter","fontSize":12,"fontWeight":"500","id":"BTPW1","name":"addPlus","type":"text"}],"cornerRadius":10,"height":20,"id":"2A0u4","justifyContent":"center","name":"addBtn","type":"frame","width":20}],"gap":4,"id":"wQwb9","name":"pillWrap","type":"frame"}],"gap":8,"id":"9vZUA","name":"row1","type":"frame","width":"fill_container"}],"cornerRadius":8,"fill":"$--background","gap":12,"id":"5Co69","layout":"vertical","name":"Tags Property — Display (pills with color + X)","padding":16,"type":"frame","width":280,"x":0,"y":0},{"children":[{"content":"Tags Dropdown — Multi-Select","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"600","id":"EYQrz","letterSpacing":0.5,"name":"title2","type":"text"},{"children":[{"content":"Type a tag...","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"K4O5T","name":"searchInput","type":"text"}],"id":"FiJYp","name":"searchBox","padding":[8,12],"type":"frame","width":"fill_container"},{"children":[{"content":"FROM VAULT","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":9,"fontWeight":"500","id":"TTrI3","letterSpacing":1.2,"name":"secLabel1","type":"text"},{"alignItems":"center","children":[{"content":"\u2713","fill":"$--accent-blue","fontFamily":"Inter","fontSize":10,"fontWeight":"600","id":"hvC9R","name":"check1","type":"text"},{"children":[{"content":"REACT","fill":"$--accent-blue","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","id":"uafDO","letterSpacing":1.2,"name":"optPillText1","type":"text"}],"cornerRadius":12,"fill":"$--accent-blue-light","id":"fSE56","name":"optPill1","padding":[1,6],"type":"frame"},{"fill":"$--accent-blue","height":14,"id":"hm8BB","name":"optColor1","type":"ellipse","width":14}],"cornerRadius":4,"fill":"$--muted","gap":6,"id":"sEjJ4","name":"opt1","padding":[6,8],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"content":"\u2713","fill":"$--accent-green","fontFamily":"Inter","fontSize":10,"fontWeight":"600","id":"xMjpj","name":"check2","type":"text"},{"children":[{"content":"TYPESCRIPT","fill":"$--accent-green","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","id":"QkrlH","letterSpacing":1.2,"name":"optPillText2","type":"text"}],"cornerRadius":12,"fill":"$--accent-green-light","id":"mFD4b","name":"optPill2","padding":[1,6],"type":"frame"},{"fill":"$--accent-green","height":14,"id":"IeC8h","name":"optColor2","type":"ellipse","width":14}],"cornerRadius":4,"gap":6,"id":"jTW08","name":"opt2","padding":[6,8],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"height":14,"id":"VWb7I","name":"spacer3","type":"frame","width":14},{"children":[{"content":"VITE","fill":"$--accent-orange","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","id":"IBEpk","letterSpacing":1.2,"name":"optPillText3","type":"text"}],"cornerRadius":12,"fill":"$--accent-orange-light","id":"juYuA","name":"optPill3","padding":[1,6],"type":"frame"},{"fill":"$--accent-orange","height":14,"id":"aTF8b","name":"optColor3","type":"ellipse","width":14}],"cornerRadius":4,"gap":6,"id":"byNKM","name":"opt3","padding":[6,8],"type":"frame","width":"fill_container"},{"fill":"$--border","height":1,"id":"5sC2Y","name":"divider","type":"rectangle","width":"fill_container"},{"content":"SUGGESTED","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":9,"fontWeight":"500","id":"7vME3","letterSpacing":1.2,"name":"secLabel2","type":"text"}],"id":"KsIE1","layout":"vertical","name":"listArea","padding":[4,0],"type":"frame","width":"fill_container"}],"cornerRadius":8,"fill":"$--background","id":"aWUZr","layout":"vertical","name":"Tags Property — Input (entering new tag)","type":"frame","width":220,"x":320,"y":0},{"children":[{"content":"Color Picker — Per Tag","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"600","id":"eUqcb","letterSpacing":0.5,"name":"title3","type":"text"},{"content":"Click the color swatch next to any tag to expand the color picker row","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":10,"fontWeight":"normal","id":"9JSxa","name":"desc3","type":"text"},{"alignItems":"center","children":[{"content":"\u2713","fill":"$--accent-purple","fontFamily":"Inter","fontSize":10,"fontWeight":"600","id":"8P4r5","name":"optCheck","type":"text"},{"children":[{"content":"TAURI","fill":"$--accent-purple","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","id":"u9Ljh","letterSpacing":1.2,"name":"optPillTxt","type":"text"}],"cornerRadius":12,"fill":"$--accent-purple-light","id":"KLwLI","name":"optPill","padding":[1,6],"type":"frame"},{"fill":"$--accent-purple","height":14,"id":"XMIpZ","name":"optSwatch","type":"ellipse","width":14}],"cornerRadius":4,"fill":"$--muted","gap":6,"id":"osSzq","name":"optRow","padding":[6,4],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"fill":"$--accent-red","height":16,"id":"4lhx5","name":"c1","type":"ellipse","width":16},{"fill":"$--accent-orange","height":16,"id":"aa0VW","name":"c2","type":"ellipse","width":16},{"fill":"$--accent-yellow","height":16,"id":"Vdkb2","name":"c3","type":"ellipse","width":16},{"fill":"$--accent-green","height":16,"id":"nb62h","name":"c4","type":"ellipse","width":16},{"fill":"$--accent-blue","height":16,"id":"lFU1J","name":"c5","type":"ellipse","width":16},{"fill":"$--accent-purple","height":16,"id":"d7Vkz","name":"c6","type":"ellipse","width":16}],"gap":6,"id":"aHm6t","name":"colorRow","padding":[8,12],"type":"frame"}],"cornerRadius":8,"fill":"$--background","id":"SBYSL","layout":"vertical","name":"Tags Property — Color Picker (selecting tag color)","padding":12,"type":"frame","width":220,"x":580,"y":0}],"variables":{"--background":{"type":"color","value":"#FFFFFF"},"--foreground":{"type":"color","value":"#37352F"},"--muted":{"type":"color","value":"#F0F0EF"},"--muted-foreground":{"type":"color","value":"#787774"},"--border":{"type":"color","value":"#E9E9E7"},"--accent-blue":{"type":"color","value":"#155DFF"},"--accent-blue-light":{"type":"color","value":"#155DFF14"},"--accent-green":{"type":"color","value":"#00B38B"},"--accent-green-light":{"type":"color","value":"#00B38B14"},"--accent-red":{"type":"color","value":"#E03E3E"},"--accent-red-light":{"type":"color","value":"#E03E3E14"},"--accent-purple":{"type":"color","value":"#A932FF"},"--accent-purple-light":{"type":"color","value":"#A932FF14"},"--accent-orange":{"type":"color","value":"#D9730D"},"--accent-orange-light":{"type":"color","value":"#D9730D14"},"--accent-yellow":{"type":"color","value":"#F0B100"},"--accent-yellow-light":{"type":"color","value":"#F0B10014"},"--primary":{"type":"color","value":"#155DFF"},"--font-mono":{"type":"string","value":"IBM Plex Mono, monospace"},"--font-primary":{"type":"string","value":"Inter"}}} \ No newline at end of file diff --git a/design/themes-editable.pen b/design/themes-editable.pen new file mode 100644 index 0000000..747b5ab --- /dev/null +++ b/design/themes-editable.pen @@ -0,0 +1 @@ +{"children":[],"variables":{}} diff --git a/design/theming-system.pen b/design/theming-system.pen new file mode 100644 index 0000000..e75de45 --- /dev/null +++ b/design/theming-system.pen @@ -0,0 +1 @@ +{"children":[{"children":[{"alignItems":"center","children":[{"content":"Settings","fill":"#37352F","fontSize":16,"fontWeight":"600","id":"o6HJK","type":"text"},{"fill":"#787774","height":16,"iconFontFamily":"lucide","iconFontName":"x","id":"zgXAf","type":"icon_font","width":16}],"height":56,"id":"mxSw1","justifyContent":"space_between","name":"Header","padding":[0,24],"stroke":{"align":"inside","fill":"#E9E9E7","thickness":{"bottom":1}},"type":"frame","width":"fill_container"},{"children":[{"content":"Appearance","fill":"#37352F","fontSize":13,"fontWeight":"600","id":"NhMui","type":"text"},{"content":"Choose a theme for your vault. Themes are JSON files in _themes/ — edit them to customize.","fill":"#787774","fontSize":12,"id":"atYQn","lineHeight":1.5,"textGrowth":"fixed-width","type":"text","width":"fill_container"},{"children":[{"alignItems":"center","children":[{"children":[{"fill":"#FFFFFF","height":32,"type":"rectangle","width":20},{"fill":"#F7F6F3","height":32,"type":"rectangle","width":20}],"cornerRadius":4,"height":32,"type":"frame","width":40},{"children":[{"content":"Default","fill":"#37352F","fontSize":13,"fontWeight":"500","type":"text"},{"content":"Light theme with warm tones","fill":"#787774","fontSize":11,"type":"text"}],"gap":2,"layout":"vertical","type":"frame","width":"fill_container"},{"content":"Active","fill":"#155DFF","fontSize":11,"fontWeight":"500","type":"text"}],"cornerRadius":6,"gap":12,"height":56,"name":"Theme Item — Default (Active)","padding":[0,12],"stroke":{"align":"inside","fill":"#155DFF","thickness":2},"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"children":[{"fill":"#1a1a2e","height":32,"type":"rectangle","width":20},{"fill":"#0f0f1a","height":32,"type":"rectangle","width":20}],"cornerRadius":4,"height":32,"type":"frame","width":40},{"children":[{"content":"Dark","fill":"#37352F","fontSize":13,"fontWeight":"500","type":"text"},{"content":"Dark variant with deep navy tones","fill":"#787774","fontSize":11,"type":"text"}],"gap":2,"layout":"vertical","type":"frame","width":"fill_container"}],"cornerRadius":6,"gap":12,"height":56,"name":"Theme Item — Dark","padding":[0,12],"stroke":{"align":"inside","fill":"#E9E9E7","thickness":1},"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"children":[{"fill":"#FAFAFA","height":32,"type":"rectangle","width":20},{"fill":"#111111","height":32,"type":"rectangle","width":20}],"cornerRadius":4,"height":32,"type":"frame","width":40},{"children":[{"content":"Minimal","fill":"#37352F","fontSize":13,"fontWeight":"500","type":"text"},{"content":"High contrast, minimal chrome","fill":"#787774","fontSize":11,"type":"text"}],"gap":2,"layout":"vertical","type":"frame","width":"fill_container"}],"cornerRadius":6,"gap":12,"height":56,"name":"Theme Item — Minimal","padding":[0,12],"stroke":{"align":"inside","fill":"#E9E9E7","thickness":1},"type":"frame","width":"fill_container"}],"gap":8,"layout":"vertical","name":"Theme List","type":"frame","width":"fill_container"},{"children":[{"alignItems":"center","children":[{"fill":"#787774","height":14,"iconFontFamily":"lucide","iconFontName":"plus","type":"icon_font","width":14},{"content":"New Theme","fill":"#37352F","fontSize":12,"fontWeight":"500","type":"text"}],"cornerRadius":6,"gap":6,"height":32,"name":"New Theme Button","padding":[0,12],"stroke":{"align":"inside","fill":"#E9E9E7","thickness":1},"type":"frame"},{"alignItems":"center","children":[{"fill":"#787774","height":14,"iconFontFamily":"lucide","iconFontName":"pencil","type":"icon_font","width":14},{"content":"Open in Editor","fill":"#37352F","fontSize":12,"fontWeight":"500","type":"text"}],"cornerRadius":6,"gap":6,"height":32,"name":"Open in Editor Button","padding":[0,12],"stroke":{"align":"inside","fill":"#E9E9E7","thickness":1},"type":"frame"}],"gap":8,"name":"Actions","type":"frame","width":"fill_container"}],"gap":20,"layout":"vertical","name":"Body","padding":24,"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"alignItems":"center","children":[{"content":"Cancel","fill":"#37352F","fontSize":13,"type":"text"}],"cornerRadius":6,"height":32,"padding":[0,16],"stroke":{"align":"inside","fill":"#E9E9E7","thickness":1},"type":"frame"},{"alignItems":"center","children":[{"content":"Save","fill":"#FFFFFF","fontSize":13,"type":"text"}],"cornerRadius":6,"fill":"#155DFF","height":32,"padding":[0,16],"type":"frame"}],"gap":8,"height":56,"justifyContent":"end","name":"Footer","padding":[0,24],"stroke":{"align":"inside","fill":"#E9E9E7","thickness":{"top":1}},"type":"frame","width":"fill_container"}],"fill":"#FFFFFF","layout":"vertical","name":"Settings — Appearance","type":"frame","width":520,"x":0,"y":0},{"children":[{"alignItems":"center","children":[{"content":"Switch theme...","fill":"#787774","fontSize":15,"type":"text"}],"height":44,"name":"Search","padding":[0,16],"stroke":{"align":"inside","fill":"#E9E9E7","thickness":{"bottom":1}},"type":"frame","width":"fill_container"},{"children":[{"content":"APPEARANCE","fill":"#787774","fontSize":11,"fontWeight":"600","letterSpacing":0.8,"type":"text"},{"alignItems":"center","children":[{"children":[{"fill":"#FFFFFF","height":16,"type":"rectangle","width":10},{"fill":"#F7F6F3","height":16,"type":"rectangle","width":10}],"cornerRadius":3,"height":16,"type":"frame","width":20},{"content":"Switch to Default","fill":"#37352F","fontSize":13,"type":"text"},{"fill":"#155DFF","height":14,"iconFontFamily":"lucide","iconFontName":"check","type":"icon_font","width":14}],"cornerRadius":6,"fill":"#EBEBEA","gap":8,"height":36,"name":"Row — Active","padding":[0,12],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"children":[{"fill":"#1a1a2e","height":16,"type":"rectangle","width":10},{"fill":"#0f0f1a","height":16,"type":"rectangle","width":10}],"cornerRadius":3,"height":16,"type":"frame","width":20},{"content":"Switch to Dark","fill":"#37352F","fontSize":13,"type":"text"}],"cornerRadius":6,"gap":8,"height":36,"name":"Row — Dark","padding":[0,12],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"children":[{"fill":"#FAFAFA","height":16,"type":"rectangle","width":10},{"fill":"#111111","height":16,"type":"rectangle","width":10}],"cornerRadius":3,"height":16,"type":"frame","width":20},{"content":"Switch to Minimal","fill":"#37352F","fontSize":13,"type":"text"}],"cornerRadius":6,"gap":8,"height":36,"name":"Row — Minimal","padding":[0,12],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"fill":"#787774","height":16,"iconFontFamily":"lucide","iconFontName":"plus","type":"icon_font","width":16},{"content":"New Theme","fill":"#37352F","fontSize":13,"type":"text"}],"cornerRadius":6,"gap":8,"height":36,"name":"Row — New Theme","padding":[0,12],"type":"frame","width":"fill_container"}],"layout":"vertical","name":"Results","padding":[4,0],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"content":"↑↓ navigate","fill":"#787774","fontSize":11,"type":"text"},{"content":"↵ select","fill":"#787774","fontSize":11,"type":"text"},{"content":"esc close","fill":"#787774","fontSize":11,"type":"text"}],"gap":16,"height":28,"name":"Footer","padding":[0,16],"stroke":{"align":"inside","fill":"#E9E9E7","thickness":{"top":1}},"type":"frame","width":"fill_container"}],"cornerRadius":12,"fill":"#FFFFFF","layout":"vertical","name":"Command Palette — Theme Switching","stroke":{"align":"inside","fill":"#E9E9E7","thickness":1},"type":"frame","width":520,"x":620,"y":0},{"children":[{"children":[{"alignItems":"center","children":[{"fill":"#787774","height":14,"iconFontFamily":"lucide","iconFontName":"file","type":"icon_font","width":14},{"content":"_themes/default.json","fill":"#37352F","fontSize":12,"fontWeight":"500","type":"text"}],"gap":8,"height":36,"padding":[0,12],"stroke":{"align":"inside","fill":"#E9E9E7","thickness":{"bottom":1}},"type":"frame","width":"fill_container"},{"children":[{"content":"{","fill":"#37352F","fontFamily":"IBM Plex Mono","fontSize":12,"type":"text"},{"content":" \"name\": \"Default\",","fill":"#37352F","fontFamily":"IBM Plex Mono","fontSize":12,"type":"text"},{"content":" \"colors\": {","fill":"#37352F","fontFamily":"IBM Plex Mono","fontSize":12,"type":"text"},{"content":" \"background\": \"#FFFFFF\",","fill":"#155DFF","fontFamily":"IBM Plex Mono","fontSize":12,"fontWeight":"600","type":"text"},{"content":" \"sidebar-background\": \"#F7F6F3\",","fill":"#37352F","fontFamily":"IBM Plex Mono","fontSize":12,"type":"text"},{"content":" \"accent\": \"#155DFF\"","fill":"#37352F","fontFamily":"IBM Plex Mono","fontSize":12,"type":"text"},{"content":" }","fill":"#37352F","fontFamily":"IBM Plex Mono","fontSize":12,"type":"text"},{"content":"}","fill":"#37352F","fontFamily":"IBM Plex Mono","fontSize":12,"type":"text"}],"gap":2,"layout":"vertical","padding":16,"type":"frame","width":"fill_container"}],"cornerRadius":8,"fill":"#FFFFFF","layout":"vertical","name":"Editor — Theme JSON","stroke":{"align":"inside","fill":"#E9E9E7","thickness":1},"type":"frame","width":400},{"alignItems":"center","children":[{"fill":"#787774","height":24,"iconFontFamily":"lucide","iconFontName":"arrow-right","type":"icon_font","width":24},{"content":"~300ms","fill":"#787774","fontSize":10,"fontWeight":"500","type":"text"}],"justifyContent":"center","layout":"vertical","name":"Arrow","type":"frame","width":40},{"children":[{"alignItems":"center","children":[{"fill":"#FF5F57","height":8,"type":"ellipse","width":8},{"fill":"#FEBC2E","height":8,"type":"ellipse","width":8},{"fill":"#28C840","height":8,"type":"ellipse","width":8}],"fill":"#F7F6F3","gap":6,"height":28,"padding":[0,8],"stroke":{"align":"inside","fill":"#E9E9E7","thickness":{"bottom":1}},"type":"frame","width":"fill_container"},{"children":[{"children":[{"cornerRadius":4,"fill":"#EBEBEA","height":20,"type":"frame","width":"fill_container"},{"cornerRadius":4,"height":20,"type":"frame","width":"fill_container"},{"cornerRadius":4,"height":20,"type":"frame","width":"fill_container"}],"fill":"#F7F6F3","gap":4,"height":"fill_container","layout":"vertical","padding":[8,6],"stroke":{"align":"inside","fill":"#E9E9E7","thickness":{"right":1}},"type":"frame","width":80},{"children":[{"cornerRadius":2,"fill":"#37352F","height":12,"type":"frame","width":160},{"cornerRadius":2,"fill":"#F0F0EF","height":8,"type":"frame","width":"fill_container"},{"cornerRadius":2,"fill":"#F0F0EF","height":8,"type":"frame","width":220},{"cornerRadius":2,"fill":"#F0F0EF","height":8,"type":"frame","width":180}],"gap":8,"height":"fill_container","layout":"vertical","padding":16,"type":"frame","width":"fill_container"}],"height":260,"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"fill":"#38A169","height":6,"type":"ellipse","width":6},{"content":"Live — CSS variables update instantly","fill":"#787774","fontSize":10,"type":"text"}],"gap":6,"height":28,"padding":[0,12],"stroke":{"align":"inside","fill":"#E9E9E7","thickness":{"top":1}},"type":"frame","width":"fill_container"}],"cornerRadius":8,"fill":"#FFFFFF","layout":"vertical","name":"App Preview","stroke":{"align":"inside","fill":"#E9E9E7","thickness":1},"type":"frame","width":380}],"fill":"#F0F0EF","gap":24,"name":"Live Theme Editing Flow","padding":24,"type":"frame","width":900,"x":0,"y":700}],"variables":{}} \ No newline at end of file diff --git a/design/trash-management.pen b/design/trash-management.pen new file mode 100644 index 0000000..ebcc2d6 --- /dev/null +++ b/design/trash-management.pen @@ -0,0 +1 @@ +{"children":[],"variables":{}} \ No newline at end of file diff --git a/design/trashed-note-editor.pen b/design/trashed-note-editor.pen new file mode 100644 index 0000000..a604284 --- /dev/null +++ b/design/trashed-note-editor.pen @@ -0,0 +1 @@ +{"children":[{"id":"trashed-banner-frame","type":"frame","name":"Trashed Note Banner","x":0,"y":0,"width":720,"height":340,"fill":"#FFFFFF","cornerRadius":[8,8,8,8],"children":[{"id":"breadcrumb","type":"frame","name":"Breadcrumb Bar","width":720,"height":45,"fill":"#FFFFFF","layout":"horizontal","mainAxisAlignment":"space-between","crossAxisAlignment":"center","padding":16,"children":[{"id":"breadcrumb-left","type":"frame","layout":"horizontal","gap":4,"crossAxisAlignment":"center","children":[{"id":"type-label","type":"text","content":"Note","fontSize":12,"textColor":"#6B7280"},{"id":"sep","type":"text","content":"›","fontSize":12,"textColor":"#6B7280"},{"id":"title","type":"text","content":"My Trashed Note","fontSize":12,"fontWeight":"600","textColor":"#1F2937"},{"id":"dot","type":"text","content":"·","fontSize":12,"textColor":"#6B7280"},{"id":"words","type":"text","content":"1,234 words","fontSize":12,"textColor":"#6B7280"}]},{"id":"breadcrumb-right","type":"frame","layout":"horizontal","gap":12,"crossAxisAlignment":"center","children":[{"id":"restore-icon","type":"text","content":"↺","fontSize":14,"textColor":"#6B7280"},{"id":"inspector-icon","type":"text","content":"⚙","fontSize":14,"textColor":"#6B7280"}]}]},{"id":"banner","type":"frame","name":"Trashed Note Banner","width":720,"height":32,"fill":"#FEF2F2","layout":"horizontal","crossAxisAlignment":"center","gap":8,"padding":16,"children":[{"id":"trash-icon","type":"text","content":"🗑","fontSize":12,"textColor":"#DC2626"},{"id":"banner-text","type":"text","content":"This note is in the Trash","fontSize":12,"textColor":"#6B7280","width":400},{"id":"restore-btn","type":"frame","layout":"horizontal","gap":4,"crossAxisAlignment":"center","padding":4,"cornerRadius":[4,4,4,4],"children":[{"id":"restore-icon-2","type":"text","content":"↺","fontSize":11,"textColor":"#2563EB"},{"id":"restore-label","type":"text","content":"Restore","fontSize":11,"textColor":"#2563EB"}]},{"id":"delete-btn","type":"frame","layout":"horizontal","gap":4,"crossAxisAlignment":"center","padding":4,"cornerRadius":[4,4,4,4],"children":[{"id":"delete-icon","type":"text","content":"🗑","fontSize":11,"textColor":"#DC2626"},{"id":"delete-label","type":"text","content":"Delete permanently","fontSize":11,"textColor":"#DC2626"}]}]},{"id":"editor-area","type":"frame","name":"Editor (read-only, muted)","width":720,"height":260,"fill":"#FAFAFA","padding":32,"children":[{"id":"h1","type":"text","content":"My Trashed Note","fontSize":24,"fontWeight":"700","textColor":"#9CA3AF"},{"id":"p1","type":"text","content":"This is the content of a trashed note. The editor is non-editable.","fontSize":14,"textColor":"#9CA3AF","y":40},{"id":"p2","type":"text","content":"Navigation and copy still work, but typing and editing are disabled.","fontSize":14,"textColor":"#9CA3AF","y":64}]}]}],"variables":{}} \ No newline at end of file diff --git a/design/ui-design-cleanup.pen b/design/ui-design-cleanup.pen new file mode 100644 index 0000000..c0fd64a --- /dev/null +++ b/design/ui-design-cleanup.pen @@ -0,0 +1,329 @@ +{ + "children": [ + { + "id": "cleanup-summary", + "type": "frame", + "name": "ui-design-cleanup — Summary of Changes", + "x": 0, + "y": 0, + "width": 800, + "height": "fit_content(600)", + "fill": "$--background", + "layout": "vertical", + "padding": 32, + "gap": 24, + "children": [ + { + "id": "title", + "type": "text", + "content": "ui-design.pen Cleanup — 2026-02-27", + "fontSize": 28, + "fontWeight": "700", + "color": "$--foreground" + }, + { + "id": "counts", + "type": "frame", + "layout": "horizontal", + "gap": 40, + "children": [ + { + "id": "count-before", + "type": "frame", + "layout": "vertical", + "gap": 4, + "children": [ + { + "id": "cb-label", + "type": "text", + "content": "Frames Before", + "fontSize": 12, + "color": "$--muted-foreground", + "fontFamily": "$--font-mono" + }, + { + "id": "cb-value", + "type": "text", + "content": "113", + "fontSize": 32, + "fontWeight": "700", + "color": "$--foreground" + } + ] + }, + { + "id": "count-after", + "type": "frame", + "layout": "vertical", + "gap": 4, + "children": [ + { + "id": "ca-label", + "type": "text", + "content": "Frames After", + "fontSize": 12, + "color": "$--muted-foreground", + "fontFamily": "$--font-mono" + }, + { + "id": "ca-value", + "type": "text", + "content": "129", + "fontSize": 32, + "fontWeight": "700", + "color": "$--accent-green" + } + ] + }, + { + "id": "count-removed", + "type": "frame", + "layout": "vertical", + "gap": 4, + "children": [ + { + "id": "cr-label", + "type": "text", + "content": "Before Variants Removed", + "fontSize": 12, + "color": "$--muted-foreground", + "fontFamily": "$--font-mono" + }, + { + "id": "cr-value", + "type": "text", + "content": "3", + "fontSize": 32, + "fontWeight": "700", + "color": "$--destructive" + } + ] + }, + { + "id": "count-integrated", + "type": "frame", + "layout": "vertical", + "gap": 4, + "children": [ + { + "id": "ci-label", + "type": "text", + "content": "Frames Integrated", + "fontSize": 12, + "color": "$--muted-foreground", + "fontFamily": "$--font-mono" + }, + { + "id": "ci-value", + "type": "text", + "content": "18", + "fontSize": 32, + "fontWeight": "700", + "color": "$--accent-blue" + } + ] + } + ] + }, + { + "id": "sep1", + "type": "line", + "width": "fill_container", + "stroke": "$--border", + "strokeWidth": 1 + }, + { + "id": "section-removed", + "type": "frame", + "layout": "vertical", + "gap": 8, + "children": [ + { + "id": "sr-title", + "type": "text", + "content": "Removed (before variants)", + "fontSize": 16, + "fontWeight": "600", + "color": "$--foreground" + }, + { + "id": "sr1", + "type": "text", + "content": "- csB01: Sections Header — Before (old design)", + "fontSize": 13, + "color": "$--muted-foreground" + }, + { + "id": "sr2", + "type": "text", + "content": "- mb001: macOS Border — Before (no border)", + "fontSize": 13, + "color": "$--muted-foreground" + }, + { + "id": "sr3", + "type": "text", + "content": "- hideBacklinksEmpty_before: Hide Empty Backlinks Label — Before", + "fontSize": 13, + "color": "$--muted-foreground" + } + ] + }, + { + "id": "section-renamed", + "type": "frame", + "layout": "vertical", + "gap": 8, + "children": [ + { + "id": "sn-title", + "type": "text", + "content": "Renamed (dropped After/Before labels)", + "fontSize": 16, + "fontWeight": "600", + "color": "$--foreground" + }, + { + "id": "sn1", + "type": "text", + "content": "- csA01: Sections Header — After (new design) → Sections Header — Redesigned", + "fontSize": 13, + "color": "$--muted-foreground" + }, + { + "id": "sn2", + "type": "text", + "content": "- mb011: macOS Border — After (with border) → macOS Border — With Border", + "fontSize": 13, + "color": "$--muted-foreground" + }, + { + "id": "sn3", + "type": "text", + "content": "- hideBacklinksEmpty_after: ... → Hide Empty Backlinks Label", + "fontSize": 13, + "color": "$--muted-foreground" + } + ] + }, + { + "id": "sep2", + "type": "line", + "width": "fill_container", + "stroke": "$--border", + "strokeWidth": 1 + }, + { + "id": "section-integrated", + "type": "frame", + "layout": "vertical", + "gap": 8, + "children": [ + { + "id": "si-title", + "type": "text", + "content": "Integrated from design/*.pen", + "fontSize": 16, + "fontWeight": "600", + "color": "$--foreground" + }, + { + "id": "si1", + "type": "text", + "content": "- add-property-inline.pen (2 frames) → Inspector section", + "fontSize": 13, + "color": "$--muted-foreground" + }, + { + "id": "si2", + "type": "text", + "content": "- autocomplete-type-color.pen (2 frames) → Inspector section", + "fontSize": 13, + "color": "$--muted-foreground" + }, + { + "id": "si3", + "type": "text", + "content": "- date-picker-shadcn.pen (2 frames) → Inspector section", + "fontSize": 13, + "color": "$--muted-foreground" + }, + { + "id": "si4", + "type": "text", + "content": "- design-full-layouts.pen (4 frames) → Full Layouts section", + "fontSize": 13, + "color": "$--muted-foreground" + }, + { + "id": "si5", + "type": "text", + "content": "- hide-backlinks-empty.pen (1 after variant) → Inspector section", + "fontSize": 13, + "color": "$--muted-foreground" + }, + { + "id": "si6", + "type": "text", + "content": "- smart-property-display.pen (4 frames) → Inspector section", + "fontSize": 13, + "color": "$--muted-foreground" + }, + { + "id": "si7", + "type": "text", + "content": "- status-property-dropdown.pen (3 frames) → Inspector section", + "fontSize": 13, + "color": "$--muted-foreground" + } + ] + }, + { + "id": "sep3", + "type": "line", + "width": "fill_container", + "stroke": "$--border", + "strokeWidth": 1 + }, + { + "id": "section-notes", + "type": "frame", + "layout": "vertical", + "gap": 8, + "children": [ + { + "id": "notes-title", + "type": "text", + "content": "Notes", + "fontSize": 16, + "fontWeight": "600", + "color": "$--foreground" + }, + { + "id": "note1", + "type": "text", + "content": "Dark mode conversion: Not needed — all frames already use CSS variables (light theme)", + "fontSize": 13, + "color": "$--muted-foreground" + }, + { + "id": "note2", + "type": "text", + "content": "Deduplication: No true duplicates found — unnamed frames are section dividers", + "fontSize": 13, + "color": "$--muted-foreground" + }, + { + "id": "note3", + "type": "text", + "content": "Empty design files (0 frames): auto-pull-vault, git-status-bar, note-subtitle-metadata, note-type-labels, properties-panel-header", + "fontSize": 13, + "color": "$--muted-foreground" + } + ] + } + ] + } + ], + "variables": {} +} \ No newline at end of file diff --git a/design/vault-agents-md.pen b/design/vault-agents-md.pen new file mode 100644 index 0000000..c3c1c91 --- /dev/null +++ b/design/vault-agents-md.pen @@ -0,0 +1 @@ +{"children":[{"children":[{"alignItems":"center","children":[{"alignItems":"center","children":[{"fill":"$--traffic-red","height":12,"id":"t9oxy","name":"tl1","type":"ellipse","width":12},{"fill":"$--traffic-yellow","height":12,"id":"l5RlH","name":"tl2","type":"ellipse","width":12},{"fill":"$--traffic-green","height":12,"id":"K0bAe","name":"tl3","type":"ellipse","width":12}],"gap":8,"id":"1ph7l","name":"trafficLights","type":"frame"}],"fill":"$--sidebar","height":38,"id":"pNQ1R","name":"macOS Title Bar","padding":[0,12],"stroke":{"align":"inside","fill":"$--border","thickness":{"bottom":1}},"type":"frame","width":"fill_container"},{"children":[{"children":[{"children":[{"alignItems":"center","children":[{"fill":"$--muted-foreground","height":16,"iconFontFamily":"lucide","iconFontName":"inbox","id":"e3bcH","name":"filterAllIcon","type":"icon_font","width":16},{"content":"All Notes","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"500","id":"U0SDl","name":"filterAllLabel","type":"text"},{"content":"12","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"normal","id":"3s4eX","name":"filterAllCount","type":"text"}],"cornerRadius":6,"fill":"$--muted","gap":8,"id":"UPibw","name":"filterAll","padding":[6,16],"type":"frame","width":"fill_container"}],"gap":1,"id":"vX4kL","layout":"vertical","name":"Filters","padding":[8,8,16,8],"stroke":{"align":"inside","fill":"$--border","thickness":{"bottom":1}},"type":"frame","width":"fill_container"},{"children":[{"alignItems":"center","children":[{"fill":"$--accent-purple","height":16,"iconFontFamily":"phosphor","iconFontName":"rocket-launch","id":"303MY","name":"secProjIcon","type":"icon_font","width":16},{"content":"Projects","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"500","id":"FLQBt","name":"secProjLabel","type":"text"},{"content":"1","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"normal","id":"M1fpt","name":"secProjCount","type":"text"}],"gap":8,"id":"HI1le","name":"secProjects","padding":[6,16],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"fill":"$--accent-blue","height":16,"iconFontFamily":"phosphor","iconFontName":"note","id":"uuuIy","name":"secNotesIcon","type":"icon_font","width":16},{"content":"Notes","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"500","id":"ShBbX","name":"secNotesLabel","type":"text"},{"content":"4","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"normal","id":"XqaZ6","name":"secNotesCount","type":"text"}],"gap":8,"id":"fu0S4","name":"secNotes","padding":[6,16],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"fill":"$--accent-green","height":16,"iconFontFamily":"phosphor","iconFontName":"user","id":"GidgQ","name":"secPeopleIcon","type":"icon_font","width":16},{"content":"People","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"500","id":"8p9jY","name":"secPeopleLabel","type":"text"},{"content":"1","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"normal","id":"xkgKZ","name":"secPeopleCount","type":"text"}],"gap":8,"id":"vllAy","name":"secPeople","padding":[6,16],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"fill":"$--accent-yellow","height":16,"iconFontFamily":"phosphor","iconFontName":"tag","id":"rih06","name":"secTopicsIcon","type":"icon_font","width":16},{"content":"Topics","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"500","id":"RMMWq","name":"secTopicsLabel","type":"text"},{"content":"1","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"normal","id":"aBI8L","name":"secTopicsCount","type":"text"}],"gap":8,"id":"tzibu","name":"secTopics","padding":[6,16],"type":"frame","width":"fill_container"},{"height":8,"id":"QNr5k","name":"spacer","type":"frame","width":"fill_container"},{"content":"VAULT ROOT","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":10,"fontWeight":"600","id":"uLv01","letterSpacing":1,"name":"rootLabel","type":"text"},{"alignItems":"center","children":[{"fill":"$--primary-foreground","height":16,"iconFontFamily":"lucide","iconFontName":"file-text","id":"QFDjK","name":"agentsIcon","type":"icon_font","width":16},{"content":"AGENTS.md","fill":"$--primary-foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"500","id":"u2NF1","name":"agentsLabel","type":"text"}],"cornerRadius":6,"fill":"$--primary","gap":8,"id":"p1FHZ","name":"agentsItem","padding":[6,16],"type":"frame","width":"fill_container"}],"height":"fill_container","id":"5DwQB","layout":"vertical","name":"Sections","padding":[8,0],"type":"frame","width":"fill_container"}],"clip":true,"fill":"$--sidebar","height":"fill_container","id":"t643b","layout":"vertical","name":"Panel: Sidebar","type":"frame","width":250},{"fill":"$--border","height":"fill_container","id":"EHHaL","name":"Divider","type":"rectangle","width":1},{"children":[{"alignItems":"center","children":[{"content":"All Notes","fill":"$--foreground","fontFamily":"Inter","fontSize":14,"fontWeight":"600","id":"PJU4E","name":"nlTitle","type":"text"},{"content":"12 notes","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"SA33b","name":"nlCount","type":"text"}],"height":45,"id":"ixPvn","justifyContent":"space_between","name":"NoteList Header","padding":[14,16],"stroke":{"align":"inside","fill":"$--border","thickness":{"bottom":1}},"type":"frame","width":"fill_container"},{"children":[{"children":[{"content":"AGENTS.md","fill":"$--foreground","fontFamily":"Inter","fontSize":14,"fontWeight":"600","id":"4wQGe","name":"ni1Title","type":"text"},{"content":"Vault Instructions for AI Agents — This is a Laputa vault...","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"ywIcy","lineHeight":1.5,"name":"ni1Snippet","textGrowth":"fixed-width","type":"text","width":"fill_container"}],"fill":"$--muted","gap":4,"id":"vJ8oO","layout":"vertical","name":"ni1","padding":[14,16],"stroke":{"align":"inside","fill":"$--border","thickness":{"bottom":1}},"type":"frame","width":"fill_container"},{"children":[{"content":"Welcome to Laputa","fill":"$--foreground","fontFamily":"Inter","fontSize":14,"fontWeight":"500","id":"hBzxZ","name":"ni2Title","type":"text"},{"content":"Welcome to your new knowledge vault! Laputa helps you organize...","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"ndPAL","lineHeight":1.5,"name":"ni2Snippet","textGrowth":"fixed-width","type":"text","width":"fill_container"}],"gap":4,"id":"dOhss","layout":"vertical","name":"ni2","padding":[14,16],"stroke":{"align":"inside","fill":"$--border","thickness":{"bottom":1}},"type":"frame","width":"fill_container"},{"children":[{"content":"Editor Basics","fill":"$--foreground","fontFamily":"Inter","fontSize":14,"fontWeight":"500","id":"86Z5D","name":"ni3Title","type":"text"},{"content":"Laputa uses a rich markdown editor. Here are the key formatting...","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"FJRW7","lineHeight":1.5,"name":"ni3Snippet","textGrowth":"fixed-width","type":"text","width":"fill_container"}],"gap":4,"id":"R6muw","layout":"vertical","name":"ni3","padding":[14,16],"stroke":{"align":"inside","fill":"$--border","thickness":{"bottom":1}},"type":"frame","width":"fill_container"},{"children":[{"content":"Sample Project","fill":"$--foreground","fontFamily":"Inter","fontSize":14,"fontWeight":"500","id":"O7zNH","name":"ni4Title","type":"text"},{"content":"This is an example project to show how projects work in Laputa...","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"kv72Q","lineHeight":1.5,"name":"ni4Snippet","textGrowth":"fixed-width","type":"text","width":"fill_container"}],"gap":4,"id":"YvP6c","layout":"vertical","name":"ni4","padding":[14,16],"stroke":{"align":"inside","fill":"$--border","thickness":{"bottom":1}},"type":"frame","width":"fill_container"}],"clip":true,"height":"fill_container","id":"qeL3K","layout":"vertical","name":"Note Items","type":"frame","width":"fill_container"}],"clip":true,"fill":"$--card","height":"fill_container","id":"tvBvY","layout":"vertical","name":"Panel: NoteList","type":"frame","width":300},{"fill":"$--border","height":"fill_container","id":"tjXh5","name":"Divider 2","type":"rectangle","width":1},{"children":[{"alignItems":"center","children":[{"alignItems":"center","children":[{"fill":"$--foreground","height":14,"iconFontFamily":"lucide","iconFontName":"file-text","id":"ujeOy","name":"tab1Icon","type":"icon_font","width":14},{"content":"AGENTS.md","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"500","id":"Mu8XO","name":"tab1Label","type":"text"}],"gap":6,"height":"fill_container","id":"2eSEH","name":"tab1","padding":[0,16],"stroke":{"align":"inside","fill":"$--border","thickness":{"bottom":2}},"type":"frame"}],"fill":"$--sidebar","height":45,"id":"JhTD2","name":"Tab Bar","stroke":{"align":"inside","fill":"$--sidebar-border","thickness":{"bottom":1}},"type":"frame","width":"fill_container"},{"children":[{"content":"AGENTS.md — Vault Instructions for AI Agents","fill":"$--foreground","fontFamily":"Inter","fontSize":28,"fontWeight":"700","id":"FJTxP","name":"edH1","textGrowth":"fixed-width","type":"text","width":"fill_container"},{"content":"This is a Laputa vault — a folder of markdown files with YAML frontmatter that form a personal knowledge graph.","fill":"$--foreground","fontFamily":"Inter","fontSize":16,"fontWeight":"normal","id":"KLRkh","lineHeight":1.6,"name":"edIntro","textGrowth":"fixed-width","type":"text","width":"fill_container"},{"content":"Structure","fill":"$--foreground","fontFamily":"Inter","fontSize":22,"fontWeight":"600","id":"StcZL","name":"edH2","type":"text"},{"content":"Files are organized in folders by type:","fill":"$--foreground","fontFamily":"Inter","fontSize":16,"fontWeight":"normal","id":"05oUA","lineHeight":1.6,"name":"edStructIntro","textGrowth":"fixed-width","type":"text","width":"fill_container"},{"children":[{"children":[{"content":"Folder","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"600","id":"H11FL","name":"hCol1","type":"text"},{"content":"Type","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"600","id":"JD8fz","name":"hCol2","type":"text"},{"content":"Purpose","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"600","id":"uU5Jz","name":"hCol3","type":"text"}],"fill":"$--muted","gap":8,"id":"mwbUg","name":"headerRow","padding":[8,12],"type":"frame","width":"fill_container"},{"children":[{"content":"note/","fill":"$--accent-blue","fontFamily":"IBM Plex Mono","fontSize":12,"fontWeight":"normal","id":"sb2yD","name":"r1c1","type":"text"},{"content":"Note","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"normal","id":"aGztj","name":"r1c2","type":"text"},{"content":"General-purpose documents, research, meeting notes","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"normal","id":"QagHP","name":"r1c3","type":"text"}],"gap":8,"id":"H8GnG","name":"row1","padding":[8,12],"stroke":{"align":"inside","fill":"$--border","thickness":{"top":1}},"type":"frame","width":"fill_container"},{"children":[{"content":"project/","fill":"$--accent-purple","fontFamily":"IBM Plex Mono","fontSize":12,"fontWeight":"normal","id":"S7jzm","name":"r2c1","type":"text"},{"content":"Project","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"normal","id":"2DvpL","name":"r2c2","type":"text"},{"content":"Time-bounded efforts with clear goals","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"normal","id":"xOISN","name":"r2c3","type":"text"}],"gap":8,"id":"1X3Ey","name":"row2","padding":[8,12],"stroke":{"align":"inside","fill":"$--border","thickness":{"top":1}},"type":"frame","width":"fill_container"},{"children":[{"content":"person/","fill":"$--accent-green","fontFamily":"IBM Plex Mono","fontSize":12,"fontWeight":"normal","id":"Dfb6K","name":"r3c1","type":"text"},{"content":"Person","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"normal","id":"yhjgH","name":"r3c2","type":"text"},{"content":"People — colleagues, collaborators, contacts","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"normal","id":"NL1qV","name":"r3c3","type":"text"}],"gap":8,"id":"sdSbp","name":"row3","padding":[8,12],"stroke":{"align":"inside","fill":"$--border","thickness":{"top":1}},"type":"frame","width":"fill_container"},{"children":[{"content":"topic/","fill":"$--accent-yellow","fontFamily":"IBM Plex Mono","fontSize":12,"fontWeight":"normal","id":"t6yAK","name":"r4c1","type":"text"},{"content":"Topic","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"normal","id":"3hrTN","name":"r4c2","type":"text"},{"content":"Subject areas that group related notes","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"normal","id":"S2l0W","name":"r4c3","type":"text"}],"gap":8,"id":"3VN69","name":"row4","padding":[8,12],"stroke":{"align":"inside","fill":"$--border","thickness":{"top":1}},"type":"frame","width":"fill_container"},{"children":[{"content":"type/","fill":"$--accent-orange","fontFamily":"IBM Plex Mono","fontSize":12,"fontWeight":"normal","id":"W2m7P","name":"r5c1","type":"text"},{"content":"Type","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"normal","id":"7hzGz","name":"r5c2","type":"text"},{"content":"Type definitions — icon, color, ordering","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"normal","id":"TNj3M","name":"r5c3","type":"text"}],"gap":8,"id":"BNbt7","name":"row5","padding":[8,12],"stroke":{"align":"inside","fill":"$--border","thickness":{"top":1}},"type":"frame","width":"fill_container"}],"clip":true,"cornerRadius":6,"id":"20lr0","layout":"vertical","name":"Structure Table","stroke":{"align":"inside","fill":"$--border","thickness":1},"type":"frame","width":"fill_container"},{"content":"Frontmatter","fill":"$--foreground","fontFamily":"Inter","fontSize":22,"fontWeight":"600","id":"5BCet","name":"edH3","type":"text"},{"content":"YAML frontmatter between --- delimiters defines metadata:","fill":"$--foreground","fontFamily":"Inter","fontSize":16,"fontWeight":"normal","id":"eJvEG","lineHeight":1.6,"name":"edFmDesc","textGrowth":"fixed-width","type":"text","width":"fill_container"},{"children":[{"content":"---","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":13,"fontWeight":"normal","id":"tX6oL","name":"codeLine1","type":"text"},{"content":"Is A: Project","fill":"$--foreground","fontFamily":"IBM Plex Mono","fontSize":13,"fontWeight":"normal","id":"Q0NAS","name":"codeLine2","type":"text"},{"content":"Status: Active","fill":"$--foreground","fontFamily":"IBM Plex Mono","fontSize":13,"fontWeight":"normal","id":"iAS2l","name":"codeLine3","type":"text"},{"content":"Owner: \"[[person/jane-doe]]\"","fill":"$--accent-blue","fontFamily":"IBM Plex Mono","fontSize":13,"fontWeight":"normal","id":"f9aJW","name":"codeLine4","type":"text"},{"content":"Belongs to: \"[[quarter/24q1]]\"","fill":"$--accent-blue","fontFamily":"IBM Plex Mono","fontSize":13,"fontWeight":"normal","id":"vHqgV","name":"codeLine5","type":"text"},{"content":"---","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":13,"fontWeight":"normal","id":"m4081","name":"codeLine6","type":"text"}],"cornerRadius":6,"fill":"$--muted","gap":2,"id":"spZfl","layout":"vertical","name":"codeBlock","padding":16,"type":"frame","width":"fill_container"}],"clip":true,"gap":16,"height":"fill_container","id":"C7esA","layout":"vertical","name":"Editor Body","padding":[32,80],"type":"frame","width":"fill_container"}],"clip":true,"fill":"$--background","height":"fill_container","id":"koB66","layout":"vertical","name":"Panel: Editor","type":"frame","width":"fill_container"}],"height":"fill_container","id":"sqOO6","name":"Content","type":"frame","width":"fill_container"}],"clip":true,"fill":"$--background","height":900,"id":"iiSrM","layout":"vertical","name":"AGENTS.md — Vault Root + Editor View","type":"frame","width":1440,"x":0,"y":0},{"children":[{"children":[{"alignItems":"center","children":[{"fill":"$--muted-foreground","height":16,"iconFontFamily":"lucide","iconFontName":"layers","id":"tX1IJ","name":"allNotesIcon","type":"icon_font","width":16},{"content":"All Notes","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"500","id":"h1AvC","name":"allNotesLabel","type":"text"},{"content":"12","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"normal","id":"E50Rv","name":"allNotesCount","type":"text"}],"cornerRadius":6,"gap":8,"id":"cHU7c","name":"filterAllNotes","padding":[6,16],"type":"frame","width":"fill_container"}],"gap":1,"id":"rn991","layout":"vertical","name":"Filters","padding":[8,8,16,8],"stroke":{"align":"inside","fill":"$--border","thickness":{"bottom":1}},"type":"frame","width":"fill_container"},{"children":[{"alignItems":"center","children":[{"fill":"$--accent-purple","height":16,"iconFontFamily":"phosphor","iconFontName":"rocket-launch","id":"LCxKU","name":"secProjIcon","type":"icon_font","width":16},{"content":"Projects","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"500","id":"tCo0m","name":"secProjLabel","type":"text"},{"content":"1","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"normal","id":"WmcbF","name":"secProjCount","type":"text"}],"gap":8,"id":"ZV8hM","name":"secProjects","padding":[6,16],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"fill":"$--accent-blue","height":16,"iconFontFamily":"phosphor","iconFontName":"note","id":"9N6hy","name":"secNotesIcon","type":"icon_font","width":16},{"content":"Notes","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"500","id":"dwLa6","name":"secNotesLabel","type":"text"},{"content":"4","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"normal","id":"pF6Vk","name":"secNotesCount","type":"text"}],"gap":8,"id":"TF7eK","name":"secNotes","padding":[6,16],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"fill":"$--accent-green","height":16,"iconFontFamily":"phosphor","iconFontName":"user","id":"aeUGs","name":"secPeopleIcon","type":"icon_font","width":16},{"content":"People","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"500","id":"4Pksr","name":"secPeopleLabel","type":"text"},{"content":"1","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"normal","id":"mZkh4","name":"secPeopleCount","type":"text"}],"gap":8,"id":"5AJ0W","name":"secPeople","padding":[6,16],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"fill":"$--accent-yellow","height":16,"iconFontFamily":"phosphor","iconFontName":"tag","id":"Lu26g","name":"secTopicsIcon","type":"icon_font","width":16},{"content":"Topics","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"500","id":"pd7sa","name":"secTopicsLabel","type":"text"},{"content":"1","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"normal","id":"zvf8X","name":"secTopicsCount","type":"text"}],"gap":8,"id":"uCdg1","name":"secTopics","padding":[6,16],"type":"frame","width":"fill_container"},{"height":8,"id":"XZNp8","name":"spacer","type":"frame","width":"fill_container"},{"content":"VAULT ROOT","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":10,"fontWeight":"600","id":"JH4k8","letterSpacing":1,"name":"rootLabel","type":"text"},{"alignItems":"center","children":[{"fill":"$--primary-foreground","height":16,"iconFontFamily":"lucide","iconFontName":"file-text","id":"TeldH","name":"agentsIcon","type":"icon_font","width":16},{"content":"AGENTS.md","fill":"$--primary-foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"500","id":"goqrg","name":"agentsLabel","type":"text"}],"cornerRadius":6,"fill":"$--primary","gap":8,"id":"Vnjwi","name":"agentsItem","padding":[6,16],"type":"frame","width":"fill_container"}],"id":"kZsX6","layout":"vertical","name":"Sections","padding":[8,0],"type":"frame","width":"fill_container"},{"children":[{"content":"AGENTS.md appears in the vault root section of the sidebar, below type sections. It is always present and selected highlights it with the primary accent.","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"normal","id":"gXRLY","lineHeight":1.5,"name":"annotationText","textGrowth":"fixed-width","type":"text","width":"fill_container"}],"fill":"$--muted","id":"bbJjG","name":"Annotation","padding":[12,16],"type":"frame","width":"fill_container"}],"clip":true,"fill":"$--sidebar","id":"omsAY","layout":"vertical","name":"AGENTS.md — Sidebar Vault Root Section","type":"frame","width":250,"x":1540,"y":0}],"variables":{"--accent-blue":{"type":"color","value":"#2383E2"},"--accent-green":{"type":"color","value":"#0F7B6C"},"--accent-orange":{"type":"color","value":"#D9730D"},"--accent-purple":{"type":"color","value":"#9065B0"},"--accent-yellow":{"type":"color","value":"#F0B100"},"--background":{"type":"color","value":"#FFFFFF"},"--border":{"type":"color","value":"#E9E9E7"},"--card":{"type":"color","value":"#FFFFFF"},"--foreground":{"type":"color","value":"#37352F"},"--muted":{"type":"color","value":"#F0F0EF"},"--muted-foreground":{"type":"color","value":"#787774"},"--primary":{"type":"color","value":"#155DFF"},"--primary-foreground":{"type":"color","value":"#FFFFFF"},"--sidebar":{"type":"color","value":"#F7F6F3"},"--sidebar-border":{"type":"color","value":"#E9E9E7"},"--traffic-green":{"type":"color","value":"#28c840"},"--traffic-red":{"type":"color","value":"#ff5f57"},"--traffic-yellow":{"type":"color","value":"#febc2e"}}} diff --git a/design/vault-management-remove.pen b/design/vault-management-remove.pen new file mode 100644 index 0000000..4609e8d --- /dev/null +++ b/design/vault-management-remove.pen @@ -0,0 +1,281 @@ +{ + "children": [ + { + "type": "frame", + "id": "vault_menu_remove", + "name": "Vault Menu — Remove from List", + "x": 0, + "y": 0, + "width": 380, + "height": 320, + "fill": "#F7F6F3", + "layout": "vertical", + "gap": 16, + "padding": [24, 24, 24, 24], + "theme": { "Mode": "Light" }, + "children": [ + { + "type": "text", + "id": "vault_menu_title", + "content": "Vault Menu — Remove Action", + "fill": "#37352F", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "vault_menu_dropdown", + "name": "Vault Dropdown", + "width": "fill_container", + "height": "fit_content", + "fill": "#FFFFFF", + "cornerRadius": [6, 6, 6, 6], + "stroke": "#E9E9E7", + "strokeThickness": 1, + "layout": "vertical", + "padding": [4, 4, 4, 4], + "gap": 0, + "children": [ + { + "type": "frame", + "id": "vault_item_active", + "name": "Active Vault Item", + "width": "fill_container", + "height": 32, + "fill": "#EBEBEA", + "cornerRadius": [4, 4, 4, 4], + "layout": "horizontal", + "alignItems": "center", + "justifyContent": "space-between", + "padding": [4, 8, 4, 8], + "children": [ + { + "type": "frame", + "id": "vault_item_active_left", + "layout": "horizontal", + "alignItems": "center", + "gap": 6, + "children": [ + { "type": "text", "id": "vault_check", "content": "✓", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 12 }, + { "type": "text", "id": "vault_label_active", "content": "My Vault", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 12 } + ] + }, + { + "type": "frame", + "id": "remove_btn_active", + "name": "Remove Button", + "width": 18, + "height": 18, + "cornerRadius": [3, 3, 3, 3], + "layout": "vertical", + "alignItems": "center", + "justifyContent": "center", + "children": [ + { "type": "text", "id": "x_icon_active", "content": "×", "fill": "#787774", "fontFamily": "Inter", "fontSize": 12 } + ] + } + ] + }, + { + "type": "frame", + "id": "vault_item_other", + "name": "Other Vault Item", + "width": "fill_container", + "height": 32, + "cornerRadius": [4, 4, 4, 4], + "layout": "horizontal", + "alignItems": "center", + "justifyContent": "space-between", + "padding": [4, 8, 4, 8], + "children": [ + { + "type": "frame", + "id": "vault_item_other_left", + "layout": "horizontal", + "alignItems": "center", + "gap": 6, + "children": [ + { "type": "text", "id": "vault_spacer", "content": " ", "fill": "transparent", "fontFamily": "Inter", "fontSize": 12 }, + { "type": "text", "id": "vault_label_other", "content": "Work Vault", "fill": "#787774", "fontFamily": "Inter", "fontSize": 12 } + ] + }, + { + "type": "frame", + "id": "remove_btn_other", + "name": "Remove Button", + "width": 18, + "height": 18, + "cornerRadius": [3, 3, 3, 3], + "layout": "vertical", + "alignItems": "center", + "justifyContent": "center", + "children": [ + { "type": "text", "id": "x_icon_other", "content": "×", "fill": "#787774", "fontFamily": "Inter", "fontSize": 12 } + ] + } + ] + }, + { + "type": "frame", + "id": "vault_separator", + "name": "Separator", + "width": "fill_container", + "height": 1, + "fill": "#E9E9E7" + }, + { + "type": "frame", + "id": "vault_open_folder", + "name": "Open Local Folder", + "width": "fill_container", + "height": 32, + "cornerRadius": [4, 4, 4, 4], + "layout": "horizontal", + "alignItems": "center", + "gap": 6, + "padding": [4, 8, 4, 8], + "children": [ + { "type": "text", "id": "folder_icon", "content": "📁", "fontFamily": "Inter", "fontSize": 12 }, + { "type": "text", "id": "open_folder_label", "content": "Open local folder", "fill": "#787774", "fontFamily": "Inter", "fontSize": 12 } + ] + } + ] + }, + { + "type": "text", + "id": "vault_menu_note", + "content": "× button removes vault from list without deleting files.\nAvailable when 2+ vaults in list.", + "fill": "#787774", + "fontFamily": "Inter", + "fontSize": 11, + "width": "fill_container" + } + ] + }, + { + "type": "frame", + "id": "cmd_k_vault_commands", + "name": "Cmd+K — Vault Commands", + "x": 420, + "y": 0, + "width": 520, + "height": 320, + "fill": "#F7F6F3", + "layout": "vertical", + "gap": 16, + "padding": [24, 24, 24, 24], + "theme": { "Mode": "Light" }, + "children": [ + { + "type": "text", + "id": "cmdk_title", + "content": "Command Palette — Vault Commands", + "fill": "#37352F", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "cmdk_palette", + "name": "Command Palette", + "width": "fill_container", + "height": "fit_content", + "fill": "#FFFFFF", + "cornerRadius": [8, 8, 8, 8], + "stroke": "#E9E9E7", + "strokeThickness": 1, + "layout": "vertical", + "padding": [0, 0, 0, 0], + "gap": 0, + "children": [ + { + "type": "frame", + "id": "cmdk_search", + "name": "Search Input", + "width": "fill_container", + "height": 44, + "layout": "horizontal", + "alignItems": "center", + "padding": [0, 16, 0, 16], + "children": [ + { "type": "text", "id": "cmdk_search_text", "content": "vault", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 14 } + ] + }, + { + "type": "frame", + "id": "cmdk_separator", + "width": "fill_container", + "height": 1, + "fill": "#E9E9E7" + }, + { + "type": "frame", + "id": "cmdk_group_header", + "name": "Settings Group", + "width": "fill_container", + "height": 28, + "layout": "horizontal", + "alignItems": "center", + "padding": [0, 12, 0, 12], + "children": [ + { "type": "text", "id": "group_label", "content": "Settings", "fill": "#B4B4B4", "fontFamily": "Inter", "fontSize": 11, "fontWeight": "500" } + ] + }, + { + "type": "frame", + "id": "cmdk_open_vault", + "name": "Open Vault Command", + "width": "fill_container", + "height": 36, + "layout": "horizontal", + "alignItems": "center", + "padding": [0, 12, 0, 12], + "children": [ + { "type": "text", "id": "cmd_open_vault", "content": "Open Vault…", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 13 } + ] + }, + { + "type": "frame", + "id": "cmdk_remove_vault", + "name": "Remove Vault Command (highlighted)", + "width": "fill_container", + "height": 36, + "fill": "#E8F4FE", + "layout": "horizontal", + "alignItems": "center", + "padding": [0, 12, 0, 12], + "children": [ + { "type": "text", "id": "cmd_remove_vault", "content": "Remove Vault from List", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 13 } + ] + }, + { + "type": "frame", + "id": "cmdk_restore_gs", + "name": "Restore Getting Started Command", + "width": "fill_container", + "height": 36, + "layout": "horizontal", + "alignItems": "center", + "padding": [0, 12, 0, 12], + "children": [ + { "type": "text", "id": "cmd_restore_gs", "content": "Restore Getting Started Vault", "fill": "#37352F", "fontFamily": "Inter", "fontSize": 13 } + ] + } + ] + }, + { + "type": "text", + "id": "cmdk_note", + "content": "• 'Remove Vault from List' removes active vault (disabled if only 1 vault)\n• 'Restore Getting Started Vault' shown when GS vault is hidden\n• Both accessible via Cmd+K search with 'vault' keyword", + "fill": "#787774", + "fontFamily": "Inter", + "fontSize": 11, + "width": "fill_container" + } + ] + } + ], + "variables": {} +} diff --git a/design/zoom-shortcuts.pen b/design/zoom-shortcuts.pen new file mode 100644 index 0000000..78ec492 --- /dev/null +++ b/design/zoom-shortcuts.pen @@ -0,0 +1,202 @@ +{ + "children": [ + { + "type": "frame", + "id": "zs_normal", + "name": "Zoom Shortcuts — StatusBar (100% / default)", + "x": 0, + "y": 0, + "theme": { "Mode": "Light" }, + "width": 800, + "height": 32, + "fill": "$--background", + "layout": "horizontal", + "gap": 0, + "padding": [0, 12], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "zs_normal_desc", + "name": "description", + "width": "fill_container", + "children": [ + { + "type": "text", + "id": "zs_normal_desc_text", + "fill": "$--muted-foreground", + "content": "StatusBar at 100% zoom — zoom indicator is HIDDEN. Cmd+= zooms in, Cmd+- zooms out, Cmd+0 resets to 100%.", + "fontFamily": "Inter", + "fontSize": 11 + } + ] + } + ] + }, + { + "type": "frame", + "id": "zs_zoomed", + "name": "Zoom Shortcuts — StatusBar (150% / zoomed in)", + "x": 0, + "y": 60, + "theme": { "Mode": "Light" }, + "width": 800, + "height": 32, + "fill": "$--background", + "layout": "horizontal", + "gap": 0, + "padding": [0, 12], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "zs_statusbar_zoomed", + "name": "StatusBar — 150% (zoom indicator visible)", + "layout": "horizontal", + "gap": 8, + "alignItems": "center", + "padding": [0, 8], + "children": [ + { + "type": "text", + "id": "zs_wordcount_z", + "fill": "$--muted-foreground", + "content": "1,247 words", + "fontFamily": "IBM Plex Mono", + "fontSize": 11 + }, + { + "type": "frame", + "id": "zs_zoom_badge", + "name": "Zoom % Badge", + "fill": "$--muted", + "cornerRadius": 4, + "padding": [2, 6], + "alignItems": "center", + "gap": 4, + "children": [ + { + "type": "text", + "id": "zs_zoom_label", + "fill": "$--foreground", + "content": "150%", + "fontFamily": "IBM Plex Mono", + "fontSize": 11, + "fontWeight": "500" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "zs_cmd_palette", + "name": "Zoom Shortcuts — Command Palette entries", + "x": 0, + "y": 120, + "theme": { "Mode": "Light" }, + "width": 540, + "height": 120, + "fill": "$--popover", + "cornerRadius": 12, + "layout": "vertical", + "gap": 0, + "children": [ + { + "type": "frame", + "id": "zs_cmd_row1", + "name": "Command — Zoom In (⌘=)", + "layout": "horizontal", + "width": "fill_container", + "padding": [8, 12], + "gap": 8, + "alignItems": "center", + "fill": "$--accent", + "children": [ + { + "type": "text", + "id": "zs_cmd_row1_label", + "fill": "$--accent-foreground", + "content": "Zoom In", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500", + "width": "fill_container" + }, + { + "type": "text", + "id": "zs_cmd_row1_shortcut", + "fill": "$--muted-foreground", + "content": "Command+=", + "fontFamily": "IBM Plex Mono", + "fontSize": 11 + } + ] + }, + { + "type": "frame", + "id": "zs_cmd_row2", + "name": "Command — Zoom Out (⌘-)", + "layout": "horizontal", + "width": "fill_container", + "padding": [8, 12], + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "zs_cmd_row2_label", + "fill": "$--foreground", + "content": "Zoom Out", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500", + "width": "fill_container" + }, + { + "type": "text", + "id": "zs_cmd_row2_shortcut", + "fill": "$--muted-foreground", + "content": "Command+-", + "fontFamily": "IBM Plex Mono", + "fontSize": 11 + } + ] + }, + { + "type": "frame", + "id": "zs_cmd_row3", + "name": "Command — Reset Zoom (⌘0)", + "layout": "horizontal", + "width": "fill_container", + "padding": [8, 12], + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "zs_cmd_row3_label", + "fill": "$--foreground", + "content": "Reset Zoom", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500", + "width": "fill_container" + }, + { + "type": "text", + "id": "zs_cmd_row3_shortcut", + "fill": "$--muted-foreground", + "content": "Command+0", + "fontFamily": "IBM Plex Mono", + "fontSize": 11 + } + ] + } + ] + } + ], + "variables": {} +} diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md new file mode 100644 index 0000000..a35eb91 --- /dev/null +++ b/docs/ABSTRACTIONS.md @@ -0,0 +1,1042 @@ +# Abstractions + +Key abstractions and domain models in Tolaria. + +## Design Philosophy + +Tolaria's abstractions follow the **convention over configuration** principle: standard field names, types, and relationships have well-defined meanings and trigger UI behavior automatically. This makes vaults legible both to humans and to AI agents — the more a vault follows conventions, the less custom configuration an AI needs to navigate it correctly. + +The full set of design principles is documented in [ARCHITECTURE.md](./ARCHITECTURE.md#design-principles). + +## Semantic Field Names (conventions) + +These frontmatter field names have special meaning in Tolaria's UI: + +| Field | Meaning | UI behavior | +|---|---|---| +| `title:` | Legacy display-title fallback for older notes | Used only when a note has no H1; new notes do not write it automatically | +| `type:` | Entity type (Project, Person, Quarter…) | Type chip in note list + sidebar grouping | +| `status:` | Lifecycle stage (active, done, blocked…) | Colored chip in note list + editor header | +| `icon:` | Per-note icon (emoji, Phosphor name, or HTTP/HTTPS image URL) | Rendered on note title surfaces; editable from the Properties panel | +| `url:` | External link | Clickable link chip in editor header | +| `date:` | Single date | Formatted date badge | +| `start_date:` + `end_date:` | Duration/timespan | Date range badge | +| `goal:` + `result:` | Progress | Progress indicator in editor header | +| `Workspace:` | Vault context filter | Global workspace filter | +| `belongs_to:` | Parent relationship | Humanized to `Belongs to` in the UI | +| `related_to:` | Lateral relationship | Humanized to `Related to` in the UI | +| `has:` | Contained relationship | Humanized to `Has` in the UI | + +Relationship fields are detected dynamically — any frontmatter field containing `[[wikilink]]` values is treated as a relationship (see [ADR-0010](adr/0010-dynamic-wikilink-relationship-detection.md)). Tolaria's own default relationship vocabulary uses snake_case on disk, but labels are humanized at render time and existing user-authored keys are left untouched. + +### System Properties (underscore convention) + +Any frontmatter field whose name starts with `_` is a **system property**: + +- It is **not shown** in the Properties panel (neither for notes nor for Type notes) +- It is **not exposed** as a user-visible property in search, filters, or the UI +- It **is editable** directly in the raw editor (power users can access it if needed) +- It is used by Tolaria internally for configuration, behavior, and UI preferences + +Examples: +```yaml +_pinned_properties: # which properties appear in the editor inline bar (per-type) + - key: status + icon: circle-dot +_icon: shapes # icon assigned to a type +_color: blue # color assigned to a type +_order: 10 # sort order in the sidebar +_sidebar_label: Projects # override label in sidebar +_width: wide # rich-editor width override for this note +``` + +**This convention is universal** — apply it to all future system-level frontmatter fields. When a new feature needs to store configuration in a note's frontmatter (especially in Type notes), use `_field_name` to keep it hidden from normal user-facing surfaces while still stored on-disk as plain text. + +The frontmatter parser (Rust: `vault/mod.rs`, TS: `utils/frontmatter.ts`) must filter out `_*` fields before passing `properties` to the UI. + +## Document Model + +All data lives in markdown files with YAML frontmatter. There is no database — the filesystem is the source of truth. + +### Rich-Editor Markdown Serialization + +`src/utils/richEditorMarkdown.ts` is the canonical owner for turning a BlockNote document back into vault Markdown. It restores Tolaria's durable Markdown syntax for wikilinks, math, highlights, attachments, Mermaid, and tldraw blocks before the save path writes bytes to disk. Hot save paths should pass an already-read block snapshot when they have one; a debounced rich-editor flush should not ask BlockNote for `editor.document` twice. + +Real BlockNote editor instances install the experimental direct serializer from `src/utils/blockNoteDirectMarkdown.ts`. Callers should go through `serializeBlockNoteMarkdown()` rather than invoking `blocksToMarkdownLossy()` directly: the helper uses the direct serializer when every block shape is supported and falls back to BlockNote's exporter when a custom or unknown block appears. + +The direct serializer cache is derived state, scoped to the editor instance, and disposable. Bridge helpers that transform blocks for Markdown restoration should preserve object identity when a block, its children, and table cells are unchanged; that lets frequent debounced saves reuse cached Markdown for stable subtrees without treating the cache as the source of truth. + +Opening a Markdown note goes through `resolveBlocksForTarget()` rather than calling BlockNote's parser unconditionally. The resolver first checks exact-source block caches, then uses cheap blank/H1 recognition, then tries Tolaria's worker-backed direct parser for large common Markdown. Unsupported Markdown returns to BlockNote's parser with the same durable-token injection path, preserving correctness while keeping the common large-note path off BlockNote's heavier import lifecycle. Large block sets are then mounted through `applyBlocksToEditorProgressively()`, which locks the editor while chunks append over animation frames and commits the path only after the full document is present. Ordinary BlockNote block wrappers remain fully measurable after mount so side menus and dialogs can preserve viewport position in long documents. + +Hot edit paths should avoid document-wide BlockNote lifecycle work. `richEditorBlockNoteOptions.ts` disables BlockNote's previous-block animation tracker for Tolaria editors because it scans the full old and new ProseMirror documents for every doc-changing transaction. `richEditorDispatchPerformance.ts` wraps the ProseMirror dispatch function once and logs `richEditorDispatch` timings for large or slow transactions without traversing the document. Feature code that subscribes to `editor.onChange` must either be debounced/coalesced or active only while its UI state requires it; collapsed-heading rendering follows this rule by subscribing only while sections are collapsed. + +Block-selection behavior keeps UI/plugin wiring separate from block operations. `richEditorBlockSelectionExtension.ts` owns the ProseMirror plugin, state reducer, decorations, and keyboard/clipboard event dispatch. `richEditorBlockSelectionDocument.ts` owns BlockNote document traversal, nested-selection pruning, collapsed hidden-content operation IDs, and block-move helpers. `richEditorBlockSelectionClipboard.ts` owns Tolaria clipboard MIME data, BlockNote HTML/Markdown fallback parsing, and ID stripping before paste. + +Focused block type changes share the same block-type catalog in `src/utils/richEditorBlockTypes.ts` across the command palette and block handle menu. UI surfaces should call the helpers in `src/components/richEditorBlockTypeCommands.ts` instead of manually constructing BlockNote updates so conversion preserves the current block content, runs inside one editor transaction, and emits the shared `editor_block_type_changed` analytics event. + +### Vault Git Capability + +Git is a per-vault capability, not a prerequisite for the document model. A vault can be: + +| State | Meaning | UI behavior | +|---|---|---| +| Git-backed | The vault path is inside a Git work tree, including a parent repository above the mounted folder | History, changes, commits, sync, conflict resolution, remotes, AutoGit, and auto-sync are available according to remote/config state | +| Non-git | The vault path is a plain folder | Markdown scanning, editing, search, and navigation work; Git-dependent status-bar controls and command-palette entries are replaced by `Git disabled` + `Initialize Git for Current Vault` | + +Plain folders become Git-backed only when the user explicitly runs Git initialization from the setup dialog, status bar, or command palette. The setup dialog supports "not now" for a one-time dismissal and "never for this vault" for a local per-vault opt-out from future automatic prompts. Features that depend on Git must check both the vault capability and the installation-local `git_enabled` setting instead of assuming every vault has `.git` or that Git chrome is globally visible. + +The Git executable provider is an installation-local setting layered under that vault capability. Native Git is the default provider. On Windows, users may explicitly select WSL2 Git and a distribution in Settings; command launch then goes through the Rust Git provider abstraction, translates repository paths to WSL form, and never switches away from native Git without the stored provider setting. + +Git initialization is intentionally scoped to dedicated vault folders. When the current non-git folder looks like a broad personal root such as Documents, Desktop, or Downloads and does not already carry Tolaria-managed vault markers, `init_git_repo` refuses to run Git and asks the user to select or create a dedicated subfolder instead. + +### VaultEntry + +The core data type representing a single note, defined in Rust (`src-tauri/src/vault/mod.rs`) and TypeScript (`src/types.ts`). + +```mermaid +classDiagram + class VaultEntry { + +String path + +String filename + +String title + +String? isA + +String[] aliases + +String[] belongsTo + +String[] relatedTo + +Record~string,string[]~ relationships + +String[] outgoingLinks + +String? status + +String? noteWidth + +Number? modifiedAt + +Number? createdAt + +Number wordCount + +String? snippet + +Boolean archived + +WorkspaceIdentity? workspace + +Boolean trashed ⚠ legacy + +Number? trashedAt ⚠ legacy + +Record~string,VaultPropertyValue~ properties + } + + class TypeDocument { + +String icon + +String color + +Number order + +String sidebarLabel + +String template + +String sort + +Boolean visible + } + + class Frontmatter { + +String type + +String status + +String url + +String[] belongsTo + +String[] relatedTo + +String[] aliases + ...custom fields + } + + VaultEntry --> Frontmatter : parsed from + VaultEntry --> TypeDocument : isA resolves to + VaultEntry "many" --> "1" TypeDocument : grouped by type +``` + +```typescript +// src/types.ts +interface VaultEntry { + path: string // Absolute file path + filename: string // Just the filename + title: string // From first # heading, or filename fallback + isA: string | null // Entity type: Project, Procedure, Person, etc. (from frontmatter `type:` field) + aliases: string[] // Alternative names for wikilink resolution + belongsTo: string[] // Parent relationships (wikilinks) + relatedTo: string[] // Related entity links (wikilinks) + relationships: Record // All frontmatter fields containing wikilinks + outgoingLinks: string[] // All [[wikilinks]] found in note body + status: string | null // Active, Done, Paused, Archived, Dropped + noteWidth?: 'normal' | 'wide' | null // Rich-editor width mode from `_width` + modifiedAt: number | null // Unix timestamp (seconds) + // Note: owner and cadence are now in the generic `properties` map + createdAt: number | null // Unix timestamp (seconds) + fileSize: number + wordCount: number | null // Body word count (excludes frontmatter) + snippet: string | null // First 200 chars of body + workspace?: WorkspaceIdentity // Mounted-workspace provenance for cross-vault graph entries + archived: boolean // Archived flag + trashed: boolean // Kept for backward compatibility (Trash system removed — delete is permanent) + trashedAt: number | null // Kept for backward compatibility (Trash system removed) + properties: Record // Scalar and scalar-array custom properties + fileKind?: 'markdown' | 'text' | 'binary' // Controls editor/raw/preview behavior +} +``` + +### WorkspaceIdentity + +Mounted workspace provenance is renderer-owned metadata attached to `VaultEntry.workspace` when entries are loaded through the registered workspace set. It is not parsed from note frontmatter and is not written into vault files. + +```typescript +interface WorkspaceIdentity { + id: string + label: string + alias: string // Stable prefix used in cross-workspace wikilinks + path: string // Absolute workspace root + shortLabel: string // Compact note-list badge text + color: string | null + icon: string | null + mounted: boolean + available: boolean + defaultForNewNotes: boolean +} +``` + +The status-bar workspace manager edits installation-local identity and mount state. The alias is the durable user-facing namespace for cross-workspace links such as `[[team/projects/alpha]]`; labels and colors are display affordances only. The default workspace controls where new notes and Type files are created; it is not a claim that only one vault is active. When multiple workspaces are enabled, every mounted available workspace participates in the graph and the active Git repository set. + +Git-facing renderer code must pass an explicit repository path instead of assuming a single active vault. Changes and Pulse/history display one selected repository at a time, manual commit selects one target repository, and AutoGit checkpoints iterate every active repository. Diff, file history, note saves, and discarded changes resolve the repository from the note's workspace provenance or from the selected Git surface. Manual Sync refreshes vault-derived sidebar state and bumps a shared Git history refresh key after successful pulls, including `up_to_date` pulls, while automatic up-to-date checks avoid that heavier reload path. + +`useGitFileWorkflows` is the renderer abstraction for note-scoped Git file actions. It translates active tabs, visible entries, and modified-file surfaces into the correct repository path for diff/history commands, deleted-note previews, queued editor diff requests, and discard refresh behavior. + +### Tolaria Deep Links + +Deep links identify existing vault items with `tolaria:///`. The slug is derived from the registered workspace alias, then label, then path basename; generated links append a stable short hash when two vaults share the same base slug. A manually typed ambiguous base slug is rejected instead of choosing the wrong vault. + +The relative path is encoded per segment, preserving `/` as the separator while allowing spaces, Unicode, and reserved characters inside filenames. Decoding rejects `.`, `..`, encoded slashes, backslashes, empty segments, and any resolved path outside the target vault root. Links keep the file extension so Markdown, text, media, PDFs, and other vault files can all route through the same `VaultEntry` lookup. + +Deep links are navigation-only. Opening one can focus Tolaria, switch to a registered vault, reload the index once, and open an existing item; it never creates missing files, imports external files, or silently falls back to another vault. v1 links are path-based, so renaming or moving a file changes the canonical link. macOS and Windows are the verified v1 desktop targets; Linux registration is best-effort until package-level QA covers the supported desktop environments. + +### File kinds and binary previews + +`VaultEntry.fileKind` comes from the Rust vault scanner and intentionally stays coarse-grained: + +| `fileKind` | Source files | UI behavior | +|---|---|---| +| `markdown` or absent | `.md`, `.markdown` | Full Tolaria note model: frontmatter, BlockNote, raw editor, relationships, title sync | +| `text` | UTF-8 editable formats such as `.yml`, `.json`, `.ts`, `.py`, `.sh` | Opens through the raw editor without Markdown note semantics; supported extensions get CodeMirror syntax highlighting | +| `binary` | Images, audio, video, PDFs, archives, other non-text files | Stays a normal vault file; previewable media and PDFs open in `FilePreview`, unsupported or broken binaries show an explicit fallback | + +Asset previewability is inferred in the renderer from the filename extension (`src/utils/filePreview.ts`) rather than stored as a new persisted kind. Supported images render through ``, supported audio/video render through native HTML media controls, and supported PDFs render through the webview's PDF object renderer, all backed by Tauri asset URLs. HEIC/HEIF is intentionally handled as an unsupported image import format until Tolaria has a deterministic cross-platform decoder/conversion path; Windows webviews cannot be assumed to decode iPhone HEIC files, so picker uploads and native drops report a localized fallback message and do not save a broken attachment. On Linux AppImage builds, `should_use_external_media_preview` can disable in-webview audio/video rendering so the same file blocks show filename/external-open fallback controls instead of triggering unstable WebKitGTK media playback. Runtime asset access is accumulated only for vault roots Tolaria has loaded in the current app session, because Tauri directory forbids cannot be safely reversed after a vault switch. The "open in default app" action re-enters the active-vault command boundary through `open_vault_file_external` before delegating to the native opener. This keeps the filesystem as source of truth and avoids converting assets into proprietary objects. + +Markdown note PDF export is not a stored file-kind transformation. `src/utils/notePdfExport.ts` temporarily marks the current webview for print-only rendering, asks for a `.pdf` filesystem destination only when the native capability reports direct save support, and invokes Tauri's native `WKWebView` PDF export command on macOS. Windows/Linux Tauri builds and browser mode keep print-dialog fallback behavior. `src/components/useEditorPdfExport.ts` ensures the rich rendered note is active before export, so frontmatter is ignored and the PDF reflects the current rendered editor DOM while leaving the vault file unchanged. + +### Sheet Nodes + +A Markdown note with `_display: sheet` displays in the dedicated sheet editor instead of BlockNote. `type` remains semantic metadata, so a sheet can still be a `Project`, `Responsibility`, or any other Tolaria type. The note remains a plain-text file: frontmatter stores ordinary Tolaria properties plus `_sheet` presentation metadata, while the body stores CSV-like rows containing cell inputs and formulas. + +`_sheet` follows the underscore system-property convention, so it is hidden from normal Properties UI but remains raw-editor editable. It currently stores workbook presentation state such as grid-line visibility and frozen rows/columns, plus column widths, row heights, and cell-level formatting such as number format, bold, italic, font size, color, fill, borders, alignment, and wrapping. + +The renderer adapts this file format to an IronCalc workbook model when the note opens, then serializes the active single sheet back into the same frontmatter-plus-CSV representation after sheet interactions. Serialization is debounced and deferred to idle time when the webview supports it. The prototype intentionally does not expose multiple sheets inside one note; cross-note cell references use Tolaria wikilink syntax such as `[[other-sheet]].B5` instead of IronCalc workbook tabs. Sheet formulas can also read scalar frontmatter properties from a single note with explicit dot notation such as `[[device]].power.watts`; unresolved, ambiguous, or non-scalar property references become spreadsheet `#N/A` errors rather than choosing a note silently. + +For import ergonomics, simple Markdown wrappers in non-formula CSV cells are interpreted as initial cell styles (`**bold**`, `_italic_`, `***bold italic***`, and `~~strike~~`). After the sheet is saved, those styles persist through `_sheet` metadata and the CSV body keeps the unwrapped cell text. + +### Note Content Freshness + +The renderer may cache recently opened or preloaded markdown content, but cached content is only a performance hint. `useTabManagement` can reuse cached text immediately when it carries the same `modifiedAt` and `fileSize` identity as the current `VaultEntry`; otherwise it validates the cached string with the `validate_note_content` Tauri command. That command re-enters the same vault path boundary checks as `get_note_content` and compares the cached text against the current on-disk file bytes. A mismatch, missing file, or unreadable file falls back to the normal fresh-read path and existing missing/unreadable recovery. Background note prefetch is bounded to a small number of concurrent native reads, and a note opened while queued is promoted to foreground instead of waiting behind the prefetch backlog. Note-open entry objects are re-normalized at the tab boundary, so transient reload or bridge payloads with missing display metadata fall back to filename/title defaults before editor chrome renders; entries without a usable path are ignored instead of opening a broken tab. + +`useEditorTabSwap` may reuse BlockNote blocks that were already opened successfully or warmed from prefetched raw content, keyed by vault, path, and exact source content. Background warming is limited to likely next large Markdown notes and defers while the editor is unmounted, raw mode is active, or recent typing/navigation is still inside the foreground idle window. Every async editor swap carries a generation and source-content token so stale conversion results cannot overwrite newer file content or dirty editor state. + +`scripts/editor-performance-benchmark.mjs` exercises the same renderer note-open path without touching a real vault. It injects synthetic small and large Markdown notes through the browser/Tauri mock boundary, measures editor visibility, first rendered content, full block application, edit frame latency, and DEV-only editor timing logs, then compares medians against `.editor-performance-thresholds.json`. The threshold file is a ratchet: normal runs fail when medians exceed stored `maxMs` values, while `pnpm perf:editor:update` records the current baseline and only tightens existing maxima. + +### Table of Contents Outline + +The editor Table of Contents is derived from the live BlockNote document, not from saved Markdown text. `src/utils/tableOfContents.ts` reads structural `heading` blocks with stable ids and levels, extracts inline text from nested BlockNote content, and nests headings by level while preserving document order. `TableOfContentsPanel` receives a document revision from `Editor`, so rich-editor edits refresh the outline immediately without waiting for autosave or a vault reload. Selecting a heading focuses BlockNote and moves the cursor to that block id, while nested headings can be collapsed independently in panel-local UI state. + +### Entity Types (isA / type) + +Entity type is stored in the `type:` frontmatter field (e.g. `type: Quarter`). The legacy field name `Is A:` is still accepted as an alias for backwards compatibility but new notes use `type:`. The `VaultEntry.isA` property in TypeScript/Rust holds the resolved value. + +Type is determined **purely** from the `type:` frontmatter field — it is never inferred from the file's folder location. All notes live at the vault root as flat `.md` files: + +``` +~/Laputa/ +├── my-project.md ← type: Project (in frontmatter) +├── weekly-review.md ← type: Procedure +├── john-doe.md ← type: Person +├── some-topic.md ← type: Topic +├── AGENTS.md ← canonical Tolaria AI guidance +├── CLAUDE.md ← compatibility shim pointing at AGENTS.md +├── GEMINI.md ← optional Antigravity/Gemini shim pointing at AGENTS.md +├── project.md ← type: Type (definition document) +├── person.md ← type: Type (definition document) +├── ... +``` + +New notes are created at the vault root: `{vault}/{slug}.md`. Changing a note's type only requires updating the `type:` field in frontmatter — the file does not move. Moving a note into a user folder is a separate filesystem concern: the folder path changes, but the note keeps the same filename and `type:` value. Legacy `type/` and `types/` folders are still scanned like other non-hidden vault folders, so existing type documents in those folders continue to work, but new type documents created by Tolaria are written at the vault root. Legacy `config/` content is still recognized during migration and repair, but Tolaria's managed AI guidance now lives at the vault root. + +A `flatten_vault` migration command is available to move existing notes from type-based subfolders to the vault root. + +### Types as Files + +Each entity type can have a corresponding **type document**: any markdown note with `type: Type` in its frontmatter. Tolaria creates new type documents at the vault root (e.g., `project.md`, `person.md`) and still reads existing type documents from subfolders. Type documents: + +- Have `type: Type` in their frontmatter (`Is A: Type` also accepted as legacy alias) +- Define type metadata: icon, color, order, sidebar label, template, sort, view, visibility +- Define instance schema/defaults through ordinary custom frontmatter properties and relationship fields +- Are navigable entities — they appear in the sidebar under "Types" and can be opened/edited like any note +- Serve as the "definition" for their type category + +**Type document properties** (read by Rust and used in the UI): + +| Property | Type | Description | +|----------|------|-------------| +| `icon` | string | Type icon as a Phosphor name (kebab-case, e.g., "cooking-pot") | +| `color` | string | Accent palette key (`red`, `purple`, `blue`, `green`, `yellow`, `orange`, `teal`, `pink`, `gray`) or a valid CSS color value such as `cyan`, `#22d3ee`, or `rgb(34, 211, 238)` | +| `order` | number | Sidebar display order (lower = higher priority) | +| `sidebar_label` | string | Custom label overriding auto-pluralization | +| `template` | string | Markdown template for new notes of this type | +| `sort` | string | Default sort: "modified:desc", "title:asc", "property:Priority:asc"; bare custom-property form such as "Priority:asc" is accepted and normalized in the UI | +| `view` | string | Default view mode: "all", "editor-list", "editor-only" | +| `visible` | bool | Whether type appears in sidebar (default: true) | + +Type templates can be stored explicitly in the `template` frontmatter property. For hand-edited Type documents, Tolaria also treats the body after the Type note's own matching `# TypeName` heading as a new-note template when that body looks like a template (for example field labels, secondary headings, or checklist starters). Plain descriptive Type bodies are ignored so type documentation does not leak into every new note. + +**Type relationship**: When any entry has an `isA` value (e.g., "Project"), the Rust backend automatically adds a `"Type"` entry to its `relationships` map pointing to `[[project]]`. This makes the type navigable from the Inspector panel while keeping location as an implementation detail. + +**Instance schema/defaults**: Custom scalar/scalar-array properties and relationship fields on a type document define the expected shape for notes of that type. Existing instances do not get mutated when a type changes; the Inspector enriches their real frontmatter with gray placeholders for missing type-defined properties/relationships. Valued type fields are copied into frontmatter only when Tolaria creates a new instance of that type. Blank type fields stay as placeholders. + +**UI behavior**: +- Clicking a section group header pins the type document at the top of the NoteList if it exists +- Viewing a type document in entity view shows an "Instances" group listing all entries of that type +- The Type field in the Inspector is rendered as a clickable chip that navigates to the type document + +### Frontmatter Format + +Standard YAML frontmatter between `---` delimiters: + +```yaml +--- +title: Write Weekly Essays +type: Procedure +status: Active +belongs_to: + - "[[grow-newsletter]]" +related_to: + - "[[writing]]" +aliases: + - Weekly Writing +--- +``` + +Supported value types (defined in `src-tauri/src/frontmatter/yaml.rs` as `FrontmatterValue`): +- **String**: `status: Active` +- **Number**: `priority: 5` +- **Bool**: `archived: true` +- **List**: Multi-line ` - item` or inline `[item1, item2]` +- **Null**: `owner:` (empty value) + +Custom frontmatter fields with scalar values are exposed through `VaultEntry.properties`. Custom fields with scalar arrays are also exposed there, unless any array value contains a wikilink; wikilink-bearing fields belong to `VaultEntry.relationships`. Single-item scalar arrays continue to normalize to their scalar value for compatibility, while multi-item scalar arrays remain arrays so saved view filters can match exact elements. + +### Custom Relationships + +The Rust parser scans all frontmatter keys for fields containing `[[wikilinks]]`. Any non-standard field with wikilink values is captured in the `relationships` HashMap: + +```yaml +--- +Topics: + - "[[writing]]" + - "[[productivity]]" +Key People: + - "[[matteo-cellini]]" +--- +``` + +Becomes: `relationships["Topics"] = ["[[writing]]", "[[productivity]]"]` + +This enables arbitrary, extensible relationship types without code changes. + +### Outgoing Links + +All `[[wikilinks]]` in the note body (not frontmatter) are extracted by regex and stored in `outgoingLinks`. Used for backlink detection and relationship graphs. + +### Title / Filename Sync + +Tolaria separates **display title** from the file identifier: + +- **Display title resolution** (`extract_title` in `vault/parsing.rs`): first `# H1` on the first non-empty body line, then legacy frontmatter `title:`, then slug-to-title from the filename stem. +- **Opening a note is read-only**: selecting a note does not inject or auto-correct `title:` frontmatter. +- **Explicit filename actions** (`rename_note`): breadcrumb rename/sync actions stage crash-safe note renames through a hidden `.tolaria-rename-txn/` transaction directory, recover unfinished renames on the next vault scan, update wikilinks across the vault, and surface any failed backlink rewrites instead of silently reporting partial success. The editor body remains the title editing surface. +- **Unicode-aware note stems** (`src/utils/noteSlug.ts`, `vault/rename.rs`): frontend and backend slugging preserve Unicode letters/digits in note filenames, untitled-rename detection, and fallback wikilink targets while still collapsing symbol-only titles to `untitled`. +- **Path identity rules** (`src/utils/notePathIdentity.ts`, `vault/path_identity.rs`): note creation, tab selection, rename bookkeeping, pull refresh, git history, and vault cache updates normalize path separators and macOS `/private/tmp` aliases through one owner. Case folding is reserved for collision/deduplication checks; active-note identity remains case-sensitive. +- **Portable filename validation** (`vault/filename_rules.rs`): note filenames, folder names, and custom view filenames all reject Windows-reserved device names, invalid characters, and trailing dot/space suffixes so a vault created on macOS/Linux still clones and syncs cleanly on Windows. +- **Recoverable save failures** (`useEditorSave`, `vault/file.rs`): invalid platform path syntax is reported as a clear retryable save error, while transient access-denied writes are retried briefly before surfacing failure. The editor keeps the unsaved buffer intact for another attempt. +- **Untitled drafts** start as `untitled-*.md` and are auto-renamed on save once the note gains an H1. + +### Title Surface (UI) + +The BlockNote body is the only title editing surface: + +- The first H1 is the canonical display title. +- There is no separate title row above the editor, even when a note has no H1. +- Notes without an H1 show the editor body and placeholder only. +- Legacy no-H1 notes whose display title differs from the filename show that title as read-only breadcrumb context beside the editable filename, so referenced notes remain identifiable without raw mode. +- Filename changes are explicit breadcrumb actions, not a dedicated title-input side effect. + +### Sidebar Selection + +Navigation state is modeled as a discriminated union: + +```typescript +type SidebarFilter = 'all' | 'archived' | 'changes' | 'pulse' + +type SidebarSelection = + | { kind: 'filter'; filter: SidebarFilter } + | { kind: 'sectionGroup'; type: string } // e.g. type: 'Project' + | { kind: 'folder'; path: string; rootPath?: string } + | { kind: 'entity'; entry: VaultEntry } // Neighborhood source note + | { kind: 'view'; filename: string } +``` + +`SidebarSelection.kind === 'folder'` is a first-class navigation target, not just a visual highlight. + +- `FolderTree` keeps the folder interaction surface decomposed into `FolderTreeRow`, `FolderNameInput`, `FolderContextMenu`, and disclosure/context-menu hooks so nested row rendering, inline rename, and right-click actions stay isolated. The UI wraps backend folder nodes in a synthetic vault-root row with `path: ""` and `rootPath` set to the opened vault so root-level files can be listed without turning the vault root into a mutable folder. Inline folder creation carries an optional `FolderCreationParent` (`path` plus `rootPath`) through `App` to the `create_vault_folder` command, so new folders land under the selected folder or selected mounted vault root while preserving the active-vault path boundary. Non-mutating reveal/copy-path menu items stay callback-driven from `App` so filesystem convenience actions do not leak into folder mutation hooks. +- `src/components/sidebar/sidebarHooks.ts` owns the shared sidebar interaction primitives for menu positioning/dismissal and inline rename input behavior. Folder, Type, and saved View rows keep their domain-specific actions local, but use those primitives so right-click menus and rename fields have the same outside-click, Escape, focus, blur, and submit semantics. +- `useFolderActions()` composes `useFolderRename()` and `useFolderDelete()` to keep folder mutations selection-aware while the rest of `App.tsx` only wires the resulting callbacks into `Sidebar` and the command registry. +- `useNoteRetargeting()` is the shared retargeting abstraction for note drops and command-palette actions. It owns the "can drop here?" checks, updates `type:` via frontmatter when a note lands on a type section, and delegates folder moves through the same crash-safe rename pipeline used by the backend rename commands. +- A successful folder rename reloads the folder tree plus vault entries, rewrites any affected folder-scoped tabs, and updates `SidebarSelection` to the new relative path when the renamed folder stays selected. +- Folder deletion clears pending rename state, confirms destructive intent, drops affected folder-scoped tabs, reloads vault data, and resets folder selection if the deleted subtree owned the current selection. + +### Collections + +Collections are the renderer-side foundation for note groups plus their presentation configuration. A collection can come from a built-in filter, type section, folder, saved View, or Neighborhood mode. Product terminology can stay simple: users select a collection and Tolaria presents it. + +`SidebarSelection` remains the navigation state for now, but it is adapted into a `CollectionDefinition` by `src/collections/collectionFromSelection.ts`. The collection keeps the original selection for compatibility, adds an `origin` for implementation routing, and normalizes the current presentation to: + +```typescript +type CollectionPresentationConfig = + | { type: 'list'; sort: string | null; properties: string[] } +``` + +`src/collections/resolveCollectionEntries.ts` resolves the selected collection through the existing note-list filtering rules. Changes and Inbox stay caller-supplied transient flows because they depend on git state and inbox-period state outside saved-view YAML. Neighborhood resolves to grouped relationship data instead of a flat row list. + +This layer is intentionally internal and behavior-preserving. It lets future presentations such as board, calendar, table, timeline, or graph consume the same resolved collection model instead of branching directly on sidebar state. Presentation config maps existing note properties; it does not create a separate data model. + +### Saved Views + +Saved Views live as YAML files under `views/`. Their definition includes user-visible fields (`name`, `icon`, `color`), note-list preferences (`sort`, `listPropertiesDisplay`), filters, and an optional top-level `order` number. The renderer treats saved Views as the most configurable persisted Collection artifact. Existing top-level `sort` and `listPropertiesDisplay` fields normalize into the list presentation config, and a future nested `presentation` block may override them in memory when present. The `sort` value accepts built-in sort forms such as `"modified:desc"` and custom-property forms such as `"property:Priority:asc"` or bare `"Priority:asc"`; the renderer keeps configured custom-property sorts visible even when the current result set has no populated values for that property. Filter conditions on scalar-array custom properties, such as `tags: [blues, chicago]`, evaluate `contains`, `any_of`, and related set operators against exact array elements rather than substrings. The `order` value is stored directly in the YAML document, not in Markdown frontmatter, and lower values render earlier in every saved-View list. Views without an explicit order sort after ordered views by filename for stable fallback behavior. + +In a mounted-workspace graph, each loaded `ViewFile` carries optional renderer-owned `rootPath` and `workspace` provenance. `SidebarSelection.kind === 'view'` can include that `rootPath`, and view identity is `(rootPath, filename)` rather than filename alone. This lets two vaults both expose `views/focus.yml` without colliding in sidebar selection, note-list filtering, counts, sort/column persistence, edit, or delete flows. A saved View with `rootPath` filters only entries from its own workspace and persists changes through `save_view_cmd` / `delete_view_cmd` against that source vault. + +`useAppViewActions()` owns the renderer-side saved View lifecycle: choosing the target workspace, preserving mounted-view identity, saving/deleting YAML definitions, reloading affected vault state, and exposing the available note-list fields for the create/edit dialog. `App.tsx` wires those callbacks into `Sidebar`, `NoteList`, `CreateViewDialog`, and command surfaces without duplicating the persistence rules. + +`useMcpSetupDialogController()` owns MCP setup dialog state, busy actions, and manual config callbacks so `App.tsx` only passes the controller into settings/status surfaces. `useAiWorkspaceWindowBridgeEvents()` owns native AI-workspace event subscriptions and listener cleanup for popped-out workspace windows. + +`createCrossWindowPersistedStore()` is the shared renderer primitive for AI workspace state that must stay synchronized across the main window and popped-out workspace windows. It owns localStorage reads/writes, BroadcastChannel publishing, storage-event synchronization, and external-store subscribers; domain modules such as `aiWorkspaceSessionStore` and `aiWorkspaceWindowSharedContext` provide sanitizers and mutations around that shell. + +The renderer uses `viewOrdering` helpers to convert drag or command-palette move intent into dense order updates before saving each affected view file through `save_view_cmd`. The sidebar treats saved View rows like Type rows for direct customization: double-click starts inline rename, right-click opens edit/rename/icon-color/delete actions, and keyboard users can open that same menu from the focused row while command-palette actions remain responsible for saved View ordering. + +### Neighborhood Mode + +`SidebarSelection.kind === 'entity'` is Tolaria's Neighborhood mode for note-list browsing. + +- The selected `entry` is the neighborhood source note. +- The source note stays pinned at the top of the note list as a standard active row, not a special card. +- Outgoing relationship groups render first using the note's `relationships` map. +- Inverse groups (`Children`, `Events`, `Referenced by`) and `Backlinks` render after the outgoing groups. +- Empty groups stay visible with count `0`. +- Notes may appear in multiple groups when multiple relationships are true; Neighborhood mode does not deduplicate them across sections. +- Plain click / `Enter` open the focused note without replacing the current Neighborhood. +- Cmd/Ctrl-click and Cmd/Ctrl-`Enter` open the note and pivot the note list into that note's Neighborhood. + +## Command Surface + +`src/shared/appCommandManifest.json` is the cross-runtime source for stable app command IDs, menu structure, display labels, accelerators, deterministic shortcut QA metadata, and native menu enablement groups. The renderer imports it through `src/hooks/appCommandCatalog.ts`, which derives `APP_COMMAND_IDS`, shortcut lookup maps, custom titlebar menu sections, native-menu command membership, and test helpers. Tauri includes the same JSON in `src-tauri/src/menu.rs` and uses it to build custom menu items, emit overridden menu item IDs such as the quick-open alias as their primary command IDs, and toggle state-dependent menu items from manifest groups. + +Domain command builders still own context-sensitive command-palette entries, availability, and execution callbacks. The manifest owns metadata that must stay identical across native menus, renderer shortcuts, deterministic QA bridges, and the custom desktop titlebar menu; OS-native menu items such as Undo, Copy/Paste, Services, Quit, and Window controls remain local to the native menu implementation. + +`useActionHistory` is the renderer-owned stack for reversible app-level actions. It records note-state actions only after persistence succeeds, replays one undo/redo at a time, and reveals the affected note before applying the reversal so editor pending-content flushes stay path-correct. Text editors and text inputs keep their native undo/redo history; app-level Undo/Redo shortcuts are handled only when focus is outside text-editing surfaces. + +## File System Integration + +### Vault Scanning (Rust) + +`vault::scan_vault(path)` in `src-tauri/src/vault/mod.rs`: + +1. Validates the path exists and is a directory +2. Recursively scans non-hidden files while skipping hidden directories such as `.git/` +3. For each `.md` file, calls `parse_md_file()`: + - Reads content with `fs::read_to_string()` + - Parses frontmatter with `gray_matter::Matter::` + - Extracts title from first `#` heading + - Reads entity type from `type:` frontmatter field (`Is A:` accepted as legacy alias); type is never inferred from folder + - Parses dates as ISO 8601 to Unix timestamps + - Extracts relationships, outgoing links, custom properties, word count, snippet +4. For recognized non-markdown text and binary files, emits a minimal `VaultEntry` with `fileKind` +5. Sorts by `modified_at` descending +6. Skips unparseable files with a warning log + +All Notes starts from Markdown notes and excludes Markdown files under `attachments/`. `src/utils/allNotesFileVisibility.ts` resolves the installation-local PDF, image, and unsupported-file toggles from app settings; `noteListHelpers` applies that policy only to All Notes filtering and counts. Folder/root browsing continues to show files from the selected folder independently of those All Notes toggles. + +The folder tree hides the legacy `type/` directory, since those type documents already appear through the Types sidebar section. Default vault folders such as `attachments/` and `views/` remain visible alongside user-created folders under the synthetic vault-root row. + +Command-facing vault content is filtered through `vault::filter_gitignored_entries`, `vault::filter_gitignored_folders`, and `vault::filter_gitignored_paths` when the app setting `hide_gitignored_files` is enabled. The cache still stores the complete scan; `list_vault`, `reload_vault`, `list_vault_folders`, and search apply the visibility filter at the boundary before React consumes entries. The filter batches paths through `git check-ignore --no-index --stdin`, drains stdout while stdin is still being written, and short-circuits root `.gitignore` detection before walking for nested ignore files, so large ignored folder sets cannot deadlock the native UI while preserving Git semantics as closely as the app can reasonably support. + +A `vault_health_check` command detects stray files in non-protected subfolders and filename-title mismatches. On vault load, a migration banner offers to flatten stray files to the root via `flatten_vault`. + +Command-layer path access is fenced to the active vault before file operations reach the vault backend. `src-tauri/src/commands/vault/boundary.rs` canonicalizes the configured/requested vault root, rejects `..` escapes and absolute paths outside that root, and validates writable targets through the nearest existing ancestor so note reads, saves, deletes, view-file edits, folder mutations, and image attachment writes cannot step outside the active vault. If the active root itself cannot be canonicalized, the renderer treats `Active vault is not available` the same as no active vault: it clears stale vault state, drops prefetched note content, and shows the missing-vault recovery screen instead of continuing note/view requests against the disappeared path. Image attachment commands add the current vault root to the runtime asset scope after saving so files created under a previously missing `attachments/` directory can render immediately. + +Renderer attachment paths are normalized through `src/utils/vaultAttachments.ts`. That module is the single owner for converting between portable markdown references such as `attachments/image.png`, Tauri asset URLs, and absolute active-vault filesystem paths. Editor markdown rendering, raw-mode serialization, image upload/drop handling, file-block open actions, and parsed image cleanup all call this primitive instead of carrying their own asset URL prefixes, Windows path normalization, or `attachments/` join rules. + +UI-only file actions operate on paths that are already selected or indexed in React state. Reveal-in-Finder routes through the Tauri opener plugin, external-open routes through the `open_vault_file_external` command and active-vault boundary before invoking the native opener, and copy-path uses the browser clipboard API. Plain-text paste reads the desktop clipboard through `read_text_from_clipboard` in Tauri so macOS WKWebView clipboard permissions do not block the command; browser/mock mode falls back to the Web Clipboard API or mock handlers. None of those actions mutate vault contents or bypass the backend write boundary. + +The local MCP WebSocket bridge follows the same active-vault boundary. `useVaultSwitcher` calls `sync_mcp_bridge_vault` after the persisted selection loads and after each vault switch; the desktop command starts/restarts the bridge with the active mounted workspace set in `VAULT_PATHS`, or stops it when there is no selected vault. App exit uses the same child cleanup path and waits for the bridge process after killing it. MCP Node entrypoints accept explicit `VAULT_PATH`/`VAULT_PATHS` for app-owned or legacy launches; durable external registrations omit vault env and resolve the current mounted workspace set from Tolaria's `vaults.json` at tool-call time. `mcp-server/tool-service.js` owns the shared tool semantics for active-vault resolution, cross-vault lookup/search, note-creation defaults, vault listing, and UI action intents; `mcp-server/index.js` and `mcp-server/ws-bridge.js` remain transport adapters around that service. Manual MCP config export uses the same packaged `mcp-server/` resolver as registration and app-managed AI agents, including Windows executable-adjacent installs under `%LOCALAPPDATA%\Tolaria`, and strips Windows extended-length `\\?\` prefixes from the exported `index.js` argument before handing it to Node, so the copied snippet stays durable across active-workspace changes without writing third-party config files. Vault context checks each active workspace root for `AGENTS.md` and returns those instructions alongside note counts, folders, and recent notes. Desktop snippet copy goes through the native `copy_text_to_clipboard` command, while browser/mock mode keeps using the Web Clipboard API. External-client stdio MCP processes also exit when stdin closes; their UI-bridge reconnect timers and WebSocket are canceled during shutdown so disconnected clients do not leave extra Node processes behind. + +### Vault Caching + +`vault::scan_vault_cached(path)` wraps scanning with git-based caching: + +1. Reads cache from `~/.laputa/cache/.json` (external to vault) +2. Compares cache version, vault path, and git HEAD commit hash +3. If cache is valid and same commit → only re-parse uncommitted changed files +4. If different commit → use `git diff` to find changed files → selective re-parse +5. If no cache → full scan +6. Replaces the cache with a temp-file write + rename only if a short-lived writer lock and cache fingerprint check show another scan has not already refreshed it +7. On first run, migrates any legacy `.laputa-cache.json` from inside the vault + +Startup entry hydration calls the cached `list_vault` path first for both the main window and secondary note windows. The main window alone treats an empty cached startup result as suspicious and retries with `reload_vault`; explicit user reloads, watcher refreshes, and Git pull refreshes still use reload paths when disk freshness is required. + +### Frontmatter Manipulation (Rust) + +`frontmatter/ops.rs:update_frontmatter_content()` performs line-by-line YAML editing: + +1. Finds the frontmatter block between `---` delimiters +2. Iterates through lines looking for the target key +3. If found: replaces the value (consuming multi-line list items if present) +4. If not found: appends the new key-value at the end +5. If no frontmatter exists: creates a new `---` block + +The `with_frontmatter()` helper wraps this in a read-transform-write cycle on the actual file. + +### Content Loading + +- **Tauri mode**: Content loaded on-demand when a tab is opened via `invoke('get_note_content', { path })` +- **Browser mode**: All content loaded at startup from mock data +- Content for backlink detection (`allContent`) is stored in memory as `Record` + +## Git Integration + +Git operations live in `src-tauri/src/git/`. All operations shell out to the `git` CLI (not libgit2). Path-producing commands use `core.quotePath=false` so Unicode note filenames stay as UTF-8 paths across status, history, cache invalidation, and rename detection. Git subprocesses also inherit the user's shell-managed Git identity/config environment when the app process is missing it, including `GIT_AUTHOR_*`, `GIT_COMMITTER_*`, `GIT_CONFIG_*`, `XDG_CONFIG_HOME`, and `EMAIL`. + +### Data Types + +```typescript +interface GitCommit { + hash: string + shortHash: string + message: string + author: string + date: number // Unix timestamp +} + +interface ModifiedFile { + path: string // Absolute path + relativePath: string // Relative to vault root + status: 'modified' | 'added' | 'deleted' | 'untracked' | 'renamed' + addedLines?: number | null + deletedLines?: number | null + binary?: boolean +} + +interface GitRemoteStatus { + branch: string + ahead: number + behind: number + hasRemote: boolean + hasUpstream: boolean + upstream: string | null +} + +interface GitAddRemoteResult { + status: 'connected' | 'already_configured' | 'incompatible_history' | 'auth_error' | 'network_error' | 'error' + message: string +} + +interface PulseCommit { + hash: string + shortHash: string + message: string + date: number + githubUrl: string | null + files: PulseFile[] + added: number + modified: number + deleted: number +} +``` + +### Operations + +| Module | Operation | Notes | +|--------|-----------|-------| +| `history.rs` | File history | `git log` — last 20 commits per file | +| `status.rs` | Modified files | `git status --porcelain` — filtered to `.md` | +| `status.rs` | File diff | `git diff`, fallback to `--cached`, then synthetic for untracked | +| `file_url.rs` | File URL | Builds a copyable remote URL from the primary remote, current branch, and vault-relative path without exposing remote credentials | +| `author.rs` | Author identity | Resolves the exact commit author Tolaria will use, heals the legacy Tolaria fallback email, and reports when repo-local identity shadows the global Git identity | +| `commit.rs` | Commit | Ensures a local author fallback when needed, then runs `git add -A && git commit -m "..."`; broken signing helpers trigger one unsigned retry for the same app-managed commit | +| `remote.rs` | Pull / Push | Resolves the current branch's configured upstream, then runs explicit pull/push commands against that remote branch; missing upstream and detached HEAD states return actionable sync errors | +| `connect.rs` | Add remote | Adds `origin`, fetches it, validates history compatibility, and only starts tracking when the remote is safe | +| `conflict.rs` | Conflict resolution | Detect conflicts, resolve with ours/theirs/manual, and ensure a local author fallback before commit/rebase continuation | +| `pulse.rs` | Activity feed | `git log` with `--name-status` for file changes | + +### Auto-Sync + +`useAutoSync` hook handles automatic git sync across every active Git repository: +- Configurable interval (from app settings: `auto_pull_interval_minutes`) +- Pulls the active repository set concurrently on launch, focus, interval, and manual sync +- Budgets automatic launch/focus/interval pulls per repository with a short cooldown so focus or low interval settings do not repeat network Git work immediately after a recent sync; manual sync bypasses this budget +- Refreshes aggregate remote status after a pull, and avoids a separate startup status fetch when the initial pull will already refresh it +- Pushes the active repository set during divergence recovery +- Awaits the post-pull vault refreshes so toasts land after note-list state is fresh +- Reopens the clean active tab from disk only when the pull changed that active note, then restores editor focus if the editor owned focus before the remount +- Detects merge conflicts → opens `ConflictResolverModal` +- Tracks aggregate remote status (ahead/behind via `git_remote_status`) +- Tracks the active branch and whether it has an upstream, so status UI can explain non-main branches and missing tracking setup before pull/push +- Handles push rejection (divergence) → sets `pull_required` status +- `pullAndPush()`: pulls then auto-pushes each active repository for divergence recovery +- `ConflictNoteBanner`: inline banner in editor for conflicted notes (Keep mine / Keep theirs) + +### External Vault Refresh + +External vault mutations are any disk writes Tolaria did not just perform through its own save path: Git pulls, AI-agent writes, filesystem watcher events, and edits from another app. These changes must route through `refreshPulledVaultState()` rather than calling `reloadVault()` in isolation. The shared refresh abstraction reloads entries, folders, and saved views together, preserves unsaved active-editor content, reopens a clean active note when the changed-path list includes that note, and closes the active tab if the file disappeared. Editor focus does not block the clean active note from converging to disk when its own file changed externally; if the active editor owned focus before that remount, the app requests editor focus again after the fresh tab is mounted. Unknown or unrelated watcher updates refresh vault-derived state without remounting the active editor. `useVaultWatcher` supplies changed filesystem paths to this abstraction after debouncing and after filtering recent app-owned saves. Overlapping entry reloads and modified-file polls are coalesced with a single trailing rerun so watcher and sync bursts do not stack native vault scans or Git status processes. + +`useGitRepositories` is the commit-time companion to `useAutoSync`: +- Owns repository picker validation plus `get_modified_files` and `git_remote_status` loading for active Git repositories +- Re-checks the selected repository when the Commit dialog opens and right before submit +- Reloads the selected repository with line stats when the user asks to generate a commit-message draft +- Converts `hasRemote: false` into a local-only commit path +- Keeps the normal push path unchanged for repositories that do have a remote + +`AddRemoteModal` is the explicit recovery path for those local-only vaults: +- Opens from the `No remote` status-bar chip and the command palette +- Calls `git_add_remote` with the current vault path and the pasted repository URL +- Shows auth, network, and incompatible-history failures inline without rewriting the local vault's history + +`useAutoGit` is the checkpoint-time companion to both hooks: +- Consumes installation-local AutoGit settings (`autogit_enabled`, idle threshold, inactive threshold) +- Tracks the last meaningful editor activity plus app focus/visibility transitions +- Triggers `useCommitFlow.runAutomaticCheckpoint()` only when the vault is git-backed, pending changes exist, and no unsaved edits remain +- Shares the same deterministic automatic commit message generator with the bottom-bar Commit button, so timer-driven checkpoints and manual quick commits produce the same `Updated N note(s)` / `Updated N file(s)` messages + +### Frontend Integration + +- **Modified file badges**: Orange dots in sidebar +- **Diff view**: Toggle in breadcrumb bar → shows unified diff +- **Git history**: Shown in Inspector panel for active note +- **Commit dialog**: Triggered from sidebar or Cmd+K; can prefill an editable generated message from current changed-file metadata before any commit runs +- **Generate commit message command**: Cmd+K action that opens the commit dialog, reloads the selected repository with line stats, and fills the message field. Direct configured model targets receive only structured path/status/line-count metadata with a bounded path list; disabled AI, offline AI, agent targets, or model failures use a deterministic changed-file summary instead. +- **Branch indicator**: Current Git branch chip in the bottom bar for Git-backed vaults +- **No remote indicator**: Neutral chip in the bottom bar when `GitRemoteStatus.hasRemote === false` +- **Pulse view**: Activity feed when Pulse filter is selected +- **Pull command**: Cmd+K → "Pull from Remote", also in Vault menu +- **Git status popup**: Click sync badge → shows active branch, upstream/missing-upstream state, aggregate ahead/behind, and a Pull button when tracking is configured for the active repository set +- **Conflict banner**: Inline banner in editor with Keep mine / Keep theirs for conflicted notes + +## BlockNote Customization + +The editor uses [BlockNote](https://www.blocknotejs.org/) for rich text editing, with CodeMirror 6 available as a raw editing alternative. + +### Custom Wikilink Inline Content + +Defined in `src/components/editorSchema.tsx`: + +```typescript +const WikiLink = createReactInlineContentSpec( + { + type: "wikilink", + propSchema: { target: { default: "" } }, + content: "none", + }, + { render: (props) => ... } +) +``` + +### Code Block Highlighting + +Defined in `src/components/editorSchema.tsx` and styled in `src/components/EditorTheme.css`: + +- The schema overrides BlockNote's default `codeBlock` spec with `createCodeBlockSpec({ ...codeBlockOptions, defaultLanguage: "text" })` from `@blocknote/code-block`. +- Fenced code blocks now use BlockNote's supported Shiki-backed highlighter path, which renders `.shiki` token spans directly inside the editor DOM. +- Missing common grammars live in `src/utils/codeBlockLanguageCatalog.ts` and are registered lazily from direct `@shikijs/langs` imports by `src/components/codeBlockOptions.ts`; known aliases such as `ps1` and `vb` normalize to canonical picker values during Markdown import. +- Tolaria keeps `defaultLanguage: "text"` so unlabeled code blocks do not silently become JavaScript at creation time. Parsed unlabeled code blocks then run through Tolaria's lightweight language inference, while explicit fence languages and user dropdown choices still win. +- Inline-code chip styling remains scoped to `.bn-inline-content code`, so fenced `pre > code` nodes keep the dedicated code-block shell instead of inheriting the muted inline surface. + +### Markdown Math + +Defined in `src/utils/mathMarkdown.ts`, `src/components/editorSchema.tsx`, and styled in `src/components/EditorTheme.css`: + +- `$...$` becomes a `mathInline` schema node and line-owned `$$...$$` / multiline `$$` blocks become `mathBlock` nodes. +- The rich editor renders both node types through KaTeX with `throwOnError: false`, so malformed formulas keep their source visible instead of breaking the note. +- Double-clicking rendered display math edits the math block's `latex` property in-place; Markdown delimiters remain owned by serialization. Inline math can still be reopened as source text for direct editing. +- `serializeMathAwareBlocks()` converts math nodes back to Markdown delimiters before save, raw-mode entry, and editor-position snapshots. +- Raw CodeMirror mode always shows the plain Markdown source, so imported technical notes stay editable outside Tolaria. + +### Mermaid Diagrams + +Defined in `src/utils/durableMarkdownBlocks.ts`, `src/utils/editorDurableMarkdown.ts`, `src/utils/mermaidMarkdown.ts`, `src/components/MermaidDiagram.tsx`, `src/components/editorSchema.tsx`, and styled in `src/components/EditorTheme.css`: + +- Fenced `mermaid` blocks become `mermaidBlock` schema nodes before BlockNote sees the Markdown body. +- Each `mermaidBlock` stores the original fenced Markdown plus the diagram body, so raw-mode entry and saves can restore the canonical source instead of serializing generated SVG. +- The rich editor renders diagrams with the `mermaid` package and uses the original source as an inline fallback when rendering fails. +- `serializeDurableEditorBlocks()` wraps the math-aware serializer so math, wikilinks, Mermaid diagrams, and whiteboards share the same Markdown-first save path. +- The `/mermaid` slash command inserts a placeholder rectangle diagram using the same schema-backed Markdown storage path, avoiding an invalid empty diagram state. + +### Tldraw Whiteboards + +Defined in `src/utils/durableMarkdownBlocks.ts`, `src/utils/editorDurableMarkdown.ts`, `src/utils/tldrawMarkdown.ts`, `src/components/TldrawWhiteboard.tsx`, `src/components/editorSchema.tsx`, and styled in `src/components/EditorTheme.css`: + +- Fenced `tldraw` blocks become `tldrawBlock` schema nodes before BlockNote sees the Markdown body. +- Each `tldrawBlock` stores a stable `boardId` plus the tldraw document snapshot JSON. Session state such as camera, selected tool, and current selection is not persisted into the note. +- The rich editor renders the block with the `tldraw` package and saves debounced document snapshot changes back into the block props, so normal Tolaria autosave writes the board into the `.md` file. +- Whiteboard prop writes re-resolve the live BlockNote block by id before mutating it, and disappear as no-ops if a note reload or mode switch has already removed that block. +- The tldraw runtime receives Tolaria's resolved light/dark mode as its user color scheme, so embedded whiteboards follow the app appearance and update while mounted. +- Embedded whiteboards expose a session-only full-window workspace that reuses the same tldraw store and Markdown snapshot; expanding or closing it does not persist camera, tool, or size state. +- Mermaid and tldraw both register small codecs with the shared durable fenced-block pipeline; scanner, token, block injection, and mixed serialization mechanics live in one owner. +- The `/whiteboard` slash command inserts an empty tldraw block using the same Markdown-durable storage path. Preview images are intentionally omitted; thumbnails can be added later as derived cache artifacts. + +### Formatting Surface Policy + +Defined in `src/components/tolariaEditorFormatting.tsx` and `src/components/tolariaEditorFormattingConfig.ts`: + +- `SingleEditorView` disables BlockNote's default formatting toolbar, `/` menu, and side menu, then mounts Tolaria-owned controllers so the visible formatting surface matches Tolaria's markdown round-trip guarantees. +- `SingleEditorView` owns a whitespace mouse-selection bridge around BlockNote and its rich-editor scroll area: drag starts that land outside the editable text DOM are remapped through the ProseMirror view with clamped coordinates, while drags below the rendered document fall back to the document end. Drags that begin inside BlockNote's contenteditable surface, toolbars, side menu, dialogs, or non-primary mouse buttons stay on BlockNote/native handling. +- `editorRichCopy.ts` owns rich-editor copy serialization for external apps. Normal selections use BlockNote's external clipboard HTML so tables, lists, checklists, and inline marks paste as rich content outside Tolaria, while `SingleEditorView` still normalizes `text/plain` and keeps fenced code-block selections on raw code text. +- The formatting toolbar only exposes inline controls that persist through Tolaria's Markdown serialization pipeline: bold, italic, strike, inline code, `==highlight==`, nesting, and link creation. Controls that BlockNote can render temporarily but Tolaria cannot faithfully persist, such as underline, color, alignment, and the block-type dropdown, are hidden instead of appearing to work and later disappearing. +- Tolaria's formatting-toolbar controller also keeps file/image actions mounted across the tiny hover gap between an image block and the floating toolbar, and while the toolbar itself is hovered, so image controls remain usable instead of collapsing mid-interaction. +- `useEditorComposing` tracks editor-owned IME composition events and closes the floating formatting toolbar during composition plus a short post-composition settle window, keeping CJK candidate windows unobstructed without changing normal selection toolbar behavior. +- `createImeCompositionKeyGuardExtension()` intercepts composing `Enter` keydown events before BlockNote's list shortcuts see them, so Korean/Japanese/Chinese IMEs can commit text at the start of list items without Tolaria splitting the current bullet. It stops editor shortcut propagation only; it does not prevent the browser/IME default composition action. +- `createMarkdownHighlightShortcutExtension()` owns the rich-editor Cmd/Ctrl+Shift+M formatting shortcut. It skips IME composition and read-only editors, calls the same highlight style toggle as the formatting toolbar, and records `markdown_highlight_shortcut_used` with keyboard-only metadata. +- `richEditorBlockSelectionExtension.ts` owns Notion-style block selection as an editor-level ProseMirror plugin. A first `Escape` promotes the current caret or native text selection into one or more selected blocks, renders block-level decoration chrome, and keeps arrow/delete/enter handling inside the editor; a second `Escape` clears the block selection without preventing the app-level note-list escape path from taking over. Native drag text selection remains content selection, while block mode uses decoration chrome so empty spacer lines are not treated as selected content. +- `richEditorRecoveryClassifier.ts` is the shared taxonomy for recoverable BlockNote/ProseMirror failures used by render recovery and transform recovery. Missing block IDs plus paragraph and table-row index failures keep one canonical reason across `editor_render_recovered` and `rich_editor_transform_error_recovered`; the two recovery surfaces differ only in retry, repair, and dispatch behavior. +- `richEditorInputTransform.ts` is the shared execution shell for rich-editor Markdown `beforeinput` transforms. It reads the live ProseMirror view, skips IME composition, resets state when a stale view is detected, dispatches transform transactions, prevents native input only after successful dispatch, and reports recoverable editor-transform errors through the shared classifier. Arrow ligatures, inline math conversion, and `==highlight==` keep their syntax-specific matching in their feature files and are composed for the main editor by `richEditorInputTransformExtension.ts`. +- `richEditorTextDirection.ts` uses a BlockNote extension with ProseMirror decorations for per-node RTL quote rendering. It handles callout-marker quotes such as `[!note] כותרת` at the editor render layer, so BlockNote does not strip post-render DOM mutations and quote rails can follow logical inline-start. +- `focusOwnershipGuard.ts` is the shared global focus interception primitive for editor-like surfaces. It owns the single `HTMLElement.prototype.focus` patch, document focus/pointer listeners, outside-target restoration, and cleanup; rich-editor and sheet modules keep only their surface-specific focus-claim policy. +- `useImageLightbox` listens for `dblclick` on the rich-editor container and opens `ImageLightbox` only when the event target resolves to a viewable BlockNote image. The target resolver handles media wrappers, ignores image captions/resize controls, missing sources, and tiny tracking-style images, preserving BlockNote's ordinary single-click image selection path. +- The `/` slash menu remains the supported path for markdown-safe block transformations such as headings, quotes, list blocks, Mermaid diagrams, and whiteboards. Tolaria filters out BlockNote's toggle-heading and toggle-list variants because those do not map cleanly to the markdown note model. +- The block-handle side menu keeps only actions that survive Tolaria's markdown round-trip. Delete and table-header toggles remain available; BlockNote's `Colors` submenu is removed because block colors are not part of Tolaria's supported markdown surface. Tolaria renders the add-block button outside the drag handle so the handle stays next to the block content. `tolariaSideMenuAlignment.ts` aligns the side menu to the first rendered text line for the hovered block, so H1/H2 typography, line-height, wrapping, and theme changes do not need per-heading offsets. `tolariaBlockReorder.ts` owns block reordering as a pointer gesture with direct BlockNote block moves instead of HTML5 `DataTransfer`, keeping it independent from Tauri's native file-drop system. `tolariaSideMenuBlocks.ts` owns stale-block lookup and mutation guards, so block-handle actions re-resolve the current live BlockNote block before mutating or dragging and note reloads/sync churn cannot leave controls acting on stale block references. +- BlockNote's table row/column handles are patched so stale or missing hovered-table state cancels the drag and hides handles instead of throwing. Add/remove row and column actions also validate the table position and cell indexes before resolving a ProseMirror `CellSelection`, so reloads or menu lag cannot turn stale handles into invalid table-selection positions. Checklist checkbox handlers also re-resolve the live block before updating `checked`, making delayed clicks after note reloads a no-op instead of a stale block mutation. Browser and native table regressions should exercise row and column dragging plus add-menu actions because the state is tracked per orientation. +- `SingleEditorView` wraps the BlockNote surface in a narrow render-recovery boundary for recoverable BlockNote node-view failures classified by `richEditorRecoveryClassifier.ts`. The boundary retries the BlockNote view once, records `editor_render_recovered`, and marks the recovered error so the React root handler does not send that handled case back to Sentry. Other render errors still propagate through the normal root error path. +- `useNoteWikilinkDrop()` is the shared editor-drop abstraction for dragging note rows into either editor mode. It reads the existing note-retargeting drag payload, resolves the vault-relative stem, and inserts a canonical `[[wikilink]]` without hijacking unrelated plain-text drags. +- `plainTextPaste.ts` is the shared plain-text paste target registry. Rich BlockNote and raw CodeMirror surfaces register focused insertion targets, while ordinary focused text controls use DOM selection replacement, so the `Cmd+Shift+V` command can preserve caret/selection behavior without each surface inventing its own clipboard reader. +- `tauriEventCleanup.ts` owns safe Tauri event unlisten cleanup. Hooks and stream utilities route listener teardown through it so stale or duplicate native listener removals cannot surface as unhandled promise rejections during fast remounts, window teardown, or stream completion. +- `useTauriDragDropEvent()` owns the shared Tauri window drag/drop subscription used by native drop features. +- `useNativePathDrop()` is the shared Tauri file/folder-drop abstraction for text inputs that need filesystem paths instead of attachment import. It consumes native window drag/drop events, gates them to the target element bounds or focused text selection, and lets AI composer / command-palette inputs insert formatted paths at the current cursor. + +### Markdown-to-BlockNote Pipeline + +```mermaid +flowchart LR + A["📄 Raw markdown\n(from disk)"] --> B["splitFrontmatter()\n→ yaml + body"] + B --> C{"exact-source\nblock cache?"} + C -->|"hit"| D["reuse cached blocks"] + C -->|"miss"| E["preprocess durable blocks,\nwikilinks, math, images"] + E --> F{"large common\nMarkdown?"} + F -->|"yes"| G["worker-backed direct Markdown parser\n→ block tree"] + F -->|"unsupported or small"| H["tryParseMarkdownToBlocks()\n→ BlockNote block tree"] + G --> I["inject wikilinks, math, highlights,\nand durable schema nodes"] + H --> I + D --> J{"large block set?"} + I --> J + J -->|"yes"| K["progressive locked apply\nfirst chunk + frame-yielded appends"] + J -->|"no"| L["single replaceBlocks()\nsmall/fast swap"] + + style A fill:#f8f9fa,stroke:#6c757d,color:#000 + style K fill:#d4edda,stroke:#28a745,color:#000 + style L fill:#d4edda,stroke:#28a745,color:#000 +``` + +> Wikilink placeholder tokens use `\u2039` and `\u203A`; math, Mermaid, tldraw, and standalone file-attachment link placeholders use ASCII sentinels with URI-encoded payloads. + +### BlockNote-to-Markdown Pipeline (Save) + +```mermaid +flowchart LR + A["✏️ BlockNote blocks\n(editor state)"] --> B["restore durable Markdown tokens\nwhile preserving block identity"] + B --> C{"all block shapes\nsupported directly?"} + C -->|"yes"| D["direct serializer\nwith per-editor WeakMap cache"] + C -->|"no"| E["BlockNote Markdown exporter fallback"] + D --> F["prepend frontmatter yaml"] + E --> F + F --> G["invoke('save_note_content')\n→ disk write"] + + style A fill:#cce5ff,stroke:#004085,color:#000 + style G fill:#d4edda,stroke:#28a745,color:#000 +``` + +Rich-editor change events are coalesced before this serialization runs. `useEditorTabSwap` keeps the latest BlockNote state in the editor, schedules one Markdown serialization for a short idle window, and exposes an explicit flush hook for save, note switch, raw-mode entry, and destructive note actions. `src/utils/richEditorMarkdown.ts` is the shared BlockNote-to-Markdown owner for autosave/tab-swap and raw-mode entry, so wikilink restoration, durable schema-node serialization, frontmatter preservation, file-attachment block round-tripping, and portable attachment paths cannot drift between editor modes. This keeps long notes from paying full-document Markdown serialization on every keystroke while preserving the disk-first save path. + +Autosave then waits for a 1.5s idle window before invoking `save_note_content`. If an older save resolves after the user has already typed newer content, the older save is treated as stale and cannot clear the newer pending buffer or repaint tab state over it; the latest pending content remains scheduled for its own save. + +### Wikilink Navigation + +Two navigation mechanisms: + +1. **Click handler**: DOM event listener on `.editor__blocknote-container` catches clicks on `.wikilink` elements → `onNavigateWikilink(target)`. +2. **Suggestion menu**: Typing `[[` or `@` triggers `SuggestionMenuController` with the same filtered vault-entry suggestions and inserts normal wikilink inline content. + +Wikilink resolution (`resolveEntry` in `src/utils/wikilink.ts`) uses multi-pass matching with global priority: path suffix for path-style targets, filename stem, alias, exact title, then humanized title (kebab-case -> words). In a mounted-workspace graph, unprefixed links prefer the source note's workspace, while links prefixed by a known workspace alias resolve inside that workspace (`[[team/projects/alpha]]`). Cross-workspace canonical link insertion prefixes the target alias only when source and target workspaces differ; same-workspace links stay vault-relative. + +### Raw Editor Mode + +Toggle via Cmd+K → "Raw Editor" or breadcrumb bar button. Uses CodeMirror 6 (`useCodeMirror` hook) to edit the raw markdown + frontmatter directly. Changes saved via the same `save_note_content` command. +`useRawModeWithFlush` owns the rich/raw transition model: pending raw-exit content and raw-mode overrides move together as one content transition, while cursor/scroll restoration moves through one restore-transition ref consumed by `useEditorModePositionSync`. The raw editor should not carry independent pending-content or pending-position refs outside that handoff. +While the user types, `useEditorSaveWithLinks` derives a transient `VaultEntry` patch from parseable frontmatter so the Inspector, relationship chips, and note-list-visible metadata stay in sync with the raw editor before the next vault reload. Temporarily invalid or half-typed frontmatter is ignored until it becomes parseable again, which avoids clobbering the last known good derived state. + +Current-note find/replace is intentionally backed by raw CodeMirror mode. `Cmd+F`, "Find in Note", and "Replace in Note" switch the active Markdown/text note to raw mode, show the compact find bar above CodeMirror, and operate on the current note only. Plain text matching is case-insensitive by default, `Aa` toggles case sensitivity, `.*` toggles JavaScript-regex matching, and regex replacement supports capture groups through JavaScript replacement syntax. + +### Rich Editor Width Modes + +Rich Markdown editing supports `normal` and `wide` note widths. The effective mode is resolved in `App.tsx` from, in order, the current session's transient note-width cache, `VaultEntry.noteWidth` parsed from `_width`, and the installation-local `settings.note_width_mode` default. The breadcrumb toggle calls the same setter exposed through the command palette. + +Per-note width is persisted as hidden `_width` frontmatter only when the note already has a valid or empty frontmatter block. Notes without frontmatter use the transient cache for the current session, so toggling width never creates frontmatter solely to store UI state. The width class is applied around `SingleEditorView` only; raw CodeMirror mode stays outside `.editor-content-wrapper` and remains full-width. + +### Arrow Ligature Normalization + +Typed ASCII arrow sequences are normalized consistently in both editor modes: + +- Rich editor input mounts `createArrowLigaturesExtension()` (`src/components/arrowLigaturesExtension.ts`) into BlockNote and intercepts typed `beforeinput` events before ProseMirror commits the character. +- Raw editor input uses the CodeMirror `inputHandler` path in `useCodeMirror` so the same ligature rules apply while editing markdown source directly. +- Both paths delegate to the shared `resolveArrowLigatureInput()` helper in `src/utils/arrowLigatures.ts`, which prioritizes `<->` over partial matches, keeps paste literal, and lets escaped forms such as `\\->` and `\\<->` remain ASCII. +- The rich-editor extension treats stale, disconnected, or mid-reload ProseMirror views as a no-op. It never blocks the native input path unless it has already built and dispatched a valid ligature transaction. + +## Styling + +The app uses internal light and dark themes owned by Tolaria, with System as an installation-local preference that follows the OS appearance (see [ADR-0081](adr/0081-internal-light-dark-theme-runtime.md) and [ADR-0112](adr/0112-system-theme-mode.md)). The previous vault-authored theming system remains removed. + +1. **Global CSS variables** (`src/index.css`): Semantic app colors, borders, surfaces, and interaction states via `:root` / `[data-theme]`, bridged to Tailwind v4 +2. **Editor theme** (`src/theme.json`): BlockNote typography, flattened to CSS vars by `useEditorTheme` +3. **Runtime theme bridge**: Resolves the selected preference to `light` / `dark`, applies `data-theme` and `.dark` for shadcn/ui, and subscribes to `prefers-color-scheme` while System is selected +4. **Theme mode commands**: Command-palette actions for Light, Dark, and System call the same `saveSettings` path as the Settings panel and persist only `settings.theme_mode` + +## Localization + +App UI strings are resolved through `src/lib/i18n.ts`, with flat JSON catalogs in `src/lib/locales/*.json` (see [ADR-0087](adr/0087-json-catalogs-and-lara-cli-localization.md)): + +- `AppLocale`: canonical locale tags such as `'en'`, `'zh-CN'`, `'fr-FR'`, `'es-419'` +- `UiLanguagePreference`: `'system' | AppLocale`; persisted settings serialize `system` as `null` +- `resolveEffectiveLocale()`: maps an explicit preference or system/browser language list to the effective supported locale, including legacy aliases +- `translate()` / `createTranslator()`: resolve keys with English fallback and simple `{name}` interpolation +- `scripts/validate-locales.mjs`: asserts every checked-in locale catalog matches the English keyset and stays flat-string-only + +`App.tsx` owns the effective locale and passes it to localized app chrome through props. Settings and command-palette language commands call back into `saveSettings`, so UI language changes update the current session without touching vault content or reopening the vault. + +## Inspector Abstraction + +The Inspector panel (`src/components/Inspector.tsx`) is composed of sub-panels: + +1. **DynamicPropertiesPanel** (`src/components/DynamicPropertiesPanel.tsx`): Renders frontmatter as editable key-value pairs: + - **Editable properties** (top): Type badge, Status pill with dropdown, number fields, boolean toggles, array tag pills, text fields. Click-to-edit interaction. + - **Property display modes**: `text`, `number`, `date`, `boolean`, `status`, `url`, `tags`, and `color`. Numeric frontmatter values auto-detect as `number`, and custom scalar keys can be explicitly switched to `Number` through the property-type control. + - **Anchored dropdowns**: Fixed-position property menus and note-list sort menus use `src/components/anchoredDropdown.ts` for anchor measurement, viewport clamping, scroll/resize repositioning, and optional max-height calculations. Property-specific filtering and keyboard navigation stay in `propertyDropdownUtils.ts`. + - **Present empty properties**: A top-level frontmatter key with a blank scalar value (for example `start date:`) is treated as present and renders as an editable empty row. Only absent keys are omitted. + - **Type-derived placeholders**: For typed instances, missing custom properties declared on the type document render as gray editable placeholders. Editing one writes the value to the instance frontmatter; merely displaying it does not backfill the note. + - **Info section** (bottom, separated by border): Read-only derived metadata — Modified, Created, Words, File Size. Uses muted styling with no interaction. + - Keys in `SKIP_KEYS` (`type`, `aliases`, `notion_id`, `workspace`, `is_a`, `Is A`) are hidden from the editable section. + +2. **RelationshipsPanel**: Shows `belongs_to`, `related_to`, `has`, and all custom relationship fields as clickable wikilink chips. Relationship labels are humanized for display, but stored keys remain unchanged. For typed instances, missing relationship fields declared on the type document render as gray editable placeholders without copying any default relationship targets into existing notes. + +3. **BacklinksPanel**: Scans `allContent` for notes that reference the current note via `[[title]]` or `[[path]]`. + +4. **GitHistoryPanel**: Shows recent commits from file history with relative timestamps. + +## Search + +### Search + +Keyword-based search scans all vault `.md` files using `walkdir` and applies the same Gitignored-content visibility filter as vault loading: + +```typescript +interface SearchResult { + title: string + path: string + snippet: string + score: number +} +``` + +### Search Integration + +`SearchPanel` component provides the search UI: +- Real-time results as user types (300ms debounce) +- Click result to open note in editor +- Shows relevance score and snippet + +The NoteList header search keeps its local title/snippet/property filtering for immediate scoped results, then augments the match set with `search_vault` hits from the visible workspace roots using the command's frontmatter-excluding search option. React stores only matching paths so body-only matches appear in the current list scope without a second content-read pass or rendering private matched text in note rows. + +No indexing step required — search runs directly against the filesystem. + +## Vault Management + +### Vault Switching + +`useVaultSwitcher` hook manages multiple vaults: +- Persists vault list to `$XDG_CONFIG_HOME/com.tolaria.app/vaults.json`, defaulting to `$HOME/.config/com.tolaria.app/vaults.json` on Unix platforms. App config path policy is declared once in `mcp-server/app-config-policy.json` and consumed by both the Rust app helper and Node MCP server: reads check the current Tolaria namespace, then legacy `com.laputa.app`, first in the preferred config root and then in the platform config root when it differs; writes target the current namespace in the preferred root. +- Switching closes all tabs and resets sidebar +- Supports adding, removing, hiding/restoring vaults +- Persists workspace aliases, colors, mount state, and the default new-note destination for the unified graph +- Default vault: public Getting Started starter vault cloned on demand + +Mounted workspaces are loaded together by `useVaultLoader` for note-list, quick-open, keyword search, wikilink navigation, and saved View discovery. Workspace switching remains a focus operation for per-vault capabilities (Git status, folders, AutoGit, watchers, and repair commands), not a graph isolation boundary. + +### Vault Config + +Per-vault settings stored locally and scoped by vault path: +- Managed by `useVaultConfig` hook and `vaultConfigStore` +- Settings: zoom, view mode, editor mode, tag colors, status colors, property display modes, Inbox/All Notes note-list column overrides, explicit organization workflow toggle, Git setup prompt preference, AI agent permission mode (`safe` / `power_user`) +- Missing, null, and unknown AI agent permission modes normalize to `safe`; the AI panel can switch modes per vault, preserving the transcript and applying the new mode only to the next agent run +- One-time migration from localStorage (`configMigration.ts`) + +Installation-local layout state that should not sync through a vault stays in localStorage. `useLayoutPanels` stores the clamped sidebar, note-list, and inspector widths under `tolaria:layout-panels` so pane sizing survives app relaunches on the same machine. + +### AI Guidance Files + +Tolaria tracks managed vault-level AI guidance separately from normal note content: +- `AGENTS.md` is the canonical managed guidance file for Tolaria-aware coding agents +- `CLAUDE.md` is a compatibility shim that points Claude Code back to `AGENTS.md` +- `GEMINI.md` is an optional Antigravity/Gemini compatibility shim that points Google-backed CLI agents back to `AGENTS.md` +- `useVaultAiGuidanceStatus` reads `get_vault_ai_guidance_status` and normalizes the backend state into four UI cases: `managed`, `missing`, `broken`, and `custom` +- `restore_vault_ai_guidance` repairs only Tolaria-managed files and creates the optional Antigravity/Gemini shim on explicit request; user-authored custom `AGENTS.md` / `CLAUDE.md` / `GEMINI.md` files are surfaced as custom and left untouched +- Editing a usable `AGENTS.md`, including changing its frontmatter `type`, makes the file custom rather than broken; broken is reserved for missing, empty, frontmatter-only, unreadable, or exact replaceable managed templates/stubs +- The status bar AI badge and command palette consume that abstraction to expose restore actions only when the managed guidance is missing or broken + +Vault guidance is intentionally short and vault-specific. General Tolaria product behavior is delivered through the bundled agent docs resource instead: +- `scripts/build-agent-docs.mjs` compiles the public `site/` Markdown into `src-tauri/resources/agent-docs/` +- `src-tauri/resources/agent-docs/AGENTS.md` orients agents to the generated docs bundle, while `index.md`, section bundles, `all.md`, `search-index.json`, and `pages/` provide fast local lookup +- `get_agent_docs_path` exposes the resolved resource folder to the renderer, and `buildAgentSystemPrompt()` tells every app-managed CLI agent to read vault `AGENTS.md` first, then search the bundled docs for Tolaria behavior + +### Action History + +`useActionHistory` owns renderer-scoped app undo/redo state. It stores explicit action entries with labels plus undo/redo callbacks, suppresses nested recording during replay, and exposes the top labels to command-palette commands. + +- Frontmatter mutations record history only after the write succeeds and only for non-silent user actions. +- Entry state toggles such as archive, favorite, and organized record explicit before/after replay callbacks after persistence succeeds. +- Text inputs, contenteditable surfaces, and editor-owned text history keep native undo/redo first; app-level history runs only when focus is outside text editing. +- Irreversible destructive actions stay outside the stack and continue to use confirmation/destructive affordances. + +### Getting Started / Onboarding + +`useOnboarding` hook detects first launch: +- If vault path doesn't exist → show `WelcomeScreen` +- User can create a new empty vault, open an existing folder, or clone the public Getting Started vault into a chosen parent folder; Tolaria derives the final `Getting Started` child path before cloning +- After the starter repo clone completes, Tolaria removes every remote so the new vault opens local-only by default +- Welcome state tracked in localStorage (`tolaria_welcome_dismissed`, with legacy fallback) + +`useGettingStartedClone` encapsulates the non-onboarding Getting Started action: +- Opens the same parent-folder picker used by onboarding +- Derives the final `.../Getting Started` destination path +- Surfaces the resolved path through the app toast after a successful clone + +`useAiAgentsOnboarding(enabled)` adds a separate first-launch agent step: +- Reads a local dismissal flag for the AI agents prompt (with a legacy fallback to the older Claude-only key) +- Only shows after vault onboarding has already resolved to a ready state +- Uses `get_ai_agents_status`, whose backend checks Claude Code, Codex, GitHub Copilot, OpenCode, Pi, Antigravity, Kiro, and Hermes Agent by treating the app process path, login-shell path, and supported local/toolchain/app install locations, including nvm-managed Node installs plus Windows `.exe` and npm/pnpm/Scoop shim paths, as valid CLI-agent sources +- App-managed Claude Code runs preserve the same user-managed Anthropic/provider env behavior by forwarding selected exported variables from the app process or the user's zsh/bash startup files without persisting those secrets +- The shared `useAiAgentsStatus` hook defers that command until after the first render, skips it when AI features are disabled or the current window cannot render AI status surfaces, and falls back to missing-agent statuses if the native probe does not return promptly so first-launch onboarding keeps a recovery path +- Persists dismissal locally once the user continues + +### Remote Git Operations + +Tolaria delegates remote auth to the user's system git setup: +- `CloneVaultModal` captures a supported remote URL (`https://`, `http://`, `ssh://`, or `git@host:path`) and local destination +- `clone_git_repo` and `create_getting_started_vault` both run system git clone work in blocking Tokio tasks so clone UIs stay responsive +- On macOS, system-git commands prefer the user's login-shell `git` and `PATH`, and `git_add_remote` preflights HTTPS remotes through `git credential fill` so Keychain can prompt/grant access before the first fetch or push +- On Linux AppImage launches, every system-git command and MCP runtime subprocess (Node.js or Bun) removes AppImage loader overrides such as `LD_LIBRARY_PATH`, `LD_PRELOAD`, and `GIT_EXEC_PATH` before spawning, so helpers like `git-remote-https` and the system MCP runtime bind against the host library stack instead of Tolaria's bundled WebKit/AppImage libraries +- On native Linux Wayland launches, startup environment safeguards set `WEBKIT_DISABLE_DMABUF_RENDERER=1` unless the user already provided it, keeping the broad WebKitGTK DMABUF crash workaround while avoiding the last-resort compositing fallback that can make WebKitGTK windows feel unresponsive. Linux AppImage launches still set both `WEBKIT_DISABLE_DMABUF_RENDERER=1` and `WEBKIT_DISABLE_COMPOSITING_MODE=1` unless overridden because the sealed AppImage path has the verified rendering failure this fallback protects. +- On Linux AppImage launches, release packaging bundles the GTK3 fcitx immodule into the AppImage and startup environment safeguards write a cache-local `GTK_IM_MODULE_FILE` that points GTK at the mounted module whenever fcitx is configured. If the user has not explicitly chosen a GTK IM module, Tolaria also sets `GTK_IM_MODULE=fcitx`, allowing WebKitGTK editor input to reach fcitx5 on both Wayland and X11 fallback launches without relying on host GTK module cache paths. +- `git_add_remote` uses the same system git path, validates the pasted URL at the Tauri command boundary, and refuses remotes whose history is unrelated or ahead of the local vault +- Shared git subprocess setup rejects the `ext::` transport, inserts an end-of-options separator for clone URLs, disables repo-configured fsmonitor hooks, and ignores repo-configured SSH command overrides +- Existing `git_pull` / `git_push` commands keep surfacing raw git errors, and clone commands fail fast when git wants interactive terminal input +- No provider-specific token or username is stored in app settings + +## Settings + +App-level settings persisted at `$XDG_CONFIG_HOME/com.tolaria.app/settings.json`, defaulting to `$HOME/.config/com.tolaria.app/settings.json` on Unix platforms. `settings.json` and `vaults.json` share the same `mcp-server/app-config-policy.json` search order used by Rust and the external MCP server, so durable agent registrations resolve mounted workspaces the same way the app resolves installation-local settings: + +```typescript +interface AiWorkspaceConversationSetting { + archived: boolean | null + id: string + target_id: string | null + title: string +} + +interface Settings { + auto_pull_interval_minutes: number | null + autogit_enabled: boolean | null + autogit_idle_threshold_seconds: number | null + autogit_inactive_threshold_seconds: number | null + telemetry_consent: boolean | null + crash_reporting_enabled: boolean | null + analytics_enabled: boolean | null + anonymous_id: string | null + release_channel: string | null // null = stable default, "alpha" = every-push prerelease feed + automatic_update_checks_enabled: boolean | null // null = default true + theme_mode: 'light' | 'dark' | 'system' | null + ui_language: AppLocale | null + date_display_format: 'us' | 'european' | 'friendly' | 'iso' | null + note_width_mode: 'normal' | 'wide' | null + sidebar_type_pluralization_enabled: boolean | null // null = default true + ai_features_enabled: boolean | null // null = default true + git_enabled: boolean | null // null = default true + default_ai_agent: 'claude_code' | 'codex' | 'copilot' | 'opencode' | 'pi' | 'antigravity' | 'kiro' | 'hermes' | null + default_ai_target: string | null // "agent:codex" or "model:/" + ai_model_providers: AiModelProvider[] | null + ai_workspace_conversations: AiWorkspaceConversationSetting[] | null + hide_gitignored_files: boolean | null // null = default true + all_notes_show_pdfs: boolean | null // null = default false + all_notes_show_images: boolean | null // null = default false + all_notes_show_unsupported: boolean | null // null = default false +} +``` + +Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is installation-local because it controls device comfort rather than vault structure; the Settings panel and command-palette Light/Dark/System actions both update that same value. `system` remains a stored preference, while the runtime resolves it to `light` or `dark` for `data-theme` and app consumers. `ui_language` is also installation-local: `null` follows the supported system language with English fallback, while explicit values pin the UI language for this installation. Stored legacy aliases such as `zh-Hans` are normalized to canonical locale codes before the setting reaches React state. `date_display_format` is installation-local and controls rendered dates in note rows, property chips/cells, note info, table-of-contents metadata, and search result subtitles; `AppPreferencesProvider` owns the UI-level value so rendering surfaces can consume it without prop forwarding, while date picker text input remains ISO for predictable manual entry and storage. `note_width_mode` is the installation-local default for rich-editor note width; individual notes can override it with `_width` when they already have frontmatter. `sidebar_type_pluralization_enabled` is installation-local and defaults to `true`; when false, type rows use exact type names unless the type document defines an explicit `sidebar_label` override. `automatic_update_checks_enabled` is installation-local and defaults to `true`; when false, `useUpdater` skips the startup/background update probe while the status-bar/manual "Check for updates" action remains available. `ai_features_enabled` is installation-local and defaults to `true`; when false, Tolaria hides AI panel controls, status bar AI indicators, command-palette AI mode, and missing-agent prompts while leaving Settings as the re-enable path. `git_enabled` is also installation-local and defaults to `true`; when false, Tolaria hides Git status-bar entries and command-palette actions, disables AutoGit controls, and avoids background Git refresh/sync work while leaving Settings as the re-enable path. `default_ai_agent` remains the legacy installation-local CLI fallback. `default_ai_target` is the active AI target used by the AI panel and status bar; it can point at a coding agent or a configured direct model. `ai_model_providers` stores non-secret provider metadata for local/API model targets, while hosted API keys live in Tolaria's local app-data secrets file or user-managed environment variables instead of being persisted in app settings; env-backed keys can come from the app process or exported zsh/bash startup values on Unix. Direct OpenAI-compatible model streams receive the active vault root and may execute Tolaria's native create-only `create_note` tool, but they do not receive shell access or general file-write tools. `ai_workspace_conversations` stores installation-local AI chat sidebar metadata only: conversation ids, titles, archive state, and explicit target overrides. It does not store vault content, prompts, transcripts, or model credentials. Provider defaults and local/API grouping come from the shared `src/shared/aiModelProviderCatalog.json` catalog used by both renderer settings and the Tauri direct-model runtime. `hide_gitignored_files` is also installation-local and defaults to `true`; changing it reloads entries, search, saved views, and folders without restarting. The `all_notes_show_pdfs`, `all_notes_show_images`, and `all_notes_show_unsupported` flags are installation-local All Notes category toggles that default off and update the list/counts without changing vault files. The AutoGit fields are also installation-local: `useAutoGit` consumes them to schedule automatic checkpoints, while `useCommitFlow` and the status bar quick action reuse the same checkpoint runner and deterministic automatic commit message generation. + +## Telemetry + +### Components +- **`TelemetryConsentDialog`** — First-launch dialog asking user to opt in to anonymous crash reporting. Two buttons: accept (sets `telemetry_consent: true`, generates `anonymous_id`) or decline. +- **`TelemetryToggle`** — Checkbox component in `SettingsPanel` for crash reporting and analytics toggles. + +### Hooks +- **`useTelemetry(settings, loaded)`** — Reactively initializes/tears down Sentry and PostHog based on settings. Called once in `App`. + +### Libraries +- **`src/lib/telemetry.ts`** — `initSentry()`, `teardownSentry()`, `initPostHog()`, `teardownPostHog()`, `trackEvent()`. Path scrubber via `beforeSend` hook. The same hook drops known benign browser ResizeObserver loop-limit notifications before they become crash issues, while keeping unrelated ResizeObserver failures reportable. DSN/key from `VITE_SENTRY_DSN` and `VITE_POSTHOG_KEY`; `VITE_SENTRY_RELEASE` is treated as the build version and only becomes Sentry's `release` for stable calendar builds (`YYYY.M.D`). Alpha/prerelease/internal builds tag `tolaria.build_version` and `tolaria.release_kind` without creating normal Sentry Releases entries. +- **`src/main.tsx`** — React root error callbacks (`onCaughtError`, `onUncaughtError`, `onRecoverableError`) forward component-stack context to `Sentry.reactErrorHandler()` for debuggable production React errors. +- **`src-tauri/src/telemetry.rs`** — Rust-side Sentry init with `beforeSend` path scrubber. `init_sentry_from_settings()` reads settings and conditionally initializes; stable calendar `CARGO_PKG_VERSION` values become Sentry releases, while alpha/prerelease/internal versions are kept as diagnostic tags only. `reinit_sentry()` for runtime toggle. + +### Product Events +- **File previews** — `file_preview_opened`, `file_preview_action`, and `file_preview_failed` report only preview/action categories such as `image`, `pdf`, `unsupported`, `open_external`, `copy_path`, and `reveal`. +- **Inline image lightbox** — `inline_image_lightbox_opened` records that a rich-editor inline image was opened from double-click, without sending note paths, image URLs, alt text, or file names. +- **Code block copy** — `code_block_copied` records that the rich-editor code-block copy action was used, without sending note paths, languages, or code content. +- **AI agent sessions** — `ai_agent_message_sent`, `ai_agent_message_blocked`, `ai_agent_response_completed`, `ai_agent_response_failed`, `ai_agent_response_stopped`, and `ai_agent_permission_mode_changed` use only agent ids, permission modes, counts, and coarse status categories. +- **AI feature visibility** — `ai_features_visibility_changed` records only whether installation-level AI surfaces were enabled or hidden. +- **Automatic update checks** — `automatic_update_checks_changed` records only whether startup/background update checks were enabled or disabled. +- **All Notes visibility** — `all_notes_visibility_changed` records only the toggled category and enabled state. + +### Tauri Commands +- **`reinit_telemetry`** — Re-reads settings and toggles Rust Sentry on/off. Called from frontend when user changes crash reporting setting. + +--- + +## Updates & Feature Flags + +### Hooks +- **`useUpdater(releaseChannel, automaticChecksEnabled)`** — Channel-aware updater state machine. When automatic checks are enabled, it checks the selected feed after startup; manual checks always remain available. It surfaces checking/available/downloading/ready states and delegates install work to Rust. +- **`useFeatureFlag(flag)`** — Returns boolean for a named feature flag. Checks `localStorage` override (`ff_`), then falls back to telemetry-backed evaluation. Type-safe via `FeatureFlagName` union. + +### Frontend helpers +- **`src/lib/releaseChannel.ts`** — Normalizes persisted channel values so legacy or invalid settings fall back to Stable, while Stable serializes back to `null`. +- **`src/lib/appUpdater.ts`** — Thin wrapper around the Tauri updater commands. Keeps the React hook free of endpoint-selection details. + +### Rust +- **`src-tauri/src/app_updater.rs`** — Chooses the correct update endpoint and adapts Tauri updater results into frontend-friendly payloads. Stable uses the public `stable/latest.json` feed. Alpha first resolves the newest non-draft `alpha-vYYYY.M.D-alpha.NNNN` GitHub Release asset named `alpha-latest.json`, then falls back to the public `alpha/latest.json` feed if the release lookup is unavailable. +- **`src-tauri/src/commands/version.rs`** — Formats app build/version labels for the status bar, including calendar alpha labels and legacy release compatibility. + +### Tauri Commands +- **`check_for_app_update`** — Channel-aware update manifest lookup. +- **`download_and_install_app_update`** — Channel-aware download/install with streamed progress events. + +### CI/CD +- **`.github/workflows/release.yml`** — Alpha prereleases from every push to `main` using calendar-semver technical versions (`YYYY.M.D-alpha.N`) and clean `Alpha YYYY.M.D.N` release names. GitHub alpha tags zero-pad the prerelease sequence (`alpha-vYYYY.M.D-alpha.NNNN`) so GitHub release ordering stays chronological while the shipped app version remains `YYYY.M.D-alpha.N`. Publishes `alpha/latest.json` with macOS Apple Silicon/Intel, Linux x64, and Windows x64 updater entries, then refreshes the legacy `latest.json` / `latest-canary.json` aliases to the alpha feed. The Windows job always requires Tauri updater signatures, uses Authenticode signing and `Get-AuthenticodeSignature` verification when Windows certificate secrets are present, and warns while building updater-signed-only artifacts when those secrets are absent. The Linux job uses Tauri's stock linuxdeploy AppImage output plugin and validates that installer and updater-signature artifacts exist before upload. The docs/release Pages job reads the stable manifest from the latest stable release asset instead of copying the live Pages URL, uploads the built site as a Pages artifact, and deploys it with GitHub's official Pages action so the public updater JSON changes as part of the release workflow. Changes to the shared artifact workflow are not ignored by the alpha trigger, so release-pipeline fixes produce a fresh alpha run. macOS release assets use `Tolaria__macOS_Silicon` and `Tolaria__macOS_Intel` base names. Packaged builds pass the computed version as `VITE_SENTRY_RELEASE`, which is retained as a diagnostic build-version tag but not registered as a normal Sentry release for alpha builds. +- **`.github/workflows/release-stable.yml`** — Stable releases from `stable-vYYYY.M.D` tags. Publishes `stable/latest.json`, macOS Apple Silicon and Intel DMG/updater artifacts, Windows x64 installers plus Tauri-signed updater bundles, Linux x86_64 `.deb` / `.rpm` / AppImage artifacts, and a static public download page that starts selected non-Windows installers without replacing the page with a blank download navigation. Windows visitors see an explicit installer action and managed-device guidance instead of an automatic download. Authenticode publisher signing is added to Windows artifacts when certificate secrets are configured; until then, the workflow warns and publishes updater-signed-only Windows artifacts. Linux visitors default to the AppImage target while the page exposes RPM as a manual Linux package option when the stable release includes one. The Linux job uses the same stock Tauri/linuxdeploy AppImage packaging and artifact validation as alpha releases. The Pages job reads the alpha manifest from the latest alpha release asset instead of copying the live Pages URL, uploads the built site as a Pages artifact, and deploys it with GitHub's official Pages action so stable and alpha manifests stay fresh. Stable macOS DMG/updater assets use the same `Tolaria__macOS_Silicon` and `Tolaria__macOS_Intel` base names. Packaged builds pass the computed stable version as `VITE_SENTRY_RELEASE`, which is registered as Sentry's release. +- **Beta cohorts** are handled in PostHog targeting only. There is no beta updater feed. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..6a29442 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,1176 @@ +# Architecture + +Tolaria is a personal knowledge and life management desktop app. It reads a vault of markdown files with YAML frontmatter and presents them in a four-panel UI inspired by Bear Notes. + +## Design Principles + +### Filesystem as the single source of truth + +The vault is a folder of plain markdown files. The app never owns the data — it only reads and writes files. The cache, React state, and any in-memory representation are always derived from the filesystem and must be reconstructible by deleting them. When in doubt, the file on disk wins. + +### Convention over configuration + +Tolaria is opinionated. Standard field names (`type:`, `status:`, `url:`, `Workspace:`, `belongs_to:`, `related_to:`, `has:`, `start_date:`, `end_date:`) have well-defined meanings and trigger specific UI behavior — without any setup. Relationship defaults are stored in snake_case on disk and humanized in the UI. This is not convention *instead of* configuration: users can override defaults via config files in their vault (e.g. `config/relations.md`, `config/semantic-properties.md`). But the defaults work out of the box, and most users never need to touch them. + +This principle directly serves AI-readability: the more structure comes from shared conventions rather than per-user custom configurations, the easier it is for an AI agent to understand and navigate the vault correctly — without needing bespoke instructions for every setup. + +### Where to store state: vault vs. app settings + +When deciding where to persist a piece of data, ask: **"Would the user want this to follow them across all their Tolaria installations — other devices, future platforms (tablet, web)?"** + +| Follows the vault | Stays with the installation | +|-------------------|-----------------------------| +| Type icon, type color | Editor zoom level | +| Pinned properties per type | API keys (OpenAI, Google) | +| Sidebar label overrides | Auto-sync interval | +| Property display order | Window size / position | +| Per-note `_width` rich-editor width override | Default rich-editor note width | +| Vault-authored `.gitignore` patterns | Whether this installation hides Gitignored files | +| N/A | Whether this installation shows Git features | +| N/A | Git executable provider (`native` vs WSL2 distribution) | +| Per-vault All Notes note-list column overrides | All Notes PDF/image/unsupported file visibility | +| N/A | Per-vault Git setup prompt opt-out | +| Type `_sidebar_label` overrides | Whether this installation auto-pluralizes type labels | +| N/A | Registered workspace labels, aliases, mount state, and default new-note destination | +| Any user-visible customization of how content is organized or displayed | Any machine-specific or credential-type setting | + +**Rule:** If the information is about *how the content is structured or presented* and the user would expect it to be consistent wherever they open their vault, store it in the vault (frontmatter of the relevant note, using the `_field` underscore convention for system properties). If it's about *this specific installation of the app*, store it in app config JSON under `$XDG_CONFIG_HOME/com.tolaria.app/` (defaulting to `$HOME/.config/com.tolaria.app/` on Unix platforms), or in localStorage for renderer-only transient layout state. + +Examples: +- ✅ Vault: `_pinned_properties` in a Type note (every device should show the same pinned properties) +- ✅ Vault: `_icon: shapes` in a Type note (icon is part of the type's identity) +- ✅ Vault: `_width: wide` in a note that already has frontmatter (per-note reading/editing preference) +- ✅ App settings: `zoom: 1.3` (machine-specific preference) +- ✅ App settings: `ui_language: "zh-CN"` (installation-specific UI language) +- ✅ App settings: `note_width_mode: "wide"` (installation-specific default for notes without an override) +- ✅ App settings: `date_display_format: "friendly"` (installation-specific date rendering preference) +- ✅ App settings: `sidebar_type_pluralization_enabled: false` (installation-specific sidebar label preference) +- ✅ App settings: `all_notes_show_images: true` (installation-specific All Notes file-category visibility) +- ✅ App settings: `git_provider: "wsl"` plus `git_wsl_distro: "Ubuntu"` (machine-specific Git executable location) + +### No hardcoded exceptions + +No field names, folder paths, or vault-specific values should be hardcoded in the application source code. What can be a convention should be a convention. What needs to be configurable should live in a file. Relationship fields are detected dynamically by checking whether values contain `[[wikilinks]]` — no hardcoded field name lists. + +### AI-first knowledge graph + +Notes are not just documents — they are nodes in a structured graph of people, projects, events, responsibilities, and ideas. Every design decision should ask: "Does this make the knowledge graph easier for a human *and* an AI to navigate?" Conventions that are legible to both are better than conventions that are legible only to one. + +### Three representations, one authority + +Vault data exists in three forms simultaneously: +1. **Filesystem** — the `.md` files on disk. This is the single source of truth. +2. **Cache** — `~/.laputa/cache/.json`, an index for fast startup. Always reconstructible from the filesystem. +3. **React state** — the in-memory `VaultEntry[]` during a session. Always derived from the cache or filesystem. + +These must never diverge permanently. If they do, the filesystem wins and the cache/state are rebuilt. + +```mermaid +flowchart LR + FS["🗂️ Filesystem\n.md files on disk\n(source of truth)"] + Cache["⚡ Cache\n~/.laputa/cache/\n(fast startup index)"] + RS["⚛️ React State\nVaultEntry[]\n(in-memory session)"] + + FS -->|"scan_vault_cached()"| Cache + Cache -->|"useVaultLoader on load"| RS + FS -->|"reload_vault (full rescan)"| RS + RS -.->|"write via Tauri IPC first"| FS + + style FS fill:#d4edda,stroke:#28a745,color:#000 + style Cache fill:#fff3cd,stroke:#ffc107,color:#000 + style RS fill:#cce5ff,stroke:#004085,color:#000 +``` + +#### Ownership rules + +| Layer | Owner | Writes to | Reads from | +|-------|-------|-----------|------------| +| Filesystem | Tauri Rust commands (`save_note_content`, `update_frontmatter`, etc.) | Disk | — | +| Cache | `scan_vault_cached()` in `vault/cache.rs` | `~/.laputa/cache/` | Filesystem + git diff | +| React state | `useVaultLoader` + `useEntryActions` + `useNoteActions` | In-memory `entries` | Cache (on load), filesystem (on reload) | + +#### Invariants + +1. **Disk-first writes**: All functions that change vault data must write to disk (via Tauri IPC) *before* updating React state. This ensures that if the disk write fails, React state remains consistent with what's actually on disk. +2. **Optimistic UI with rollback**: Where responsiveness matters (e.g. `persistOptimistic` in `useNoteCreation`), state may update before disk confirmation — but a failure callback must revert the optimistic state. +3. **No orphan state updates**: Never call `updateEntry()` before the corresponding `handleUpdateFrontmatter()` or `handleDeleteProperty()` has resolved. Type metadata actions in `useEntryActions` follow this rule — create the missing type document if needed, write frontmatter first, then update state. If missing type creation collides with an existing note filename, the action stops after the existing creation toast instead of applying orphaned state. +4. **Recovery via reload**: If state ever diverges from disk (crash, external edit, race condition), `Reload Vault` (Cmd+K → "Reload Vault") invalidates the cache and does a full filesystem rescan via the `reload_vault` Tauri command, replacing all React state. The `reload_vault_entry` command can re-read a single file. +5. **Cache is disposable**: The `reload_vault` command deletes the cache file before rescanning, guaranteeing fresh data. The cache never contains data that doesn't exist on the filesystem. +6. **Visibility filters are command-boundary concerns**: Gitignored-content visibility is applied after scanning/caching, before entries, folders, or search results reach React. The cache remains complete so toggling the setting can show ignored content again without rebuilding a different cache shape. Large folder filtering runs on the blocking Tokio pool and drains `git check-ignore` output while feeding stdin so broad ignore matches cannot freeze the native UI thread. + +#### External Change Detection + +The main window starts a native watcher for the active vault through `start_vault_watcher` / `stop_vault_watcher` (`src-tauri/src/vault_watcher.rs`, backed by Rust `notify`). The watcher emits `vault-changed` events for content paths and ignores churn from `.git/`, `node_modules/`, temp files, and `.tolaria-rename-txn`. `useVaultWatcher` batches those events, suppresses recent app-owned saves, and sends the remaining external paths through `refreshPulledVaultState()` so folders, saved views, note-list state, and clean active-editor content refresh under the ADR-0135 unsaved-edit rules. When that clean active-editor remount starts from a focused editor, the main app dispatches a post-replacement editor focus request so typing can continue in the refreshed tab. `useVaultLoader.isReloading` drives the status-bar reload spinner for both manual and watcher-triggered reloads. + +#### Progressive Vault Loading + +Vault opening is allowed to render the main app shell while the entry index is still in flight. Initial startup hydration uses the cached/incremental `list_vault` path; the main window falls back to `reload_vault` only when that cached startup result is empty, while explicit refresh paths still force a fresh reload. `useVaultLoader` keeps `isLoading` true until entries are ready, but folders and saved views load independently so the sidebar can become useful before the note index completes. The status bar uses the vault activity badge during this initial indexing state, while command-palette and editor-shell interactions remain mounted instead of being hidden behind the full app skeleton. The full skeleton is reserved for app-level capability checks such as the initial Git-state probe. + +Large-vault reproduction and keyboard QA steps live in [LARGE-VAULT-LOADING-QA.md](./LARGE-VAULT-LOADING-QA.md). + +#### Mounted Workspaces + +The registered vault list can act as a mounted-workspace set. `useVaultSwitcher` persists each workspace's installation-local identity (`label`, stable `alias`, color, mount flag) and the default destination for newly created notes in `vaults.json` under Tolaria's app config directory (`$XDG_CONFIG_HOME/com.tolaria.app/`, defaulting to `$HOME/.config/com.tolaria.app/` on Unix platforms). `useVaultLoader` scans every available mounted workspace and annotates each `VaultEntry` with provenance before React consumes the combined graph. The default workspace is the write target for new notes and Type documents; it is not the only active vault when multiple workspaces are enabled. + +Vault item deep links use the registered vault list as their resolver namespace. `src/utils/deepLinks.ts` builds `tolaria:///` URLs from workspace aliases, labels, and paths, appending a short stable hash when generated slugs would collide. `useDeepLinks` validates incoming links, switches vaults when required, reloads the vault index once for recently changed files, and opens the matching `VaultEntry` through the normal note-selection path. + +Saved Views participate in that mounted graph as source-scoped chrome. `useVaultLoader` loads view definitions from every mounted vault, annotates each `ViewFile` with its owning `rootPath` and workspace identity, and keeps sidebar selection/persistence keyed by `(rootPath, filename)` so same-named view files from different vaults stay independent. + +Git surfaces resolve repository paths explicitly. `useGitRepositories` derives the active repository set from the mounted available workspaces, keeps separate selected repositories for Changes, Pulse/history, and manual commits, and exposes the combined modified-file count for status/commands. Native Git capability checks treat a mounted workspace as Git-backed when it is inside any Git work tree, so included vault folders can reuse a parent repository without creating embedded `.git` directories. Mounted folders outside Git return no changes/no remote for aggregate probes instead of failing workspace-wide sync. AutoGit checkpoints iterate that repository set, while manual commit, history, diff, and discard operations use the selected surface or the note's workspace provenance. + +Git command launch is also installation-local. The Rust Git module resolves a `GitLaunchConfig` from app settings before spawning Git, defaulting to native `git` while allowing Windows installations to explicitly select WSL2 Git and a distribution. WSL-selected launches go through `wsl.exe --exec git` and translate Windows/WSL UNC vault paths before passing repository paths across the command boundary; Tolaria never silently switches providers based on detection alone. + +Renderer git file workflows stay behind `useGitFileWorkflows`. The hook resolves per-note repository paths, queues editor diff requests, opens Pulse history entries including deleted-file previews, and keeps discard/reload handling close to the selected Git surface while `App.tsx` only wires the resulting callbacks into `NoteList`, `PulseView`, and `Editor`. + +Cross-workspace note reads and writes keep the disk-first invariant. When an absolute note path is saved or read without an explicit `vaultPath`, the Tauri boundary resolves the deepest registered vault root that contains the path and validates against that root before touching disk. This lets an editor tab opened from a mounted workspace save back to its source repository while preserving the same path-escape protections as active-vault operations. + +#### Note Opening Fast Path + +Note opening uses bounded in-memory fast paths for raw content and parsed editor blocks. `useTabManagement` owns the markdown/text prefetch cache and treats every cached value as a performance hint only: identity-matched entries (`modifiedAt` + `fileSize`) can be reused immediately, while identity-missing or identity-mismatched cached text is checked with `validate_note_content`, which compares the cached text with the current file bytes inside the validated vault boundary. If validation fails, Tolaria discards the cached entry and reads fresh disk content before swapping the editor. + +The note list opportunistically preloads visible and adjacent markdown/text entries after a short delay. When a large warmed Markdown note resolves, `useEditorTabSwap` may parse it into a bounded parsed-block cache only after foreground editor work has been idle and the rich editor is mounted. On foreground open, block resolution checks the hot tab cache, exact-source parsed-block cache, blank/H1 fast path, and a direct Markdown-to-block parser for large common Markdown before falling back to BlockNote's parser. The direct parser runs in a module worker when available and falls back synchronously only for tests or runtimes without workers. It is conservative: unsupported Markdown constructs return to the BlockNote path rather than changing semantics. Large resolved block sets mount progressively: the editor is locked, the first chunk is applied immediately, remaining chunks append across animation frames, and the same generation/source-content token can abort the partial mount before it is committed. Ordinary BlockNote block wrappers stay fully measurable after mount so hover side menus, slash dialogs, and document-end interactions do not inherit browser lazy-height scroll jumps. Parsed blocks are keyed by vault, path, and exact source content; every async swap carries a generation/source-content token so stale conversion results cannot overwrite newer file content or dirty editor state. The editor never renders a preview surface that later morphs into BlockNote. Development builds log `editorBlockResolve`, `editorBlockApply`, and `parsedBlockPreload` timings for large or slow note opens. See [ADR-0105](./adr/0105-editor-correctness-and-responsiveness-contract.md). + +Rich-editor save serialization stays on the editor fast path but avoids BlockNote's full Markdown exporter when Tolaria knows the block tree is representable directly. Real BlockNote editor instances install `src/utils/blockNoteDirectMarkdown.ts`, which serializes common Tolaria block shapes to Markdown with a per-editor weak cache keyed by stable block object identity and list context. Durable Markdown bridges for wikilinks, math, highlights, files, Mermaid, and tldraw remain owned by `src/utils/richEditorMarkdown.ts`; the bridge helpers preserve unchanged block objects so repeated debounced saves can reuse direct Markdown projections. Stable active-tab rerenders trust the exact-source block cache before doing any full-document Markdown comparison, so unrelated React state changes do not serialize large notes. Unsupported block shapes fall back to BlockNote's lossy serializer, and development builds log `richEditorSerialize` timing/cache/fallback metrics plus rare `editorStabilityCheck` fallback comparisons for large or slow serializations. + +Rich-editor typing keeps BlockNote's per-transaction work minimal. Tolaria creates BlockNote with animation bookkeeping disabled so the `previousBlockType` extension, which scans the old and new ProseMirror documents on every edit, is not installed. Collapsed-heading rendering attaches its mutation observer and `editor.onChange` subscription only while at least one heading/list section is collapsed, then detaches when the final section expands. Development builds wrap ProseMirror dispatch once per editor view and log `richEditorDispatch` timings for large or slow transactions, giving QA a direct signal for edit-time stutter instead of only note-open speed. + +Editor responsiveness is also protected by a synthetic browser benchmark. `pnpm perf:editor` launches an isolated Vite server, injects small and large synthetic Markdown vault entries, opens each note repeatedly, and records medians for editor visibility, first content rendered, full note application, post-open edit frame latency, and development timing logs such as block resolution/application. `.editor-performance-thresholds.json` stores ratcheted local budgets for those medians; `pnpm perf:editor:update` refreshes baselines only when intentionally accepting a new local floor. + +## Tech Stack + +| Layer | Technology | Version | +|-------|-----------|---------| +| Desktop shell | Tauri v2 | 2.10.0 | +| Frontend | React + TypeScript | React 19, TS 5.9 | +| Editor | BlockNote | 0.46.2 | +| Editor render extensions | @tiptap/pm | ProseMirror decorations for rich-editor node presentation | +| Code block highlighting | @blocknote/code-block | 0.46.2 | +| Additional code grammars | @shikijs/langs | 3.23.0 | +| Diagram rendering | Mermaid | 11.14.0 | +| Whiteboard rendering | tldraw | 4.5.10 | +| Raw editor | CodeMirror 6 + official language packages | Markdown, YAML, JSON, Python, SQL, JS/TS | +| Spreadsheet editor | IronCalc workbook + WASM | 0.5.x | +| Styling | Tailwind CSS v4 + CSS variables | 4.1.18 | +| UI primitives | Radix UI + shadcn/ui | - | +| Icons | Phosphor Icons | - | +| Build | Vite | 7.3.1 | +| Backend language | Rust (edition 2021) | 1.77.2 | +| Frontmatter parsing | gray_matter | 0.2 | +| Filesystem watcher | notify | 6.1 | +| AI (workspace) | CLI agent adapters (Claude Code + Codex + GitHub Copilot + OpenCode + Pi + Antigravity + Kiro + Hermes Agent) plus configured local/API model targets | - | +| Search | Keyword (walkdir-based file scan) | - | +| Localization | App-owned runtime + JSON catalogs (`src/lib/i18n.ts`, `src/lib/locales/*.json`, `lara.yaml`) | English fallback + Lara CLI sync | +| MCP | @modelcontextprotocol/sdk | 1.0 | +| Tests | Vitest (unit), Playwright (E2E/smoke), cargo test (Rust) | - | +| Package manager | pnpm | - | + +## System Overview + +```mermaid +flowchart TD + subgraph TW["Tauri v2 Window"] + subgraph FE["React Frontend"] + App["App.tsx (orchestrator)"] + WS["WelcomeScreen\n(onboarding)"] + SB["Sidebar\n(navigation + filters + types)"] + NL["NoteList / PulseView\n(filtered list / activity)"] + ED["Editor\n(BlockNote + diff + raw)"] + IN["Right Panel\n(Inspector + TOC)"] + AIW["AiWorkspace\n(docked or native window)"] + AIP["AiPanel\n(transcript + composer)"] + SP["SearchPanel\n(keyword search)"] + ST["StatusBar\n(vault picker + sync + version)"] + CP["CommandPalette\n(Cmd+K launcher)"] + + App --> WS & SB & NL & ED & AIW & SP & ST & CP + ED --> IN + AIW --> AIP + end + + subgraph RB["Rust Backend"] + LIB["lib.rs → Tauri commands"] + VAULT["vault/"] + FM["frontmatter/"] + GIT["git/\n(commit, sync, clone)"] + SETTINGS["settings.rs"] + SEARCH["search.rs"] + CLI["ai_agents.rs\n+ CLI adapters"] + end + + subgraph EXT["External Services"] + CCLI["Claude / Codex / Copilot / OpenCode / Pi / Antigravity / Kiro / Hermes CLI\n(agent subprocesses)"] + MCP["MCP Server\n(ws://9710, 9711)"] + GCLI["git CLI\n(native or selected WSL2 executable)"] + REMOTE["Git remotes\n(GitHub/GitLab/Gitea/etc.)"] + end + + FE -->|"Tauri IPC"| RB + CLI -->|"spawn subprocess"| CCLI + LIB -->|"register / monitor"| MCP + GIT -->|"clone / fetch / push / pull via GitLaunchConfig"| GCLI + GCLI -->|"network auth via user config"| REMOTE + end + + style FE fill:#e8f4fd,stroke:#2196f3,color:#000 + style RB fill:#fff8e1,stroke:#ff9800,color:#000 + style EXT fill:#f3e5f5,stroke:#9c27b0,color:#000 +``` + +## Four-Panel Layout + +``` +┌────────┬─────────────┬─────────────────────────┬────────────┐ +│Sidebar │ Note List │ Editor │ Right Panel│ +│(250px) │ (300px) │ (flex-1) │ (280px) │ +│ │ OR │ │ OR │ +│ All │ Pulse View │ [Breadcrumb Bar] │ TOC │ +│ Changes│ │ │ OR │ +│ Pulse │ [Search] │ # My Note │ Properties │ +│ Inbox │ [Sort/Filt] │ │ │ +│ │ │ │ Context │ +│Projects│ Note 1 │ Content here... │ Messages │ +│Experim.│ Note 2 │ (BlockNote or Raw) │ Actions │ +│Respons.│ Note 3 │ │ Input │ +│People │ ... │ │ │ +│Events │ │ │ │ +│Topics │ │ │ │ +├────────┴─────────────┴─────────────────────────┴────────────┤ +│ StatusBar: v0.4.2 │ main │ Synced 2m ago │ Vault: ~/Laputa │ +└──────────────────────────────────────────────────────────────┘ +``` + +- **Sidebar** (220-400px, resizable): Top-level filters (All Notes, Changes, Pulse), saved Views, collapsible type-based section groups, and a dedicated folder tree. The folder tree starts with a vault-root row labeled from the opened vault path, shows root-level files when selected, and nests user-created folders plus default vault folders such as `attachments/` and `views/` underneath it; only the dedicated `type/` directory stays hidden because note types already have their own sidebar section. Saved Views persist a top-level YAML `order` field in each view file and use the same ordered-list mental model as Types for single-vault lists: pointer users can drag the existing view row, double-click to rename it, or right-click for edit/rename/appearance/delete actions, while keyboard users can use the row context key for the same menu and command-palette move actions for ordering. In multiple-vault mode, saved View rows are keyed by source vault plus filename so duplicate filenames do not collide, and edits/deletes route to the owning vault. The folder tree supports inline folder creation and rename, exposes a right-click menu for rename/delete plus filesystem reveal/copy-path actions on mutable folders, and auto-expands ancestor folders when the current selection or rename target is nested. Folder creation sends the selected folder's vault-relative path and mounted root to `create_vault_folder`, so a new folder is created under the focused parent instead of defaulting to the active vault root. Type sections and folder rows also act as note drop targets: dropping a note on a type updates its `type:` frontmatter, while dropping it on a folder runs the same crash-safe move path as the command palette flow. Each type can have a custom icon, color, sort, and visibility set via its `type: Type` document; new type documents created by Tolaria are written at the vault root. In mounted multi-vault graphs, duplicate type names still render as one sidebar section, but the visibility picker becomes a workspace matrix and writes visibility to the specific vault's Type document, so hidden type definitions suppress only notes of that type from the same workspace. +- **Note List / Pulse View** (220-500px, resizable): When a section group, filter, folder, saved view, or Neighborhood selection is active, the renderer first adapts that navigation state into a Collection (`src/collections/collectionFromSelection.ts`) and then resolves visible entries or relationship groups through `src/collections/resolveCollectionEntries.ts`. The only active collection presentation is currently `list`, so the pane still shows filtered notes with snippets, modified dates, status indicators, and per-context note-list controls. When `selection.kind === 'entity'`, the same pane enters **Neighborhood** mode: the source note is pinned at the top as a normal active row, outgoing relationship groups render first, inverse/backlink groups follow, empty groups stay visible with `0`, and duplicates across groups are allowed when multiple relationships are true. Plain click / `Enter` open the focused note without replacing the current Neighborhood, while Cmd/Ctrl-click and Cmd/Ctrl-`Enter` pivot the pane into the clicked note's Neighborhood. Inbox organization auto-advance is coordinated by `useInboxOrganizeAdvance`, which only opens the next visible Inbox note when the organized note is still the active requested tab after the write finishes. Folder-backed lists also show non-Markdown files: previewable media and PDF binaries get file indicators and open in the editor pane, while unsupported binaries remain muted instead of auto-launching an external app. Saved views reuse the same sort and visible-column controls as the built-in lists, and those changes persist back into the view `.yml` definition (`sort`, `listPropertiesDisplay`). The renderer normalizes those legacy list fields into `presentation: { type: "list" }` in memory so future saved-view presentation settings can share the same collection model without changing existing YAML. When Pulse filter is active, shows `PulseView` — a chronological git activity feed grouped by day. +- **Editor** (flex, fills remaining space): Single note open at a time (no tabs — see ADR-0003). Breadcrumb bar with filename controls, read-only legacy display-title context when a no-H1 note's title differs from its filename, word count, rich-editor width toggle, and the secondary-overflow Table of Contents action, BlockNote rich text editor with wikilink support, Markdown-compatible inline/display math rendering, first-class Mermaid diagram blocks, markdown-safe formatting controls, and schema-backed fenced code block highlighting via `@blocknote/code-block` plus lazy direct `@shikijs/langs` registrations for missing common grammars. Can toggle to diff view (modified files), raw CodeMirror view, or a wide rich-editor reading surface with preserved side margins; raw CodeMirror remains full-width and unaffected by note width mode. Raw CodeMirror chooses syntax highlighting from the file extension for Markdown, YAML, JSON, Python, SQL, JavaScript, and TypeScript files, while unknown text files stay plain. Inline rich-editor images open in a localized shadcn lightbox on double-click while normal single-click BlockNote selection remains untouched, and tiny tracking-style images are ignored. Binary image, audio, video, and PDF files render through `FilePreview` as ordinary vault files using Tauri asset URLs; editor-embedded audio and video use the same scoped asset sources through the CSP `media-src` allow-list. Linux AppImage builds ask the native runtime whether audio/video should fall back to external-open controls before mounting webview media elements. External-open actions call `open_vault_file_external` so the target is validated against the active vault before the native default app opens it. Unsupported/broken binaries show explicit fallback states and keyboard focus returns to the note list on `Escape`. Decomposed into `Editor` (orchestrator), `EditorContent`, `FilePreview`, `EditorRightPanel`, `TableOfContentsPanel`, `SingleEditorView`, with hooks `useDiffMode`, `useEditorFocus`, and `useEditorSave`, plus the `useRawMode`/`RawEditorView` pair for raw source editing. Rich BlockNote input and raw CodeMirror input both route typed `->`, `<-`, and `<->` through the shared `src/utils/arrowLigatures.ts` resolver so arrow ligatures stay consistent across mode switches while escaped ASCII sequences remain literal. Rich-editor Markdown input transforms for arrows, inline math, and `==highlight==` share one capture-phase `beforeinput` execution path in `src/components/richEditorInputTransform.ts` and are composed by `src/components/richEditorInputTransformExtension.ts`. Navigation history (Cmd+[/]) replaces tabs. + Rich-editor Markdown output is serialized through `src/utils/richEditorMarkdown.ts`, which restores Tolaria's durable Markdown tokens, tries the installed direct BlockNote block serializer, and falls back to BlockNote's Markdown exporter for unsupported shapes. + Rich-editor copy uses BlockNote's external HTML serializer for selected note content so tables, lists, checklists, and inline formatting paste richly into other apps, with Tolaria keeping fenced-code selections as raw code text and normalized plain text on the clipboard. + Rich-editor block selection keeps ProseMirror plugin state, decorations, and key dispatch in `src/components/richEditorBlockSelectionExtension.ts`, while document traversal/collapsed-content operation IDs live in `src/components/richEditorBlockSelectionDocument.ts` and block clipboard serialization/parsing lives in `src/components/richEditorBlockSelectionClipboard.ts`. + Tolaria's BlockNote side menu keeps UI composition in `tolariaBlockNoteSideMenu.tsx`, while collapsed-section rendering and ellipsis hit-testing live in `tolariaCollapsedSections.ts`, pointer block reordering lives in `tolariaBlockReorder.ts`, measured side-menu positioning lives in `tolariaSideMenuAlignment.ts`, and stale-block lookup helpers live in `tolariaSideMenuBlocks.ts`. + Note PDF export stays renderer-owned for layout: `useEditorPdfExport` exits diff/raw views, applies a print-only stylesheet to the rendered note root, and checks the native PDF capability before choosing a platform path. On macOS, the renderer asks for a filesystem PDF destination before the Tauri `export_current_webview_pdf` command saves the current `WKWebView` print operation directly; on Windows/Linux Tauri builds and in browser mode, the same export action falls back to the native/browser print dialog. The export reuses rendered BlockNote output so frontmatter is omitted, while math, images, Mermaid diagrams, tldraw blocks, code, tables, and links degrade through their existing DOM rather than a second Markdown-to-PDF renderer, and the source Markdown is never modified. Markdown notes expose the same export action from Cmd+K, the native Note menu, the breadcrumb overflow menu, and each Markdown row's note-list context menu. +- **Right side panels** (200-500px or hidden): Properties and Table of Contents are mutually exclusive panels mounted by `EditorRightPanel` and coordinated by `useRightPanelExclusion`. Properties shows frontmatter, relationships, instances, backlinks, and git history; Table of Contents is lazy-mounted only while open, derives a title-rooted H1/H2/H3 hierarchy through a debounced Web Worker per ADR-0109, and reuses folder-tree indentation/guide geometry with heading icons while resolving live BlockNote block IDs at click time for navigation. The breadcrumb bar toggles Table of Contents and Properties actions. Per-note `icon` is a suggested Properties field and the command palette's "Set Note Icon" action opens that field directly. When viewing a Type note, Properties shows an **Instances** section listing all notes of that type (sorted by modified_at desc, capped at 50). + +- **AI workspace** (docked panel or native window): `AiWorkspace` owns the multi-chat orchestration, sidebar tabs, installed-only target picker, permission-mode picker, and dock/pop-out controls. Header/guidance chrome lives in `AiWorkspaceChrome`, edge-resize handles in `AiWorkspaceResizeHandles`, conversation metadata/settings persistence in `aiWorkspaceConversations`, and sizing/class/style helpers in `aiWorkspaceSizing`. The status-bar AI affordance opens this workspace instead of changing the default target inline. Docked workspace mode renders as a compact bounded desktop tool inside the main app; users resize the anchored panel from its left/top edges and resize the chat-list sidebar separately from the transcript area. Pop-out mode opens a dedicated undecorated transparent Tauri webview window labeled `ai-workspace` and boots the lightweight `AiWorkspaceWindowApp` route instead of the full vault shell. The chat header and sidebar header are draggable in native-window mode; closing the pop-out only closes that window, while the dock control emits a dock request back to the main window before closing the pop-out. Chat sessions reuse `AiPanelView` for transcript/composer rendering with the old panel header disabled; target and permission controls live in the composer toolbar so there is one workspace header per active chat. AI workspace cross-window localStorage, BroadcastChannel, storage-event, and subscriber-set plumbing is owned by `createCrossWindowPersistedStore`; domain stores keep only sanitization, mutation, and native persistence behavior. + +Panels are separated by `ResizeHandle` components that support drag-to-resize. `useLayoutPanels` clamps the sidebar, note-list, and inspector widths before applying them, keeps the side panes from flex-shrinking below their protected widths, and persists the last chosen widths in installation-local localStorage under `tolaria:layout-panels`. + +The main Tauri window derives its minimum width from the visible panes instead of a single fixed floor. `useMainWindowSizeConstraints` treats the editor-only shell as the 480px baseline, adds the current sidebar / note-list / expanded-inspector widths on top with minimum floors, and calls the native `update_current_window_min_size` command whenever view mode, inspector visibility, or restored pane widths change. That same native command can grow the current window back out when a wider pane combination is restored, but Windows keeps grow-to-fit disabled and skips min-size mutation while fullscreen or maximized so navigation/sidebar interactions do not unfullscreen or reposition the main window. Note windows skip this path and keep their dedicated 800×700 initial sizing. + +The main Tauri window also persists its last normal size and screen position in the app config directory as `window-state.json`. The state stores logical window points, while `window_state.rs` migrates older physical-pixel state on read so Retina and non-Retina launches restore the same user-facing bounds. On startup, the restored frame applies only to the main window and clamps to the currently available monitor work areas, so stale coordinates from a disconnected display fall back to a visible placement. Maximized, fullscreen, minimized, and detached note-window frames are not written as the restore baseline. + +Tauri setup keeps launch-time filesystem and subprocess work off the window creation critical path. Legacy `~/Laputa` housekeeping and the initial persisted-vault MCP bridge sync run on named background threads, so large legacy vaults, stale active-vault paths, or slow process startup cannot beachball the macOS app before React mounts. React still resyncs the bridge from `useVaultSwitcher` after the persisted selection loads, and no selected vault stops the bridge. AI-agent CLI availability probing is also off the first-paint path: the renderer defers `get_ai_agents_status` until an idle/timeout tick, skips it for disabled AI surfaces and secondary windows, falls back to missing-agent onboarding state if the status IPC does not settle promptly, and the Rust command fans per-agent CLI checks across Tokio's blocking pool with per-agent timeouts. The HTML bootstrap also installs a Tauri-only one-shot watchdog: React reports readiness from an effect after the root commits, and if that readiness signal never arrives the WebView reloads once instead of leaving macOS users in an inert rendered shell. + +Desktop startup registers `tauri-plugin-deep-link` and `tauri-plugin-single-instance` before setup so `tolaria://` links can focus the existing main window and deliver URL events to the renderer. `tauri.conf.json` declares the `tolaria` scheme for bundled desktop builds; Windows and Linux also run `register_all()` as a runtime repair path, while macOS relies on bundle registration. + +Linux and Windows use custom React-rendered window chrome instead of the native Tauri menu bar. `setup_custom_window_chrome()` drops server-side decorations on the main window, `openNoteInNewWindow()` does the same for detached note windows, and `LinuxTitlebar`/`LinuxMenuButton` route both window controls and menu actions back through the same shared command pipeline that the desktop native menus use. The native app menu is macOS-only so Services/Hide/Quit and the reserved `WINDOW_SUBMENU_ID` keep behaving like normal NSApp menu items, while cross-platform custom items such as Check for Updates emit Tolaria command IDs with visible updater feedback from the renderer menu. +On Linux, `run()` applies WebKitGTK startup safeguards before Tauri creates the webview. Native Wayland launches and AppImage launches inject `WEBKIT_DISABLE_DMABUF_RENDERER=1` and `WEBKIT_DISABLE_COMPOSITING_MODE=1` independently unless the user already set either variable, covering compositor-specific WebKit crashes without changing native X11 launches. AppImage launches keep the additional AppImage-only safeguards: on Wayland sessions Tolaria re-execs once with the first architecture-matching system `libwayland-client.so` in `LD_PRELOAD` when the user has not provided their own preload. The candidate order prefers Fedora-style `lib64` and Debian-style `x86_64-linux-gnu` paths before generic `/usr/lib`, and the ELF header is checked so a 64-bit Tolaria process does not retry with a 32-bit Wayland client library. Runtime startup writes a mount-path-specific `GTK_IM_MODULE_FILE` cache when fcitx is configured via `GTK_IM_MODULE=fcitx` or common fcitx environment hints; release packaging currently uses Tauri's stock linuxdeploy AppImage output plugin instead of Tolaria's experimental output-plugin shim. If the user has not already chosen `GTK_IM_MODULE`, Tolaria sets `GTK_IM_MODULE=fcitx` before WebKit starts. The same AppImage path checks whether `fc-match` resolves the default emoji font to `Noto-COLRv1.ttf`; when the user has not provided `FONTCONFIG_FILE` or `FONTCONFIG_PATH`, Tolaria writes a cache-local fontconfig file that rejects only that matched font file and exports it before WebKit starts. The rendering overrides keep WebViews from blanking or crashing after accelerated compositing/DMA-BUF failures, the re-exec addresses AppImage library-order failures that can surface as `Could not create default EGL display: EGL_BAD_PARAMETER`, and the fontconfig guard avoids known WebKit crashes in COLRv1 emoji font rendering while leaving other emoji fonts available. + +## Multi-Window (Note Windows) + +Notes can be opened in separate Tauri windows for focused editing. Secondary windows boot the same `App` shell and load the same active workspace graph as the main window, but they start in the editor-only view mode with side panels collapsed. + +The AI workspace can also open in a separate Tauri window through `openAiWorkspaceWindow()`. That window uses `?window=ai-workspace` to boot the lightweight `AiWorkspaceWindowApp` route, receives the active vault context through URL params, opts out of main-window size constraints and startup AI onboarding, and redocks by emitting `tolaria:ai-workspace-dock-requested` to the main window. Its webview is transparent so the rounded workspace shell defines the visible floating-window corners across desktop platforms. + +**Triggers:** +- `Cmd+Shift+Click` on any note in the note list or sidebar +- `Cmd+K` → "Open in New Window" (command palette, requires active note) +- `Cmd+Shift+O` keyboard shortcut +- Note → "Open in New Window" menu bar item + +**Architecture:** +- `openNoteInNewWindow()` (`src/utils/openNoteWindow.ts`) creates a new `WebviewWindow` via the Tauri v2 JS API with URL query params (`?window=note&path=...&vault=...&title=...`) +- `main.tsx` always mounts `App`; `App` checks `isNoteWindow()` at startup, keeps normal vault/workspace loading active, and `useNoteWindowLifecycle` opens the requested note after the app graph is ready +- Each window has its own auto-save via `useEditorSaveWithLinks` (same 1.5s low-end-safe idle debounce, same Rust `save_note_content` command), and raw-editor typing also derives frontmatter-backed `VaultEntry` state in the renderer so Inspector and note-list surfaces react immediately without waiting for a full reload +- Secondary windows are sized 800×700; macOS keeps the overlay title bar, while Linux mounts the shared React titlebar on undecorated windows +- Capabilities config (`src-tauri/capabilities/default.json`) grants permissions to both `main` and `note-*` window labels + +## AI System + +### AI Agent (AiPanel) + +Full agent mode — spawns the selected local CLI agent as a subprocess with tool access and MCP vault integration. + +1. **Frontend** (`AiPanel` + `useCliAiAgent` + `aiAgentSession.ts` + `aiAgents.ts` + `aiTargets.ts`) — one normalized session lifecycle for message state, reasoning blocks, tool action cards, response display, onboarding, default-target selection, bundled-docs prompt injection, and the per-vault Safe / Power User permission mode shown in the panel header for coding agents +2. **Backend orchestration** (`ai_agents.rs`) — normalizes agent availability, streaming, and the request permission mode before dispatching to per-agent adapters +3. **Shared runtime scaffold** (`cli_agent_runtime.rs` and submodules) — owns the common request shape, prompt wrapping, JSON-line and line-oriented subprocess lifecycle, stdout/stderr/stdin plumbing, normalized error/done handling, version probing, Tolaria stdio MCP server entry generation, and MCP server path resolution used by app-managed CLI agents +4. **Agent adapters** — Shared prompts are mode-aware on every turn, including turns with note context snapshots: Vault Safe tells agents not to use or advertise shell, while Power User tells shell-capable agents to keep local commands scoped to the active vault. Claude Code still uses `claude_cli.rs` with `acceptEdits`, strict Tolaria MCP config, and a scoped tool list: Safe enables file/search/edit tools only, while Power User adds Bash to the available tools and pre-approves Bash with `--allowedTools` without using dangerous permission-bypass flags. Codex runtime specifics live in `codex_cli.rs`; Safe runs `codex --sandbox read-only --ask-for-approval untrusted exec --json`, while Power User runs `codex --sandbox workspace-write --ask-for-approval never exec --json` so shell execution stays enabled across repeated turns. GitHub Copilot runs through `copilot -p -s --no-ask-user` from the active vault, streams line-oriented stdout, passes Tolaria MCP through `--additional-mcp-config`, uses Safe mode with only write and Tolaria MCP tools preapproved, and maps Power User to `--allow-all-tools` without `--allow-all`, `--yolo`, or global path/URL bypasses. OpenCode runs through `opencode run --format json` with transient permissions: Safe denies bash and external directories, while Power User allows bash but still denies external directories. Pi runs through `pi --mode json --no-session` with `npm:pi-mcp-adapter`; both modes currently share the same transient MCP config and the prompt does not promise shell for Pi Power User. Antigravity runs through `agy -p --add-dir `, streams line-oriented stdout, and writes Tolaria MCP into the active vault's `.agents/mcp_config.json`; Safe uses `--sandbox=true --toolPermission=proceed-in-sandbox`, while Power User uses `--sandbox=false --toolPermission=always-proceed` without `--dangerously-skip-permissions`. Kiro runs through `kiro-cli chat --no-interactive --trust-all-tools`, streams line-oriented stdout, drains stderr concurrently, and writes prompt content through stdin to avoid OS argument length limits. Hermes Agent runs through `hermes chat --quiet --source tolaria -q`, streams line-oriented stdout, and uses the user's existing Hermes profile/configuration without mutating `~/.hermes/config.yaml`; setup errors point users to `hermes setup`, `hermes model`, and `hermes doctor`. Codex, GitHub Copilot, OpenCode, Pi, Antigravity, Kiro, and Hermes all launch from the active vault cwd; Codex, GitHub Copilot, OpenCode, Pi, Antigravity, and Kiro receive transient Tolaria MCP config. Pi seeds its transient agent directory from the user's Pi agent directory before merging Tolaria MCP, so app-managed runs keep standalone Pi provider/auth settings. All app-launched paths use hidden Windows launches and avoid dangerous permission-bypass flags. +5. **MCP Integration** — Claude receives the generated MCP config file path, Codex receives the same Tolaria MCP server via transient `-c mcp_servers.tolaria.*` config overrides using Tolaria's resolved Node path plus `VAULT_PATH` and `WS_UI_PORT`, GitHub Copilot receives an inline session MCP config through `--additional-mcp-config`, OpenCode receives it through `OPENCODE_CONFIG_CONTENT`, Pi receives it through a temporary `PI_CODING_AGENT_DIR/mcp.json` consumed by `pi-mcp-adapter` after copying and merging the user's Pi agent config, Antigravity receives it through `.agents/mcp_config.json` in the active vault, and Kiro receives it through `.kiro/settings/mcp.json` in the active vault. Hermes uses the user's own Hermes MCP/profile configuration; Tolaria does not rewrite third-party Hermes config files. + +Each `stream_ai_agent` call also receives a request-scoped `ai-agent-stream-*` event name. Desktop runs bind that scoped name to any spawned CLI child through `ai_agent_processes.rs`, and `abort_ai_agent_stream` validates the same scoped name before killing the registered child. Renderer stop controls therefore cancel the local subprocess instead of only ignoring stale events, while natural completion and repeated stop requests remain no-ops. + +CLI-agent availability intentionally does not depend only on the desktop app's inherited `PATH`. The detectors check the current process path, the user's login shell, and supported local/toolchain install locations such as native `~/.local/bin`, local `~/.claude/local`, Mise/asdf shims, nvm-managed Node installs, npm-global, Homebrew, Windows `%APPDATA%\npm`/pnpm/Scoop shims, Windows `.exe` launchers, and the macOS Codex app resource path so first-run onboarding works on fresh macOS and Windows installs. App-managed CLI spawns also expand the active vault path before using it as the subprocess working directory, then extend the child process `PATH` with the resolved binary directory plus those common toolchain directories, which lets GUI-launched macOS sessions run Homebrew/npm shims and their `node`-backed MCP subprocesses even when Finder/Dock did not inherit a terminal shell path. Claude Code launches copy a narrow set of exported provider/auth environment variables from the app process or the user's zsh/bash startup files, including Anthropic API/base URL values. OpenCode launches do the same for common provider variables and any `{env:NAME}` placeholders found in OpenCode config files, so company proxy and API-key setups that work in Terminal also work when Tolaria is opened from Finder or Dock. Windows npm `.cmd` shims are not spawned directly; the shared CLI runtime resolves them to their quoted Node script or native executable target first so prompt arguments do not hit batch-file argument validation. + +CLI-agent system prompts also include a local Tolaria docs orientation when the bundled docs resource is present. `scripts/build-agent-docs.mjs` generates `src-tauri/resources/agent-docs/` from the public VitePress Markdown sources, including `index.md`, `AGENTS.md`, per-section bundles, `all.md`, `search-index.json`, and generated per-page files. Tauri bundles that folder as `agent-docs/`; `get_agent_docs_path` resolves the installed resource path, with a repository fallback for development, and `getAgentDocsPath()` caches it before each agent run. Agents are instructed to read the active vault's `AGENTS.md` for local conventions and search the bundled docs for Tolaria product behavior. + +Renderer-facing CLI setup errors can carry a `tolaria:i18n-error:` JSON marker with a translation key and primitive interpolation values. `localizedStreamError.ts` resolves that marker through the active app locale before the AI panel appends stream failure text, so native adapters such as Pi can keep CLI diagnostics in Rust while user-facing setup copy remains in the JSON catalogs. + +#### Agent Event Flow + +```mermaid +sequenceDiagram + participant U as User (AiPanel) + participant FE as useCliAiAgent (Frontend) + participant R as ai_agents.rs (Rust) + participant C as Selected CLI Agent + participant V as Vault (MCP) + + U->>FE: sendMessage(text, references) + FE->>FE: buildContextSnapshot(activeNote, linkedNotes, openTabs) + FE->>R: invoke('stream_ai_agent', {agent, message, systemPrompt, vaultPath, permissionMode, eventName}) + R->>R: pick adapter for claude_code, codex, opencode, pi, antigravity, kiro, or hermes + R->>C: spawn agent with MCP-enabled config + R->>R: register child under scoped eventName + + loop Normalized stream + C-->>R: Claude NDJSON, Codex JSONL, Copilot text, OpenCode JSON, Pi JSON, Antigravity text, Kiro text, or Hermes text events + R-->>FE: emit(eventName, event) + alt TextDelta + FE->>FE: accumulate response (revealed on Done) + else ThinkingDelta + FE->>FE: show reasoning block (collapses on first text) + else ToolStart + FE->>FE: add AiActionCard with spinner + else ToolDone + FE->>FE: update card with output + else Done + FE->>FE: reveal full response + FE->>FE: detect file operations → reload vault if needed + end + end + + opt User stops response + U->>FE: click stop + FE->>R: invoke('abort_ai_agent_stream', {eventName}) + R->>C: kill registered child if still running + FE->>FE: mark response stopped and return composer to idle + end + + C->>V: MCP tool calls (search_notes, read_note, edit_note…) + V-->>C: tool results +``` + +#### File Operation Detection + +When the agent writes or edits vault files, `aiAgentFileOperations.ts` detects this from normalized tool inputs and calls `onFileCreated` or `onFileModified` callbacks to trigger vault reload. Unrecognized write-like operations fall back to a full vault refresh. + +### Context Building + +The agent panel (`ai-context.ts`) builds a structured JSON snapshot from the active note and linked entries: + +```json +{ + "activeNote": { "path", "title", "type", "frontmatter", "body", "wordCount", "bodyTruncated?" }, + "openTabs": [{ "path", "title", "type", "frontmatter" }], + "noteList": [{ "path", "title", "type" }], + "vault": { "types", "totalNotes" }, + "referencedNotes": [{ "title", "path", "type" }] +} +``` + +Large active notes are compacted into a head/tail body snapshot before they enter the CLI prompt. The snapshot records `bodyTruncated` metadata and instructs agents to call `get_note(path)` before content-sensitive edits or summaries, keeping lower-context OpenCode providers from failing on oversized active-note context while preserving access to the full note through MCP. + +### Direct Model Targets + +Tolaria also supports direct model targets for local servers and API providers. These targets are stored as app-level provider metadata and can be selected in Settings or the status bar alongside coding agents. `src/shared/aiModelProviderCatalog.json` is the shared source for provider defaults, local/API grouping, API-key environment placeholders, and runtime fallback base URLs; the renderer imports it through `aiTargets.ts`, and Tauri includes the same JSON in `ai_models.rs`. Direct model targets run in Chat mode: they receive the same note-context snapshot and conversation history, but they do not receive shell access. OpenAI-compatible direct targets can use Tolaria's narrow native `create_note` tool when an active vault is loaded; the tool calls the same create-only, active-vault-bounded note write command as the UI and emits tool events so the renderer refreshes and opens the created note. The backend `stream_ai_model` command supports OpenAI-compatible chat completions and Anthropic Messages-compatible calls, including Ollama, LM Studio, OpenRouter, OpenAI, Anthropic, Gemini, and custom compatible endpoints. + +Provider secrets are not written to `settings.json`. Hosted API targets can use Tolaria's local app-data secrets file (`ai-provider-secrets.json`, outside vaults/worktrees and owner-only on Unix) or reference an environment variable name. Env-backed provider keys are resolved from the app process first, then from exported values in the user's zsh/bash startup files on Unix so GUI-launched sessions can still use shell-managed secrets. Local endpoints can omit authentication. + +### Authentication + +Each CLI agent authenticates itself outside Tolaria. Claude Code uses its existing CLI login or user-managed Anthropic/provider environment variables; Codex surfaces a friendly prompt to run `codex login` when needed; GitHub Copilot surfaces a friendly prompt to run `copilot login`, open `copilot` in the vault, and trust the directory when needed; OpenCode surfaces a friendly prompt to run `opencode auth login` or configure a provider when needed; Pi surfaces a friendly prompt to run `pi /login` or configure a provider API key when needed. Tolaria does not store model-provider API keys in app settings; direct provider secrets stay in local app data or user-managed environment variables. App-managed Pi sessions copy that local Pi agent config into a per-run temporary directory before adding Tolaria MCP, so Tolaria does not overwrite global Pi files and does not drop a working standalone Pi setup. + +## MCP Server + +The MCP server (`mcp-server/`) exposes vault operations as tools for AI assistants (Claude Code, Antigravity CLI, Cursor, or any MCP-compatible client). +The stdio entrypoint and desktop WebSocket bridge share `mcp-server/tool-service.js` for mounted-vault resolution, note lookup/search, note creation defaults, vault listing, and UI action intents; `index.js` and `ws-bridge.js` only adapt those semantics to their transport-specific request and response shapes. + +### Tool Surface + +| Tool | Params | Description | +|------|--------|-------------| +| `open_note` | `path` | Open and read a note by relative path | +| `read_note` | `path` | Read note content (alias for `open_note`) | +| `create_note` | `path, content, [title], [type], [vaultPath]` | Create a new markdown note inside an active vault without overwriting existing files | +| `search_notes` | `query, [limit]` | Search notes by title or content substring | +| `list_vaults` | — | List active mounted vaults and whether each has root `AGENTS.md` instructions | +| `append_to_note` | `path, text` | Append text to end of existing note | +| `edit_note_frontmatter` | `path, patch` | Merge key-value patch into YAML frontmatter | +| `delete_note` | `path` | Delete a note file from the vault | +| `link_notes` | `source_path, property, target_title` | Add a target to an array property in frontmatter | +| `list_notes` | `[type_filter], [sort]` | List all notes, optionally filtered by type | +| `vault_context` / `get_vault_context` | `[vaultPath]` | Get mounted-vault summary: entity types, folders, recent notes, and root `AGENTS.md` instructions | +| `ui_open_note` | `path` | Open a note in the Tolaria UI editor | +| `ui_open_tab` | `path` | Open a note in a new UI tab | +| `ui_highlight` | `element, [path]` | Highlight a UI element (editor, tab, properties, notelist) | +| `ui_set_filter` | `type` | Set the sidebar filter to a specific type | + +### Transports + +- **stdio** — standard MCP transport for Claude Code / Cursor (`node mcp-server/index.js`) +- **WebSocket** — live bridge for Tolaria app integration: + - Port **9710**: Tool bridge — AI/Claude clients call vault tools here + - Port **9711**: UI bridge — Frontend listens for UI action broadcasts from MCP tools + +### Explicit External Tool Setup + +Tolaria can register itself as an MCP server in: +- `~/.claude.json` and `~/.claude/mcp.json` (Claude Code compatibility across current CLI and legacy MCP-file setups) +- `~/.gemini/config/mcp_config.json` (Antigravity CLI) +- `~/.cursor/mcp.json` (Cursor) +- `~/.config/mcp/mcp.json` (generic MCP-compatible clients) +- `~/.config/opencode/opencode.json` (OpenCode, using its `mcp` config key) + +That setup is user-initiated through the status bar / command palette flow, not a startup side effect. Registration is non-destructive (additive, preserves other servers and Antigravity/OpenCode settings), uses `upsert` semantics, and can be reversed by removing Tolaria's entry again. Tolaria resolves an MCP runtime (Node.js 18+ preferred, Bun 1+ as fallback) before writing config so external clients are not left pointing at a missing binary, writes a vault-neutral `type: "stdio"` entry for standard MCP clients, writes OpenCode's vault-neutral `type: "local"` entry, and sets `WS_UI_PORT=9711` so UI actions route back to the desktop app. Durable and transient client-facing MCP entries convert Windows `mcp-server/index.js` paths from Rust's extended-length `\\?\` spelling back to normal drive or UNC spelling before passing the script argument to Node. Durable external MCP processes resolve active workspaces at tool-call time: explicit `VAULT_PATH`/`VAULT_PATHS` env still wins for app-owned and legacy launches, otherwise the MCP server reads Tolaria's `vaults.json`, uses `active_vault` first, and includes every workspace not marked `mounted: false`. Vault context checks each active workspace root for `AGENTS.md` and includes those instructions in the returned context. The generated standard entry is exposed as an `mcpServers` manual JSON snippet, while OpenCode gets a separate top-level `mcp` snippet with a `command` array through `get_opencode_mcp_config_snippet`; both appear in the MCP setup dialog and copy through the native clipboard path. In the desktop app, `useMcpStatus` copies those snippets through the native `copy_text_to_clipboard` command instead of the Web Clipboard API so macOS WKWebView permission policy cannot block setup. Packaged builds resolve `mcp-server/` from the installed resource directory next to the executable before falling back to macOS `Resources`, Linux package roots such as `/usr/local/Tolaria`, `/usr/lib/tolaria`, and `/usr/lib/tolaria/resources`, and AppImage paths. Linux AppImage startup extracts the bundled `mcp-server/` to `~/.local/share/tolaria/mcp-server/` with a `.tolaria-version` marker, so durable external registrations use a stable path instead of the changing AppImage mount point. The `useMcpStatus` hook tracks whether Tolaria's durable MCP entry is connected (`checking | installed | not_installed`) and owns connect, disconnect, exact-snippet load, and copy-to-clipboard actions. Antigravity CLI still owns its own install and sign-in; Tolaria writes the durable external MCP entry only on explicit setup, while app-managed Antigravity sessions use workspace MCP config and optional vault guidance. The desktop WebSocket bridge is started only when a persisted active vault exists and is resynced from React state on vault changes; no selected vault stops the bridge instead of falling back to `~/Laputa`. Stdio MCP server processes are owned by the external client that launched them: when that client closes stdin, Tolaria cancels UI-bridge reconnect timers, closes any UI WebSocket, and exits the runtime process instead of keeping it alive in the background. + +### Architecture + +```mermaid +flowchart TD + subgraph MCP["MCP Server (Node.js or Bun) — mounted-workspace scoped"] + IDX["index.js"] + SVC["tool-service.js\n(vault resolution, note operations,\nUI action intents)"] + VAULT["vault.js\n(findMarkdownFiles, readNote, createNote,\nsearchNotes, appendToNote, editNoteFrontmatter,\ndeleteNote, linkNotes, listNotes, vaultContext)"] + WSB["ws-bridge.js"] + + IDX -->|"stdio transport"| STDIO["Claude Code / Cursor / Antigravity / OpenCode"] + IDX --> SVC + SVC --> VAULT + IDX -.->|"UI action WebSocket client"| WSB + WSB --> SVC + WSB -->|"port 9710 — tool bridge"| AI["AI Clients\n(Claude Code, external)"] + WSB -->|"port 9711 — UI bridge"| FE["Frontend\n(useAiActivity)"] + end + + TAURI["Tauri bridge lifecycle"] -->|"start/stop/restart with VAULT_PATHS"| MCP + UI["Status bar / Command Palette"] -->|"explicit setup or disconnect"| CFG["~/.claude.json\n~/.claude/mcp.json\n~/.cursor/mcp.json\n~/.config/mcp/mcp.json\n~/.config/opencode/opencode.json"] +``` + +### WebSocket Bridge + +```mermaid +flowchart LR + FE["Frontend\n(useMcpBridge)"] <-->|"ws://localhost:9710"| WSB["ws-bridge.js"] + WSB <--> VAULT["vault.js"] + STDIO["MCP stdio tools"] <-->|"ws://localhost:9711"| FE2["Frontend UI actions\n(useAiActivity)"] +``` + +**Tool bridge protocol** (port 9710): +- Request: `{ "id": "req-1", "tool": "search_notes", "args": { "query": "test" } }` +- Response: `{ "id": "req-1", "result": { ... } }` + +**UI bridge protocol** (port 9711): +- Broadcast: `{ "type": "ui_action", "action": "open_note", "path": "..." }` +- `useAiActivity` hook receives these and applies them (highlight with 800ms feedback, open note, set filter, etc.) + +### Rust MCP Module + +`src-tauri/src/mcp.rs` manages the MCP server lifecycle: + +| Function | Purpose | +|----------|---------| +| `spawn_ws_bridge(vault_path)` | Spawns `ws-bridge.js` as child process with `VAULT_PATH`/`VAULT_PATHS` env | +| `sync_mcp_bridge_vault(vault_path?)` | Starts, restarts, or stops the desktop WebSocket bridge as the selected vault changes | +| `extract_mcp_server_to_stable_dir(app_version)` | On Linux AppImage launches, copies bundled MCP files to `~/.local/share/tolaria/mcp-server/` with version-gated replacement so external clients can keep a stable `index.js` path | +| `register_mcp(vault_path)` | Resolves an MCP runtime (Node.js 18+ preferred, Bun 1+ fallback), resolves the packaged or stable extracted `mcp-server/`, and writes Tolaria's vault-neutral entry to Claude Code, Antigravity CLI, Cursor, OpenCode, and generic MCP configs on user request | +| `mcp_config_snippet(vault_path)` | Builds the exact vault-neutral `mcpServers.tolaria` JSON users can copy into any compatible client without writing third-party config files | +| `remove_mcp()` | Removes Tolaria's MCP entry from Claude Code, Antigravity CLI, Cursor, OpenCode, and generic MCP configs | +| `upsert_mcp_config(path, entry)` | Atomic config file update (create/merge, preserves others) | + +The `WsBridgeChild` state wrapper in `lib.rs` ensures the bridge process is replaced on vault switches, stopped when no active vault is selected, and killed plus waited on app exit via the `RunEvent::Exit` handler. The same desktop layer keeps Tauri asset protocol access limited to vault roots loaded during the current app session; command calls remain active-vault scoped for reads, writes, and external opens. + +## Search + +Search is keyword-based, using `walkdir` to scan all `.md` files in the vault directory. No external binary or indexing step required. + +- Matches query against file titles and content (case-insensitive) +- Scores results: title matches ranked higher than content-only matches +- Extracts contextual snippets around the first match +- Skips hidden files + +The `search_vault` Tauri command runs the scan in a blocking Tokio task and returns results sorted by relevance score. + +The note-list search field combines client-side scoped filtering with that same command: title, snippet, and visible-property matches resolve immediately, while backend body-content hits use `search_vault` with frontmatter excluded before adding matching paths for the currently visible workspace roots without displaying matched body text in the note row. + +## Vault Cache System + +The vault cache (`src-tauri/src/vault/cache.rs`) accelerates vault scanning using git-based incremental updates. + +### Cache File + +`~/.laputa/cache/.json` — stored outside the vault directory so it never pollutes the user's git repo. The vault path is normalized through `vault/path_identity.rs` before hashing, so macOS `/tmp` aliases and separator variants share the same cache identity. Stores: vault path, git HEAD commit hash, all VaultEntry objects. Version: v14 (bumped on VaultEntry field changes to force full rescan). Cache replacement is best-effort: Tolaria writes a temp file, fsyncs it, and renames it into place only after a short-lived writer lock plus an on-disk fingerprint check confirm another window/process has not already refreshed the cache. Failures are logged and the app falls back to rebuilding from the filesystem. + +`/.tolaria-rename-txn/` — hidden, scan-ignored staging directory for crash-safe note renames. Tolaria stores temporary backup files plus one manifest per in-flight rename here. On the next vault scan, unfinished transactions are recovered before entries are listed so users do not see a missing note or a visible duplicate after a crash. + +### Three Cache Strategies + +```mermaid +flowchart TD + A([scan_vault_cached]) --> B{Cache exists\nand valid?} + B -->|No / Corrupt| C["🔴 Full Scan\nwalkdir all .md files\n→ full parse"] + B -->|Yes| D{Git HEAD\nmatches cache?} + D -->|Same commit| E["🟢 Cache Hit\ngit status --porcelain\n→ re-parse only uncommitted changes"] + D -->|Different commit| F["🟡 Incremental Update\ngit diff old..new --name-only\n→ selective re-parse of changed files"] + + C --> G[Replace cache if unchanged\nwriter lock + temp file → rename] + E --> G + F --> G + G --> H([VaultEntry list ready]) +``` + +## Styling + +The app uses internal app-owned light and dark themes with an optional System preference (see [ADR-0081](adr/0081-internal-light-dark-theme-runtime.md) and [ADR-0112](adr/0112-system-theme-mode.md)). This is not the old vault-authored theming system from ADR-0013: users choose a mode, but themes are owned by the app. + +1. **Global CSS variables** (`src/index.css`): Semantic app colors, borders, surfaces, and interaction states. Bridged to Tailwind v4 via `@theme inline`. +2. **Editor theme** (`src/theme.json`): BlockNote-specific typography. Flattened to CSS vars by `useEditorTheme`; editor colors resolve through the same semantic app variables. +3. **Theme runtime**: Applies resolved `light` / `dark` values to `data-theme` and the shadcn-compatible `.dark` class before React consumers render, with a localStorage mirror to avoid startup flash when dark mode or System-on-dark is selected. Settings and command-palette theme actions both write the same installation-local `settings.theme_mode` value; `system` subscribes to `prefers-color-scheme` changes at runtime while explicit Light/Dark remain overrides. + +## Localization + +Tolaria's app chrome uses an app-owned localization runtime in `src/lib/i18n.ts`, backed by flat JSON catalogs in `src/lib/locales/` and Lara CLI synchronization through `lara.yaml` (see [ADR-0087](adr/0087-json-catalogs-and-lara-cli-localization.md)). `en.json` is the canonical source catalog, locale files are one file per locale, and English remains the fallback for any missing locale file or key. The installation-local `ui_language` setting stores an explicit locale when the user chooses one; `null` means "follow the system language when Tolaria supports it, otherwise English." Legacy stored values such as `zh-Hans` are normalized to canonical locale codes like `zh-CN`. + +`App.tsx` derives the effective locale from settings and browser/system language hints, then passes it down to localized surfaces. Settings exposes a keyboard-accessible shadcn `Select`, and the command palette includes actions to open language settings or switch directly to a supported language. + +`App.tsx` also resolves the installation-local date display format from `settings.date_display_format` and publishes it through `AppPreferencesProvider` in `src/hooks/useAppPreferences.ts`. Note rows, note-list property chips, inspector property cells, note info, table-of-contents metadata, and search result subtitles read that shared preference and render dates through `src/utils/dateDisplay.ts` so the visible style stays consistent. Date picker text entry remains ISO (`YYYY-MM-DD`) to preserve predictable manual input and frontmatter storage. + +## Vault Management + +### Vault List + +Persisted at `~/.config/com.tolaria.app/vaults.json` (reads legacy `com.laputa.app` on upgrade): +```json +{ + "vaults": [{ "label": "My Vault", "path": "/path/to/vault" }], + "active_vault": "/path/to/vault", + "hidden_defaults": [] +} +``` + +Managed by `useVaultSwitcher` hook. Switching vaults resets sidebar and clears the active note. + +### Vault Config + +Per-vault UI settings stored locally per vault path (currently in browser/Tauri localStorage, not synced via git): +- `zoom`: Float zoom level (0.8–1.5) +- `view_mode`: "all" | "editor-list" | "editor-only" +- `editor_mode`: "raw" | "preview" (persists across note switches and sessions) +- `note_layout`: "centered" | "left" (wide-screen note column alignment for rich and raw editors) +- `tag_colors`, `status_colors`: Custom color overrides +- `property_display_modes`: Property display preferences +- `inbox.noteListProperties`: Optional Inbox-only property chip override for the note list +- `allNotes.noteListProperties`: Optional All Notes-only property chip override for the note list +- `inbox.explicitOrganization`: When `false`, hide Inbox and the organized toggle so the vault behaves like a plain note collection +- `git_setup_preference`: `"never"` when the user has opted out of future automatic Git setup prompts for that vault + +### Getting Started Vault + +On first launch, `useOnboarding` checks if the default vault exists. If not, it shows `WelcomeScreen` with three options: +- **Create a new vault** → creates an empty git repo in a folder the user chooses +- **Open an existing folder** → system file picker; plain Markdown folders without `.git` open immediately in supported non-git mode +- **Get started with a template** → pick a parent folder, then call `create_getting_started_vault()` with the derived `.../Getting Started` child path so the cloned vault opens into the populated repo root immediately + +If the selected vault disappears after startup, `useVaultLoader` re-checks `check_vault_exists` when reloads or vault-derived surfaces fail. A confirmed missing path clears cached entries, folders, views, modified-file state, and prefetched note content, then `App` reuses the `vault-missing` `WelcomeScreen` state so note and view actions cannot keep targeting the stale active vault. + +When an opened folder is not yet a git repo, Tolaria can show a Git setup dialog with Initialize, Not now, and Never for this vault actions. The Never choice stores a local per-vault `git_setup_preference` so the automatic dialog does not return for that vault, while manual initialization remains reachable from Git commands when global Git features are enabled. Markdown scanning, note browsing, note editing, and search continue normally in plain folders. Git-dependent surfaces (history, changes, commit, sync, conflict resolution, remotes, AutoGit, and auto-sync) stay unavailable until the user explicitly initializes Git. + +When the user enables Git later, `init_git_repo` runs `git init`, ensures Tolaria's default `.gitignore`, stages the vault, and writes the initial `Initial vault setup` commit. Before app-managed setup, remote-connection, manual/automatic, and conflict-resolution commits, Tolaria ensures Git can resolve an author identity without overriding one the user configured: it heals the legacy repo-local `vault@tolaria.md` email earlier versions wrote, respects identities resolvable from local, global, or system scope, skips the legacy email wherever it resolves, and only when nothing resolves writes a repo-local `Tolaria ` fallback. That app-managed setup commit explicitly disables commit signing for the single command so inherited global or local `commit.gpgsign` preferences cannot strand onboarding when GPG is missing or misconfigured. Later `git_commit` calls honor the user's signing configuration first, then retry the same app-managed commit once with `commit.gpgsign=false` only when Git reports a signing-helper failure, so working GPG/SSH signing setups continue to sign while broken GPG setups do not create repeated opaque commit failures. + +Once a vault is ready, `useAiAgentsOnboarding` can show a one-time `AiAgentsOnboardingPrompt`. That prompt reads `useAiAgentsStatus` so first launch surfaces whether Claude Code, Codex, GitHub Copilot, OpenCode, Pi, Antigravity, Kiro, and Hermes Agent CLI are installed, offers per-agent install links when they are missing, and stores local dismissal so the prompt does not repeat on every launch. + +`useGettingStartedClone` reuses the same parent-folder semantics for the status-bar / command-palette clone action, and `Toast` is rendered through the AI-agents onboarding gate so the resolved destination path stays visible right after a successful clone. + +The starter content no longer lives in the app repo. `src-tauri/src/vault/getting_started.rs` holds the public starter repo URL (`refactoringhq/tolaria-getting-started`), delegates the clone to the git backend, then normalizes Tolaria-managed root guidance and type scaffolding (`AGENTS.md`, `CLAUDE.md`, `type.md`, `note.md`) so fresh starter vaults pick up the current defaults even when the remote starter repo still carries a legacy copy or an older pre-`type:` `is_a`-era template. `AGENTS.md` stays the canonical vault guidance file; `CLAUDE.md` is a compatibility shim that imports it for Claude Code without duplicating the instructions, and Tolaria seeds it as an organized `Note` so it stays out of the way in a fresh vault. Optional `GEMINI.md` guidance is created only by the explicit AI guidance restore action. Once a user edits a usable `AGENTS.md`, including changing its frontmatter `type`, the status command treats it as custom guidance instead of broken; repair remains reserved for missing, empty, frontmatter-only, unreadable, or exact replaceable managed templates/stubs. The clone helper still accepts the legacy `LAPUTA_GETTING_STARTED_REPO_URL` environment override so older automation can continue to redirect the starter source during the transition. + +After the clone completes, Tolaria removes every configured git remote from the new starter vault. Getting Started vaults therefore open as local-only by default, and users opt into a remote later with the explicit Add Remote flow. + +### Remote Clone & Auth Model + +Tolaria no longer implements provider-specific OAuth or remote-repository APIs. All remote git work goes through the user's existing system git configuration. Git subprocesses first honor an installation-local `git_path` in `settings.json` when it points at an existing executable. On macOS, they then prefer the user's login-shell `git` and `PATH`, fall back to standard Apple/Homebrew locations such as `/opt/homebrew/bin/git`, `/usr/local/bin/git`, and `/usr/bin/git`, and only then use the inherited `PATH`. This keeps Homebrew/Xcode Git, Git Credential Manager, and `git-credential-osxkeychain` resolving the same way they do in Terminal even when Tolaria is launched from Finder, Dock, or Homebrew Cask. + +**Flow:** +1. User opens `CloneVaultModal` from onboarding or the vault menu +2. User pastes a supported remote URL (`https://`, `http://`, `ssh://`, or `git@host:path`) and chooses a local destination +3. The `clone_git_repo()` Tauri command validates that URL, then runs `git clone -- ` inside a blocking Tokio task so the Tauri window stays responsive during slow or failing clones +4. Linux AppImage builds strip AppImage loader variables from system-git and MCP Node subprocesses before spawning them, keeping `git-remote-https` and system `node` on the host library stack +5. `git_push()` / `git_pull()` continue to use the same system git path +6. On macOS, `git_add_remote()` asks Git's credential helper for HTTPS credentials before the first fetch so Keychain can grant access to the same saved credential item the shell uses +7. Every app-managed git subprocess disables the `ext::` transport, keeps file transport at Git's user-initiated policy, disables repo-configured fsmonitor hooks, ignores repo-configured SSH command overrides, and preserves quoted path output +8. Clone commands disable interactive terminal / askpass prompts and surface the git failure back to the UI instead of freezing the app waiting for input + +**Auth model:** +- SSH keys, Git Credential Manager, macOS Keychain helpers, `gh auth`, and other git helpers all work without app-specific setup +- No provider tokens are stored in Tolaria settings +- The same flow works for GitHub, GitLab, Bitbucket, Gitea, and self-hosted remotes over HTTPS or SSH + +## Pulse View + +`PulseView` is a git activity feed that replaces the NoteList when the Pulse filter is selected. + +- Groups commits by day ("Today", "Yesterday", or full date) +- Shows commit message, short hash, timestamp, and changed files +- Files have status icons (added/modified/deleted) and are clickable to open in editor +- Links to GitHub commits when `githubUrl` is available +- Infinite scroll pagination (20 commits per page) via Intersection Observer + +Backend: `get_vault_pulse` Tauri command parses `git log` with `--name-status`. + +## Data Flow + +### Startup Sequence + +```mermaid +sequenceDiagram + participant T as Tauri (Rust) + participant A as App.tsx + participant VL as useVaultLoader + participant MCP as MCP Server + participant U as User + + T->>T: apply Linux WebKit env safeguards
(Wayland/AppImage) + T->>T: start background legacy vault housekeeping
(does not block setup) + T->>MCP: start background initial ws-bridge sync
(if active vault exists) + T->>A: App mounts + + A->>A: useOnboarding — vault exists? + alt Vault missing + A-->>U: WelcomeScreen + else Vault found + A->>VL: useVaultLoader fires + VL->>T: invoke('reload_vault') → allow requested vault roots in asset scope + scan_vault_cached() + T-->>VL: VaultEntry[] + VL->>T: invoke('get_modified_files') + alt Runtime vault path disappears + VL->>T: invoke('check_vault_exists') + VL-->>A: unavailable vault path + cleared stale state + A-->>U: WelcomeScreen (vault missing) + end + A->>T: useMcpStatus — check explicit MCP setup state + A->>T: sync_mcp_bridge_vault(selected path) + VL-->>A: entries ready + end + + U->>A: clicks note in NoteList + A->>T: invoke('get_note_content') + T-->>A: raw markdown + A->>A: resolveBlocksForTarget(path, raw markdown) + alt tab cache or parsed-block cache hit + A->>A: reuse exact-source BlockNote blocks + else large common Markdown + A->>A: direct Markdown-to-block parser + else unsupported Markdown or cold small note + A->>A: tryParseMarkdownToBlocks() + end + A->>A: inject wikilinks, math, and durable schema blocks + A-->>U: Editor renders note +``` + +### Auto-Save Flow + +```mermaid +flowchart LR + A["✏️ Editor content changes"] --> B["useEditorSave\n(debounced)"] + B --> C["restore durable Markdown tokens"] + C --> D["direct block Markdown serializer\nor BlockNote fallback"] + D --> E["invoke('save_note_content')"] + E --> F["💾 Disk write"] + F --> G["Update tab status indicator"] +``` + +### Git Sync Flow + +```mermaid +flowchart TD + AS["useAutoSync\n(configurable interval)"] --> PULL["invoke('git_pull')"] + PULL --> PC{Result?} + PC -->|Conflicts| CM["ConflictResolverModal\nor ConflictNoteBanner"] + PC -->|Fast-forward| RV["reload vault + folders/views"] + RV --> TAB{"clean active tab?"} + TAB -->|Yes| RT["replace active tab\nwith fresh disk content"] + TAB -->|No| DONE["idle"] + RT --> RF["restore editor focus\nwhen previously focused"] + RF --> DONE + PC -->|Manual up to date| RV + PC -->|Auto up to date| DONE["idle"] + + MAN["Manual commit\n(CommitDialog)"] --> RS["useGitRemoteStatus\n(commit-time check)"] + RS --> RCHK["invoke('git_remote_status')"] + RCHK --> RMODE{Remote configured?} + RMODE -->|No| GC["invoke('git_commit', message)"] + GC --> LOCAL["Local commit only\nNo remote chip + local toast"] + RMODE -->|Yes| GC2["invoke('git_commit', message)"] + GC2 --> GP["invoke('git_push')"] + GP --> PR{Push result?} + PR -->|ok| RM["Reload modified files"] + PR -->|rejected| DIV["syncStatus = pull_required"] + DIV -->|User clicks badge| PAP["pullAndPush()"] + PAP --> PULL2["invoke('git_pull')"] + PULL2 --> GP2["invoke('git_push')"] + GP2 --> RM + + CMD["Cmd+K → Pull\nor Menu → Pull"] --> PULL + STATUS["Click sync badge"] --> POPUP["GitStatusPopup\n(branch, upstream, ahead/behind)"] +``` + +Manual Sync resolves the current branch's configured upstream and pulls that remote/branch explicitly, so non-`main` vault branches and local branches that track differently named remote branches follow normal Git tracking configuration instead of assuming `origin/main`. Missing upstreams and detached HEAD states return actionable sync errors while leaving branch creation, checkout, and tracking setup to external Git tooling. Manual Sync still forces a visible-state refresh even when `git_pull` reports `up_to_date`, because the working tree may have already changed through another process while the app still holds stale vault and History state. Updated pulls refresh the vault index, folders, saved views, clean active-editor content, and Git history surfaces; manual up-to-date pulls refresh the vault/sidebar surfaces with unknown changed files and bump the History refresh key without showing a "Pulled 0 updates" toast. Automatic mount/focus/interval up-to-date checks stay cheap and do not reload the vault. + +`useGitRemoteStatus` re-checks `git_remote_status` for the default repository, and `useCommitFlow` can resolve remote status for an explicit selected repository when the commit dialog opens and again right before submit. If `hasRemote` is false, Tolaria keeps that repository's flow local-only: the status bar shows a neutral `No remote` chip for the default repository, the dialog copy switches from "Commit & Push" to "Commit", and no `git_push` call is attempted. + +The manual commit dialog can also draft an editable commit message before submit. `useCommitFlow.generateCommitMessageForDialog()` saves pending edits, reloads the selected repository with `get_modified_files(includeStats: true)`, and fills the textarea without invoking `git_commit`. When the active AI target is a configured direct model and AI features are enabled, Tolaria sends bounded path/status/line-count metadata only; agent targets, unavailable/offline AI, large omitted paths, and model failures fall back to the deterministic changed-file summary. The command palette action "Generate Commit Message from Diff" opens the same dialog and focuses the generated message field for keyboard editing. + +If the current vault is not a Git repository, Tolaria treats Git as unavailable instead of degraded. With global Git features enabled, the status bar replaces changes, commit, sync, remote, conflict, and history controls with a `Git disabled` warning that reopens Git setup unless the user has chosen not to be prompted automatically for that vault. Command registration follows the same state: only `Initialize Git for Current Vault` is available in the Git group, while pull, commit, changes, conflict, and remote commands are hidden. `useAutoSync` is disabled for non-git vaults so the app does not run background Git commands against plain folders. + +The installation-local `git_enabled` setting is a broader visibility switch. When it is `false`, Tolaria hides Git status-bar entries and Git command-palette actions completely, disables AutoGit controls in Settings, and prevents background Git refresh/sync work even for repositories that are otherwise Git-backed. Settings remains the re-enable path. + +The same local-only state enables the explicit Add Remote flow. `AddRemoteModal` is reachable from the `No remote` chip and the command palette. The backend `git_add_remote` command validates the pasted remote URL, ensures the local author identity, adds `origin`, fetches it, refuses incompatible histories, and only enables tracking after a safe push or fast-forward-compatible check succeeds. + +`useCommitFlow` also exposes `runAutomaticCheckpoint()`, a dialog-free commit path shared by AutoGit and the bottom-bar Commit button. `useAutoGit` watches the last editor activity plus app focus/visibility state, and when the default vault is git-backed, all saves are flushed, and no unsaved edits remain, it triggers the deterministic `Updated N note(s)` / `Updated N file(s)` commit message path after the configured idle or inactive thresholds. In multiple-workspace mode, that checkpoint reads, commits, and pushes every active repository independently; one failed or rejected repository does not prevent the remaining repositories from being attempted. The manual commit dialog remains single-repository and requires the user to choose the target repository when more than one is active. + +#### Sync States + +| State | Indicator | Color | Trigger | +|-------|-----------|-------|---------| +| `idle` | Synced / Synced Xm ago | green | Successful sync | +| `syncing` | Syncing... | blue | Pull/push in progress | +| `pull_required` | Pull required | orange | Push rejected (divergence) | +| `conflict` | Conflict | orange | Merge conflicts detected | +| `error` | Sync failed | grey | Network/auth error | + +## Vault Module Structure + +The vault backend (`src-tauri/src/vault/`) is split into focused submodules: + +| File | Purpose | +|------|---------| +| `mod.rs` | Core types (`VaultEntry`, `Frontmatter`), `parse_md_file`, `scan_vault`, relationship/link extraction | +| `parsing.rs` | Text processing: snippet extraction, markdown stripping, ISO date parsing, `extract_title` (H1 → legacy frontmatter → filename), `slug_to_title` | +| `title_sync.rs` | Legacy filename → `title` frontmatter sync helper; no longer used by the normal note-open flow | +| `cache.rs` | Git-based incremental vault caching (`scan_vault_cached`), git helpers | +| `ignored.rs` | Gitignored-content visibility filtering via batched, pipe-safe `git check-ignore` | +| `filename_rules.rs` | Cross-platform validation for note filenames, folder names, and custom view filenames | +| `rename.rs` | `rename_note` / `rename_note_filename` / `move_note_to_folder` — stage crash-safe file moves, update `title` frontmatter when needed, recover unfinished rename transactions, and report backlink rewrite failures | +| `image.rs` | `save_image` / `copy_image_to_vault` — save editor image attachments with sanitized filenames | +| `migration.rs` | `flatten_vault`, `vault_health_check`, `migrate_is_a_to_type` | +| `config_seed.rs` | Maintains vault AI guidance (`AGENTS.md`, `CLAUDE.md`, and optional `GEMINI.md` shims), migrates legacy `config/agents.md`, and repairs missing root type scaffolding such as `type.md` and `note.md` | +| `getting_started.rs` | Clones and normalizes the public Getting Started starter vault | + +## Rust Backend Modules + +| Module | Purpose | +|--------|---------| +| `vault/` | Vault scanning, caching, parsing, rename, image, migration | +| `frontmatter/` | YAML frontmatter read/write (`mod.rs`, `yaml.rs`, `ops.rs`) | +| `git/` | Git operations and shared system-git helpers (`command.rs`, `remote_config.rs`, `commit.rs`, `status.rs`, `history.rs`, `conflict.rs`, `remote.rs`, `pulse.rs`, `clone.rs`, `connect.rs`) | +| `search.rs` | Keyword search — walkdir-based vault file scan with Gitignored-content visibility filtering | +| `ai_agents.rs` | CLI-agent request normalization and adapter dispatch | +| `cli_agent_runtime.rs` | Shared CLI-agent request, prompt, subprocess, version, and MCP path helpers | +| `claude_cli.rs`, `codex_cli.rs`, `copilot_cli.rs`, `opencode_cli.rs`, `pi_cli.rs`, `antigravity_cli.rs` | CLI-agent command/config/event adapters | +| `pi_cli.rs`, `pi_config.rs`, `pi_discovery.rs`, `pi_events.rs` | Pi subprocess launch, user-config-seeded transient MCP adapter config, discovery, and JSON stream parsing | +| `mcp.rs` | MCP server spawning + explicit config registration/removal | +| `commands/` | Tauri command handlers (split into submodules) | +| `settings.rs` | App settings persistence | +| `vault_config.rs` | Per-vault UI config | +| `vault_list.rs` | Vault list persistence | +| `menu.rs` | Native desktop menu definitions and command IDs (not mounted on Linux) | + +## Tauri IPC Commands + +### Vault Operations + +| Command | Description | +|---------|-------------| +| `list_vault` | Scan vault (cached), then apply Gitignored-content visibility → `Vec` | +| `get_note_content` | Read note file content | +| `save_note_content` | Write note content to disk | +| `delete_note` | Permanently delete note from disk (with confirm dialog) | +| `rename_note` | Crash-safe note rename + `title` frontmatter update + cross-vault wikilinks + failed backlink counts | +| `move_note_to_folder` | Crash-safe folder move that preserves the filename, reloads the moved note, and rewrites path-based wikilinks | +| `create_vault_folder` | Create a folder relative to the active vault root | +| `list_vault_folders` | Build the folder tree on the blocking Tokio pool, then apply Gitignored-content visibility → `Vec` | +| `rename_vault_folder` | Rename a folder relative to the active vault root and return old/new relative paths | +| `delete_vault_folder` | Permanently delete a folder subtree relative to the active vault root | +| `sync_note_title` | Legacy helper: rewrite `title` frontmatter from filename → `bool` (modified); not used by the normal note-open flow | +| `batch_archive_notes` | Archive multiple notes | +| `batch_delete_notes` | Permanently delete notes from disk | +| `reload_vault` | Allow the requested vault roots in the runtime asset scope, invalidate cache, full rescan from filesystem, then apply Gitignored-content visibility → `Vec` | +| `reload_vault_entry` | Re-read a single file from disk → `VaultEntry` | +| `open_vault_file_external` | Validate an existing file against the active vault boundary, then open it with the system default app | +| `start_vault_watcher` / `stop_vault_watcher` | Start or stop native active-vault filesystem change events | +| `check_vault_exists` | Check if vault path exists | +| `create_empty_vault` | Create a git-backed vault, then seed root `AGENTS.md`, `CLAUDE.md`, `type.md`, and `note.md` defaults | +| `create_getting_started_vault` | Clone the public Getting Started vault, refresh Tolaria-managed guidance/config defaults, and keep the cloned repo clean | +| `get_vault_ai_guidance_status` | Report whether `AGENTS.md`, `CLAUDE.md`, and optional `GEMINI.md` guidance are managed, missing, broken, or custom | +| `restore_vault_ai_guidance` | Restore any missing/broken Tolaria-managed guidance files without overwriting custom ones | + +### Frontmatter + +| Command | Description | +|---------|-------------| +| `update_frontmatter` | Update a frontmatter property | +| `delete_frontmatter_property` | Remove a frontmatter property | + +### Git + +| Command | Description | +|---------|-------------| +| `init_git_repo` | Initialize a local repo, add default `.gitignore`, and create the unsigned setup commit | +| `git_commit` | Stage all + commit | +| `git_pull` | Pull from remote | +| `git_push` | Push to remote | +| `git_remote_status` | Get branch name + ahead/behind counts | +| `git_file_url` | Build a remote-backed browser/git URL for a vault file | +| `git_add_remote` | Connect a local-only vault to a compatible remote and start tracking it | +| `git_resolve_conflict` | Resolve a merge conflict | +| `git_commit_conflict_resolution` | Commit conflict resolution | +| `get_file_history` | Last N commits for a file | +| `get_modified_files` | `git status` filtered to .md | +| `get_file_diff` | Unified diff for a file | +| `get_file_diff_at_commit` | Diff at a specific commit | +| `get_conflict_files` | List conflicted files | +| `get_conflict_mode` | Get conflict resolution mode | +| `get_vault_pulse` | Git activity feed (paginated) | +| `get_last_commit_info` | Latest commit metadata | +| `clone_repo` | Clone a remote repository into a local folder using system git | + +### Search + +| Command | Description | +|---------|-------------| +| `search_vault` | Keyword search across vault files | + +### Vault Maintenance + +| Command | Description | +|---------|-------------| +| `get_vault_settings` | Read `.laputa/settings.json` | +| `save_vault_settings` | Write vault settings | +| `repair_vault` | Flatten vault structure, migrate legacy frontmatter, restore root config/type defaults including `note.md` | + +### AI & MCP + +| Command | Description | +|---------|-------------| +| `stream_claude_chat` | Claude CLI chat mode (streaming) | +| `check_claude_cli` | Check if Claude CLI is available | +| `get_ai_agents_status` | Check Claude Code + Codex + GitHub Copilot + OpenCode + Pi + Antigravity + Kiro + Hermes Agent availability | +| `get_agent_docs_path` | Resolve the bundled local Tolaria docs folder used in AI-agent system prompts | +| `stream_ai_agent` | Stream Claude Code, Codex, GitHub Copilot, OpenCode, Pi, Antigravity, Kiro, or Hermes Agent through the normalized agent event layer | +| `abort_ai_agent_stream` | Stop an active app-managed CLI agent stream by its validated request-scoped event name | +| `register_mcp_tools` | Register vault-neutral MCP in Claude/Antigravity/Cursor/OpenCode/generic config | +| `remove_mcp_tools` | Remove Tolaria's MCP entry from Claude/Antigravity/Cursor/OpenCode/generic config | +| `check_mcp_status` | Check whether Tolaria's durable MCP entry is registered in Claude/Antigravity/Cursor/OpenCode/generic config | +| `get_mcp_config_snippet` | Return the exact manual MCP JSON snippet for the active vault | +| `copy_text_to_clipboard` | Copy setup snippets through the native desktop clipboard command path | +| `read_text_from_clipboard` | Read current desktop clipboard text for command-driven plain-text paste | +| `sync_mcp_bridge_vault` | Sync the desktop WebSocket bridge process to the selected vault, or stop it when no vault is selected | + +The desktop MCP WebSocket bridge is intentionally local-only. `mcp-server/ws-bridge.js` binds both bridge ports to loopback, rejects non-loopback clients, accepts browser/Tauri origins only on the UI bridge, and rejects browser-origin requests on the tool bridge so remote pages cannot drive vault tools directly. + +### Settings & Config + +| Command | Description | +|---------|-------------| +| `get_settings` | Load app settings | +| `save_settings` | Save app settings | +| `load_vault_list` | Load vault list | +| `save_vault_list` | Save vault list | +| `get_vault_config` | Load per-vault UI config | +| `save_vault_config` | Save per-vault UI config | +| `get_default_vault_path` | Get default vault path | +| `get_build_number` | Get app build number | +| `save_image` | Save base64 image to `attachments/` and ensure the vault root is in the runtime asset scope | +| `copy_image_to_vault` | Copy image file to `attachments/` and ensure the vault root is in the runtime asset scope | +| `update_menu_state` | Update native menu checkmarks and enabled/disabled state for selection-dependent actions | +| `trigger_menu_command` | Emit a native menu command ID for deterministic shortcut QA | +| `update_current_window_min_size` | Update the active Tauri window's minimum size and optionally grow it to fit restored panes | + +`get_build_number` feeds the bottom status bar label. It preserves legacy `bNNN` date-build labels, renders local `0.1.0` / `0.0.0` builds as `dev`, formats calendar alpha builds as `Alpha YYYY.M.D.N`, strips any calendar `-stable.N` suffix back to `YYYY.M.D`, and keeps legacy semver releases readable instead of falling back to `?`. + +## Mock Layer + +When running outside Tauri (browser at `localhost:5173`), `src/mock-tauri.ts` provides a transparent mock layer: + +```typescript +if (isTauri()) { + result = await invoke('command_name', { args }) +} else { + result = await mockInvoke('command_name', { args }) +} +``` + +The mock layer includes sample entries across all entity types, full markdown content with realistic frontmatter, mock git history, mock AI responses, and mock pulse commits. It also tracks per-vault remote state so browser-mode Getting Started and empty-vault flows now behave like the desktop app: local-only until `git_add_remote` succeeds. + +Browser smoke tests can also override `window.__mockHandlers` before the app boots. The AutoGit smoke bridge uses that path directly for seeded saves so the mocked git dirty-state stays synchronized even when the optional browser vault API is serving note content. + +## State Management + +No Redux or global context. State lives in the root `App.tsx` and custom hooks: + +| State owner | State | Purpose | +|-------------|-------|---------| +| `App.tsx` | `selection`, panel widths, dialog visibility, toast, view mode | UI state | +| `useVaultLoader` | `entries`, `allContent`, `modifiedFiles` | Vault data | +| `useNoteActions` | `tabs`, `activeTabPath` | Composes `useNoteCreation` + `useNoteRename` + `frontmatterOps` | +| `useNoteWindowLifecycle` | note-window open/title side effects | Opens `tauri://` note windows without full vault scans and keeps the native title current | +| `useStartupScreenState` | startup visibility booleans | Keeps onboarding, telemetry-consent, missing-vault, and initial indexing decisions out of `App.tsx` | +| `useAppWindowControls` | view mode, panel visibility, command refs, zoom/build labels | Keeps main-window sizing and editor command ref plumbing out of `App.tsx` | +| `useAppViewActions` | saved-view/type creation and saved-view mutation callbacks | Keeps saved-view persistence and Type auto-creation orchestration out of `App.tsx` | +| `useAiWorkspacePublishedContext` | AI workspace note-list snapshot and BroadcastChannel context publishing | Keeps AI workspace context derivation close to its cross-window publication side effect | +| `useMcpSetupDialogController` | MCP setup dialog state/actions | Keeps MCP status, manual config, and connect/disconnect dialog flow out of `App.tsx` | +| `useAiWorkspaceWindowBridgeEvents` | native AI workspace event subscriptions | Owns Tauri listener setup/cleanup for popped-out workspace window bridge events | +| `useGitFileWorkflows` | git diff/history/discard callbacks | Resolves note-scoped repository paths and owns deleted-file preview and queued diff side effects | +| `useVaultRenameDetection` | detected rename banner state | Detects external Git renames on focus and owns the wikilink update callback | +| `useNoteCreation` | — | Note/type creation with optimistic persistence | +| `useNoteRename` | — | Note renaming and folder moves with wikilink update | +| `useNoteRetargeting` | — | Shared note retargeting logic for drag/drop and command-palette actions | +| `useDeepLinks` | Deep-link listener and copy actions | Resolves `tolaria://` links into known vault navigation and clipboard URLs | +| `useTauriDragDropEvent` | — | Shared native window drag/drop event subscription and cleanup | +| `useNativePathDrop` | — | Tauri-native file/folder path drops for AI and command text inputs | +| `frontmatterOps` | — (pure functions) | Frontmatter CRUD: key→VaultEntry mapping, mock/Tauri dispatch | +| `useTabManagement` | Navigation history, note switching | Note navigation lifecycle | +| `useVaultSwitcher` | `vaultPath`, `extraVaults` | Vault switching | +| `useTheme` | Editor theme CSS vars and theme-mode bridge | Editor typography and app theme runtime | +| `useCliAiAgent` | `messages`, `status`, tool actions | Selected AI agent conversation backed by the shared session pipeline and vault permission mode | +| `useAutoSync` | Sync interval, pull/push state | Git auto-sync | +| `useAutoGit` | Last activity timestamp, idle/inactive checkpoint triggers | Automatic commit/push checkpoints | +| `useCommitFlow` | Commit dialog state, generated commit-message drafts, shared manual/automatic checkpoint runner | Git commit/push orchestration | +| `useGitRemoteStatus` | `remoteStatus`, `refreshRemoteStatus()` | On-demand remote detection for commit UI | +| `useUnifiedSearch` | Query, results, loading state | Keyword search | +| `useSettings` | App settings (telemetry, release channel, theme mode, UI language, date display format, auto-sync interval, Git visibility, AutoGit thresholds, default AI agent, Gitignored-content visibility, All Notes file visibility) | Persistent settings | +| `useVaultConfig` | Per-vault UI preferences, Git setup prompt preference, AI permission mode | Vault-specific config | +| `appCommandDispatcher` | Manifest-backed shortcut/menu command IDs | Shared execution path for renderer and native menu commands | + +Data flows unidirectionally: `App` passes data and callbacks as props to child components. No child-to-child communication — everything goes through `App`. + +## Keyboard Shortcuts + +| Shortcut | Action | +|----------|--------| +| Cmd+K | Open command palette | +| Cmd+P / Cmd+O | Open quick open palette | +| Cmd+N | Create new note | +| Cmd+S | Save current note | +| Cmd+F | Find in current note when the editor is focused; otherwise note-list search can claim it | +| Cmd+Shift+F | Find in vault | +| Cmd+Shift+V | Paste without Formatting into the active supported editing surface | +| Cmd+Shift+M | Toggle Markdown highlight on selected rich-editor text | +| Cmd+[ / Cmd+] | Navigate back / forward (replaces tabs) | +| Cmd+Z / Cmd+Shift+Z | Undo / Redo | +| Cmd+1–9 | Switch to tab N | +| Cmd+[ / Cmd+] | Navigate back / forward | +| `[[` in editor | Open wikilink suggestion menu | + +Selection-dependent actions are wired through the command palette and the native menus. For example, a deleted file opened from Changes view becomes a read-only diff preview, and that state enables the "Restore Deleted Note" menu/command while normal note mutation actions stay disabled. Folder selection follows the same pattern: when `selection.kind === 'folder'`, the command palette exposes "Reveal Folder in Finder", "Copy Folder Path", "Rename Folder", and "Delete Folder", and the sidebar row can launch the same flows directly through inline rename or the folder context menu. Active files also expose "Reveal in Finder" and "Copy File Path" through the command palette; non-Markdown file tabs additionally expose "Open in Default App", matching the `FilePreview` header controls. Markdown notes expose "Export note as PDF" from Cmd+K, the native Note menu, the breadcrumb overflow menu, and the note-list context menu, all routed through the same editor export hook. Remote-backed notes expose "Copy git URL" from the breadcrumb overflow and note-list context menu; the renderer gates the action per note workspace via `git_remote_status`, then asks `git_file_url` to build the copied URL from the primary remote, current branch, and vault-relative path. Active notes now follow the same shared-action model for retargeting: Cmd+K can open "Change Note Type…" and "Move Note to Folder…", and the sidebar drop targets call the same hook-backed implementations instead of maintaining separate mutation paths. + +Shortcut routing is explicit: + +- `src/shared/appCommandManifest.json` is the shared command/menu metadata source for command IDs, menu labels, accelerators, native-menu enablement groups, and deterministic QA flags +- `appCommandCatalog.ts` derives renderer command IDs, shortcut lookup maps, Linux menu sections, and QA metadata from that manifest +- `formatShortcutDisplay()` derives platform-accurate visible shortcut labels (`⌘` on macOS, `Ctrl` on Windows/Linux) from that same manifest so menus, tooltips, and command-palette copy stay aligned with real accelerators +- `useAppKeyboard` is the primary execution path for real shortcut keypresses, including Tauri runs +- macOS browser-reserved chords such as `Cmd+O`, `Cmd+F`, and `Cmd+Shift+L` are unblocked at webview init via `tauri-plugin-prevent-default`, then continue through the same renderer-first command path +- `Cmd+Shift+V` uses the same command path for "Paste without Formatting"; `plainTextPaste.ts` reads text through the native clipboard command in Tauri and inserts it through the active rich/raw editor target or the focused browser text control +- `Cmd+F` is surface-aware: editor focus opens current-note find/replace in raw CodeMirror, note-list focus preserves note-list search, and native menu enablement follows focus availability events so only one `Cmd+F` menu item is active +- Rich-editor inline formatting shortcuts that are owned by BlockNote or editor extensions stay local to the editor surface; `createMarkdownHighlightShortcutExtension()` handles Cmd/Ctrl+Shift+M and toggles the durable `==highlight==` style without entering the app-command manifest. +- `menu.rs`, `useMenuEvents`, and the custom titlebar `LinuxMenuButton` emit the same manifest-derived command IDs for native menu clicks, accelerators, and custom titlebar menu actions +- `appCommandDispatcher.ts` suppresses the paired native-menu/renderer echo from a single shortcut so the command runs once +- Deterministic QA uses two explicit proof paths from the shared manifest: + - renderer shortcut-event proof through `window.__laputaTest.triggerShortcutCommand()` + - native menu-command proof through `trigger_menu_command` +- The browser harness is only a deterministic desktop command bridge; exact native accelerator delivery still requires real Tauri QA for commands flagged as manual-native-critical + +## Auto-Release & In-App Updates + +### Release Pipeline + +Every push to `main` triggers `.github/workflows/release.yml`: + +``` +push to main + → version job: compute calendar alpha version YYYY.M.D-alpha.N + and a GitHub-sorted tag alpha-vYYYY.M.D-alpha.NNNN + → use today's UTC date unless the latest stable-vYYYY.M.D tag already uses today + → if stable already uses today, advance alpha to the next calendar day so semver still increases + → build-artifacts job, via `.github/workflows/release-build-artifacts.yml`: + → pnpm install, stamp version, pnpm build, tauri build --target aarch64-apple-darwin --bundles app + → pnpm install, stamp version, pnpm build, tauri build --target x86_64-apple-darwin --bundles app + → upload signed Apple Silicon and Intel .app.tar.gz + .sig updater artifacts named Tolaria__macOS_Silicon and Tolaria__macOS_Intel + → pnpm install, stamp version + → tauri build --target x86_64-unknown-linux-gnu --bundles deb,rpm,appimage + → verify Linux installer and updater-signature artifacts exist + → upload .deb, .rpm, .AppImage, and signed Linux updater bundles + → pnpm install, stamp version, optionally import the Windows code-signing certificate + → tauri build --target x86_64-pc-windows-msvc --bundles nsis, with Authenticode signing config only when certificate secrets are present + → verify Windows app executable and installer Authenticode signatures when Authenticode signing ran + → upload NSIS installer, optional MSI artifacts, and signed Windows updater bundles + → release job: + → generate alpha-latest.json with darwin-aarch64, darwin-x86_64, Linux, and Windows updater URLs + → publish GitHub prerelease alpha-vYYYY.M.D-alpha.NNNN named Tolaria Alpha YYYY.M.D.N + → pages job: + → build VitePress public docs into the GitHub Pages root + → build static HTML release history page at /releases/ + → publish alpha/latest.json + → refresh latest.json + latest-canary.json as compatibility aliases to alpha + → preserve stable/latest.json + → deploy to gh-pages +``` + +Stable promotions trigger `.github/workflows/release-stable.yml`: + +``` +push stable-vYYYY.M.D tag + → version job: validate YYYY.M.D from the tag + → build-artifacts job, via `.github/workflows/release-build-artifacts.yml`: + → pnpm install, stamp version, pnpm build, tauri build --target aarch64-apple-darwin + → pnpm install, stamp version, pnpm build, tauri build --target x86_64-apple-darwin + → upload signed Apple Silicon and Intel .app.tar.gz + .sig and .dmg artifacts named Tolaria__macOS_Silicon and Tolaria__macOS_Intel + → pnpm install, stamp version + → tauri build --target x86_64-unknown-linux-gnu --bundles deb,rpm,appimage + → verify Linux installer and updater-signature artifacts exist + → upload .deb, .rpm, .AppImage, and signed Linux updater bundles + → pnpm install, stamp version, optionally import the Windows code-signing certificate + → tauri build --target x86_64-pc-windows-msvc --bundles nsis, with Authenticode signing config only when certificate secrets are present + → verify Windows app executable and installer Authenticode signatures when Authenticode signing ran + → upload NSIS installer, optional MSI artifacts, and signed Windows updater bundles + → release job: + → generate stable-latest.json with macOS Apple Silicon, macOS Intel, Linux, and Windows updater URLs plus platform-specific manual download URLs + → publish GitHub release Tolaria YYYY.M.D + → pages job: + → build VitePress public docs into the GitHub Pages root + → build static HTML release history page at /releases/ + → publish stable/latest.json + → publish stable/download/ and download/ as permanent download pages that keep the browser page visible while the platform installer starts, default Linux visitors to AppImage, require an explicit Windows installer click with managed-device guidance, and expose RPM as a manual Linux option when the stable release includes one + → preserve alpha/latest.json + → deploy to gh-pages +``` + +Linux AppImage release jobs use Tauri's stock linuxdeploy AppImage output plugin. `scripts/appimage-launcher-tools.mjs` remains available for local experiments with symlink-safe AppRun patching and fcitx module bundling, but release packaging does not pre-seed that shim because linuxdeploy currently exits before sealing the AppImage when the shim replaces the stock output plugin in Tauri's tools cache. + +### Versioning + +- Stable promotions use git tags in the form `stable-vYYYY.M.D` and stamp the technical version `YYYY.M.D`. +- Alpha builds stamp the technical version `YYYY.M.D-alpha.N` and display it as `Alpha YYYY.M.D.N`. The GitHub release tag zero-pads the sequence as `alpha-vYYYY.M.D-alpha.NNNN` so GitHub release ordering remains chronological. +- If the latest stable tag already uses today's date, alpha advances to the next calendar day before assigning `-alpha.N` so Alpha remains semver-newer than Stable across channel switches. +- The workflows stamp the computed version into `tauri.conf.json` and `Cargo.toml` at build time. +- This keeps display strings clean while preserving semver monotonicity when a user switches between Stable and Alpha. + +### In-App Updates + +``` +App startup (3s delay) + → useUpdater.check() + → idle (no update) → no UI + → available → UpdateBanner with release notes + "Update Now" + → downloading → progress bar + → ready → "Restart to apply" + Restart Now + → network error → fail silently +``` + +### Telemetry (Opt-in) + +Anonymous crash reporting (Sentry) and usage analytics (PostHog), both **opt-in only**. + +```mermaid +sequenceDiagram + participant User + participant App + participant Settings + participant Sentry + participant PostHog + + Note over App: First launch or upgrade + App->>User: TelemetryConsentDialog + alt Accept + User->>Settings: telemetry_consent=true, anonymous_id=UUID + Settings->>Sentry: init(DSN, release, anonymous_id) + Settings->>PostHog: init(key, anonymous_id) + else Decline + User->>Settings: telemetry_consent=false + Note over Sentry,PostHog: Zero network requests + end + + Note over App: Settings panel toggle change + User->>Settings: crash_reporting_enabled=false + Settings->>Sentry: teardown() + Settings->>App: reinit_telemetry (Tauri cmd) +``` + +**Privacy guarantees:** +- No vault content, note titles, or file paths in payloads (regex scrubber in `beforeSend`) +- `anonymous_id` is a locally-generated UUID, never tied to identity +- `send_default_pii: false` on both SDKs +- PostHog: `autocapture: false`, `persistence: 'memory'`, no cookies +- Product events use categorical metadata only: file preview kind/action, AI agent id/permission mode/counts/status, update-check preference state, and All Notes visibility category/enabled state. + +**Architecture:** +- **Rust:** `sentry` crate initialized in `lib.rs::setup()` via `telemetry::init_sentry_from_settings()` +- **JS:** `@sentry/react` + `posthog-js` initialized lazily by `useTelemetry` hook; the React root also wires `onCaughtError`, `onUncaughtError`, and `onRecoverableError` through `Sentry.reactErrorHandler()` so production React invariants include component stack context when crash reporting is enabled. +- **Release grouping:** packaged release workflows pass `VITE_SENTRY_RELEASE` from the computed build version, but the app only assigns Sentry's `release` field for stable calendar builds (`YYYY.M.D`). Alpha/prerelease/internal builds omit `release` so they do not create normal Sentry Releases entries, while both frontend and Rust Sentry scopes tag `tolaria.build_version` and `tolaria.release_kind` for diagnostics. +- **Settings:** `telemetry_consent`, `crash_reporting_enabled`, `analytics_enabled`, `anonymous_id` in `Settings` struct +- **Consent:** `TelemetryConsentDialog` shown when `telemetry_consent === null` + +### Updates + +Tolaria uses the Tauri updater plugin for automatic updates: + +- `src-tauri/tauri.conf.json` points the default desktop feed at `stable/latest.json` +- `useUpdater(releaseChannel, automaticChecksEnabled)` waits 3 seconds after launch when automatic checks are enabled, then calls Rust commands instead of hard-coding one updater endpoint in the frontend +- `automatic_update_checks_enabled` is an installation-local Settings flag that defaults to enabled; disabling it skips only the startup/background probe, not manual status-bar update checks +- `src-tauri/src/app_updater.rs` maps the selected channel to `alpha/latest.json` or `stable/latest.json` +- `download_and_install_app_update` streams progress events back into `UpdateBanner` + +### Feature Flags (PostHog + Release Channels) + +Feature flags are backed by PostHog and evaluated per release channel: + +- **Alpha**: all features always enabled (no PostHog lookup) +- **Stable** (default): PostHog rules decide which features are enabled +- **Beta cohorts**: modeled in PostHog as tags or person-property targeting, not as a separate updater build or Settings option + +```typescript +import { useFeatureFlag } from './hooks/useFeatureFlag' + +const enabled = useFeatureFlag('example_flag') // boolean +``` + +**Resolution order:** +1. `localStorage` override: key `ff_` with value `"true"` or `"false"` +2. `isFeatureEnabled(flag)` in `telemetry.ts` → Alpha short-circuit, then PostHog, then hardcoded defaults + +**How to add a new flag:** +1. Add the flag name to the `FeatureFlagName` union type in `src/hooks/useFeatureFlag.ts` +2. Create the flag on PostHog with Stable rollout rules and any optional beta-cohort targeting +3. Use `useFeatureFlag('your_flag')` in components + +Release channel is selectable in Settings as `alpha` or `stable` and passed to PostHog as a person property via `identify()`. Beta targeting is managed in PostHog, not in the updater settings. See ADR-0057. + +## Platform Support — iOS / iPadOS (Prototype) + +Tauri v2 supports iOS as a beta target. The Rust backend cross-compiles to `aarch64-apple-ios-sim` (simulator) and `aarch64-apple-ios` (device) with zero code changes to vault/frontmatter/search logic. + +**Conditional compilation strategy:** + +``` +#[cfg(desktop)] — git CLI, menu bar, MCP server, CLI AI agents, updater +#[cfg(mobile)] — stub commands returning graceful errors or empty results +``` + +Desktop-only modules gated at the crate level: +- `pub mod menu` — macOS menu bar (entire module) + +Desktop-only features gated at the function level in `commands/`: +- Git operations (commit, pull, push, status, history, diff, conflicts) +- Clone-by-URL via system git (`clone_repo`) +- CLI AI agent streaming (Claude, Codex, GitHub Copilot, OpenCode, Pi, Antigravity, Kiro) +- MCP registration and status +- Menu state updates + +Features that work on both platforms without changes: +- Vault scan, note read/write, rename, delete, archive +- Frontmatter read/write/delete +- AI chat (Anthropic API via `reqwest`) +- Search (pure Rust in-memory) +- Settings persistence +- Vault list management + +**Capabilities:** `src-tauri/capabilities/default.json` targets desktop; `mobile.json` targets iOS/Android with a minimal permission set. + +**Detailed feasibility report:** `docs/IPAD-PROTOTYPE.md` diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md new file mode 100644 index 0000000..628a158 --- /dev/null +++ b/docs/GETTING-STARTED.md @@ -0,0 +1,534 @@ +# Getting Started + +How to navigate the codebase, run the app, and find what you need. + +## Prerequisites + +- **Node.js** 18+ and **pnpm** +- **Rust** 1.77.2+ (for the Tauri backend) +- **git** CLI (required by the git integration features; Windows users may choose native Git or WSL2 Git in Settings) + +### Linux system dependencies + +If you run the desktop app on Linux, install Tauri's WebKit2GTK 4.1 dependencies first: + +- Arch / Manjaro: + ```bash + sudo pacman -S --needed webkit2gtk-4.1 base-devel curl wget file openssl \ + appmenu-gtk-module libappindicator-gtk3 librsvg + ``` +- Debian / Ubuntu (22.04+): + ```bash + sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file \ + libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev \ + libsoup-3.0-dev patchelf + ``` +- Fedora 38+: + ```bash + sudo dnf install webkit2gtk4.1-devel openssl-devel curl wget file \ + libappindicator-gtk3-devel librsvg2-devel + ``` + +### Linux AppImage Wayland troubleshooting + +On some Wayland systems, the Linux AppImage may fail to launch with: + +```text +Could not create default EGL display: EGL_BAD_PARAMETER. Aborting... +``` + +Recent Tolaria Linux builds automatically disable the unstable WebKitGTK DMABUF renderer on native Wayland launches. AppImage launches also disable WebKitGTK compositing as a last-resort sealed-runtime fallback and retry startup with an architecture-matching system Wayland client library when they detect this class of AppImage + Wayland environment. If you are running an older build, use this workaround: + +```bash +WEBKIT_DISABLE_COMPOSITING_MODE=1 WEBKIT_DISABLE_DMABUF_RENDERER=1 LD_PRELOAD=/usr/lib64/libwayland-client.so.0 ./Tolaria*.AppImage +``` + +If your distribution stores the 64-bit library elsewhere, use that path instead, for example `/usr/lib/x86_64-linux-gnu/libwayland-client.so.0`. On 64-bit Fedora, avoid `/usr/lib/libwayland-client.so.0`; that path can point at a 32-bit library and be ignored by the loader with a wrong ELF class warning. + +### Linux AppImage packaging checks + +Linux release CI currently uses Tauri's stock linuxdeploy AppImage output plugin: + +```bash +pnpm tauri build --target x86_64-unknown-linux-gnu --bundles deb,rpm,appimage +``` + +Release validation verifies that the Linux job produced an AppImage, at least one installer bundle, and updater signature artifacts. Windows release jobs always require Tauri updater signatures; when Authenticode certificate secrets are configured, they also import the CI code-signing certificate, build NSIS with a generated Tauri Authenticode signing config, and verify the app executable plus installer signatures before upload. The experimental AppImage output-plugin shim in `scripts/appimage-launcher-tools.mjs` is retained for local investigation, but it is not wired into release packaging because linuxdeploy currently exits before sealing the AppImage when the shim is pre-seeded in Tauri's tools cache. + +## Quick Start + +```bash +# Install dependencies +pnpm install + +# Run in browser (no Rust needed — uses mock data) +pnpm dev +# Open http://localhost:5173 + +# Run with Tauri (full app, requires Rust) +pnpm tauri dev + +# Run tests +pnpm test # Vitest unit tests +cargo test # Rust tests (from src-tauri/) + +# Or, run Rust tests from root project directory +cargo test --manifest-path src-tauri/Cargo.toml + +# E2E tests +pnpm playwright:smoke # Curated Playwright core smoke lane (~5 min) +pnpm playwright:regression # Full Playwright regression suite +``` + +## Chunk Sidecar Validation + +The experimental `.chunk/config.json` mirrors the portable parts of the local git hook gate as named validations. Use it for inner-loop checks before running the full pre-push hook: + +```bash +chunk validate --list +chunk validate lint +chunk validate typecheck +chunk validate frontend-coverage +``` + +Remote sidecar validation requires CircleCI authentication: + +```bash +chunk auth set circleci +chunk sidecar setup --name tolaria-hooks +chunk validate --remote lint +``` + +For Playwright smoke, prefer the shared-server shard runner after setup: + +```bash +chunk sidecar ssh --sidecar-id -- 'cd /home/user/tolaria && PLAYWRIGHT_CONCURRENCY=4 bash .chunk/run-playwright-shards.sh 8' +``` + +The pre-push hook uses the faster sidecar path automatically when Chunk is available. It syncs the checkout to three independent sidecars and fans out the automatic gates: + +- `tolaria-hooks-frontend-2`: lint and build first, then frontend coverage. +- `tolaria-hooks-rust`: clippy, rustfmt, and Rust coverage. +- `tolaria-hooks-playwright`: curated Playwright smoke with a shared Vite server and eight shards. + +Set `LAPUTA_PREPUSH_LOCAL=1` to force the local fallback path. If CircleCI has duplicate sidecar names, pin lanes with `SIDECAR_FRONTEND_ID`, `SIDECAR_RUST_ID`, and `SIDECAR_PLAYWRIGHT_ID`. + +The sidecar is Linux-based, so keep native macOS Tauri QA and app-focus screenshot checks on the host machine. The Chunk config is intended for portable frontend, Rust, coverage, and Playwright smoke checks. Avoid starting multiple `chunk validate --remote ...` processes against the same sidecar at once; each validate run syncs the checkout, so concurrent validates can race. + +## Starter Vaults And Remotes + +`create_getting_started_vault` clones the public starter repo and then removes every git remote from the new local copy. That means Getting Started vaults open local-only by default. Users connect a compatible remote later through the bottom-bar `No remote` chip or the command palette, both of which feed the same `AddRemoteModal` and `git_add_remote` backend flow. + +Linux AppImage builds still use the user's system `git` and `node`. Before Tolaria spawns those Git or MCP Node subprocesses, it removes AppImage loader overrides such as `LD_LIBRARY_PATH`, `LD_PRELOAD`, and `GIT_EXEC_PATH` so HTTPS clone helpers and MCP tooling use the host library stack instead of bundled AppImage libraries. + +On Windows, Settings > Git exposes an explicit Git provider choice. Native Git remains the default. Selecting WSL2 Git stores the chosen distribution in app settings, launches Git through `wsl.exe --exec git`, and translates vault paths before clone, status, commit, sync, and remote operations use them. + +## Multiple Vaults At The Same Time + +The `settings.multi_workspace_enabled` flag turns the registered vault list into a unified graph. When enabled, `useVaultLoader` loads every available mounted vault, annotates entries with workspace provenance, and lets note lists, quick open, keyword search, backlinks, and wikilink navigation span those vaults. + +The selected/default vault remains the write target for new notes and Type documents when `defaultWorkspacePath` points at an available mounted vault. Git status, changes, AutoGit checkpointing, and sync operate across the active mounted repository set, while history, diff, repair, and file operations still resolve explicit repository roots from the selected surface or entry provenance. Saved Views are listed from every mounted vault with source-vault identity, so duplicate view filenames remain separate and edits persist back to the view's owning vault. + +The bottom-left `VaultMenu` exposes quick include/exclude controls and a `Manage vaults` entry. The Vaults settings section owns the full identity controls: display name, short label, read-only alias, accent color, removal, and default destination for new notes. + +## Directory Structure + +``` +tolaria/ +├── src/ # React frontend +│ ├── main.tsx # Entry point (renders ) +│ ├── App.tsx # Root component — wires layout + state hooks +│ ├── App.css # App shell layout styles +│ ├── types.ts # Shared TS types (VaultEntry, Settings, etc.) +│ ├── mock-tauri.ts # Mock Tauri layer for browser testing +│ ├── theme.json # Editor typography theme configuration +│ ├── index.css # Semantic app theme variables + Tailwind setup +│ │ +│ ├── components/ # UI components (~100 files) +│ │ ├── Sidebar.tsx # Left panel: filters + type groups +│ │ ├── SidebarParts.tsx # Sidebar subcomponents +│ │ ├── NoteList.tsx # Second panel: filtered note list +│ │ ├── NoteItem.tsx # Individual note item +│ │ ├── PulseView.tsx # Git activity feed (replaces NoteList) +│ │ ├── Editor.tsx # Third panel: editor orchestration +│ │ ├── EditorContent.tsx # Editor content area +│ │ ├── EditorRightPanel.tsx # Right panel toggle +│ │ ├── editorSchema.tsx # BlockNote schema + wikilink type +│ │ ├── RawEditorView.tsx # CodeMirror raw editor +│ │ ├── Inspector.tsx # Fourth panel: metadata + relationships +│ │ ├── DynamicPropertiesPanel.tsx # Editable frontmatter properties +│ │ ├── AiWorkspace.tsx # Multi-chat AI workspace orchestration (docked or native window) +│ │ ├── AiWorkspaceChrome.tsx # AI workspace header and vault-guidance chrome +│ │ ├── AiWorkspaceResizeHandles.tsx # AI workspace edge resize handles +│ │ ├── AiWorkspaceSideHeader.tsx # Side-mode AI workspace chat tabs and chrome +│ │ ├── AiPanel.tsx # AI transcript/composer surface (selected target + per-vault permission mode) +│ │ ├── AiMessage.tsx # Agent message display +│ │ ├── AiActionCard.tsx # Agent tool action cards +│ │ ├── AiAgentsOnboardingPrompt.tsx # First-launch AI agent installer prompt +│ │ ├── SearchPanel.tsx # Search interface +│ │ ├── SettingsPanel.tsx # App settings +│ │ ├── StatusBar.tsx # Bottom bar: vault picker + sync +│ │ ├── CommandPalette.tsx # Cmd+K command launcher +│ │ ├── BreadcrumbBar.tsx # Breadcrumb + word count + actions +│ │ ├── WelcomeScreen.tsx # Onboarding screen +│ │ ├── LinuxTitlebar.tsx # Linux/Windows custom window chrome + controls +│ │ ├── LinuxMenuButton.tsx # Linux titlebar menu mirroring app commands +│ │ ├── CloneVaultModal.tsx # Clone a vault from any git URL +│ │ ├── AddRemoteModal.tsx # Connect a local-only vault to a remote later +│ │ ├── ConflictResolverModal.tsx # Git conflict resolution +│ │ ├── CommitDialog.tsx # Git commit modal +│ │ ├── CreateNoteDialog.tsx # New note modal +│ │ ├── CreateTypeDialog.tsx # New type modal +│ │ ├── UpdateBanner.tsx # In-app update notification +│ │ ├── inspector/ # Inspector sub-panels +│ │ │ ├── BacklinksPanel.tsx +│ │ │ ├── RelationshipsPanel.tsx +│ │ │ ├── GitHistoryPanel.tsx +│ │ │ └── ... +│ │ └── ui/ # shadcn/ui primitives +│ │ ├── button.tsx, dialog.tsx, input.tsx, ... +│ │ +│ ├── hooks/ # Custom React hooks (~90 files) +│ │ ├── useVaultLoader.ts # Loads vault entries + content +│ │ ├── useVaultSwitcher.ts # Multi-vault management +│ │ ├── useVaultConfig.ts # Per-vault UI settings +│ │ ├── useNoteActions.ts # Composes creation + rename + frontmatter +│ │ ├── useNoteCreation.ts # Note/type creation +│ │ ├── useNoteRename.ts # Note renaming + wikilink updates +│ │ ├── useCliAiAgent.ts # Selected AI agent state + normalized session pipeline +│ │ ├── aiAgentPermissionMode.ts # Safe/Power User mode normalization + labels +│ │ ├── useAiAgentsStatus.ts # Claude/Codex/OpenCode/Pi/Antigravity/Kiro availability polling +│ │ ├── useAiAgentPreferences.ts # Default-agent persistence + cycling +│ │ ├── useAiActivity.ts # MCP UI bridge listener +│ │ ├── useAutoSync.ts # Auto git pull/push +│ │ ├── useConflictResolver.ts # Git conflict handling +│ │ ├── useEditorSave.ts # Auto-save with debounce +│ │ ├── useTheme.ts # Flatten theme.json → CSS vars +│ │ ├── useUnifiedSearch.ts # Keyword search +│ │ ├── useNoteSearch.ts # Note search +│ │ ├── useCommandRegistry.ts # Command palette registry +│ │ ├── useAppCommands.ts # App-level commands +│ │ ├── useAppKeyboard.ts # Keyboard shortcuts +│ │ ├── appCommandCatalog.ts # Shortcut combos + command metadata +│ │ ├── appCommandDispatcher.ts # Shared shortcut/menu command IDs + dispatch +│ │ ├── useSettings.ts # App settings +│ │ ├── useGettingStartedClone.ts # Shared Getting Started clone action +│ │ ├── useOnboarding.ts # First-launch flow +│ │ ├── useCodeMirror.ts # CodeMirror raw editor +│ │ ├── useMcpBridge.ts # MCP WebSocket client +│ │ ├── useMcpStatus.ts # Explicit external AI tool connection status + connect/disconnect actions +│ │ ├── useUpdater.ts # In-app updates +│ │ └── ... +│ │ +│ ├── utils/ # Pure utility functions (~48 files) +│ │ ├── wikilinks.ts # Wikilink preprocessing pipeline +│ │ ├── frontmatter.ts # TypeScript YAML parser +│ │ ├── plainTextPaste.ts # Shared Paste without Formatting command target registry +│ │ ├── platform.ts # Runtime platform + Linux chrome gating helpers +│ │ ├── ai-agent.ts # Agent stream utilities +│ │ ├── ai-chat.ts # Token estimation utilities +│ │ ├── ai-context.ts # Context snapshot builder +│ │ ├── noteListHelpers.ts # Sorting, filtering, date formatting +│ │ ├── wikilink.ts # Wikilink resolution +│ │ ├── configMigration.ts # localStorage → vault config migration +│ │ ├── iconRegistry.ts # Phosphor icon registry +│ │ ├── propertyTypes.ts # Property type definitions +│ │ ├── vaultListStore.ts # Vault list persistence +│ │ ├── vaultConfigStore.ts # Vault config store +│ │ └── ... +│ │ +│ ├── lib/ +│ │ ├── aiAgents.ts # Shared agent registry + status helpers +│ │ ├── appUpdater.ts # Frontend wrapper around channel-aware updater commands +│ │ ├── i18n.ts # App-owned localization runtime and locale resolution +│ │ ├── locales/ # JSON locale catalogs (English source + translated locales) +│ │ ├── releaseChannel.ts # Alpha/stable normalization helpers +│ │ └── utils.ts # Tailwind merge + cn() helper +│ │ +│ └── test/ +│ └── setup.ts # Vitest test environment setup +│ +├── src-tauri/ # Rust backend +│ ├── Cargo.toml # Rust dependencies +│ ├── build.rs # Tauri build script +│ ├── tauri.conf.json # Tauri app configuration +│ ├── capabilities/ # Tauri v2 security capabilities +│ ├── src/ +│ │ ├── main.rs # Entry point (calls lib::run()) +│ │ ├── lib.rs # Tauri setup + command registration +│ │ ├── commands/ # Tauri command handlers (split into modules) +│ │ ├── vault/ # Vault module +│ │ │ ├── mod.rs # Core types, parse_md_file, scan_vault +│ │ │ ├── cache.rs # Git-based incremental caching +│ │ │ ├── parsing.rs # Text processing + title extraction +│ │ │ ├── rename.rs # Rename + cross-vault wikilink update +│ │ │ ├── image.rs # Image attachment saving +│ │ │ ├── migration.rs # Frontmatter migration +│ │ │ └── getting_started.rs # Getting Started vault clone orchestration +│ │ ├── frontmatter/ # Frontmatter module +│ │ │ ├── mod.rs, yaml.rs, ops.rs +│ │ ├── git/ # Git module +│ │ │ ├── mod.rs, command.rs, remote_config.rs, commit.rs, status.rs +│ │ │ ├── history.rs, clone.rs, connect.rs, conflict.rs, remote.rs, pulse.rs +│ │ ├── telemetry.rs # Sentry init + path scrubber +│ │ ├── search.rs # Keyword search (walkdir-based) +│ │ ├── ai_agents.rs # CLI-agent request normalization + adapter dispatch +│ │ ├── cli_agent_runtime.rs # Shared CLI-agent runtime process/prompt/MCP helpers +│ │ ├── claude_cli.rs # Claude CLI adapter +│ │ ├── codex_cli.rs # Codex CLI adapter +│ │ ├── pi_cli.rs # Pi CLI adapter +│ │ ├── kiro_cli.rs # Kiro CLI adapter +│ │ ├── mcp.rs # MCP server lifecycle + explicit config registration/removal +│ │ ├── app_updater.rs # Alpha/stable updater metadata resolution +│ │ ├── settings.rs # App settings persistence +│ │ ├── vault_config.rs # Per-vault UI config +│ │ ├── vault_list.rs # Vault list persistence +│ │ └── menu.rs # Native macOS menu bar +│ └── icons/ # App icons +│ +├── mcp-server/ # MCP bridge (Node.js or Bun) +│ ├── index.js # MCP server entry (stdio tools) +│ ├── vault.js # Vault file operations +│ ├── ws-bridge.js # WebSocket bridge (ports 9710, 9711) +│ ├── test.js # MCP server tests +│ └── package.json +│ +├── e2e/ # Playwright E2E tests (~26 specs) +├── tests/smoke/ # Playwright specs (full regression + @smoke subset) +├── design/ # Per-task design files +├── demo-vault-v2/ # Curated local QA fixture for native/dev flows +├── scripts/ # Build/utility scripts +│ +├── package.json # Frontend dependencies + scripts +├── lara.yaml # Lara CLI locale sync configuration +├── vite.config.ts # Vite bundler config +├── tsconfig.json # TypeScript config +├── playwright.config.ts # Full Playwright regression config +├── playwright.smoke.config.ts # Curated pre-push Playwright config +├── ui-design.pen # Master design file +├── AGENTS.md # Canonical shared instructions for coding agents +├── CLAUDE.md # Claude Code compatibility shim importing AGENTS.md as an organized Note +└── docs/ # This documentation +``` + +## Key Files to Know + +### Fixtures + +- `demo-vault-v2/` is the small checked-in QA fixture used for native/manual Tolaria flows. It is intentionally curated around a handful of search, relationship, project-navigation, and attachment scenarios. +- `tests/fixtures/test-vault/` is the deterministic Playwright fixture copied into temp directories for isolated integration and smoke tests. +- `python3 scripts/generate_demo_vault.py` generates the larger synthetic vault on demand at `generated-fixtures/demo-vault-large/` for scale/performance experiments. That output is gitignored and should not bloat the normal QA fixture. + +### Start here + +| File | Why it matters | +|------|---------------| +| `src/App.tsx` | Root component. Shows the 4-panel layout, state flow, and how orchestration hooks connect. | +| `src/types.ts` | All shared TypeScript types. Read this first to understand the data model. | +| `src-tauri/src/commands/` | Tauri command handlers (split into modules). This is the frontend-backend API surface. | +| `src-tauri/src/lib.rs` | Tauri setup, command registration, startup tasks, WebSocket bridge lifecycle. | + +### Data layer + +| File | Why it matters | +|------|---------------| +| `src/hooks/useVaultLoader.ts` | How vault data is loaded and managed. The Tauri/mock branching pattern. | +| `src/hooks/useNoteActions.ts` | Orchestrates note operations: composes `useNoteCreation`, `useNoteRename`, frontmatter CRUD, and wikilink navigation. | +| `src/hooks/useVaultSwitcher.ts` | Multi-vault management, vault switching, and persisting cloned vaults in the switcher list. | +| `src/hooks/useGettingStartedClone.ts` | Shared "Clone Getting Started Vault" action for the status bar and command palette. | +| `src/hooks/useNoteWindowLifecycle.ts` | Note-window URL opening, asset-scope sync, and window-title updates. | +| `src/hooks/useVaultRenameDetection.ts` | Focus-triggered Git rename detection and wikilink update action wiring. | +| `src/hooks/useStartupScreenState.ts` | Startup-screen and vault-content loading visibility decisions. | +| `src/hooks/useGitFileWorkflows.ts` | Git diff/history/discard wiring and deleted-note preview workflow. | +| `src/components/AddRemoteModal.tsx` | Modal UI for connecting a local-only vault to a compatible remote. | +| `src/mock-tauri.ts` | Mock data for browser testing. Shows the shape of all Tauri responses. | + +### Backend + +| File | Why it matters | +|------|---------------| +| `src-tauri/src/vault/mod.rs` | Vault scanning, frontmatter parsing, entity type inference, relationship extraction. | +| `src-tauri/src/vault/cache.rs` | Git-based incremental caching — how large vaults load fast. | +| `src-tauri/src/frontmatter/ops.rs` | YAML manipulation — how properties are updated/deleted in files. | +| `src-tauri/src/git/` | All git operations (clone, commit, pull, push, conflicts, pulse, add-remote). | +| `src-tauri/src/search.rs` | Keyword search — scans vault files with walkdir. | +| `src-tauri/src/ai_agents.rs` | CLI-agent request normalization, availability aggregation, adapter dispatch, and Claude event mapping. | +| `src-tauri/src/cli_agent_runtime.rs` | Shared CLI-agent request shape, prompt wrapping, JSON subprocess lifecycle, version probing, and MCP path helpers. | +| `src-tauri/src/claude_cli.rs`, `src-tauri/src/codex_cli.rs`, `src-tauri/src/opencode_cli.rs`, `src-tauri/src/pi_cli.rs`, `src-tauri/src/antigravity_cli.rs`, `src-tauri/src/kiro_cli.rs` | Per-agent command, config, discovery, and event adapters. | +| `src-tauri/src/app_updater.rs` | Desktop updater bridge — resolves alpha/stable manifests and streams install progress. | + +### Editor + +| File | Why it matters | +|------|---------------| +| `src/components/Editor.tsx` | BlockNote setup, breadcrumb bar, diff/raw toggle. | +| `src/components/SingleEditorView.tsx` | Shared BlockNote shell, Tolaria formatting controllers, and suggestion menus. | +| `src/components/editorSchema.tsx` | Custom wikilink inline content type definition. | +| `src/components/tolariaEditorFormatting.tsx` | Markdown-safe formatting toolbar surface for BlockNote. | +| `src/components/tolariaEditorFormattingConfig.ts` | Filters toolbar and slash-menu commands to markdown-roundtrippable actions. | +| `src/utils/wikilinks.ts` | Wikilink preprocessing pipeline (markdown ↔ BlockNote). | +| `src/components/RawEditorView.tsx` | CodeMirror 6 raw markdown editor. | + +### AI + +| File | Why it matters | +|------|---------------| +| `src/components/AiWorkspace.tsx` | Multi-chat AI workspace orchestration — chat sessions, sidebar tabs, target/permission controls, and dock/pop-out wiring. | +| `src/components/AiWorkspaceChrome.tsx` | Header and vault-guidance chrome shared by docked and popped-out AI workspace modes. | +| `src/components/AiWorkspaceResizeHandles.tsx` | Edge resize affordances for docked/side AI workspace layouts. | +| `src/components/AiWorkspaceSideHeader.tsx` | Side-mode AI workspace chat tabs, rename controls, and compact header chrome. | +| `src/components/aiWorkspaceConversations.ts` | Conversation metadata state, settings persistence, default title generation, and target resolution. | +| `src/components/aiWorkspaceSizing.ts` | AI workspace sizing, localStorage persistence, class names, and layout style helpers. | +| `src/components/AiPanel.tsx` | Reusable AI transcript/composer surface — selected target with tool execution, reasoning, actions, and per-vault permission mode. | +| `src/utils/openAiWorkspaceWindow.ts` | Native Tauri AI workspace window creation, focus, and dock-back traffic-light handling. | +| `src/hooks/useCliAiAgent.ts` | Thin React owner for the selected CLI agent session state. | +| `src/lib/aiAgentSession.ts` | Single message/session lifecycle for prompt normalization, history, streaming, and reset behavior. | +| `src/lib/aiAgentPermissionMode.ts` | Safe/Power User mode normalization, display labels, and local transcript marker text. | +| `src/lib/aiAgentFileOperations.ts` | Detects agent-created or modified vault files from normalized tool inputs. | +| `src/lib/aiAgents.ts` | Supported agent definitions, status normalization, and default-agent helpers. | +| `src/utils/ai-context.ts` | Context snapshot builder for AI conversations. | + +### Styling + +| File | Why it matters | +|------|---------------| +| `src/index.css` | Semantic CSS custom properties for app-owned light/dark themes; System mode resolves to one of these at runtime. | +| `src/theme.json` | Editor-specific typography theme (fonts, headings, lists, code blocks). | + +### Settings & Config + +| File | Why it matters | +|------|---------------| +| `src/hooks/useSettings.ts` | App settings (telemetry, release channel, theme mode, UI language, date display format, Git visibility, auto-sync interval, default note width, sidebar type pluralization, default AI agent). | +| `src/lib/releaseChannel.ts` | Normalizes persisted updater-channel values (`stable` default, optional `alpha`). | +| `src/lib/appUpdater.ts` | Frontend wrapper for channel-aware updater commands. | +| `src/hooks/useMainWindowSizeConstraints.ts` | Derives the main-window minimum width from the visible panes and asks Tauri to grow back to fit wider layouts. | +| `src/hooks/useVaultConfig.ts` | Per-vault local UI preferences (zoom, view mode, colors, Inbox columns, explicit organization workflow, Git setup prompt preference, AI permission mode). | +| `src/components/SettingsPanel.tsx` | Settings UI for telemetry, release channel, Git visibility, sync interval, UI language, content display preferences, default AI agent, and the vault-level explicit organization toggle. | +| `src/hooks/useUpdater.ts` | In-app updates using the selected alpha/stable feed. | + +## Architecture Patterns + +### Tauri/Mock Branching + +Every data-fetching operation checks `isTauri()` and branches: + +```typescript +if (isTauri()) { + result = await invoke('command', { args }) +} else { + result = await mockInvoke('command', { args }) +} +``` + +This lives in `useVaultLoader.ts` and `useNoteActions.ts`. Components never call Tauri directly. + +### Props-Down, Callbacks-Up + +No global state management (no Redux, no Context). `App.tsx` owns the state and passes it down as props. Child-to-parent communication uses callback props (`onSelectNote`, etc.). + +### Discriminated Unions for Selection State + +```typescript +type SidebarSelection = + | { kind: 'filter'; filter: SidebarFilter } + | { kind: 'sectionGroup'; type: string } + | { kind: 'folder'; path: string } + | { kind: 'entity'; entry: VaultEntry } + | { kind: 'view'; filename: string } +``` + +### Command Registry + +`useCommandRegistry` + `useAppCommands` build a centralized command registry. Commands are registered with labels, shortcuts, and handlers. The `CommandPalette` (Cmd+K) fuzzy-searches this registry. Settings commands can update installation-local preferences directly when they reuse an existing settings path, such as the Light/Dark/System theme-mode actions writing `settings.theme_mode`. Shortcut combos live in `appCommandCatalog.ts`; real keypresses always flow through `useAppKeyboard`, native menu clicks emit the same command IDs through `useMenuEvents`, and `appCommandDispatcher.ts` suppresses the duplicate native/renderer echo from a single shortcut. Plain-text paste follows this same path: the command owns `Cmd+Shift+V`, the menu and palette expose the same action, and `plainTextPaste.ts` resolves the active rich/raw editor target or focused text control before reading clipboard text. On macOS, any browser-reserved chord that WKWebView swallows before that path must also be added to the narrow `tauri-plugin-prevent-default` registration in `src-tauri/src/lib.rs`. On Linux and Windows, `LinuxTitlebar.tsx` and `LinuxMenuButton.tsx` reuse the same command IDs through `trigger_menu_command` because those builds use Tolaria's custom chrome instead of the native desktop menu bar. The same shortcut manifest also declares the deterministic QA mode for each shortcut-capable command. + +Commands whose availability depends on the current note or Git state must also flow through `update_menu_state` so the native menu stays in sync with the command palette. The deleted-note restore action in Changes view is the reference example: the row opens a deleted diff preview, the command palette exposes "Restore Deleted Note", and the Note menu enables the same action only while that preview is active. + +Current-note find/replace is a surface-aware command: editor focus enables "Find in Note" / "Replace in Note" and routes Cmd+F into raw CodeMirror mode; note-list focus enables existing note-list search instead. When adding another focus-dependent command, mirror this pattern with an availability event consumed by `useMenuEvents.ts` and `update_menu_state`. + +For automated shortcut QA, use the explicit proof path from `appCommandCatalog.ts`: + +- `window.__laputaTest.triggerShortcutCommand()` for deterministic renderer shortcut-event coverage +- `window.__laputaTest.triggerMenuCommand()` for deterministic native menu-command coverage + +That browser harness is a deterministic desktop command bridge, not real native accelerator QA. For macOS browser-reserved chords, still perform native QA in the real Tauri app because the webview-init prevent-default layer is only active there. Do not treat flaky synthesized macOS keystrokes as proof that a shortcut works unless you also confirm the visible app behavior. + +## Running Tests + +```bash +# Unit tests (fast, no browser) +pnpm test + +# Unit tests with coverage (must pass ≥70%) +pnpm test:coverage + +# Rust tests +cargo test + +# Rust coverage (must pass ≥85% line coverage) +cargo llvm-cov --manifest-path src-tauri/Cargo.toml --no-clean --fail-under-lines 85 + +# Playwright core smoke lane (requires dev server) +BASE_URL="http://localhost:5173" pnpm playwright:smoke + +# Full Playwright regression suite +BASE_URL="http://localhost:5173" pnpm playwright:regression + +# Single Playwright test +BASE_URL="http://localhost:5173" npx playwright test tests/smoke/.spec.ts +``` + +## Common Tasks + +### Add a new Tauri command + +1. Write the Rust function in the appropriate module (`vault/`, `git/`, etc.) +2. Add a command handler in `commands/` +3. Register it in the `generate_handler![]` macro in `lib.rs` +4. Call it from the frontend via `invoke()` in the appropriate hook or utility, keeping native-only permission work behind the Tauri command boundary +5. Add a mock handler in `mock-tauri.ts` + +### Add a new component + +1. Create `src/components/MyComponent.tsx` +2. If it needs vault data, receive it as props from the parent +3. Wire it into `App.tsx` or the relevant parent component +4. Add a test file `src/components/MyComponent.test.tsx` + +### Add a new entity type + +1. Create a type document at the vault root: `mytype.md` with `type: Type` frontmatter (icon, color, order, etc.) +2. The sidebar section groups are auto-generated from type documents — no code change needed if `visible: true` +3. Update `CreateNoteDialog.tsx` type options if users should be able to create it from the dialog +4. Notes of this type are created at the vault root with `type: MyType` in frontmatter — no dedicated folder needed + +### Add a command palette entry + +1. Register the command in `useAppCommands.ts` via the command registry +2. Add a corresponding menu bar item in `menu.rs` for discoverability +3. If it has a keyboard shortcut, register it in `appCommandCatalog.ts` with the canonical command ID, modifier rule, and deterministic QA mode, then wire the matching native menu item in `menu.rs` if it should also appear in the menu bar +4. If its enabled state depends on runtime selection (active note, deleted preview, Git status, etc.), thread that flag through `useMenuEvents.ts` and `update_menu_state` so the native menu enables/disables correctly + +### Modify styling + +1. **Global app/theme variables**: Edit `src/index.css` +2. **Editor typography**: Edit `src/theme.json` + +### Work with the AI agent + +1. **Agent system prompt**: Edit `src/utils/ai-agent.ts` (inline system prompt string) +2. **Context building**: Edit `src/utils/ai-context.ts` for what data is sent to the agent +3. **Tool action display**: Edit `src/components/AiActionCard.tsx` +4. **Permission-mode UI and request plumbing**: Edit `src/lib/aiAgentPermissionMode.ts`, `src/components/AiPanel*.tsx`, `src/hooks/useCliAiAgent.ts`, and `src/utils/streamAiAgent.ts` +5. **Shared CLI runtime behavior**: Edit `src-tauri/src/cli_agent_runtime.rs` for process lifecycle, prompt wrapping, version probing, and common Tolaria MCP path handling. +6. **Agent-specific arguments/events**: Edit the per-agent adapter modules (`claude_cli.rs`, `codex_cli.rs`, `opencode_*`, `pi_*`, `antigravity_*`, `kiro_*`). Keep Codex Safe on `read-only` + `untrusted` and Codex Power User on active-vault `workspace-write` + `never`, keep Pi, Antigravity, and Kiro on transient MCP config, and do not use dangerous permission bypasses unless an ADR explicitly designs a new mode. Pi's transient agent directory must be seeded from the user's existing Pi agent directory before Tolaria MCP is merged so standalone provider/auth setup keeps working. Antigravity Safe uses sandboxed `proceed-in-sandbox`, Power User uses `always-proceed` without `--dangerously-skip-permissions`, and workspace MCP config lives in `.agents/mcp_config.json`. Kiro receives prompt content over stdin and writes Tolaria MCP config into `.kiro/settings/mcp.json` in the active vault. +7. **Availability probing**: Edit `src/hooks/useAiAgentsStatus.ts` and `src-tauri/src/ai_agents.rs` for AI-agent install/status detection. Keep renderer probing deferred until after first paint, skip it when AI features or AI surfaces are unavailable, and keep backend per-agent CLI checks parallel so missing tools do not serialize shell startup cost. + +### Work with external MCP setup + +1. **Backend registration/status/snippets**: Edit `src-tauri/src/mcp.rs` and its `src-tauri/src/mcp/` helpers; registration and manual config generation must resolve an MCP runtime via `find_mcp_runtime` (Node.js 18+ preferred, Bun 1+ fallback) first, resolve the packaged `mcp-server/` for macOS, Windows executable-adjacent installs such as `%LOCALAPPDATA%\Tolaria`, Linux package roots (`/usr/local/Tolaria`, `/usr/local/lib/tolaria`, `/usr/lib/tolaria`, `/usr/lib/tolaria/resources`), and AppImage installs, and use a vault-neutral entry with `WS_UI_PORT=9711`. Client-facing Node script paths strip Windows extended-length `\\?\` prefixes before Tolaria writes durable config or transient agent entries, because stdio MCP clients pass that argument back to Node as the main module path. Linux AppImage startup must extract `mcp-server/` to `~/.local/share/tolaria/mcp-server/` before durable registration uses that stable path. App-owned bridge launches still pass `VAULT_PATH`/`VAULT_PATHS`; durable external registrations rely on the MCP server reading `vaults.json` at tool-call time. +2. **Setup dialog copy/actions**: Edit `src/components/McpSetupDialog.tsx` and `src/hooks/useMcpStatus.ts`; users should see the runtime prerequisite (Node.js 18+ or Bun 1+), the exact generated standard `mcpServers` manual config, the exact generated OpenCode top-level `mcp` config, and copy actions before Tolaria writes third-party config files +3. **Status hook/toasts**: Edit `src/hooks/useMcpStatus.ts` when setup, reconnect, disconnect, or failure messaging changes +4. **Antigravity CLI compatibility**: Keep `~/.gemini/config/mcp_config.json` in the registration path list and keep optional `GEMINI.md` generation behind `restore_vault_ai_guidance`; app-managed Antigravity sessions still require the user to install and sign in to `agy`, but Tolaria supplies workspace MCP config when Antigravity is selected as the default AI agent +5. **OpenCode compatibility**: Keep `~/.config/opencode/opencode.json` in durable registration. OpenCode uses the top-level `mcp` key, `command` as an array, `environment` for env vars, `type: "local"`, and `enabled: true`; it must remain vault-neutral like the standard `mcpServers` entry. +6. **Process lifecycle and vault guidance**: Stdio MCP servers in `mcp-server/index.js` must exit when their external client closes stdin, and the desktop-owned `ws-bridge.js` child must be stopped on vault deselection, vault switch, and app exit. MCP context must include root `AGENTS.md` instructions for every active mounted workspace when those files exist. diff --git a/docs/PUBLIC-DOCS-PLAN.md b/docs/PUBLIC-DOCS-PLAN.md new file mode 100644 index 0000000..d10dd59 --- /dev/null +++ b/docs/PUBLIC-DOCS-PLAN.md @@ -0,0 +1,47 @@ +# Public Docs Plan + +This document records the phase 1 information architecture for public Tolaria documentation. The public docs source lives in `site/`; the existing `docs/` directory remains contributor, architecture, and agent context. + +## Audiences + +| Audience | Needs | Primary location | +|---|---|---| +| New users | Install, first launch, understand the app layout, clone the starter vault | `site/start/` | +| Active users | Learn concrete workflows such as organizing, Git sync, custom views, and AI | `site/guides/` | +| Power users | Understand file layout, frontmatter, filters, release channels, shortcuts, and platform support | `site/reference/` | +| Contributors and agents | Architecture, abstractions, ADRs, development workflow | `docs/`, `AGENTS.md` | + +## Hosting Shape + +The GitHub Pages output should reserve the root for public docs and mount release assets underneath it: + +```text +/ public docs home +/releases/ release history +/download/ latest stable download redirect +/stable/latest.json +/alpha/latest.json +/latest.json compatibility alias for alpha latest +/latest-canary.json compatibility alias for alpha latest +``` + +## Current Coverage + +The phase 1 site now covers post-branch features added after the original April docs snapshot: + +- Windows and Linux release artifacts. +- Stable and Alpha updater channels. +- Direct AI model providers and local/API model setup. +- Claude Code, Codex, OpenCode, Pi, and Antigravity CLI agent targets. +- Explicit MCP setup for external AI tools. +- Table of contents, note width, raw mode, and paste-without-formatting workflows. +- Media/PDF previews, image attachments, All Notes visibility, and Markdown whiteboards. +- System theme mode and sidebar pluralization settings. + +Every user-visible app change should answer: + +```text +Public docs impact: +- updated: +- not needed because: +``` diff --git a/docs/VISION.md b/docs/VISION.md new file mode 100644 index 0000000..fd67a35 --- /dev/null +++ b/docs/VISION.md @@ -0,0 +1,195 @@ +# Tolaria — Product Vision + +*Written by Brian based on conversations with Luca Rossi, Feb–Mar 2026.* +*This is a living document — update it as the vision evolves.* + +--- + +## Why this, why now, why us + +Before the what and how: the why. + +The best projects are built by people who have an unusually strong answer to "why are you the right person to build this?" This is that answer. + +**Luca Rossi** is a startup founder and former generalist CTO — someone who can build a product end-to-end across code, design, scope, and product. And for the last five years, full-time, he has run Refactoring: a technical newsletter with nearly 200,000 subscribers, for which he has written over 300 original articles. In word count, that's roughly two *Lord of the Rings* novels. + +Personal knowledge management has been an obsession since university. But over the last five years it stopped being a hobby and became *table stakes* — the system that makes writing 300 articles possible. Tolaria is an attempt to bottle that system. + +The credibility is real: if you wonder whether this person knows how to organize knowledge for sustained output, the output speaks for itself. The method inside Tolaria is not theorized — it's been battle-tested for years at scale. + +**The distribution is built in.** Refactoring reaches ~200,000 engineers, managers, and technical leaders — exactly the people most receptive to a tool like this. The audience already trusts the author on this topic, because they've been reading his writing about knowledge management and learning for years. + +This is not a product looking for a market. It's a tool built by its first power user, for an audience that already knows and trusts him. + +**Why Tolaria, in the context of Refactoring.** + +Refactoring is a newsletter about how software is built, how teams work, and how digital products are developed — written from Luca's experience and conversations with other tech leaders. A natural question follows: what is the author's own current experience building software with AI? + +Tolaria answers that question directly and publicly. If it works — if it becomes a real product used by real people — it validates the author's capabilities and authority to write about these topics. Not as theory, but as demonstrated practice. Anyone can look at the GitHub repository, see 100 commits a day, and verify: this person actually does this. + +This is why Tolaria is **free and open source**: success becomes a reputation and acquisition channel for Refactoring. The attention and trust earned through a well-executed open source project converts — through sponsorships, paid subscriptions, and brand authority — into the business that Refactoring runs on. + +The strategy is coherent: build the tool you describe, make the work visible, let the product speak for the author. + +--- + +## The problem + +Most people who want to work effectively with AI face a version of the same problem: **they don't have their knowledge organized in a way that AI can actually use.** + +They have notes scattered across Notion, Apple Notes, browser bookmarks, and email. Some of it is structured, most of it isn't. Even the people who do maintain a knowledge base discover that AI tools — ChatGPT, Notion AI, others — struggle to navigate it meaningfully. The knowledge is there, but it's not *accessible*. + +The problem has two distinct layers: + +1. **Architectural**: most knowledge tools store data in proprietary formats on remote servers. AI tools can't read them efficiently, can't commit changes back, can't reason over the full structure. The format itself creates a ceiling. + +2. **Methodological**: even with the right tool, most people don't know *how* to organize knowledge so it becomes useful over time — what to capture, how to connect things, how to turn raw notes into a system that works with you instead of against you. + +Tolaria addresses both layers, together. That's what makes it different. + +--- + +## The insight: tool and method, together + +Most PKM tools give you a blank canvas and leave the rest to you. They solve the first problem (somewhere to put things) but not the second (how to organize them). The result is that sophisticated users build complex custom systems, while everyone else gives up. + +Tolaria's position is different: **we ship the method alongside the tool.** + +The method is opinionated but not rigid. It tells you: here's how to think about your work, here's where different kinds of notes belong, here's how to connect them. If it fits your needs — great, start immediately. If your situation is different — customize it. The types, the relationships, the structure can all be changed. But you don't have to figure it out from scratch. + +This combination — an opinionated method on top of a technically excellent foundation — is what makes Tolaria genuinely useful to people who are stuck, not just people who already know what they're doing. + +--- + +## The method: a framework for knowledge work + +### The knowledge ontology + +Tolaria organizes work around two axes: + +| | **One-time** | **Recurring** | +|---|---|---| +| **Multi-session** | **Project** (has a start and end) | **Responsibility** (no end, measured by KPIs) | +| **Single-session** | *Task* (lives in a task manager) | **Procedure** (checklist, routine) | + +Everything else is context: +- **Notes** — the atomic unit. Any note connects to one or more of the above. +- **Topics** — areas of interest with no performance expectation. A knowledge repository. +- **Events** — things that happened, anchored to a date. +- **People** — contacts and their history. + +Relations between notes are first-class citizens — not just wiki-links, but typed, bidirectional connections that make the knowledge graph navigable. + +This ontology is not arbitrary. It maps cleanly to how both individuals and organizations actually structure their work: companies have projects, responsibilities, procedures, and people. So do independent creators. So do individuals managing their lives. + +### Knowledge has a purpose + +A principle that underlies everything in Tolaria: **notes exist to get things done.** Not to be stored for some abstract future use. Not to show how organized you are. To do something. + +This is the difference between a knowledge system that works over years and one that collapses after a few weeks. Without a real purpose, the maintenance cost of taking notes is never justified, and people stop. With a purpose — writing regularly, building things, making decisions — the system pays for itself. + +What you *do* with organized knowledge depends on who you are: + +- **Writers and content creators** — the output is articles, essays, posts. Captures become highlights, highlights become **evergreen notes** (small, atomic, timeless ideas), evergreen notes become building blocks for articles. Evergreen notes are a middle layer: not the raw input, not the final output, but the refined reusable units that make writing easier and faster. +- **Builders and project-driven people** — the output is shipped work. Captures feed projects, decisions, and procedures. Evergreen notes matter less; the project knowledge graph matters more. +- **Operators and managers** — the output is better systems and decisions. Captures feed responsibilities (KPIs, workflows) and procedures (how we do things). The value accumulates in the recurring structure, not in individual notes. + +The framework is flexible enough to fit all three — and most people are a mix. What stays constant is the flow: **capture → organize → express**. The *what* of expressing changes; the discipline doesn't. + +### The two-phase workflow: capture and organize + +Notes move through two distinct phases, and the transition between them is intentional. + +**Capture** — fast, frictionless, available everywhere. A thought, a saved article, a Kindle highlight, a voice memo. The cardinal rule: never let friction during capture cause a good idea to be lost. Captured notes land in the vault unconnected — no relationships, no organization. That's fine. That's the point. + +**Organize** — a deliberate, periodic activity (weekly is the natural cadence). You ask: *what is this useful for?* Many things that seemed important when captured won't survive this question — deleting >50% of captures is normal and healthy. For the things that survive: connect them. Link to a Project, a Responsibility, a Topic. Every note should eventually connect to at least one actionable container. If you can't connect something to anything, that's a signal worth paying attention to. + +**The Inbox** is the UI expression of this split: a smart section that shows all unorganized notes — those with no outgoing relationships. The goal is Inbox Zero, reached periodically (weekly). The inbox is not a folder; it's a derived state. Connecting a note to something removes it automatically. + +### Convention over configuration + +The method lives in the app as *conventions*: standard field names and folder structures that have well-defined meanings and trigger specific behavior. + +`status:` shows a colored chip. `Workspace: [[workspace/refactoring]]` assigns a note to a context. `Belongs to:` connects it to its parent. `start_date:` and `end_date:` show a duration badge. The app recognizes these by convention, without any setup. + +Users who want more can override the defaults: `config/relations.md` changes which relationship fields appear by default; `config/semantic-properties.md` controls how fields are rendered. But the defaults work immediately, for everyone. + +This is convention *over* configuration — not convention *instead of* it. + +--- + +## The foundation: architecture that earns trust + +The method is only as good as the system it runs on. Tolaria's architecture is built around a single principle: **your knowledge is yours, permanently and unconditionally.** + +### Local files, version-controlled with Git + +Every note is a plain Markdown file on your disk. There is no database, no proprietary format, no sync lock-in. The files are readable by any tool that can open a text file — today and in twenty years. + +Git provides version control: every change is tracked, diffable, reversible. You have a full audit trail of what changed, when, and why. Collaboration happens via Git — the same way software teams have collaborated for decades, without any proprietary cloud in between. + +### AI-native by design + +A vault of plain Markdown files, version-controlled with Git, is dramatically more AI-friendly than any SaaS-based system. + +An AI agent working on a local vault can read thousands of notes in seconds, understand their structure, write new ones, connect existing ones, and commit the changes back — all with full comprehension. Notion's AI can't do this. No SaaS-based AI can do this, because the architecture doesn't allow it. + +More importantly: the more a vault follows Tolaria's conventions, the *less configuration an AI needs* to navigate it. Shared conventions make knowledge legible to both humans and AI without bespoke instructions for every setup. The method and the AI-native architecture reinforce each other. + +### Open and exit-friendly + +The trust between Tolaria and the user is earned daily, not enforced by format. If something better comes along, you take your Markdown files and leave. The exit door is always open. + +--- + +## Why not Obsidian? + +Obsidian is the obvious comparison. The difference is philosophy: + +- **Obsidian** is a blank canvas. Infinitely configurable via plugins and community extensions. Powerful for users who want to build their own system — and who have the time and patience to do so. +- **Tolaria** is opinionated. It ships with a complete point of view: a knowledge framework, semantic conventions, and defaults that work immediately. No plugin hunting. No configuration required to get started. + +Obsidian also treats Git as an afterthought — its business model is built around proprietary sync. In Tolaria, Git is a first-class citizen: the natural, obvious way to sync, collaborate, and maintain history. + +--- + +## Who it's for, and where it's going + +### Three stages of adoption + +Tolaria is designed to grow through three natural stages — not pivots, but extensions of the same foundation: + +**Stage 1: Personal PKM + AI context** *(current)* +A single person manages their knowledge, life, and work in a local vault. The primary collaborator is AI. The vault gives structure to one person's context, making it legible to an AI that can assist meaningfully across all areas of work and life. The method helps structure the knowledge; the AI helps use it. + +**Stage 2: Independent knowledge workers** +Content creators, freelancers, consultants. People with maximum incentive *and* maximum agency to build a real system. They have projects, clients, responsibilities — and they work alone or in very small teams. The same ontology applies: a newsletter creator has editorial projects, a subscriber-growth responsibility, and a publishing procedure. AI collaboration deepens: the AI can see not just personal notes but client commitments, content pipelines, recurring workflows. + +**Stage 3: Small teams** +The ontology scales to organizations. Companies have projects, responsibilities, procedures, and people — the same categories, at a larger scale. The access model changes: different people see different subsets of the vault, via workspace filtering and Git-based access control. Version history gives teams a full audit trail. AI agents become shared collaborators on team knowledge, not just personal assistants. + +**What makes this trajectory coherent:** the foundational model — local files, Git-versioned, structured by conventions — doesn't need to be rebuilt at each stage. It extends naturally. + +### The right early adopters + +The first users who will get the most from Tolaria are technically-minded individuals who: +- Are frustrated with Notion's performance, complexity, or lock-in +- Understand or are comfortable with Git +- Want a system that's AI-native by design, not by bolted-on features +- Value owning their data + +Broader audiences will follow as the onboarding experience matures and the conventions become easier to adopt. + +--- + +## Design principles + +1. **Opinionated but not rigid** — ship the method and the defaults; allow customization where it matters +2. **Convention over configuration** — standard field names trigger rich behavior automatically; users can override via vault config files +3. **Git-first** — sync, history, collaboration, and audit trail via Git; no proprietary cloud +4. **AI-native architecture** — local files, open formats, structured by conventions legible to both humans and AI +5. **Zero lock-in** — earn trust daily; the exit door is always open +6. **Capture and organize are separate** — the inbox makes unorganized notes visible; Inbox Zero is the discipline +7. **Relations as first-class citizens** — connections between notes are as important as the notes themselves +8. **Filesystem as the single source of truth** — the app never owns the data; cache and UI state are always derived and reconstructible +9. **Convention over system config files** — app configuration and preferences that belong to a note (e.g. type-level UI preferences) are stored in that note's frontmatter using the `_field` underscore convention, not in separate config files or localStorage. Everything that matters lives in the vault as plain text. diff --git a/docs/adr/0001-tauri-react-stack.md b/docs/adr/0001-tauri-react-stack.md new file mode 100644 index 0000000..c537726 --- /dev/null +++ b/docs/adr/0001-tauri-react-stack.md @@ -0,0 +1,31 @@ +--- +type: ADR +id: "0001" +title: "Tauri v2 + React as application stack" +status: active +date: 2026-02-14 +--- + +## Context + +Laputa is a desktop app for macOS (with iPad as a future target) that reads and writes a vault of markdown files. The app needs a native feel, filesystem access, git integration, and a rich text editor. A single developer (with AI assistance) is building it. + +## Decision + +Use **Tauri v2** (Rust backend + WebView frontend) with **React + TypeScript** for the UI, **BlockNote** for the editor, and **Vitest + Playwright** for testing. + +## Alternatives considered + +- **Electron**: heavier runtime (~150MB), slower, but more mature ecosystem. Rejected — Tauri is lighter and has better native integration. +- **SwiftUI**: best native macOS/iOS experience, but locks to Apple platforms only, no code sharing with a potential web version, and requires rewriting the entire UI. Rejected for the initial version — revisited in ADR-0005. +- **Flutter**: cross-platform but WebView-based editor would have been poor; Dart ecosystem is thin for markdown tooling. +- **Pure web app**: no filesystem access, no git, would require a backend server. Rejected — offline-first is a core principle. + +## Consequences + +- React frontend can be shared with a future web version +- Rust backend provides safe, fast filesystem/git operations +- Tauri v2 supports iOS (beta) — see ADR-0005 for iPad strategy +- CodeScene code health monitoring applies to both Rust and TypeScript code +- Claude Code can work on both layers without context switching +- Triggers re-evaluation if: Tauri iOS proves unstable for production, or if SwiftUI becomes the primary target platform diff --git a/docs/adr/0002-filesystem-source-of-truth.md b/docs/adr/0002-filesystem-source-of-truth.md new file mode 100644 index 0000000..15f5db6 --- /dev/null +++ b/docs/adr/0002-filesystem-source-of-truth.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0002" +title: "Filesystem as the single source of truth" +status: active +date: 2026-02-14 +--- + +## Context + +Laputa needs a persistence model. The core question: does the app own the data, or does the filesystem? This affects sync, conflict resolution, offline support, portability, and long-term trust with users. + +## Decision + +**The vault is the source of truth.** The app never owns the data — it only reads and writes `.md` files. All cache, React state, and in-memory representations are derived from the filesystem and must be reconstructible by deleting them. When in doubt, the file on disk wins. + +## Alternatives considered + +- **Database-first (SQLite)**: faster queries, easier relationships. Rejected — creates lock-in, makes files unreadable outside the app, complicates sync. +- **Cloud-first (proprietary sync)**: easier multi-device. Rejected — zero lock-in is a core principle; git handles sync. +- **Hybrid (DB + files)**: DB as primary, files as export. Rejected — two sources of truth always diverge. + +## Consequences + +- Notes are plain markdown files, readable and editable by any text editor +- Git provides history, sync, and collaboration for free +- Vault can be opened/edited externally without app corruption +- App rebuilds cache on startup — acceptable cost for integrity guarantees +- No "save" button needed — autosave writes to disk immediately +- Triggers re-evaluation if: vault size grows to millions of files and filesystem scanning becomes a bottleneck diff --git a/docs/adr/0003-single-note-model.md b/docs/adr/0003-single-note-model.md new file mode 100644 index 0000000..d0b368a --- /dev/null +++ b/docs/adr/0003-single-note-model.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0003" +title: "Single note open at a time (no tabs)" +status: active +date: 2026-03-24 +--- + +## Context + +The app originally had a tab bar allowing multiple notes to be open simultaneously (similar to a browser or code editor). After building and shipping it, the tab model was found to add significant UI complexity, state management overhead, and confusion — without a proportional benefit for a notes app. + +## Decision + +**Remove the tab bar. Only one note is open at a time.** Navigation history (Back/Forward with Cmd+[/]) replaces tabs for moving between recently visited notes. Closed tab history and `useTabManagement` are removed. + +## Alternatives considered + +- **Keep tabs**: familiar UX, allows comparing notes side by side. Rejected — adds ~2000 lines of complexity, confusing state (which tab is "active"?), and breaks the "editor is sacred" principle. +- **Tabs + single-note toggle**: configurable per user. Rejected — doubles the state surface and testing burden. +- **Split pane (two notes at once)**: useful for reference. Deferred — can be added later without tabs, via a dedicated split layout. + +## Consequences + +- Removes ~2000 lines of code (`TabBar`, `useClosedTabHistory`, `useEditorTabSwap`, `tabLayout`) +- `handleSelectNote` replaces the current note instead of adding a tab +- Cmd+W (close tab) and Cmd+Shift+T (reopen closed tab) removed from shortcuts +- Back/Forward navigation (Cmd+[/Cmd+]) preserves history without tab state +- Significant simplification of `App.tsx` and editor state +- Triggers re-evaluation if: multi-note workflows become a top user request diff --git a/docs/adr/0004-vault-vs-app-settings-storage.md b/docs/adr/0004-vault-vs-app-settings-storage.md new file mode 100644 index 0000000..40a1ea5 --- /dev/null +++ b/docs/adr/0004-vault-vs-app-settings-storage.md @@ -0,0 +1,42 @@ +--- +type: ADR +id: "0004" +title: "Vault vs app settings for state storage" +status: active +date: 2026-03-24 +--- + +## Context + +As features were added, there was recurring ambiguity about where to persist configuration and state: in the vault (as frontmatter in `.md` files) or in app settings (`~/.config/com.laputa.app/settings.json` / localStorage). Without a clear rule, some decisions were inconsistent. + +## Decision + +**Ask: "Would the user want this to follow the vault across all their installations?"** + +- If **yes** → store in the vault (as frontmatter in the relevant `.md` file, using the `_` convention for system properties) +- If **no** → store in app settings + +Examples: +| Data | Storage | Reason | +|------|---------|--------| +| Note type icon (`_icon`) | Vault frontmatter | Follows the vault everywhere | +| Note type color (`_color`) | Vault frontmatter | Follows the vault everywhere | +| Note sort preference | Vault frontmatter (type file) | Per-vault, consistent across devices | +| API keys (Anthropic, OpenAI) | App settings | Installation-specific | +| GitHub token | App settings | Installation-specific | +| Window size / zoom | App settings | Device-specific | +| Editor zoom level | App settings | Device-specific | +| Telemetry consent | App settings | Installation-specific | + +## Alternatives considered + +- **Everything in localStorage**: simple, but breaks cross-device sync for vault-level config. +- **Everything in vault**: pure, but makes device-specific settings (zoom, window size) propagate to all devices — confusing. + +## Consequences + +- Config that "belongs to a note or type" lives in frontmatter — readable/diffable in git +- The `_` prefix convention (see ABSTRACTIONS.md) distinguishes system properties from user properties +- App rebuilds from vault state on open — no stale config files to manage +- Triggers re-evaluation if: vault files become too polluted with system frontmatter properties diff --git a/docs/adr/0005-tauri-ios-for-ipad.md b/docs/adr/0005-tauri-ios-for-ipad.md new file mode 100644 index 0000000..24a4754 --- /dev/null +++ b/docs/adr/0005-tauri-ios-for-ipad.md @@ -0,0 +1,38 @@ +--- +type: ADR +id: "0005" +title: "Tauri v2 iOS for iPad support (vs SwiftUI rewrite)" +status: active +date: 2026-03-27 +--- + +## Context + +Laputa runs on macOS via Tauri v2. The goal is to also support iPad without changing the stack or redesigning the app from scratch. The core question: extend the existing stack to iOS, or rewrite in SwiftUI for a fully native experience? + +## Decision + +**Use Tauri v2 iOS (beta) for the iPad prototype.** The React frontend stays identical. The Rust backend compiles for iOS with `#[cfg(desktop)]` / `#[cfg(mobile)]` guards for platform-specific features. Desktop-only features (git CLI, macOS menu bar, MCP server, Claude CLI) are stubbed or skipped on mobile. + +The prototype (`feat: add iPad/iOS prototype via Tauri v2 mobile target`, build `b492`) successfully builds and runs on iPad Pro 13" simulator (iOS 18.3.1). + +## Alternatives considered + +- **SwiftUI rewrite**: best native macOS/iPad experience, full App Store integration, native TextKit 2 editor. Rejected for now — would discard all existing React code, Rust backend, 2200+ tests, and Claude Code's accumulated context. Worth revisiting if Laputa becomes iOS-first. +- **Capacitor**: replaces Tauri layer, keeps React, but the Rust backend is lost entirely — git and file operations would need reimplementation in JS or Swift. +- **React Native + WebView**: wraps the React app in a WebView. Too hacky, performance concerns, App Store review risks. + +## Git on iPad + +`git` CLI is unavailable on iOS. Options for production: +- **Option A (recommended)**: `isomorphic-git` — pure JS git implementation, no native dependencies, runs in WebView. Replaces Rust git commands on mobile. +- **Option B (prototype)**: Working Copy as iOS Files provider — user manages git separately. +- **Option C**: iCloud Drive sync — no git history. Not recommended. + +## Consequences + +- Zero frontend changes needed for basic iPad support +- Desktop features (git, MCP, Claude CLI) unavailable on iPad until isomorphic-git is integrated +- Tauri v2 iOS is still beta — production stability unknown +- App Store distribution requires Apple Developer account and TestFlight +- Triggers re-evaluation if: Tauri iOS remains unstable after 6 months, or iPad becomes the primary target (in which case SwiftUI rewrite becomes rational) diff --git a/docs/adr/0006-flat-vault-structure.md b/docs/adr/0006-flat-vault-structure.md new file mode 100644 index 0000000..30f92e0 --- /dev/null +++ b/docs/adr/0006-flat-vault-structure.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0006" +title: "Flat vault structure (no type-based folders)" +status: active +date: 2026-03-15 +--- + +## Context + +Originally, notes were organized into type-based subfolders (`project/`, `person/`, `topic/`, etc.). Changing a note's type required moving it between folders, which broke wikilinks, complicated wikilink resolution (paths vs titles), and created friction for users who wanted to reorganize their knowledge. It also made vault scanning more complex and introduced edge cases around folder creation/deletion. + +## Decision + +**All user notes live as flat `.md` files at the vault root. Type is determined solely from the `type:` frontmatter field — never inferred from folder location.** Only a small set of protected folders exist: `type/` (type definition documents), `config/` (meta-configuration), and `attachments/`. + +## Options considered + +- **Option A** (chosen): Flat vault with frontmatter-only type — simple wikilink resolution (title/filename only), no file moves on type change, vault scanning restricted to root + protected folders. Downside: large vaults may look cluttered in Finder. +- **Option B**: Keep type-based folders — familiar Obsidian-like structure. Downside: type changes require file moves, wikilink resolution needs path awareness, scanning is recursive and slower. +- **Option C**: Hybrid (folders optional, type still from frontmatter) — maximum flexibility. Downside: two ways to do the same thing, confusing for AI agents and automation. + +## Consequences + +- Wikilink resolution is simplified to multi-pass title/filename matching — no path-based matching needed. +- Changing a note's type is a frontmatter edit, not a file move. +- A `flatten_vault` migration command and wizard were added for existing vaults with type folders. +- `vault_health_check` detects stray files in non-protected subfolders. +- `scan_vault` only indexes root-level `.md` files plus protected folders — non-protected subdirectories are ignored. +- Re-evaluation trigger: if users need nested folder hierarchies for non-type organization (e.g., project-specific subdirectories). diff --git a/docs/adr/0007-title-filename-sync.md b/docs/adr/0007-title-filename-sync.md new file mode 100644 index 0000000..3f959fc --- /dev/null +++ b/docs/adr/0007-title-filename-sync.md @@ -0,0 +1,31 @@ +--- +type: ADR +id: "0007" +title: "Title equals filename (slug sync)" +status: superseded +date: 2026-03-15 +superseded_by: "0044" +--- + +## Context + +With the move to a flat vault structure (ADR-0006), filenames became the primary identifier for notes. Previously, titles were extracted from the first H1 heading, which was fragile (users could delete or change the H1 without realizing it affected the note's identity). A clear, deterministic mapping between title and filename was needed. + +## Decision + +**Every note's filename is `slugify(title).md`. The `title` frontmatter field is the source of truth for the human-readable title. On note open, the system syncs the title field to match the filename if they've diverged (filename wins). On rename, both title and filename are updated atomically.** + +## Options considered + +- **Option A** (chosen): `title = slugify(filename)` with bidirectional sync — deterministic, predictable, wikilinks resolve by title/filename stem. Downside: titles with special characters get simplified in filenames. +- **Option B**: UUID-based filenames with title only in frontmatter — filenames never change. Downside: vault is unreadable in Finder/terminal, breaks the "plain markdown files" principle. +- **Option C**: H1-based title extraction — no explicit title field. Downside: fragile, H1 can be accidentally deleted or changed, decoupled from filename. + +## Consequences + +- `extract_title` reads from frontmatter `title:` field, never from H1. Falls back to `slug_to_title()` (hyphens → spaces, title-case). +- `sync_title_on_open` auto-corrects desynced frontmatter on note open. +- `rename_note` updates both `title:` frontmatter and filename atomically, plus cross-vault wikilink updates. +- The H1 block inside BlockNote is hidden via CSS; a dedicated `TitleField` component above the editor is the primary title editing surface. +- Slug collision detection prevents duplicate filenames. +- Re-evaluation trigger: if users need filenames that don't match titles (e.g., short slugs for long titles). diff --git a/docs/adr/0008-underscore-system-properties.md b/docs/adr/0008-underscore-system-properties.md new file mode 100644 index 0000000..563a1eb --- /dev/null +++ b/docs/adr/0008-underscore-system-properties.md @@ -0,0 +1,42 @@ +--- +type: ADR +id: "0008" +title: "Underscore convention for system properties" +status: active +date: 2026-03-24 +--- + +## Context + +As Laputa added more features that store configuration in note frontmatter (pinned properties, type icons, colors, sidebar labels, sort order), the Properties panel became cluttered with internal fields that users shouldn't normally edit. A convention was needed to distinguish user-visible properties from system-internal ones. + +## Decision + +**Any frontmatter field whose name starts with `_` is a system property. It is hidden from the Properties panel, not exposed in search/filters, but remains editable in the raw editor. The frontmatter parser filters out `_*` fields before passing properties to the UI.** + +## Options considered + +- **Option A** (chosen): Underscore prefix convention (`_icon`, `_color`, `_order`, `_pinned_properties`) — simple, readable in raw files, universal rule. Downside: users must know the convention to access system fields. +- **Option B**: Separate YAML block or nested `_system:` key — cleaner separation. Downside: more complex parsing, breaks flat key-value frontmatter model. +- **Option C**: Store system properties in a separate sidecar file (`.meta.yml`) — complete separation. Downside: doubles the number of files, harder to keep in sync. + +## Consequences + +- All future system-level frontmatter fields must use the `_field_name` convention. +- Both Rust (`vault/mod.rs`) and TypeScript (`utils/frontmatter.ts`) parsers filter `_*` fields before passing `properties` to the UI. +- Power users can still access and edit system properties via the raw editor. +- Type documents use `_icon`, `_color`, `_order`, `_sidebar_label`, `_pinned_properties`. +- Re-evaluation trigger: if the number of system properties grows large enough to warrant a structured sub-object. + +## Normalized system properties + +| Canonical key | Old keys (read with fallback) | Written by | +|---|---|---| +| `_archived` | `Archived`, `archived` | Archive action | +| `_trashed` | `Trashed`, `trashed` | Trash action | +| `_trashed_at` | `Trashed at`, `trashed_at` | Trash action | +| `_favorite` | — | Favorite toggle | +| `_favorite_index` | — | Favorite reorder | + +**Write rule**: always use the canonical `_`-prefixed key. +**Read rule**: accept both canonical and legacy keys (case-insensitive). Do NOT rewrite on read — migration is a separate concern. diff --git a/docs/adr/0009-keyword-only-search.md b/docs/adr/0009-keyword-only-search.md new file mode 100644 index 0000000..f7bb2b2 --- /dev/null +++ b/docs/adr/0009-keyword-only-search.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0009" +title: "Keyword-only search (remove semantic indexing)" +status: active +date: 2026-03-24 +--- + +## Context + +Laputa previously used QMD (a Go binary) for semantic vector indexing, enabling similarity-based search. This added significant complexity: a bundled Go binary requiring code-signing, an indexing step on vault open, status bar progress tracking, auto-install logic, and a separate `tools/qmd/` directory. The semantic search quality did not justify the operational burden, especially as the AI agent (with MCP vault tools) became a more natural way to do exploratory queries. + +## Decision + +**Remove QMD semantic indexing entirely and keep only keyword-based search. Search uses `walkdir` to scan all `.md` files, matching against titles and content with case-insensitive substring matching and relevance scoring.** + +## Options considered + +- **Option A** (chosen): Keyword-only search via `walkdir` — zero dependencies, no indexing step, instant results, no binary to sign/bundle. Downside: no fuzzy or semantic matching. +- **Option B**: Keep QMD semantic search — richer search results, similarity matching. Downside: bundled Go binary, code-signing, indexing latency, maintenance burden. +- **Option C**: Replace QMD with a Rust-native embedding library — no external binary. Downside: large model files, cold start time, still needs indexing. + +## Consequences + +- No external search binary to bundle, sign, or install. +- No indexing step on vault open — search is instant. +- `search_vault` Tauri command scans files directly with `walkdir`, runs in a blocking Tokio task. +- Title matches rank higher than content-only matches; exact title matches rank highest. +- The AI agent (via MCP `search_notes` tool) provides an alternative for exploratory/semantic queries. +- Re-evaluation trigger: if users report keyword search is insufficient for large vaults (9000+ notes). diff --git a/docs/adr/0010-dynamic-wikilink-relationship-detection.md b/docs/adr/0010-dynamic-wikilink-relationship-detection.md new file mode 100644 index 0000000..153c578 --- /dev/null +++ b/docs/adr/0010-dynamic-wikilink-relationship-detection.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0010" +title: "Dynamic wikilink relationship detection" +status: active +date: 2026-03-08 +--- + +## Context + +Laputa needs to support arbitrary relationship types between notes (e.g., `Topics:`, `Key People:`, `Depends on:`). Initially, a hardcoded list `RELATIONSHIP_KEYS` identified which frontmatter fields were relationships. This was fragile — adding a new relationship type required a code change, and users couldn't define their own. + +## Decision + +**The Rust parser dynamically detects relationship fields by scanning all frontmatter keys for values containing `[[wikilinks]]`. Any field with wikilink values is captured in the `relationships` HashMap — no hardcoded field name list needed.** + +## Options considered + +- **Option A** (chosen): Dynamic detection via `[[wikilink]]` presence — zero configuration, extensible, any field name works. Downside: fields with bracket-like content could false-positive (mitigated by the `[[...]]` double-bracket syntax). +- **Option B**: Hardcoded `RELATIONSHIP_KEYS` list — simple, predictable. Downside: inflexible, requires code changes for new relationship types. +- **Option C**: User-configurable relationship field list in vault config — flexible. Downside: configuration burden, doesn't work out of the box. + +## Consequences + +- Users can define arbitrary relationship types by adding wikilink values to any frontmatter field. +- No code change needed when adding new relationship types — convention over configuration. +- All relationship fields appear in the Inspector's RelationshipsPanel automatically. +- The `relationships` HashMap in `VaultEntry` captures all dynamic relationships. +- Standard fields (`belongs_to`, `related_to`) are still recognized for backward compatibility but not privileged. +- Re-evaluation trigger: if false-positive detection becomes a problem (e.g., fields with literal `[[` content that aren't relationships). diff --git a/docs/adr/0011-mcp-server-for-ai-integration.md b/docs/adr/0011-mcp-server-for-ai-integration.md new file mode 100644 index 0000000..517bf49 --- /dev/null +++ b/docs/adr/0011-mcp-server-for-ai-integration.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0011" +title: "MCP server for AI tool integration" +status: active +date: 2026-02-28 +--- + +## Context + +Laputa's AI features (agent panel, chat) need structured access to vault data — searching notes, reading content, editing frontmatter, and steering the UI. Rather than building a bespoke API, the Model Context Protocol (MCP) provides a standardized tool interface that works with Claude Code, Cursor, and any MCP-compatible client. + +## Decision + +**Laputa ships a Node.js MCP server (`mcp-server/`) that exposes vault operations as 14 tools. It runs on stdio for external clients and on two WebSocket ports (9710 for tool calls, 9711 for UI actions) for the embedded Laputa frontend. Tauri spawns the server on startup and auto-registers it in Claude Code and Cursor configs.** + +## Options considered + +- **Option A** (chosen): Node.js MCP server with stdio + WebSocket dual transport — standard MCP compatibility, works with Claude Code/Cursor out of the box, WebSocket enables real-time UI steering. Downside: Node.js dependency, two extra ports. +- **Option B**: Rust-native MCP server — no Node.js dependency. Downside: MCP SDK is JavaScript-first, Rust implementation would be custom and harder to maintain. +- **Option C**: Custom REST/gRPC API — full control. Downside: no compatibility with existing AI tool ecosystems, each client needs a custom integration. + +## Consequences + +- Vault tools (search, read, create, edit, delete, link) are available to any MCP-compatible client. +- Auto-registration in `~/.claude/mcp.json` and `~/.cursor/mcp.json` means zero setup for users. +- The WebSocket bridge enables real-time UI actions (highlight elements, open notes, set filters) from AI tools. +- `mcp-server/` is bundled into release builds and spawned as a child process by `mcp.rs`. +- Port conflicts on 9710/9711 are handled gracefully (EADDRINUSE tolerance). +- Re-evaluation trigger: if MCP SDK gains a Rust implementation that eliminates the Node.js dependency. diff --git a/docs/adr/0012-claude-cli-for-ai-agent.md b/docs/adr/0012-claude-cli-for-ai-agent.md new file mode 100644 index 0000000..9e31cab --- /dev/null +++ b/docs/adr/0012-claude-cli-for-ai-agent.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0012" +title: "Claude CLI subprocess for AI agent (replacing direct API)" +status: active +date: 2026-03-01 +--- + +## Context + +The AI agent panel initially called the Anthropic API directly from Rust, managing tool calling loops manually. This required implementing tool execution, conversation state, and streaming — all complex to maintain. Claude CLI (`claude` binary) handles all of this natively, including MCP tool integration, conversation history, and streaming NDJSON output. + +## Decision + +**The AI agent panel spawns Claude CLI as a subprocess via `claude_cli.rs`, passing messages with `--output-format stream-json` and vault MCP config via `--mcp-config`. The frontend parses the NDJSON event stream (Init, TextDelta, ThinkingDelta, ToolStart, ToolDone, Result, Done) for real-time display.** + +## Options considered + +- **Option A** (chosen): Claude CLI subprocess with NDJSON streaming — built-in tool calling, MCP integration, conversation management, no API key needed (CLI handles auth). Downside: requires Claude CLI installed, subprocess management complexity. +- **Option B**: Direct Anthropic API with manual tool loop — full control, no external dependency. Downside: must implement tool calling, retries, conversation state, MCP tool bridging. +- **Option C**: Use Anthropic Agent SDK from Rust — structured agent framework. Downside: SDK is Python/TypeScript, no Rust support. + +## Consequences + +- The AI agent gets full tool access (MCP vault tools + shell access) without custom tool-calling code. +- `claude_cli.rs` manages subprocess lifecycle: spawn, stream events, kill on cancel. +- The frontend (`useAiAgent` hook) processes NDJSON events for reasoning blocks, tool action cards, and response display. +- File operation detection (from Write/Edit tool inputs) triggers automatic vault reload. +- The simpler AI Chat panel still uses the Anthropic API directly for lightweight, no-tools conversations. +- Re-evaluation trigger: if Anthropic releases a Rust Agent SDK or if Claude CLI streaming format changes significantly. diff --git a/docs/adr/0013-remove-theming-system.md b/docs/adr/0013-remove-theming-system.md new file mode 100644 index 0000000..5bd2b92 --- /dev/null +++ b/docs/adr/0013-remove-theming-system.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0013" +title: "Remove vault-based theming system" +status: superseded +date: 2026-03-23 +superseded_by: "0081" +--- + +## Context + +Laputa had a vault-based theming system where themes were markdown notes in `theme/` with `type: Theme` frontmatter. Each property became a CSS variable. This included a `ThemeManager` hook, theme property editor, dark mode detection, live preview on save, and three built-in themes. The system was complex (spanning Rust seed/create/defaults modules, TypeScript hooks, and CSS variable bridging) and added significant maintenance burden for a feature that most users never customized beyond the defaults. + +## Decision + +**Remove the vault-based theming system entirely. The app uses a single, hardcoded light theme defined in CSS variables (`src/index.css`) and editor theme (`src/theme.json`).** The `theme/` folder, `ThemeManager` hook, theme Rust modules, theme property editor, and dark mode support were all deleted. + +## Options considered + +- **Option A** (chosen): Remove theming, ship a single polished light theme — drastically reduced complexity, fewer files to maintain, no theme-related bugs. Downside: no user customization, no dark mode. +- **Option B**: Keep theming but simplify — reduce to light/dark toggle only. Downside: still requires theme loading, CSS variable bridging, and live preview infrastructure. +- **Option C**: Keep the full theming system — maximum flexibility. Downside: high maintenance cost for a rarely-used feature, frequent source of bugs (WKWebView reflow issues, CSS var sync). + +## Consequences + +- Deleted: `src-tauri/src/theme/`, `src/hooks/useThemeManager.ts`, `ThemePropertyEditor.tsx`, theme-related commands, `_themes/` legacy support. +- Single theme defined in `src/index.css` (CSS variables) and `src/theme.json` (editor typography). +- No dark mode support — the app is light-only. +- Protected folders reduced: `theme/` is no longer scanned by `scan_vault`. +- Re-evaluation trigger: if dark mode becomes a hard requirement for accessibility or user demand. diff --git a/docs/adr/0014-git-based-vault-cache.md b/docs/adr/0014-git-based-vault-cache.md new file mode 100644 index 0000000..916863b --- /dev/null +++ b/docs/adr/0014-git-based-vault-cache.md @@ -0,0 +1,31 @@ +--- +type: ADR +id: "0014" +title: "Git-based incremental vault cache" +status: active +date: 2026-03-08 +--- + +## Context + +Scanning a vault of 9000+ markdown files on every app launch takes several seconds. A caching strategy was needed that could detect which files changed since the last scan and only re-parse those, while remaining correct even after external edits (e.g., from a text editor or git pull). + +## Decision + +**Use git as the change detection mechanism. The cache stores all `VaultEntry` objects in a JSON file at `~/.laputa/cache/.json`. On load, it compares the cached git HEAD commit hash with the current one: if the same, only re-parse uncommitted changed files; if different, use `git diff` to find changed files and selectively re-parse. Full rescan only on cache miss or version bump.** + +## Options considered + +- **Option A** (chosen): Git-based incremental cache — leverages existing git infrastructure, precise change detection, handles both committed and uncommitted changes. Downside: requires git-tracked vault, cache invalidation logic is complex. +- **Option B**: File modification time (`mtime`) based cache — works without git. Downside: unreliable across filesystems (iCloud, Dropbox), clock skew issues. +- **Option C**: File hash (content-based) cache — always correct. Downside: must read every file to compute hash, defeating the purpose of caching. + +## Consequences + +- Cache file stored outside the vault at `~/.laputa/cache/.json` — never pollutes the user's git repo. +- Writes are atomic (write to `.tmp` then rename) to prevent corruption. +- Cache version (v5) is bumped on `VaultEntry` field changes to force full rescan. +- Legacy `.laputa-cache.json` files inside the vault are auto-migrated and deleted on first run. +- `reload_vault` command deletes the cache file before rescanning, guaranteeing fresh data. +- Stale cache entries are pruned on vault open (files that no longer exist on disk). +- Re-evaluation trigger: if non-git vaults (e.g., iCloud-only) need to be supported. diff --git a/docs/adr/0015-auto-save-with-debounce.md b/docs/adr/0015-auto-save-with-debounce.md new file mode 100644 index 0000000..5601664 --- /dev/null +++ b/docs/adr/0015-auto-save-with-debounce.md @@ -0,0 +1,29 @@ +--- +type: ADR +id: "0015" +title: "Auto-save with 500ms debounce" +status: active +date: 2026-03-19 +--- + +## Context + +Manual save (Cmd+S) was the only way to persist editor changes. Users occasionally lost work when switching notes or closing the app without saving. An auto-save mechanism was needed that balanced responsiveness (no perceived lag) with disk I/O efficiency (not writing on every keystroke). + +## Decision + +**Notes auto-save with a 500ms debounce after the last keystroke. The `useEditorSave` hook watches for editor content changes and triggers a save after 500ms of inactivity. The same `save_note_content` Rust command is used for both auto-save and manual save.** + +## Options considered + +- **Option A** (chosen): 500ms debounce auto-save — fast enough to feel instant, slow enough to batch rapid keystrokes. Downside: 500ms window where unsaved changes exist. +- **Option B**: Save on every change (no debounce) — zero data loss risk. Downside: excessive disk writes, poor performance, frequent git diffs. +- **Option C**: Save on note switch / app blur only — minimal disk writes. Downside: data loss if app crashes mid-edit, no live preview of changes in other views. + +## Consequences + +- Users never need to manually save (Cmd+S still works as an immediate save). +- Auto-save triggers vault entry updates, keeping the note list, search, and relationships current. +- The same save path handles wikilink extraction and frontmatter parsing after save. +- Secondary windows (multi-window mode) each have their own auto-save via `useEditorSaveWithLinks`. +- Re-evaluation trigger: if 500ms is too aggressive for low-powered devices or network-synced vaults. diff --git a/docs/adr/0016-sentry-posthog-telemetry.md b/docs/adr/0016-sentry-posthog-telemetry.md new file mode 100644 index 0000000..6713a6b --- /dev/null +++ b/docs/adr/0016-sentry-posthog-telemetry.md @@ -0,0 +1,31 @@ +--- +type: ADR +id: "0016" +title: "Sentry crash reporting + PostHog analytics with consent" +status: active +date: 2026-03-25 +--- + +## Context + +As Laputa approaches public release, crash reports and usage analytics are needed to identify bugs and understand feature adoption. However, as a personal knowledge management app that handles sensitive data, user privacy is paramount. Any telemetry must be opt-in with clear consent. + +## Decision + +**Integrate Sentry for crash reporting and PostHog for product analytics, both gated behind an explicit consent dialog on first launch. Users can toggle each independently in Settings. No telemetry is sent without affirmative consent.** + +## Options considered + +- **Option A** (chosen): Sentry + PostHog with consent dialog — industry-standard tools, separate crash/analytics toggles, privacy-respecting opt-in. Downside: two external dependencies, two services to manage. +- **Option B**: Self-hosted error tracking — full data control. Downside: operational burden, limited analytics features. +- **Option C**: No telemetry — simplest, most private. Downside: blind to crashes and usage patterns, harder to prioritize features. + +## Consequences + +- `TelemetryConsentDialog` shows on first launch with accept/decline buttons. +- Accepting generates an `anonymous_id` (no PII) and sets `telemetry_consent: true` in settings. +- `useTelemetry` hook reactively initializes/tears down Sentry and PostHog based on settings. +- Both frontend (`src/lib/telemetry.ts`) and Rust backend (`src-tauri/src/telemetry.rs`) have path scrubbers in `beforeSend` hooks to strip vault paths. +- DSN/keys come from `VITE_SENTRY_DSN` / `VITE_POSTHOG_KEY` env vars. +- `reinit_telemetry` Tauri command toggles Rust-side Sentry at runtime. +- Re-evaluation trigger: if a unified telemetry platform (e.g., OpenTelemetry) could replace both services. diff --git a/docs/adr/0018-codescene-code-health-gates.md b/docs/adr/0018-codescene-code-health-gates.md new file mode 100644 index 0000000..68dabb6 --- /dev/null +++ b/docs/adr/0018-codescene-code-health-gates.md @@ -0,0 +1,32 @@ +--- +type: ADR +id: "0018" +title: "CodeScene code health gates in CI and git hooks" +status: superseded +date: 2026-03-13 +superseded_by: "0064" +--- + +## Context + +Code complexity tends to increase over time, especially in fast-moving projects. Without automated enforcement, hotspot files (most-edited files) degrade in quality, making future changes harder and buggier. A quantitative code health metric was needed to prevent regression. + +## Decision + +**Enforce CodeScene code health scores as mandatory gates in pre-commit and pre-push hooks. Hotspot Code Health must be >= 9.5 and Average Code Health must be >= 9.31 (project-wide). Both gates block commit/push on failure.** The Boy Scout Rule ("leave every file better than you found it") is enforced as part of every task. + +## Options considered + +- **Option A** (chosen): CodeScene with hard gates — quantitative, automated, catches complexity before it merges. Downside: can slow development if scores are borderline, requires CodeScene API access. +- **Option B**: Manual code review for complexity — human judgment. Downside: subjective, inconsistent, doesn't scale. +- **Option C**: Linter-only rules (ESLint complexity, Clippy) — built-in, no external service. Downside: coarser metrics, no hotspot awareness, no project-wide average tracking. + +## Consequences + +- Pre-commit hook runs vitest + CodeScene health check before every commit. +- Pre-push hook runs the same checks plus Playwright smoke tests. +- Developers must fix complexity regressions before committing — even in files they didn't directly modify if changes indirectly affected complexity. +- Never use `// eslint-disable`, `#[allow(...)]`, or `as any` to pass the gate. +- Common fixes: extract hooks, split large components, reduce function complexity, extract modules. +- `.codesceneignore` excludes `tools/`, `e2e/`, `tests/`, `scripts/` from analysis. +- Re-evaluation trigger: if CodeScene becomes unavailable or a better code health tool emerges. diff --git a/docs/adr/0019-github-device-flow-oauth.md b/docs/adr/0019-github-device-flow-oauth.md new file mode 100644 index 0000000..cae522e --- /dev/null +++ b/docs/adr/0019-github-device-flow-oauth.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0019" +title: "GitHub device flow OAuth for vault sync" +status: active +date: 2026-02-28 +--- + +## Context + +Laputa supports git-backed vaults synced to GitHub. Users need to authenticate with GitHub to clone repos, push changes, and create new vault repositories. In a desktop app, the standard OAuth redirect flow is awkward (no web server to receive the callback). The Device Authorization Flow is designed for exactly this scenario. + +## Decision + +**Use GitHub's Device Authorization Flow (OAuth device code grant) for GitHub authentication. The user sees a code, opens a browser to enter it, and the app polls for the token. Token is persisted in app settings for future git operations.** + +## Options considered + +- **Option A** (chosen): Device Authorization Flow — designed for desktop/CLI apps, no redirect URI needed, secure (user authenticates in their own browser). Downside: requires user to switch to browser and back. +- **Option B**: Personal Access Token (PAT) entry — user generates token on GitHub, pastes it in Settings. Downside: poor UX, users must navigate GitHub settings, token scope management is manual. +- **Option C**: OAuth redirect with local server — spawn a local HTTP server to receive the redirect. Downside: port conflicts, firewall issues, more complex implementation. + +## Consequences + +- `GitHubDeviceFlow` component handles the OAuth flow UI (device code display, polling, success/error states). +- `GitHubVaultModal` enables cloning existing repos or creating new ones. +- Token persisted in `settings.json` under `github_token` / `github_username`. +- `SettingsPanel` shows connection status with disconnect option. +- Uses Tauri opener plugin to launch the browser for user authentication. +- Re-evaluation trigger: if Tauri gains native OAuth redirect support that's simpler than the device flow. diff --git a/docs/adr/0020-keyboard-first-design.md b/docs/adr/0020-keyboard-first-design.md new file mode 100644 index 0000000..5fe8af9 --- /dev/null +++ b/docs/adr/0020-keyboard-first-design.md @@ -0,0 +1,32 @@ +--- +type: ADR +id: "0020" +title: "Keyboard-first design principle" +status: active +date: 2026-03-01 +--- + +## Context + +Laputa is a productivity tool for knowledge workers who spend most of their time typing. Mouse-heavy interactions interrupt flow. Every feature should be reachable without touching the mouse, and the app must be fully testable via keyboard events (important for Playwright automation and accessibility). + +## Decision + +**Every feature must be reachable via keyboard. Every command palette entry must also appear in the macOS menu bar (File / Edit / View / Note / Vault / Window). This is both a design principle and a QA requirement.** Navigation, note switching, panel toggling, search, and all commands work via keyboard shortcuts or the Cmd+K command palette. + +## Options considered + +- **Option A** (chosen): Keyboard-first with menu bar parity — full keyboard accessibility, menu bar for discoverability, testable via Playwright keyboard events. Downside: more work per feature (must wire shortcut + menu item + command palette entry). +- **Option B**: Mouse-primary with some shortcuts — faster to implement. Downside: poor flow for power users, harder to automate testing. +- **Option C**: Keyboard-only (no menu bar) — simplest. Downside: poor discoverability, macOS HIG violation. + +## Consequences + +- `useCommandRegistry` + `useAppCommands` build a centralized command registry with labels, shortcuts, and handlers. +- `CommandPalette` (Cmd+K) fuzzy-searches all registered commands. +- `menu.rs` defines the native macOS menu bar with accelerators matching keyboard shortcuts. +- `useAppKeyboard` registers global keyboard shortcuts. +- `useMenuEvents` bridges menu bar clicks to command handlers. +- QA uses `osascript` keyboard events for native testing — no mouse, no `cliclick`. +- macOS gotcha: `Option+N` produces special characters — use `e.code` or `Cmd+N` instead. +- Re-evaluation trigger: if a non-macOS platform (Windows, Linux) is supported and needs different menu/shortcut conventions. diff --git a/docs/adr/0021-push-to-main-workflow.md b/docs/adr/0021-push-to-main-workflow.md new file mode 100644 index 0000000..74fdecb --- /dev/null +++ b/docs/adr/0021-push-to-main-workflow.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0021" +title: "Push directly to main (no PRs or branches)" +status: active +date: 2026-03-02 +--- + +## Context + +Initially, the project used feature branches and PRs. With a single developer (assisted by Claude Code), the PR overhead — branch creation, rebase churn, merge conflicts from long-lived branches — slowed development without adding review value. The pre-commit and pre-push hooks already enforce tests, linting, type checking, and code health gates. + +## Decision + +**Push directly to main — no PRs, no feature branches. The pre-push hook runs all quality gates (tests, lint, type check, coverage, CodeScene health). Never use `--no-verify`.** + +## Options considered + +- **Option A** (chosen): Push to main with hook-enforced quality gates — fastest iteration, no rebase churn, hooks provide automated review. Downside: no PR-based review, harder to roll back a batch of changes. +- **Option B**: Feature branches with PRs — standard team workflow, code review. Downside: rebase churn for a solo developer, PR overhead with no reviewer. +- **Option C**: Feature branches without PRs (merge to main locally) — branch isolation without review overhead. Downside: still has merge conflicts, branches diverge. + +## Consequences + +- Commit every 20-30 minutes with conventional commit prefixes (`feat:`, `fix:`, `refactor:`, `test:`, `docs:`). +- Pre-commit hook: vitest + CodeScene health check. +- Pre-push hook: same + Playwright smoke tests. +- No `--no-verify` ever — the hooks are the quality gate. +- Reverting changes requires `git revert` (not force push). +- Re-evaluation trigger: if a second developer joins and needs code review. diff --git a/docs/adr/0022-blocknote-rich-text-editor.md b/docs/adr/0022-blocknote-rich-text-editor.md new file mode 100644 index 0000000..8d866c9 --- /dev/null +++ b/docs/adr/0022-blocknote-rich-text-editor.md @@ -0,0 +1,32 @@ +--- +type: ADR +id: "0022" +title: "BlockNote as the rich text editor" +status: active +date: 2026-02-15 +--- + +## Context + +Laputa needs a rich text editor that can render markdown with YAML frontmatter, support custom inline content types (wikilinks), and provide a modern editing experience. The editor must handle the markdown-to-blocks-to-markdown round-trip without data loss. + +## Decision + +**Use BlockNote as the primary rich text editor, with CodeMirror 6 as an alternative raw editing mode. Custom wikilink inline content is defined via `createReactInlineContentSpec`. Markdown round-tripping uses a pre/post-processing pipeline with placeholder tokens for wikilinks.** + +## Options considered + +- **Option A** (chosen): BlockNote + CodeMirror 6 raw mode — BlockNote provides modern block-based editing, CodeMirror gives power users direct markdown access. Downside: wikilink round-tripping requires custom preprocessing pipeline. +- **Option B**: ProseMirror directly — maximum control. Downside: much more boilerplate, no block-level abstractions, harder to maintain. +- **Option C**: CodeMirror only (no rich text) — simplest, no round-trip issues. Downside: poor UX for non-technical users, no inline previews. +- **Option D**: Monaco Editor — rich features, VS Code-like. Downside: heavy, designed for code not prose, no block-level structure. + +## Consequences + +- Custom wikilink type defined in `editorSchema.tsx` via `createReactInlineContentSpec`. +- Markdown-to-BlockNote pipeline: `splitFrontmatter()` → `preProcessWikilinks()` → `tryParseMarkdownToBlocks()` → `injectWikilinks()`. +- BlockNote-to-Markdown pipeline: `blocksToMarkdownLossy()` → `postProcessWikilinks()` → prepend frontmatter. +- Placeholder tokens use `‹` and `›` (U+2039/U+203A) to avoid colliding with markdown syntax. +- Raw editor (CodeMirror 6) toggled via Cmd+K → "Raw Editor" or breadcrumb bar button. +- The H1 block is hidden via CSS in favor of a dedicated `TitleField` component. +- Re-evaluation trigger: if BlockNote's markdown round-tripping degrades or a better block editor emerges. diff --git a/docs/adr/0023-repair-vault-auto-bootstrap.md b/docs/adr/0023-repair-vault-auto-bootstrap.md new file mode 100644 index 0000000..4d46606 --- /dev/null +++ b/docs/adr/0023-repair-vault-auto-bootstrap.md @@ -0,0 +1,31 @@ +--- +type: ADR +id: "0023" +title: "Repair Vault auto-bootstrap pattern" +status: active +date: 2026-03-07 +--- + +## Context + +As Laputa adds features that depend on vault files (type definitions, config files, agents), users with existing vaults would miss these files. Manually creating them is error-prone. Features must work on both new and existing vaults without user intervention. + +## Decision + +**Every feature that depends on vault files must auto-bootstrap: check if file/folder exists on vault open, create with defaults if missing (silent, idempotent). All bootstrap functions are registered with the central `Cmd+K → "Repair Vault"` command for manual re-creation.** + +## Options considered + +- **Option A** (chosen): Auto-bootstrap on vault open + manual Repair Vault command — works for new and existing vaults, idempotent, no user action needed. Downside: vault may accumulate files the user didn't explicitly create. +- **Option B**: Require users to run a setup wizard — explicit, user-controlled. Downside: friction, users forget, new features don't work until setup is run. +- **Option C**: Store defaults in app bundle, not vault — no vault files created. Downside: breaks the "vault as source of truth" principle, custom configs can't override defaults. + +## Consequences + +- Type definitions (`type/project.md`, etc.) are seeded on vault open if missing. +- Config files (`config/agents.md`, etc.) are seeded on vault open if missing. +- `Repair Vault` command (Cmd+K) re-creates all expected files — useful after manual deletion or vault corruption. +- All bootstrap operations are silent and idempotent — running twice has no effect. +- `getting_started.rs` creates the Getting Started demo vault with all expected structure. +- The `vault_health_check` command detects missing or misconfigured vault files. +- Re-evaluation trigger: if the number of auto-created files becomes excessive or confusing for users. diff --git a/docs/adr/0024-cache-outside-vault.md b/docs/adr/0024-cache-outside-vault.md new file mode 100644 index 0000000..a1ac1c1 --- /dev/null +++ b/docs/adr/0024-cache-outside-vault.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0024" +title: "Vault cache stored outside vault directory" +status: active +date: 2026-03-08 +--- + +## Context + +The vault cache was originally stored as `.laputa-cache.json` inside the vault directory. This caused problems: the cache file appeared in git status, polluted the user's repo, and could be accidentally committed. It also confused vault scanning (the cache file was itself a file in the vault). + +## Decision + +**Store the vault cache at `~/.laputa/cache/.json`, outside the vault directory. The vault path is hashed (via `DefaultHasher`) to produce a deterministic filename. Legacy cache files inside the vault are auto-migrated and deleted on first run.** + +## Options considered + +- **Option A** (chosen): External cache at `~/.laputa/cache/` — never pollutes the vault, no git issues, deterministic filename from vault path hash. Downside: separate cleanup needed if vault is deleted. +- **Option B**: Cache inside vault with `.gitignore` — simpler, travels with the vault. Downside: .gitignore can be overridden, users may not have one, still appears in file listings. +- **Option C**: No persistent cache (in-memory only) — simplest, no file management. Downside: full rescan on every app launch, slow for large vaults. + +## Consequences + +- Cache path: `~/.laputa/cache/.json` (e.g., `~/.laputa/cache/12345678.json`). +- Writes are atomic: write to `.tmp` then rename. +- Legacy `.laputa-cache.json` files inside the vault are auto-migrated and deleted. +- `reload_vault` command deletes the cache file before rescanning. +- The `.laputa/` directory also stores other app data (future: vault metadata, indexes). +- Re-evaluation trigger: if vaults need to be portable between machines (cache would need to travel with the vault or be regenerated). diff --git a/docs/adr/0025-type-field-canonical.md b/docs/adr/0025-type-field-canonical.md new file mode 100644 index 0000000..06f55f7 --- /dev/null +++ b/docs/adr/0025-type-field-canonical.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0025" +title: "type: as canonical field (replacing Is A:)" +status: active +date: 2026-03-08 +--- + +## Context + +The entity type field was originally stored as `Is A:` in frontmatter (e.g., `Is A: Project`), following a natural-language naming convention. This caused problems: the space and colon made it awkward to parse, `is_a` was used internally as the snake_case variant, and `type:` is the standard YAML convention for metadata classification. The field name also confused AI agents that expected standard YAML conventions. + +## Decision + +**Use `type:` as the primary frontmatter field for entity types (e.g., `type: Project`). The legacy `Is A:` field is accepted as an alias for backward compatibility but new notes always use `type:`.** The internal TypeScript/Rust property remains `isA` for backward compatibility. + +## Options considered + +- **Option A** (chosen): `type:` as canonical with `Is A:` as legacy alias — clean, standard YAML convention, AI-readable. Downside: must maintain backward compatibility with existing vaults. +- **Option B**: Keep `Is A:` as canonical — no migration needed. Downside: non-standard, awkward parsing, confusing for AI agents. +- **Option C**: `kind:` or `category:` — avoids potential YAML type conflicts. Downside: less intuitive, still requires migration from `Is A:`. + +## Consequences + +- New notes use `type: Project` (not `Is A: Project`). +- The Rust parser checks `type:` first, falls back to `Is A:` for legacy notes. +- `VaultEntry.isA` property name kept for internal backward compatibility. +- Type documents in `type/` folder use `type: Type` in their own frontmatter. +- Repair Vault migrates legacy `Is A:` fields to `type:` when run. +- Re-evaluation trigger: if YAML reserved word `type` causes parsing issues (not observed so far). diff --git a/docs/adr/0026-props-down-no-global-state.md b/docs/adr/0026-props-down-no-global-state.md new file mode 100644 index 0000000..99a1b6a --- /dev/null +++ b/docs/adr/0026-props-down-no-global-state.md @@ -0,0 +1,31 @@ +--- +type: ADR +id: "0026" +title: "Props-down callbacks-up (no global state management)" +status: superseded +date: 2026-02-15 +superseded_by: "0115" +--- + +## Context + +React apps commonly adopt global state management libraries (Redux, Zustand, Jotai, Context) to share state across components. For Laputa, the component tree is relatively shallow (App → panels → sub-components), and the data flow is predictable. Adding a state management library would increase complexity without proportional benefit. + +## Decision + +**No global state management (no Redux, no Context for data). `App.tsx` owns the state and passes it down as props. Child-to-parent communication uses callback props (`onSelectNote`, `onCloseTab`, etc.). Local state uses `useState`/`useReducer`.** + +## Options considered + +- **Option A** (chosen): Props-down, callbacks-up — simple, predictable data flow, easy to trace state changes, no library dependency. Downside: prop drilling through deep trees, verbose parent components. +- **Option B**: Redux/Zustand global store — centralized state, easy cross-component access. Downside: boilerplate, indirection, harder to trace state changes, over-engineering for a single-window app. +- **Option C**: React Context for shared state — built-in, no library. Downside: re-renders on any context value change, performance issues with large state objects. + +## Consequences + +- `App.tsx` is the state orchestrator — it holds vault entries, active note, sidebar selection, and all top-level state. +- Components receive data and callbacks as props — no `useContext` for data access. +- Hooks (`useVaultLoader`, `useNoteActions`, `useTabManagement`, etc.) encapsulate state logic but return values consumed by `App.tsx`. +- Prop drilling is mitigated by composing hooks and keeping the component tree shallow. +- Components are easy to test in isolation (just pass props). +- Re-evaluation trigger: if the component tree deepens significantly or cross-cutting state becomes unmanageable with props. diff --git a/docs/adr/0027-dual-ai-architecture.md b/docs/adr/0027-dual-ai-architecture.md new file mode 100644 index 0000000..42eb422 --- /dev/null +++ b/docs/adr/0027-dual-ai-architecture.md @@ -0,0 +1,32 @@ +--- +type: ADR +id: "0027" +title: "Dual AI architecture (API chat + CLI agent)" +status: superseded +superseded_by: "0028" +date: 2026-03-01 +--- + +## Context + +Laputa needs two distinct AI interaction modes: a lightweight chat for quick questions about the current note (no tool access, fast responses), and a full agent that can search, read, create, and modify vault notes via MCP tools. These have fundamentally different requirements — the chat needs low latency and simple streaming, while the agent needs tool calling, conversation state, and MCP integration. + +## Decision + +**Maintain two separate AI interfaces: AI Chat (AIChatPanel) uses the Anthropic API directly via Rust for simple streaming responses. AI Agent (AiPanel) spawns Claude CLI as a subprocess with MCP vault integration for full tool access.** Both share a context builder (`ai-context.ts`) that provides the active note and linked entries. + +## Options considered + +- **Option A** (chosen): Dual architecture — optimized for each use case. Chat is fast and simple; agent is powerful with tool access. Downside: two codepaths to maintain. +- **Option B**: Single agent for both — always use Claude CLI. Downside: overkill for simple questions, slower startup, unnecessary tool overhead. +- **Option C**: Single API-based chat with manual tool calling — unified codebase. Downside: complex tool-calling loop implementation, no MCP integration. + +## Consequences + +- AI Chat: `AIChatPanel` + `useAIChat` hook → Rust `ai_chat` command → Anthropic API. Default model: Haiku 3.5 (fast, cheap). +- AI Agent: `AiPanel` + `useAiAgent` hook → Rust `claude_cli.rs` → Claude CLI subprocess with MCP config. +- Both panels share a toggle in the breadcrumb bar (Sparkle icon). +- Context builder (`ai-context.ts`) provides structured JSON with active note, linked notes, open tabs, vault metadata. +- Token budget: 60% of 180k context limit (~108k tokens max). +- Chat requires an Anthropic API key in settings; agent uses Claude CLI's own authentication. +- Re-evaluation trigger: if Anthropic releases an SDK that handles both simple chat and tool calling efficiently. diff --git a/docs/adr/0028-cli-agent-only-no-api-key.md b/docs/adr/0028-cli-agent-only-no-api-key.md new file mode 100644 index 0000000..2b579be --- /dev/null +++ b/docs/adr/0028-cli-agent-only-no-api-key.md @@ -0,0 +1,40 @@ +--- +type: ADR +id: "0028" +title: "CLI agent only — no direct Anthropic API key" +status: active +date: 2026-03-29 +supersedes: "0027" +--- + +## Context + +ADR-0027 introduced a dual AI architecture: a lightweight API-based chat (AIChatPanel) using the Anthropic API directly, and a full CLI agent (AiPanel) spawning Claude CLI as a subprocess with MCP tool access. In practice, the API chat was never shipped to users — the CLI agent covered all use cases and provided a superior experience through tool access and MCP integration. Maintaining two codepaths added complexity, and requiring users to manage an Anthropic API key created friction. + +## Decision + +**Remove the direct Anthropic API integration entirely. AI is available exclusively via CLI agent subprocesses (Claude Code, and in the future Codex or other CLI agents).** No API key field in settings. The CLI agent authenticates via its own mechanism (e.g. `claude` CLI login). + +Removed: +- `AIChatPanel` component, `useAIChat` hook +- Rust `ai_chat` command and `ai_chat.rs` module +- `anthropic_key` field from Settings (Rust and TypeScript) +- Vite dev-server Anthropic API proxy (`aiChatProxyPlugin`, `aiAgentProxyPlugin`) + +Kept: +- `AiPanel` + `useAiAgent` — Claude CLI subprocess with MCP vault integration +- Shared utilities in `ai-chat.ts` (`trimHistory`, `formatMessageWithHistory`, `streamClaudeChat`, etc.) +- `Cmd+I` keyboard shortcut and menu item for toggling the AI panel + +## Options considered + +- **Option A** (chosen): Remove API chat, keep CLI agent only. Simplifies codebase, removes API key management, single codepath. +- **Option B**: Keep both but hide API chat behind feature flag. Adds dead code weight without benefit. +- **Option C**: Replace CLI agent with API chat + manual tool calling. Loses MCP integration and Claude CLI features. + +## Consequences + +- Users no longer need to obtain or manage an Anthropic API key +- Existing saved API keys are silently ignored (the field no longer exists in the Settings struct; serde skips unknown fields on deserialization) +- Future CLI agents (Codex, etc.) can plug into the same `AiPanel` architecture +- If a lightweight chat mode is needed later, it should be built as a CLI agent mode, not a separate API integration diff --git a/docs/adr/0029-domain-command-builder-pattern.md b/docs/adr/0029-domain-command-builder-pattern.md new file mode 100644 index 0000000..675f53c --- /dev/null +++ b/docs/adr/0029-domain-command-builder-pattern.md @@ -0,0 +1,29 @@ +--- +type: ADR +id: "0029" +title: "Domain command builder pattern for useCommandRegistry" +status: active +date: 2026-03-30 +--- + +## Context + +`useCommandRegistry` was a 224-line "brain method" (CodeScene hotspot) that defined all command palette commands inline: navigation, note actions, git operations, view toggles, settings, type management, and filter controls. This monolithic structure scored 39 on CodeScene's complexity scale (target: ≤9.5 for hotspots), making it increasingly hard to add new commands without touching the central file. + +## Decision + +**Split command definitions into focused domain modules under `src/hooks/commands/`, each exporting a `build*Commands(config)` factory function. `useCommandRegistry` becomes a thin assembler that calls each builder and merges the results.** Domain modules: `navigationCommands`, `noteCommands`, `gitCommands`, `viewCommands`, `settingsCommands`, `typeCommands`, `filterCommands`. Shared types live in `commands/types.ts`; public API re-exported from `commands/index.ts`. + +## Options considered + +- **Option A** (chosen): Domain builder modules — each module owns its command shape and receives typed config. `useCommandRegistry` is pure assembly. All new files score 9.58–10.0. Downside: more files to navigate. +- **Option B**: Split by file but keep one large hook calling sub-hooks — sub-hooks still need shared state passed down, similar coupling. No real complexity win. +- **Option C**: Register commands imperatively via a global registry — decouples callers entirely. Downside: harder to trace, no TypeScript inference at the registration site, over-engineering for current scale. + +## Consequences + +- Adding a new command means editing the relevant domain module (e.g. `noteCommands.ts`) only, not touching the assembler. +- Each domain module receives only the config it needs — explicit, typed interface, no hook dependency. +- `useCommandRegistry` reduced from 224 lines to a thin assembler. +- Pattern is consistent with the Rust commands/ module split (ADR-0030). +- Re-evaluation trigger: if command count grows to the point where the assembler itself becomes a complexity hotspot. diff --git a/docs/adr/0030-rust-commands-module-split.md b/docs/adr/0030-rust-commands-module-split.md new file mode 100644 index 0000000..20f8a1d --- /dev/null +++ b/docs/adr/0030-rust-commands-module-split.md @@ -0,0 +1,29 @@ +--- +type: ADR +id: "0030" +title: "Rust commands/ module split by domain" +status: active +date: 2026-03-30 +--- + +## Context + +`src-tauri/src/commands.rs` grew to 937 lines as Tauri command handlers accumulated for vault CRUD, git/GitHub sync, AI, system, and window operations. All commands shared a single file with no domain separation, making it hard to navigate, review, and extend. The file was a CodeScene hotspot dragging down overall code health. + +## Decision + +**Replace `commands.rs` with a `commands/` module split by domain: `vault.rs`, `git.rs`, `github.rs`, `ai.rs`, `system.rs`, and `mod.rs` (shared utilities + re-exports).** Each file owns the Tauri command handlers for its domain and the `#[cfg(desktop)]` / `#[cfg(mobile)]` stubs for platform-conditional availability. `mod.rs` is kept thin (≤100 lines) with no command logic — only re-exports and shared helpers (`expand_tilde`, `parse_build_label`). + +## Options considered + +- **Option A** (chosen): Domain-based module split — mirrors the TypeScript `hooks/commands/` pattern (ADR-0029). Each file is independently reviewable and scores well on code health. Downside: more files to navigate. +- **Option B**: Split by platform (`desktop.rs`, `mobile.rs`) — aligns with `#[cfg(...)]` guards but mixes domain concerns. Harder to find a specific command. +- **Option C**: Keep monolith but add section comments — zero file-count cost, but doesn't solve complexity or reviewability. + +## Consequences + +- `github.rs` separates GitHub OAuth/API commands from git sync commands (`git.rs`), matching the underlying Rust module split (`github/` vs `git/`). +- Platform stubs (`#[cfg(mobile)]` error returns) live alongside the desktop implementation in the same domain file. +- `mod.rs` re-exports all command functions so `lib.rs` `invoke_handler!` registration is unchanged. +- New Tauri commands go into the appropriate domain file; if no domain fits, create a new one rather than putting it in `mod.rs`. +- Re-evaluation trigger: if a single domain file (e.g. `vault.rs`) itself grows beyond ~300 lines and becomes a hotspot. diff --git a/docs/adr/0031-full-app-for-note-windows.md b/docs/adr/0031-full-app-for-note-windows.md new file mode 100644 index 0000000..1849052 --- /dev/null +++ b/docs/adr/0031-full-app-for-note-windows.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0031" +title: "Full App instance for secondary note windows" +status: active +date: 2026-03-31 +--- + +## Context + +Laputa supports opening a note in a secondary window ("Open in New Window"). The original implementation used a dedicated `NoteWindow` component — a thin shell that rendered only the editor, duplicating some App-level logic (vault loading, settings, keyboard shortcuts) in a simplified but diverging form. As the main App gained features (properties editing, zoom, command palette, keyboard shortcuts), the `NoteWindow` shell fell behind, requiring ongoing maintenance to keep parity. + +## Decision + +**Remove `NoteWindow` and render the full `App` component in secondary note windows.** The window type is detected at startup via URL query parameters (`?window=note&path=...&vault=...`). When in note-window mode, the App initialises with panels hidden (sidebar collapsed, inspector collapsed) and auto-opens the target note once vault entries load. The window title is kept in sync with the active note title via the Tauri window API. + +## Options considered + +- **Keep `NoteWindow` shell** (status quo): lower initial bundle weight per window, but divergence grows with every main-App feature. Rejected — maintenance cost dominates. +- **Full `App` instance with URL-param mode** (chosen): complete feature parity for free; single code path for all window types. Trade-off: slightly heavier startup for secondary windows (full vault load), acceptable given local filesystem speed. +- **IPC-driven secondary window (no vault reload)**: secondary window subscribes to primary window's vault state via Tauri events. Maximum efficiency, avoids double vault reads. Deferred — requires significant IPC plumbing; can be layered on top later without changing the rendering model. + +## Consequences + +- Removes ~163 lines (`NoteWindow.tsx` deleted entirely) +- Secondary note windows get full feature parity: all keyboard shortcuts, properties panel, zoom, command palette, diff mode, raw editor +- `useLayoutPanels` gains an `initialInspectorCollapsed` option to support the hidden-panel initial state +- A new `src/utils/windowMode.ts` utility encapsulates URL-param detection — single source of truth for window-type logic +- Vault is loaded independently in each note window (no shared state with the main window); writes go to the same filesystem so eventual consistency is maintained via file-watching +- Triggers re-evaluation if: multiple simultaneous note windows cause measurable vault-read contention, or if IPC-driven shared-state windows become a product requirement diff --git a/docs/adr/0032-status-bar-for-git-actions.md b/docs/adr/0032-status-bar-for-git-actions.md new file mode 100644 index 0000000..9accfb1 --- /dev/null +++ b/docs/adr/0032-status-bar-for-git-actions.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0032" +title: 0032 Status Bar For Git Actions +status: active +date: 2026-03-31 +--- +[[Subfolder scanning and folder tree navigation]] + +## Context + +The Laputa sidebar originally surfaced git-related affordances — a "Changes" nav item (visible when modified files > 0), a "Pulse" nav item, and a "Commit & Push" button — alongside the note-type navigation filters and sections. This mixed two concerns in the sidebar: **navigation** (where to go) and **git status / actions** (what changed, what to do). As the sidebar grew, the git items created visual noise and made the nav hierarchy harder to scan. + +## Decision + +**Move Changes, Pulse, and Commit & Push out of the sidebar and into the bottom status bar.** The status bar shows a GitDiff icon with an orange count badge for modified files; a Pulse icon sits next to it. Commit & Push is accessible via an icon button beside the Changes indicator. The sidebar now contains only navigation items (filters and type sections). + +## Options considered + +* **Keep git items in sidebar** (status quo): familiar placement, visible at all times. Rejected — mixes navigation and action concerns; sidebar becomes harder to scan. +* **Status bar** (chosen): consistent with app conventions (build number, sync status, vault switcher already live there); persistent but unobtrusive; follows macOS app patterns where status/action items live at window bottom. +* **Toolbar / breadcrumb bar**: would require a new chrome layer or polluting the per-note breadcrumb with global git state. Rejected. + +## Consequences + +* Sidebar props `modifiedCount`, `onCommitPush`, `isGitVault` removed; sidebar renders navigation-only +* `StatusBar` gains `onClickPending`, `onClickPulse`, `onCommitPush`, `isGitVault` props +* Sidebar tests for Changes/Pulse/Commit button removed; StatusBar tests extended +* Users find Commit & Push in the status bar (same location as sync indicators) rather than bottom of sidebar — small discoverability change, offset by status bar being always visible regardless of sidebar collapsed state +* Triggers re-evaluation if: user research shows git actions are hard to discover in the status bar diff --git a/docs/adr/0033-subfolder-scanning-and-folder-tree.md b/docs/adr/0033-subfolder-scanning-and-folder-tree.md new file mode 100644 index 0000000..8dfc2b1 --- /dev/null +++ b/docs/adr/0033-subfolder-scanning-and-folder-tree.md @@ -0,0 +1,34 @@ +--- +type: ADR +id: "0033" +title: "Subfolder scanning and folder tree navigation" +status: active +date: 2026-03-31 +--- +## Context + +[[0032 Status Bar For Git Actions]] + +Supersedes the scanning constraint in [ADR-0006](0006-flat-vault-structure.md) which limited vault indexing to root-level `.md` files plus protected folders (`attachments/`, `assets/`). + +Users with folder-based workflows (PARA, Zettelkasten with folders, project directories) could not see or filter notes by directory. The vault scanner silently ignored all subdirectory `.md` files, making Laputa unsuitable for vaults with any folder structure. + +## Decision + +**Extend the Rust vault scanner to index **`.md`** files in all visible subdirectories, and expose the vault's folder tree via a new **`list_vault_folders`** Tauri command so the sidebar can render a collapsible FOLDERS section.** + +Hidden directories (names starting with `.`, plus `.git` and `.laputa`) are excluded from both scanning and the folder tree. + +## Options considered + +* **Option A** (chosen): Scan all subdirectories with `walkdir`, expose separate `list_vault_folders` command — simple, no schema changes to VaultEntry, folder tree is lightweight and independent of the entry cache. +* **Option B**: Add a `folder` field to VaultEntry and derive the tree on the frontend — couples folder metadata to the entry cache, complicates cache invalidation when folders are created/deleted without file changes. +* **Option C**: Keep flat scanning, add a "virtual folders" feature that groups by path prefix from frontmatter — doesn't solve the core problem of missing notes in subdirectories. + +## Consequences + +* All `.md` files in the vault are now indexed regardless of depth — vaults with many non-note `.md` files (e.g. node_modules) will see spurious entries. Mitigation: hidden directories are already excluded; users can add a `.laputaignore` in the future if needed. +* The git-based cache in `cache.rs` already uses `walkdir` for change detection, so this change aligns scanning with caching. +* `SidebarSelection` gains a new `{ kind: 'folder'; path: string }` variant — all exhaustive switches on selection kind must handle it. +* ADR-0006's "flat vault" principle is relaxed: notes can now live in subdirectories. Type definitions still live in `type/` at the root. +* Re-evaluate if users request recursive folder filtering (currently only direct children are shown when a folder is selected). diff --git a/docs/adr/0034-git-repo-required-for-vault.md b/docs/adr/0034-git-repo-required-for-vault.md new file mode 100644 index 0000000..8da300f --- /dev/null +++ b/docs/adr/0034-git-repo-required-for-vault.md @@ -0,0 +1,29 @@ +--- +type: ADR +id: "0034" +title: "Git repo required — blocking modal enforces vault prerequisite" +status: active +date: 2026-04-01 +--- + +## Context + +ADR-0014 (git-based vault cache) and ADR-0021 (push-to-main workflow) both assume the vault is a git repository, but neither codified it as a hard enforcement. In practice, opening a non-git folder silently degraded: the cache couldn't compute a commit hash, Pulse/Changes were empty, and commit/push commands failed. The failure mode was invisible to users. + +## Decision + +**When the app opens a vault that has no `.git` directory, a blocking modal prevents all app use until the user either initialises a git repository (git init + initial commit, offered as a one-click action) or selects a different vault. The check is performed by a new `is_git_repo` Tauri command. In browser/dev mode, the check fails open (modal is skipped).** + +## Options considered + +- **Option A** (chosen): Hard block via modal on vault open — unambiguous, prevents silent failures, surfaces the fix immediately. Downside: breaks existing workflows for users with non-git vaults; requires a clear escape hatch (choose different vault). +- **Option B**: Soft warning banner, allow using the app without git — avoids blocking users, but silent failures persist for Pulse/Changes/commit features. +- **Option C**: Auto-init git on vault open without asking — less friction, but surprising; user may not want their vault in git. + +## Consequences + +- Git is now a first-class prerequisite for Laputa vaults, not just implied by the cache strategy. +- The `is_git_repo` command is intentionally lightweight (checks for `.git` existence only; does not validate remote or commit history). +- The modal offers `git init` + an initial commit as a one-click path, lowering the barrier for new users. +- Browser mode bypasses the check so dev/Storybook workflows are unaffected. +- Re-evaluate if Laputa needs to support non-git vaults (e.g., iCloud-only, shared network drive); at that point ADR-0014 would also need revisiting. diff --git a/docs/adr/0035-path-suffix-wikilink-resolution.md b/docs/adr/0035-path-suffix-wikilink-resolution.md new file mode 100644 index 0000000..b855ee0 --- /dev/null +++ b/docs/adr/0035-path-suffix-wikilink-resolution.md @@ -0,0 +1,31 @@ +--- +type: ADR +id: "0035" +title: "Path-suffix wikilink resolution for subfolder vaults" +status: active +date: 2026-04-01 +--- + +## Context + +ADR-0006 stated that wikilink resolution was "simplified to multi-pass title/filename matching — no path-based matching needed" because the vault was flat. ADR-0033 relaxed the flat-vault constraint by adding subfolder scanning. As a result, wikilinks like `[[docs/adr/0031-foo]]` or `[[adr/0031-foo]]` could not resolve to entries in subdirectories: the resolver only matched on `title` and `filename` stem, never on the vault-relative path. + +The backlink detection in the Inspector also used a hardcoded `/Laputa/` path regex, which was wrong for any vault that isn't named "Laputa". + +## Decision + +**Add path-suffix matching as Pass 1 of wikilink resolution: a link target resolves to a `VaultEntry` if the entry's vault-relative path ends with the link string (with or without `.md`). Filename-stem matching (the previous Pass 1) becomes Pass 2. Inspector backlinks replace the hardcoded `/Laputa/` regex with a generic `targetMatchesEntry` path-suffix helper. Autocomplete pre-filter also matches against the full vault-relative path so subfolder names surface results.** + +## Options considered + +- **Option A** (chosen): Path-suffix as Pass 1, then filename match as Pass 2 — consistent with how Obsidian resolves links in multi-folder vaults, zero config. Downside: if two notes share the same filename in different folders, only the first (path-suffix) match wins. +- **Option B**: Strict full-path matching only (disable title-stem resolution) — unambiguous, but breaks the majority of existing short-form `[[note-title]]` links. +- **Option C**: Keep title-only matching, require full paths for subfolder notes — backwards-compatible, but forces users to always type full paths for subfolders, defeating the purpose of wikilinks. + +## Consequences + +- Supersedes the "no path-based matching needed" clause from ADR-0006 (that assumption was contingent on the flat vault invariant, which ADR-0033 relaxed). +- `relativePathStem` utility added in `wikilink.ts` to extract the vault-relative path stem from a full `VaultEntry`. +- The Inspector's `targetMatchesEntry` helper is now the canonical way to test if a link resolves to an entry — use it everywhere instead of ad-hoc regex. +- Wikilink autocomplete suggestions now surface notes in subfolders when users type a folder prefix (e.g. `[[adr/`). +- Re-evaluate if path-suffix ambiguity (two files with the same name in different folders) becomes a user complaint. diff --git a/docs/adr/0036-external-rename-detection-via-git-diff.md b/docs/adr/0036-external-rename-detection-via-git-diff.md new file mode 100644 index 0000000..e146ae3 --- /dev/null +++ b/docs/adr/0036-external-rename-detection-via-git-diff.md @@ -0,0 +1,31 @@ +--- +type: ADR +id: "0036" +title: "External rename detection via git diff on focus regain" +status: active +date: 2026-04-01 +--- + +## Context + +Laputa handles in-app renames (rename.rs) and propagates wikilink updates across the vault. But notes can also be renamed externally — from Finder, another editor, or a git operation (e.g., `git mv`). In those cases, the app had no way to detect that a rename had occurred, leaving wikilinks broken and the vault inconsistent. + +The app already uses git for the cache (ADR-0014) and requires git as a vault prerequisite (ADR-0034), making git diff a natural and already-available detection mechanism. + +## Decision + +**When the app window regains focus, run `git diff --diff-filter=R --name-status HEAD` to detect file renames that occurred since the last committed HEAD. If any renamed `.md` files are found, show a non-blocking banner ("X file(s) renamed — update wikilinks?"). Accepting triggers the existing vault-wide wikilink replacement logic (reused from rename.rs). Ignoring dismisses the banner without changes. New Tauri commands: `detect_renames` and `update_wikilinks_for_renames`.** + +## Options considered + +- **Option A** (chosen): Git diff on focus regain, non-blocking banner — uses existing infrastructure, non-disruptive, user retains control. Downside: only detects renames that are staged/committed; uncommitted renames via `git mv` are captured, but renames done purely in Finder (no git involvement) are not. +- **Option B**: `FSEvents` / file-system watcher for rename events — catches all renames regardless of git. Downside: significantly more complex, requires Rust async machinery, false positives from editor temp files, and this feature is already planned as a separate enhancement. +- **Option C**: Scan for broken wikilinks on focus — correct but O(n) and noisy; doesn't tell us the new filename. + +## Consequences + +- Git's rename detection (`--diff-filter=R`) requires the rename to be git-tracked (either staged or committed); renames that happen outside git knowledge are not detected by this mechanism. +- The on-focus check runs `git diff HEAD` which is fast but adds a small shell invocation overhead each time the window activates. This is acceptable for typical vault sizes. +- `rename.rs` is now shared between in-app renames and external rename recovery — the replacement logic is the canonical entry point for wikilink bulk updates. +- The banner is non-blocking and "Ignore" is always available — the user never loses work. +- Re-evaluate if FS-level rename detection (outside git) becomes a priority; at that point this mechanism would be a fallback, not the primary strategy. diff --git a/docs/adr/0037-codemirror-language-markdown-highlighting.md b/docs/adr/0037-codemirror-language-markdown-highlighting.md new file mode 100644 index 0000000..ea2e62a --- /dev/null +++ b/docs/adr/0037-codemirror-language-markdown-highlighting.md @@ -0,0 +1,31 @@ +--- +type: ADR +id: "0037" +title: "Language-based markdown syntax highlighting in raw editor" +status: active +date: 2026-04-01 +--- + +## Context + +The raw editor (CodeMirror 6, introduced in ADR-0022) initially had a custom `frontmatterHighlight` extension that used regex-based decoration for YAML frontmatter and headings. Markdown body content had no syntax highlighting at all, making the raw editor feel like a plain textarea despite being a full CodeMirror instance. + +Extending the custom regex-based approach to cover all markdown syntax (bold, italic, links, lists, blockquotes, code) would have been brittle and hard to maintain. + +## Decision + +**Replace the custom heading decoration in `frontmatterHighlight.ts` with `@codemirror/lang-markdown` (the official CodeMirror language package). A custom `HighlightStyle` maps CodeMirror highlight tags to visual styles for headings, bold, italic, strikethrough, links, lists, blockquotes, and inline code. The frontmatter YAML plugin is retained for YAML-specific colouring but its heading decoration is removed in favour of the language parser.** + +## Options considered + +- **Option A** (chosen): `@codemirror/lang-markdown` with custom HighlightStyle — uses the official, maintained language parser; future highlight rules are one CSS declaration. Downside: adds a new npm dependency; the custom frontmatter plugin must be kept separately. +- **Option B**: Extend the custom regex plugin to cover all markdown — no new dependency. Downside: regex-based tokenisation is fragile (e.g., nested formatting), already proving hard to maintain after the heading/frontmatter overlap bug. +- **Option C**: Switch to a markdown-aware editor (e.g., Milkdown, Monaco) — full-featured. Downside: major migration, breaks the dual-editor architecture in ADR-0022, significant scope. + +## Consequences + +- `@codemirror/lang-markdown` added to `package.json` — this is the only new runtime dependency introduced by this change. +- `frontmatterHighlight.ts` is simplified (heading decoration removed); `markdownHighlight.ts` is the new extension responsible for body highlighting. +- The two extensions are composed in `useCodeMirror.ts` — YAML frontmatter block is still styled by the custom plugin; everything else by the language parser. +- Future syntax highlighting changes (e.g., task lists, tables) can be added by extending the `HighlightStyle` without modifying the parser. +- Re-evaluate if `@codemirror/lang-markdown` conflicts with the custom frontmatter YAML handling as the editor evolves (e.g., if frontmatter block needs to be parsed as a code block rather than decorated text). diff --git a/docs/adr/0038-frontmatter-backed-favorites.md b/docs/adr/0038-frontmatter-backed-favorites.md new file mode 100644 index 0000000..5a79a3c --- /dev/null +++ b/docs/adr/0038-frontmatter-backed-favorites.md @@ -0,0 +1,32 @@ +--- +type: ADR +id: "0038" +title: "Frontmatter-backed favorites with _favorite and _favorite_index" +status: active +date: 2026-04-02 +--- + +## Context + +Users want to pin frequently-accessed notes to a dedicated FAVORITES section in the sidebar for quick navigation. The app needs a persistence mechanism for which notes are favorited and their display order. + +## Decision + +**Favorites are stored as two system properties in each note's YAML frontmatter: `_favorite: true` and `_favorite_index: `.** + +- `_favorite`: boolean. Present and `true` = favorited. Absent = not favorited. Toggling off deletes the key entirely (no `_favorite: false`). +- `_favorite_index`: integer. Controls display order in the FAVORITES sidebar section (lower = higher). Assigned automatically on favorite, updated on drag-to-reorder. +- Both use the `_` prefix convention (ADR 0008) — they are system-owned and hidden from the Properties panel. + +## Options considered + +- **Frontmatter per-note (chosen)**: Each note carries its own favorite state. Portable across devices (synced via git). No separate metadata file. Cons: two extra frontmatter writes on reorder. +- **Separate `.laputa/favorites.json` file**: Central list of favorite paths. Simpler reorder (one file write). Cons: not portable if `.laputa/` is gitignored; path references break on rename. +- **SQLite/app-level metadata**: Fast queries. Cons: not synced via git; diverges from frontmatter-first data model established in ADR 0008. + +## Consequences + +- Favorites survive vault sync via git — any client that reads frontmatter sees them. +- Reorder writes `_favorite_index` to N files (one per affected note). Acceptable for typical favorites lists (< 20 items). +- If `_favorite: true` exists but `_favorite_index` is absent, the note is appended to the end of the list. +- Re-evaluate if favorites list exceeds ~50 items and reorder writes become a performance concern. diff --git a/docs/adr/0039-git-history-for-note-dates.md b/docs/adr/0039-git-history-for-note-dates.md new file mode 100644 index 0000000..5b9003d --- /dev/null +++ b/docs/adr/0039-git-history-for-note-dates.md @@ -0,0 +1,40 @@ +--- +type: ADR +id: "0039" +title: "Use git history for note creation and modification dates" +status: active +date: 2026-04-02 +--- + +## Context + +Filesystem metadata (`ctime`/`mtime`) is unreliable for a git-backed vault. After `git clone`, `git pull`, or iCloud sync, files appear "newly created" even when they have years of history. This causes incorrect sort ordering in the note list and wrong dates in the inspector panel. + +## Decision + +**Use `git log` to determine the true creation and modification dates for notes.** A single batch `git log --format="COMMIT %aI" --name-only` command walks the full commit history and extracts: + +- **modified_at** = author date of the most recent commit that touched the file +- **created_at** = author date of the oldest commit that touched the file + +The batch approach runs once per vault scan (not per-file), parsing the log output in a single linear pass. Results are stored in a `HashMap` keyed by vault-relative path and threaded through the existing `parse_md_file` / `scan_vault` / `scan_vault_cached` pipeline. + +### Fallback to filesystem dates + +- **Non-git vaults** (no `.git` directory): all notes use filesystem `mtime`/`ctime`. +- **Uncommitted new files**: not in git log output, so filesystem dates are used automatically. +- **Single-file reloads** (`reload_entry`): use filesystem dates since the file was just saved and the most accurate timestamp is the filesystem one. + +## Options considered + +- **Per-file `git log`**: Correct but O(n) subprocesses. Too slow for vaults with 500+ notes. +- **Frontmatter dates** (e.g., `created: 2025-01-15`): Requires user discipline. Not automatic. Breaks when users forget to set them. +- **Filesystem metadata** (current): Unreliable across clones, pulls, and cloud sync. +- **Single batch `git log`** (chosen): One subprocess, O(n) parsing, correct dates for all committed files. + +## Consequences + +- Note sort-by-created and sort-by-modified now reflect true git history, stable across clones and machines. +- First vault scan runs one `git log` over the full history. For a vault with 1000 files and 500 commits, output is ~100KB and parses in <100ms. +- Renamed files get `created_at` set to the rename commit date (not the original creation). Acceptable trade-off vs. the complexity of rename tracking. +- `CACHE_VERSION` bumped from 9 to 10 to force a full rescan with git dates on upgrade. diff --git a/docs/adr/0040-custom-views-yml-filter-engine.md b/docs/adr/0040-custom-views-yml-filter-engine.md new file mode 100644 index 0000000..794a68d --- /dev/null +++ b/docs/adr/0040-custom-views-yml-filter-engine.md @@ -0,0 +1,59 @@ +--- +type: ADR +id: "0040" +title: "Custom views as .yml files with client-side filter engine" +status: active +date: 2026-04-02 +--- + +## Context + +Users want to save reusable filtered note lists (e.g., "Active Projects", "This Week's Events") as named sidebar items. These views need to persist across sessions, sync via git, and support arbitrary frontmatter conditions. + +## Decision + +**Custom views are stored as `.yml` files in `.laputa/views/` within the vault root.** Each file defines a named view with filter conditions, optional icon/color, and sort preferences. + +### File format + +```yaml +name: Active Projects +icon: rocket +color: blue +sort: "modified:desc" +filters: + all: + - field: type + op: equals + value: Project + - field: status + op: not_equals + value: done +``` + +### Filter engine + +Filters use a tree of AND/OR groups (`all`/`any`) containing conditions. Each condition specifies a `field`, `op` (operator), and optional `value`. Supported operators: `equals`, `not_equals`, `contains`, `not_contains`, `any_of`, `none_of`, `is_empty`, `is_not_empty`, `before`, `after`. + +Field resolution: built-in fields (`type`, `status`, `title`, `archived`, `trashed`, `favorite`) map to VaultEntry struct fields. Unknown fields fall back to `entry.properties`, then `entry.relationships`. + +Wikilink values like `[[target|Alias]]` are matched by stem (stripping brackets and pipe+alias). + +### Architecture + +- **Rust backend** (`vault/views.rs`): YAML parsing via `serde_yaml`, filter evaluation, file CRUD. Three Tauri commands: `list_views`, `save_view_cmd`, `delete_view_cmd`. +- **Frontend**: Client-side filter evaluation against the already-loaded `VaultEntry[]` array. The Rust `evaluate_view` exists for MCP/CLI access but is not the primary UI path. +- **Sidebar**: VIEWS section between Favorites and Types, hidden when no views exist. + +## Options considered + +- **SQLite views table**: Fast queries, but not portable via git and diverges from the file-first data model. +- **Frontmatter on a special `.md` file**: Overloads the note format for a non-note concept. +- **Standalone `.yml` files (chosen)**: Portable (synced via git), editable by hand or UI, naturally separated from note content. + +## Consequences + +- New dependency: `serde_yaml` crate for YAML parsing. +- `.laputa/views/` directory auto-created on first view save. Already excluded from vault scanning via `HIDDEN_DIRS`. +- Views sync across devices via git. Conflicts resolved by standard git merge (YAML is line-based, merges well). +- Sort persistence: changing sort while a view is selected writes `sort` back to the `.yml` file. diff --git a/docs/adr/0041-filekind-all-files-in-vault-scanner.md b/docs/adr/0041-filekind-all-files-in-vault-scanner.md new file mode 100644 index 0000000..c8b29fe --- /dev/null +++ b/docs/adr/0041-filekind-all-files-in-vault-scanner.md @@ -0,0 +1,39 @@ +--- +type: ADR +id: "0041" +title: "fileKind field — scan all vault files, not just markdown" +status: active +date: 2026-04-02 +--- + +## Context + +Laputa vaults often contain non-markdown files alongside notes: images, PDFs, YAML configs, JSON exports, scripts, etc. Previously the vault scanner only indexed `.md` files — all other files were invisible to the app. This made the Folder view incomplete: navigating a folder containing a `config.yml` or `photo.png` showed nothing, even though the file was physically there. + +The need arose when adding a Folder tree view that is meant to mirror the actual filesystem structure. Users expect to see all files in a folder, as any file manager would show. + +## Decision + +**The vault scanner now indexes all files (not just `.md`). Every `VaultEntry` carries a `fileKind` field (`"markdown"`, `"text"`, or `"binary"`) that controls how the frontend renders and opens it.** + +- **`"markdown"`**: full Laputa behavior — frontmatter parsing, BlockNote editor, title sync, type system. +- **`"text"`**: filename as title, no frontmatter, opens in raw CodeMirror editor. Covers `.yml`, `.json`, `.ts`, `.py`, `.sh`, etc. +- **`"binary"`**: filename as title, grayed out, non-clickable. Covers images, PDFs, binaries. +- **Hidden files** (starting with `.`) are skipped regardless of extension. +- **Non-folder views** (All Notes, type sections, Custom Views) still show only `"markdown"` entries. +- **Folder view** shows all file kinds. + +## Options considered + +- **Option A** (chosen): Single `VaultEntry` model with a `fileKind` discriminator. All files go through the same pipeline; rendering is gated by `fileKind`. Simple, incremental — existing code paths untouched for markdown files. +- **Option B**: Separate data model for non-markdown files (e.g. `AssetEntry`). Cleaner type hierarchy, but requires duplicating list/filter/sort logic for two types across the codebase. +- **Option C**: Only scan `.md` + explicitly listed extensions (e.g. `.yml`, `.json`). Simpler initial implementation, but requires ongoing maintenance of an allowlist and still misses user files. Abandoned in favor of a deny-list approach (only `.`-prefixed hidden files are excluded). + +## Consequences + +- Non-markdown files are visible in Folder view — the app now behaves like a file manager in that context. +- All views except Folder view continue to show only markdown files (the `isMarkdown` guard in `filterEntries`). +- `countByFilter` / `countAllByFilter` exclude non-markdown entries to keep sidebar counters accurate. +- The vault cache version was bumped to `11` to force a full rescan after this change. +- Binary files have no click action — clicking does nothing (no editor opened). +- Re-evaluation trigger: if users need to preview or edit binary files (e.g. images), a dedicated preview pane would need a separate ADR. diff --git a/docs/adr/0042-posthog-release-channels-feature-flags.md b/docs/adr/0042-posthog-release-channels-feature-flags.md new file mode 100644 index 0000000..98205b8 --- /dev/null +++ b/docs/adr/0042-posthog-release-channels-feature-flags.md @@ -0,0 +1,37 @@ +--- +type: ADR +id: "0042" +title: "PostHog-based release channels and feature flags" +status: active +date: 2026-04-03 +supersedes: "0017" +--- +## Context + +ADR-0017 introduced canary/stable update channels with localStorage-based feature flags. This worked for local development but lacked remote flag management — promoting a feature from beta to stable required a code change and rebuild. + +## Decision + +**Replace localStorage feature flags with PostHog-based feature flags, evaluated per release channel (alpha/beta/stable). The release channel is a user-selectable setting; PostHog flag rules determine which features are visible for each channel.** + +- **Alpha**: all features always enabled (no PostHog lookup needed, works offline) +- **Beta**: sees features where the PostHog flag targets `release_channel = beta` +- **Stable** (default): sees features where the PostHog flag targets `release_channel = stable` +- Promotion = flipping a PostHog flag on the dashboard. Zero code changes, zero rebuilds. +- `isFeatureEnabled(flagKey)` in `telemetry.ts` is the single evaluation point. +- localStorage overrides (`ff_`) still work for dev/QA testing (checked first). +- Offline: PostHog caches flags in localStorage; alpha always works; first-launch-no-network falls back to hardcoded defaults. + +## Options considered + +* **Option A**: Keep localStorage-only flags (ADR-0017) — no server dependency, but no remote management. +* **Option B** (chosen): PostHog feature flags — we already use PostHog for analytics, so no new dependency. Remote flag management, per-channel targeting, gradual rollouts via PostHog dashboard. +* **Option C**: Dedicated feature flag service (LaunchDarkly, Unleash) — more powerful but adds a new vendor dependency. + +## Consequences + +* `release_channel` added to Settings (persisted via Tauri backend, not vault). +* `useTelemetry` passes `release_channel` as a PostHog person property on identify. +* `isFeatureEnabled()` checks channel → PostHog → hardcoded defaults. +* `useFeatureFlag` hook updated to delegate to `isFeatureEnabled` (after localStorage override check). +* ADR-0017 is superseded — the canary update channel remains, but feature gating moves from localStorage to PostHog. diff --git a/docs/adr/0042-trash-auto-purge-safety-model.md b/docs/adr/0042-trash-auto-purge-safety-model.md new file mode 100644 index 0000000..852abf0 --- /dev/null +++ b/docs/adr/0042-trash-auto-purge-safety-model.md @@ -0,0 +1,56 @@ +--- +type: ADR +id: "0042" +title: "Trash auto-purge safety model" +status: superseded +date: 2026-04-05 +superseded_by: "0045" +--- + +## Context + +The Trash view already shows a "Notes trashed more than 30 days ago will be permanently deleted" warning, but the app never actually enforces this. Users expect trashed notes to be cleaned up automatically after 30 days — if we advertise it, we must implement it. + +This is one of the most dangerous operations in the app: a bug could cause irreversible data loss. The safety model must be explicit and conservative. + +## Decision + +**Auto-purge trashed notes older than 30 days on app launch and window focus (max once per hour), using OS trash (`trash::delete`) for soft-deletion, with mandatory 5-point safety validation per file and an audit log at `.laputa/purge.log`.** + +### Safety checks (all must pass before deleting any file) + +1. `_trashed: true` (or legacy aliases `Trashed`, `trashed`) is present in frontmatter and set to a truthy value +2. `_trashed_at` (or legacy aliases `Trashed at`, `trashed_at`) is present and parseable as a date +3. The parsed date is **strictly more than 30 days ago** (exactly 30 days = skip) +4. The file exists on disk at the expected path +5. The file's canonical path is inside the vault root (prevents path traversal) + +If any check fails, the file is skipped with a warning log. The purge never aborts early — it processes all candidates independently. + +### Deletion method + +Use the `trash` crate (`trash::delete`) to move files to the OS trash (macOS Trash, Windows Recycle Bin) instead of `fs::remove_file`. This gives users a last-resort recovery path. If OS trash fails, fall back to `fs::remove_file` and log a warning. + +### Trigger conditions + +- On app launch (in `run_startup_tasks`) +- On window focus (`WindowEvent::Focused(true)`) — throttled to max once per hour using a `Mutex` timestamp + +### Audit log + +Every purge run appends to `.laputa/purge.log` with timestamp, files checked count, files purged count, and each purged file path. Users can inspect this file to audit what was deleted and when. + +## Options considered + +- **Option A — OS trash via `trash` crate** (chosen): moves to OS trash, user can recover from Trash app. Adds a ~small dependency. Safe default. +- **Option B — `fs::remove_file` (permanent)**: simpler, no dependency, but no recovery path. Too risky for an automatic background operation. +- **Option C — Move to `.laputa/purged/` archive folder**: custom recovery mechanism, but clutters vault directory and users wouldn't know to look there. + +## Consequences + +- Users get the auto-cleanup behavior already advertised in the UI +- Accidentally trashed notes have a second chance via OS Trash +- The `trash` crate adds a platform-specific dependency (macOS: `NSFileManager`, Windows: `IFileOperation`, Linux: freedesktop spec) +- The hourly throttle prevents excessive disk I/O on rapid focus/unfocus cycles +- The purge log provides auditability but will grow over time (acceptable for a text log) +- Re-evaluate if users report OS Trash filling up with vault files diff --git a/docs/adr/0043-reactive-vault-state-on-save.md b/docs/adr/0043-reactive-vault-state-on-save.md new file mode 100644 index 0000000..f99fd20 --- /dev/null +++ b/docs/adr/0043-reactive-vault-state-on-save.md @@ -0,0 +1,63 @@ +--- +type: ADR +id: "0043" +title: "Reactive vault state: editor changes propagate immediately to all UI" +status: active +date: 2026-04-05 +--- +## Context + +When a user edits frontmatter in the raw editor (or BlockNote preserves it), changes to metadata fields like `title`, `type`, `_favorite`, `_archived`, and `sidebar_label` must be reflected immediately across all UI components — sidebar sections, note list, breadcrumb bar, inspector, and tabs. + +Previously, after `save_note_content`, only derived fields (`outgoingLinks`, `snippet`, `wordCount`) were updated in `vault.entries`. Frontmatter-derived fields were stale until a full vault reload. + +## Decision + +**All frontmatter changes are parsed in real-time and applied to `vault.entries` via `updateEntry()` during content editing, not after save.** + +### How it works + +1. **On every content change** (keystroke in raw editor, or BlockNote onChange), `useEditorSaveWithLinks.handleContentChange` is called. +2. It invokes `contentToEntryPatch(content)` which parses frontmatter and maps known keys to `VaultEntry` fields. +3. If the parsed patch differs from the previous one, `updateEntry(path, patch)` merges it into `vault.entries`. +4. All UI components derive from `vault.entries` via React reactivity — they re-render automatically. + +### Mapped fields + +`contentToEntryPatch` maps these frontmatter keys to `VaultEntry` fields: + +| Frontmatter key | VaultEntry field | Notes | +|---|---|---| +| `title` | `title` | | +| `type` / `is_a` | `isA` | | +| `status` | `status` | | +| `_favorite` | `favorite` | | +| `_favorite_index` | `favoriteIndex` | | +| `_archived` / `archived` | `archived` | | +| `_trashed` / `trashed` | `trashed` | | +| `_organized` | `organized` | | +| `color` | `color` | Type entries | +| `icon` | `icon` | Type entries | +| `order` | `order` | Type entries | +| `sidebar_label` | `sidebarLabel` | Type entries | +| `visible` | `visible` | Type entries | +| `template` | `template` | Type entries | +| `sort` | `sort` | Type entries | +| `view` | `view` | Type entries | +| `aliases` | `aliases` | | +| `belongs_to` | `belongsTo` | | +| `related_to` | `relatedTo` | | + +### Inspector operations use a separate, more direct path + +When the user edits frontmatter via the Inspector panel, `runFrontmatterAndApply` calls the Tauri command and immediately applies the result via `updateEntry()`. This path was already reactive before this ADR. + +### View files (.yml) + +View files are not markdown notes — they have no frontmatter delimiters. When a `.yml` file is saved, `onNotePersisted` triggers `reloadViews()` to refresh the sidebar view list. + +## Consequences + +- Any new frontmatter key that should affect the UI must be added to `frontmatterToEntryPatch` and its delete counterpart. +- Components must read note metadata from `vault.entries` (via props), never from local state that could diverge. +- The `reload_vault_entry` Tauri command exists for full re-parsing from disk but is not needed in the normal editing flow — `contentToEntryPatch` handles it client-side. diff --git a/docs/adr/0044-h1-as-title-primary-source.md b/docs/adr/0044-h1-as-title-primary-source.md new file mode 100644 index 0000000..b03acee --- /dev/null +++ b/docs/adr/0044-h1-as-title-primary-source.md @@ -0,0 +1,46 @@ +--- +type: ADR +id: "0044" +title: "H1 as primary title source — filename as stable identifier" +status: active +date: 2026-04-07 +supersedes: "0007" +--- + +## Context + +ADR-0007 established that the `title:` frontmatter field is the source of truth for display titles, with filenames derived from it via slugification and kept in sync bidirectionally. This model had a key assumption: the user explicitly types a title before writing content. + +In practice this created friction: new notes required a title upfront, the TitleField was always visible cluttering the editor, and the "title = filename slug" contract was fragile when users renamed files externally. The team wanted a more natural writing flow where you just start writing — like most text editors — and the title emerges from the document. + +A pair of commits on 2026-04-06 (`377a3f8d`, `7daf6898`) implemented a fundamentally different model. + +## Decision + +**The first `# H1` heading in the note body is the canonical display title. The `title:` frontmatter field is legacy/backward-compat only. New notes are created with filename `untitled-{type}-{timestamp}.md` and no `title:` in frontmatter. On save, if the note has an H1, the file is auto-renamed to a slug derived from it (collision-safe with `-2`, `-3` suffixes).** + +Title resolution priority (Rust `extract_title`): +1. H1 on the first non-empty line of the body +2. Frontmatter `title:` field (legacy, backward-compat) +3. Slug-to-title derivation from filename stem + +The `has_h1: bool` field on `VaultEntry` signals the frontend to hide `TitleField` and the icon picker when an H1 is present, since the H1 serves as the title surface. + +The breadcrumb bar shows the **filename stem** (not display title) so users always know the actual file identifier. + +Auto-rename (`auto_rename_untitled` Tauri command) fires on save for `untitled-*` files that gain an H1, converting them to a human-readable slug. + +## Options considered + +- **Option A — H1 as primary title + auto-rename on save** (chosen): natural writing flow, filename eventually reflects content, TitleField hidden when H1 present. Downside: auto-rename can surprise users; breadcrumb must show filename to stay honest. +- **Option B — Keep `title:` frontmatter as source of truth** (ADR-0007, now superseded): explicit, deterministic. Downside: forces upfront titling, TitleField always visible, friction for quick capture. +- **Option C — UUID-based filenames, title only in H1**: filenames never change, no rename logic needed. Downside: vault unreadable in Finder/terminal, breaks the plain-files principle (ADR-0002). + +## Consequences + +- New notes start as `untitled-note-{timestamp}.md` — the vault may accumulate untitled files if users abandon drafts without writing an H1 +- `TitleField` component is hidden when `has_h1 = true`; icon picker is also hidden (icons only make sense on titled notes) +- Frontmatter `title:` still parsed for backward-compat; existing vaults with explicit titles continue to work +- Auto-rename on save introduces a file rename side-effect during editing — wikilinks pointing to the old filename may break until the rename propagates +- The breadcrumb filename display makes the system more honest but slightly more technical for non-power users +- Re-evaluate if users find auto-rename disorienting or if wikilink breakage during rename becomes a reliability concern diff --git a/docs/adr/0045-permanent-delete-no-trash.md b/docs/adr/0045-permanent-delete-no-trash.md new file mode 100644 index 0000000..843b39e --- /dev/null +++ b/docs/adr/0045-permanent-delete-no-trash.md @@ -0,0 +1,37 @@ +--- +type: ADR +id: "0045" +title: "Permanent delete with confirm modal — no Trash system" +status: active +date: 2026-04-07 +supersedes: "0042" +--- + +## Context + +ADR-0042 designed a Trash auto-purge safety model (soft-delete with 30-day retention, OS trash via `trash` crate, audit log). This was built on top of a Trash system that treated deletion as a two-phase operation: move to trash → auto-purge after 30 days. + +The Trash system was subsequently identified as unnecessary complexity: it required `trashed`/`trashedAt` frontmatter fields, sidebar filtering, editor banners, inspector components, dedicated smoke tests, and a `trash` crate dependency. The safety guarantee users actually need is a **confirmation prompt before irreversible action**, not a soft-delete buffer — especially given notes live in a git repo (vault git history is already a recovery mechanism per ADR-0034 and ADR-0014). + +Commit `e581ad36` on 2026-04-06 removed the entire Trash system (123 files changed, ~3164 lines deleted). + +## Decision + +**Delete is permanent and immediate, gated only by a confirmation modal (`useDeleteActions`). Notes with `trashed: true` in existing vault frontmatter are treated as normal notes (the flag is ignored by the parser). The `trash` crate dependency is removed.** + +The confirmation modal is the sole safety gate. No soft-delete, no Trash view, no auto-purge scheduler, no `.laputa/purge.log`. + +## Options considered + +- **Option A — Permanent delete + confirm modal** (chosen): simple, honest, no hidden state. Git history provides recovery. Removes ~3000 lines of code and a platform-specific dependency. Downside: no in-app recovery path for users who don't know about git. +- **Option B — OS Trash via `trash` crate** (ADR-0042, now superseded): soft-delete to OS Trash, user can recover from macOS Trash app. Downside: additional dependency, complex auto-purge scheduler, misleading "auto-purge" promise that was never actually implemented. +- **Option C — `.laputa/deleted/` archive folder**: custom recovery mechanism inside vault. Downside: clutters vault, users wouldn't know to look there, still requires manual cleanup. + +## Consequences + +- Users who accidentally delete a note must recover from git history (`git checkout HEAD -- path/to/note.md`) — this is a power-user action +- `trashed`/`trashedAt` frontmatter fields in existing vaults are silently ignored — no migration needed, no data loss +- The `trash` crate is removed from `Cargo.toml` — build times improve marginally +- Smoke tests for trash flows are deleted; delete-related test coverage is now purely the confirm modal behavior +- The Trash view, sidebar filter, note banners, and bulk-trash actions are all gone — simpler UI surface +- Re-evaluate if user feedback shows significant accidental deletion incidents, or if git-based recovery proves too inaccessible for non-technical users diff --git a/docs/adr/0046-starter-vault-cloned-from-github.md b/docs/adr/0046-starter-vault-cloned-from-github.md new file mode 100644 index 0000000..a493f3e --- /dev/null +++ b/docs/adr/0046-starter-vault-cloned-from-github.md @@ -0,0 +1,36 @@ +--- +type: ADR +id: "0046" +title: "Starter vault cloned from GitHub at runtime — no bundled content" +status: active +date: 2026-04-08 +--- + +## Context + +Laputa ships an optional "Getting Started" vault to help new users understand types, properties, wikilinks, and relationships. Previously, all starter content (markdown files, view YAMLs) was stored inside the app repo under `getting-started-vault/` and written to disk via `create_getting_started_vault()`. This created friction: updating sample content required a new app release, the content grew stale quickly, and the bundled files added noise to the main repo. + +## Decision + +**The Getting Started vault is no longer bundled in the app repo. On first launch, if the user selects "Get started with a template", the app clones the public starter repo (`https://github.com/refactoringhq/laputa-getting-started.git`) into a user-chosen folder using the existing git clone infrastructure.** + +- `getting_started.rs` now holds only the public repo URL constant and delegates to `clone_public_repo()`. +- The `getting-started-vault/` directory has been removed from the app repo. +- `create_getting_started_vault(targetPath)` takes an explicit target path (chosen via folder picker) instead of defaulting to Documents/Getting Started. +- Clone failures show a user-friendly error with an inline "Retry download" button (`canRetryTemplate`, `retryCreateVault`). +- A `clone_public_repo()` function was added to `github/clone.rs` to clone unauthenticated public repos without injecting OAuth tokens or configuring remote auth. + +## Options considered + +- **Option A — Keep bundled content (status quo)**: Simple, works offline. Downside: content tied to app release cycle, repo noise, growing file count. +- **Option B — Clone from GitHub at runtime (chosen)**: Content is always current; starter vault can be updated without an app release; removes ~25 markdown files + YAML from the main repo. Downside: requires network on first use; failure modes need UX handling (retry flow added). +- **Option C — Download a zip archive**: Avoids a git clone, smaller payload. Downside: loses the clean git history in the cloned vault; adds a zip extraction code path. + +## Consequences + +- New users need a network connection when selecting the template option. The empty vault and open-folder paths remain fully offline. +- The starter repo (`laputa-getting-started`) becomes a separate maintenance artifact. +- `LAPUTA_GETTING_STARTED_REPO_URL` env var allows overriding the URL in tests without hitting GitHub. +- Onboarding UX now distinguishes three creation modes: `creatingAction: 'template' | 'empty' | null`, each with distinct button state and status copy. +- Retry UX: `lastTemplatePath` is cached in `useOnboarding` so users can retry a failed clone to the same folder without re-picking it. +- Re-evaluation trigger: if offline-first support becomes a priority, consider bundling a minimal vault again or shipping a fallback zip. diff --git a/docs/adr/0047-regex-mode-for-view-filter-conditions.md b/docs/adr/0047-regex-mode-for-view-filter-conditions.md new file mode 100644 index 0000000..97d6a83 --- /dev/null +++ b/docs/adr/0047-regex-mode-for-view-filter-conditions.md @@ -0,0 +1,34 @@ +--- +type: ADR +id: "0047" +title: "Regex mode for view filter conditions" +status: active +date: 2026-04-08 +--- + +## Context + +The view filter engine (ADR 0040) supports operators like `contains`, `equals`, `not_contains`, `not_equals` with literal string matching. Power users who want pattern-based filtering (e.g., "all notes whose title matches a date pattern", "any property matching a URL regex") cannot express this with literals alone. + +## Decision + +**A `regex: true` flag is added to `FilterCondition`. When set, the `value` field is interpreted as a case-insensitive regular expression (via `regex::RegexBuilder` in Rust, and the native JS `RegExp` in TypeScript) for the operators that support it: `contains`, `equals`, `not_contains`, `not_equals`.** + +- Regex is opt-in: the `regex` field defaults to `false` and is skipped during serialization when false (no noise in existing `.yml` files). +- If the regex fails to compile, the condition evaluates to `false` rather than throwing. +- For relationship fields, the regex is tested against all candidate forms: the raw wikilink string, the inner stem, and the alias (if present). +- The `FilterBuilder` UI gains a regex toggle icon button next to value inputs for supported operators. +- TypeScript `viewFilters.ts` mirrors the same regex logic for client-side evaluation. + +## Options considered + +- **Option A — Add regex operator variants** (`regex_equals`, `regex_contains`): More explicit in YAML. Downside: doubles the operator set; no clear path to combine regex with `not_contains`. +- **Option B — Per-condition `regex: bool` flag (chosen)**: Composable with existing operators; minimal schema change; serialization skips the field when false so existing views are unaffected. +- **Option C — Full query language** (e.g., JMESPath or SQL `WHERE`): Maximum power. Out of scope; would replace rather than extend the filter engine. + +## Consequences + +- New dependency: `regex` crate in Rust (already present for other vault modules; no net new dep). +- Filter YAML files that use `regex: true` require Laputa ≥ this version to evaluate correctly; older versions silently ignore the flag (falling back to `regex: false` default via `#[serde(default)]`). +- Regex evaluation has a small performance cost vs. literal matching. No memoization of compiled regexes per evaluation call — acceptable given vault sizes (< 10k notes). +- Re-evaluation trigger: if regex performance becomes measurable, cache compiled `Regex` objects keyed by pattern string. diff --git a/docs/adr/0048-relative-date-expressions-in-view-filters.md b/docs/adr/0048-relative-date-expressions-in-view-filters.md new file mode 100644 index 0000000..b58dbd0 --- /dev/null +++ b/docs/adr/0048-relative-date-expressions-in-view-filters.md @@ -0,0 +1,46 @@ +--- +type: ADR +id: "0048" +title: "Relative date expressions in view filter conditions" +status: active +date: 2026-04-08 +--- + +## Context + +The view filter engine (ADR 0040) supports `before` and `after` operators but previously compared values as raw strings, meaning users had to write absolute ISO dates (e.g., `2026-04-01`) that became stale immediately. Views like "notes modified in the last 7 days" required updating the date manually every week. + +## Decision + +**The `before` and `after` filter operators now accept relative date expressions in addition to absolute ISO dates. Both the Rust backend and the TypeScript client independently parse the expression before comparing.** + +### Supported syntax + +| Expression | Meaning | +|---|---| +| `today` | Start of the current day (00:00 UTC) | +| `yesterday` | Start of yesterday | +| `tomorrow` | Start of tomorrow | +| `N days ago` / `N weeks ago` / `N months ago` / `N years ago` | Past relative | +| `in N days` / `in N weeks` / `in N months` / `in N years` | Future relative | + +Word-form amounts are also accepted: `one`, `two`, `three`, … `twelve`. + +### Architecture + +- **Rust** (`views.rs`): `parse_date_filter_timestamp()` resolves both field values and condition values to `i64` timestamps before comparing. Falls back gracefully when a value cannot be parsed. +- **TypeScript** (`utils/filterDates.ts`): `parseDateFilterInput()` and `toDateFilterTimestamp()` mirror the same logic for client-side filter evaluation. `date-fns` is used for date arithmetic. +- Both implementations use "start of day UTC" (00:00:00) as the anchor for relative expressions, consistent with how note creation/modification dates are stored. + +## Options considered + +- **Option A — Store and evaluate absolute dates only**: No parsing cost. Downside: views become stale; users must update dates manually. +- **Option B — Relative expressions resolved at evaluation time (chosen)**: Views stay perpetually current ("last 7 days" always means last 7 days). Downside: parallel implementation in Rust and TypeScript must stay in sync. +- **Option C — Pre-resolve relative expressions to absolute dates on save**: Expressions are human-readable when authoring but stored as ISO strings. Downside: view files drift; loses the relative intent. + +## Consequences + +- Relative expressions are evaluated at query time using the server/client clock. A view evaluated at 23:59 and 00:01 may return different results for "today". +- Both parsers share the same resolution anchor (start-of-day UTC). Timezone-sensitive relative expressions (e.g., "yesterday in Tokyo") are not supported. +- Existing `.yml` files with absolute ISO dates continue to work unchanged — the parser first tries ISO format before attempting relative parsing. +- Re-evaluation trigger: if timezone-aware relative dates become a user need, the expression syntax and anchor logic need revisiting. diff --git a/docs/adr/0049-per-note-icon-property.md b/docs/adr/0049-per-note-icon-property.md new file mode 100644 index 0000000..7592978 --- /dev/null +++ b/docs/adr/0049-per-note-icon-property.md @@ -0,0 +1,55 @@ +--- +type: ADR +id: "0049" +title: "Per-note icon property (_icon on individual notes)" +status: active +date: 2026-04-08 +--- + +## Context + +Laputa already supports type-level icons via the `_icon` system property on type documents (ADR 0008). Every note of a given type inherits the type's icon. Users needed a way to give individual notes a distinct visual identity without changing the type — e.g., marking a specific project with a rocket emoji, or a key person with a star icon — without creating a new type just for one note. + +## Decision + +**The `_icon` system property (already used by type documents) is now also supported on regular notes. When a note has an `_icon` value, it overrides the inherited type icon in all UI surfaces. The value may be an emoji, a Phosphor icon name, or an HTTP(S) image URL.** + +### Resolution logic + +`resolveNoteIcon(icon)` in `utils/noteIcon.ts` returns a discriminated union: + +| Kind | Condition | +|---|---| +| `none` | Value is empty/null | +| `emoji` | Value passes `isEmoji()` | +| `image` | Value is an HTTP(S) URL | +| `phosphor` | Value matches a registered Phosphor icon name | + +The `NoteTitleIcon` component renders the correct element for each kind (span, ``, or Phosphor SVG component). + +### UI surfaces updated + +- Editor breadcrumb bar (clicking the icon opens the `_icon` property editor) +- Note list items (`NoteItem`) +- Search panel results +- Relationship chips (shows icons on wikilink chips) +- Sidebar type sections +- Backlinks / ReferencedBy panels +- Inspector pinned area + +### Editing + +A custom event (`laputa:focus-note-icon-property`) is dispatched from the breadcrumb bar click to focus the `_icon` field in the Properties panel without scrolling. The field uses the existing property editor UI. + +## Options considered + +- **Option A — Separate `_note_icon` property**: Avoids ambiguity with the type-level `_icon`. Downside: two names for the same concept depending on context; complicates the resolver. +- **Option B — Reuse `_icon` on notes (chosen)**: Consistent with existing convention (ADR 0008); type docs and note docs follow the same schema. The distinction between type-level and note-level is determined by `is_a: Type` in the frontmatter, not by a different property name. +- **Option C — Inline emoji in note title**: Zero-friction. Downside: title is also the filename (ADR 0044); emojis in filenames cause filesystem/git pain. + +## Consequences + +- `_icon` on a note overrides the type icon everywhere. A note with no `_icon` continues to inherit the type icon (no behavior change for existing vaults). +- The icon resolver (`resolveNoteIcon`) is shared between note icons and type icons; future changes to icon resolution affect both. +- `iconRegistry.ts` grows with any new Phosphor icon additions — currently loaded eagerly. If the icon set grows large, lazy loading or a build-time icon map should be considered. +- Re-evaluation trigger: if users request per-note color (the `_color` system property currently only applies to types), the same resolution pattern can be extended. diff --git a/docs/adr/0050-deterministic-shortcut-command-routing.md b/docs/adr/0050-deterministic-shortcut-command-routing.md new file mode 100644 index 0000000..b8b1f13 --- /dev/null +++ b/docs/adr/0050-deterministic-shortcut-command-routing.md @@ -0,0 +1,29 @@ +--- +type: ADR +id: "0050" +title: "Deterministic shortcut command routing" +status: active +date: 2026-04-11 +--- + +## Context + +Laputa is keyboard-first, but shortcut execution had split ownership: `useAppKeyboard` handled some shortcuts in the renderer while `menu.rs` owned others as native Tauri menu accelerators. That split made QA unreliable. Browser tests could prove the renderer path, but not the native menu path, and flaky macOS key synthesis made `Cmd+Shift+L`, `Cmd+Shift+I`, and `Cmd+N` regressions easy to miss. + +## Decision + +**Keyboard shortcuts and native menu accelerators now dispatch through the same canonical app command IDs. Renderer-owned shortcuts call the shared dispatcher directly; native menu items emit the same IDs into the frontend, and tests get a deterministic menu-command trigger that exercises that route without relying on synthesized native keystrokes.** + +## Options considered + +- **Option A** (chosen): Shared command IDs plus deterministic menu-command trigger — keeps native desktop UX while making menu-owned commands testable in unit tests, Playwright, and native QA. Downside: one more command layer to maintain. +- **Option B**: Move every shortcut to the renderer — simpler automated testing, but worse macOS menu-bar parity and weaker native UX. +- **Option C**: Keep renderer and native shortcuts separate — lowest code churn, but continues to produce false confidence and shortcut regressions. + +## Consequences + +- `appCommandDispatcher.ts` owns the canonical shortcut command IDs and the shared execution path used by `useAppKeyboard` and `useMenuEvents`. +- Native menu routing remains explicit in `menu.rs`; adding or changing a native shortcut now requires wiring the accelerator and the matching command ID in one place. +- Automated QA can trigger menu-owned commands deterministically through the shared `window.__laputaTest.triggerMenuCommand()` bridge in browser runs and through the native `trigger_menu_command` Tauri command in desktop runs. +- Keyboard QA should prefer real menu selection or the deterministic menu-command trigger for native-owned shortcuts, and reserve synthesized keystrokes for renderer-owned shortcuts or true end-to-end spot checks. +- This decision supersedes the blanket assumption in ADR 0020 that all shortcut verification can be treated as plain keyboard-event testing. diff --git a/docs/adr/0051-shared-shortcut-manifest-for-testable-routing.md b/docs/adr/0051-shared-shortcut-manifest-for-testable-routing.md new file mode 100644 index 0000000..aa2e0d3 --- /dev/null +++ b/docs/adr/0051-shared-shortcut-manifest-for-testable-routing.md @@ -0,0 +1,31 @@ +--- +type: ADR +id: "0051" +title: "Shared shortcut manifest for testable routing" +status: active +date: 2026-04-11 +--- + +## Context + +ADR 0050 moved renderer shortcuts and native menu events onto the same command dispatcher, but shortcut ownership still drifted across multiple places: `appKeyboardShortcuts.ts`, `appCommandDispatcher.ts`, command-palette metadata, and `menu.rs`. That made shortcut regressions easy to reintroduce because the same facts had to be updated manually in several files. + +The riskiest failures were exactly the native-owned commands that matter most in a keyboard-first app: `Cmd+\` for raw editor, `Cmd+Shift+I` for properties, and `Cmd+Shift+L` for the AI panel. We need one declarative place that says which command owns which shortcut, whether the shortcut is renderer-owned or native-menu-owned, and how tests should trigger it. + +## Decision + +**Shortcut-capable app commands are now defined in a shared frontend manifest that owns command IDs, routing semantics, and shortcut ownership. Renderer keyboard handling resolves commands from that manifest, native menu routing dispatches the same command IDs, and deterministic QA for native-owned shortcuts targets those IDs rather than duplicating shortcut facts in ad hoc code paths.** + +## Options considered + +- **Option A** (chosen): Shared shortcut manifest plus shared dispatcher and deterministic menu-command QA. This reduces drift, improves CodeScene on the command router, and makes native-owned shortcuts provable without flaky macOS key synthesis. Downside: one more manifest to maintain. +- **Option B**: Keep the shared dispatcher from ADR 0050 but continue storing shortcut ownership in separate key maps and menu lists. Lower churn, but it keeps the exact source of the regressions we reopened. +- **Option C**: Move all shortcuts into renderer-only handlers. Easier to test, but weaker macOS menu-bar parity and worse native desktop UX. + +## Consequences + +- `appCommandCatalog.ts` is now the frontend source of truth for shortcut-capable command IDs, ownership, modifier rules, and dispatch kind. +- `appCommandDispatcher.ts` is reduced to route execution instead of carrying a large switch plus duplicated ownership metadata. +- `useAppKeyboard.ts` resolves shortcuts from the shared manifest, including the distinction between `Cmd+Shift+L` (macOS-only) and `CmdOrCtrl+Shift+I/F/O`. +- Native-menu smoke tests should use `window.__laputaTest.triggerMenuCommand()` or the Tauri `trigger_menu_command` bridge to prove the native command path. Renderer-only commands may still be proven with direct keyboard events. +- This ADR supersedes ADR 0050 by replacing “shared command IDs are enough” with “shared command IDs plus shared shortcut ownership metadata are required.” diff --git a/docs/adr/0052-renderer-first-shortcut-execution-with-native-menu-dedupe.md b/docs/adr/0052-renderer-first-shortcut-execution-with-native-menu-dedupe.md new file mode 100644 index 0000000..91430b2 --- /dev/null +++ b/docs/adr/0052-renderer-first-shortcut-execution-with-native-menu-dedupe.md @@ -0,0 +1,33 @@ +--- +type: ADR +id: "0052" +title: "Renderer-first shortcut execution with native-menu dedupe" +status: active +date: 2026-04-11 +--- + +## Context + +ADR 0051 gave Laputa a shared shortcut manifest and shared command IDs, but it still treated many shortcuts as native-menu-owned at execution time. In practice that meant `useAppKeyboard` deferred commands like `Cmd+Shift+I`, `Cmd+Shift+L`, and `Cmd+\` whenever the app ran under Tauri, and automated QA had to prove those flows by injecting menu-command IDs instead of pressing the real keys. + +That is not a strong enough QA story for a keyboard-first app. If a user presses a shortcut while the editor is focused, we need a deterministic way to prove the actual key combo works. At the same time, we still want a native macOS menu bar with working menu items and accelerators. + +## Decision + +**Renderer keyboard handling is now the primary execution path for all shortcut-capable app commands, including commands that also have native menu accelerators. Native menu clicks and accelerators still emit the same command IDs, but the shared dispatcher suppresses the duplicate native/renderer echo from a single keypress so the command runs exactly once.** + +## Options considered + +- **Option A** (chosen): Renderer-first shortcut execution plus native-menu dedupe. This keeps shortcuts testable with real key events in a Tauri-like environment while preserving menu-bar parity and clickable native menu items. Downside: the dispatcher has to understand and suppress paired native/renderer echoes. +- **Option B**: Keep deferring native-owned shortcuts out of the renderer and prove them only through `trigger_menu_command`. Lower implementation churn, but it still leaves the real keystroke path unproven. +- **Option C**: Remove native accelerators entirely and keep shortcuts renderer-only. Simplest to reason about, but weaker desktop UX and poorer macOS menu discoverability. + +## Consequences + +- `appCommandCatalog.ts` remains the single manifest for command IDs and shortcut combos, but keyboard execution no longer depends on a separate owner flag. +- `useAppKeyboard` handles the actual key event for every shortcut-capable command, even in Tauri mode. +- `useMenuEvents` still handles menu clicks and test-triggered native command IDs, but shared dispatcher dedupe prevents a focused keypress from firing twice when the native menu accelerator also echoes back into the renderer. +- Deterministic QA now has two complementary proofs: + - real keyboard events in a Tauri-like environment for the actual shortcut combo + - `trigger_menu_command` for the native menu click/accelerator command path +- This ADR supersedes ADR 0051 by replacing “execution ownership lives in the manifest” with “shortcut combos live in the manifest, while execution is renderer-first and native menu dispatch is deduped.” diff --git a/docs/adr/0053-webview-init-prevention-for-browser-reserved-shortcuts.md b/docs/adr/0053-webview-init-prevention-for-browser-reserved-shortcuts.md new file mode 100644 index 0000000..5c5dfa7 --- /dev/null +++ b/docs/adr/0053-webview-init-prevention-for-browser-reserved-shortcuts.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0053" +title: "Webview-init prevention for browser-reserved shortcuts" +status: active +date: 2026-04-11 +--- + +## Context + +ADR 0052 made renderer-first shortcut handling the primary path for command execution, with native menu accelerators deduped afterward. That works for normal shortcuts, but native QA on macOS showed that `Cmd+Shift+L` still failed to reach the app even though the shared command path and the Note menu item both worked. + +The gap is WKWebView itself: some browser-reserved chords are swallowed by the webview before the renderer-level shortcut listener can execute. That makes the shortcut untestable with the real native keypress even though the command bus is correct. + +## Decision + +**Laputa will keep renderer-first shortcut execution, but for macOS browser-reserved chords we will add a narrow Tauri webview-init prevention layer using `tauri-plugin-prevent-default` so the real keystroke reaches the shared command path.** + +## Options considered + +- **Option A** (chosen): Add a narrow `tauri-plugin-prevent-default` registration for only the known browser-reserved chords we actually use. This preserves ADR 0052, keeps the command bus unified, and fixes the real native keystroke path without broad shortcut capture. +- **Option B**: Keep relying on renderer capture listeners alone. Simpler, but it fails for chords that WKWebView consumes before renderer code sees them. +- **Option C**: Use a global shortcut plugin as the fallback path. This would catch the keystroke natively, but it reserves the chord outside Laputa and is too heavy for app-local shortcuts. + +## Consequences + +- Shortcut ownership stays unified: command IDs and execution still live in the shared renderer/native command bus. +- macOS-only browser-reserved chords now have one extra declaration point in `src-tauri/src/lib.rs`, and that list must stay intentionally small. +- Native QA remains mandatory for any shortcut added to that list, because browser dev and mocked Tauri tests do not exercise the webview-init layer. +- Re-evaluate this decision if Tauri/WKWebView exposes a better app-local native shortcut hook that does not require browser-reserved-key workarounds. diff --git a/docs/adr/0054-deterministic-shortcut-qa-matrix.md b/docs/adr/0054-deterministic-shortcut-qa-matrix.md new file mode 100644 index 0000000..92293b8 --- /dev/null +++ b/docs/adr/0054-deterministic-shortcut-qa-matrix.md @@ -0,0 +1,35 @@ +--- +type: ADR +id: "0054" +title: "Deterministic shortcut QA matrix" +status: active +date: 2026-04-11 +--- + +## Context + +ADR 0052 made renderer-first shortcut execution the primary runtime path, and ADR 0053 added a narrow macOS webview-init prevent-default layer for browser-reserved chords such as `Cmd+Shift+L`. Those decisions improved behavior, but the automated QA story was still muddy: + +- browser smoke tests were describing a mocked desktop harness as if it were native Tauri QA +- some tests used `page.keyboard.press()` for commands whose real desktop accelerators are intercepted or reserved by the browser shell +- native menu command coverage existed, but the catalog did not declare which deterministic proof path each shortcut should use + +That made it too easy to ship a shortcut with passing automation while overstating what the automation had actually proven. + +## Decision + +**Laputa will treat shortcut QA as an explicit part of the shared command manifest. Every shortcut-capable command must have a deterministic automated proof path, and the test harness must distinguish renderer shortcut-event proof from native menu-command proof instead of calling the browser harness “native Tauri QA”.** + +## Options considered + +- **Option A** (chosen): Add a deterministic shortcut QA matrix to the shared command catalog. Renderer shortcut handling can be exercised through synthetic `keydown` events generated from the manifest, while native menu commands are exercised through `trigger_menu_command`. Pros: deterministic, explicit, and honest about what is being proved. Cons: still requires real native QA for exact accelerator delivery on macOS. +- **Option B**: Keep using ad hoc Playwright key presses and browser-side menu shims. Lower change cost, but still allows false claims about native coverage and still depends on browser-reserved shortcuts behaving nicely. +- **Option C**: Block all shortcut work until full native Tauri automation exists. Strongest eventual guarantee, but it would leave the keyboard-first app without a usable deterministic QA strategy today. + +## Consequences + +- `appCommandCatalog.ts` now owns not just command IDs and modifier rules, but also the deterministic QA mode for each shortcut-capable command. +- Browser harness smoke tests must describe themselves as a desktop command bridge, not native app QA. +- Renderer shortcut behavior can be verified deterministically without depending on browser chrome or flaky AppleScript key synthesis. +- Native menu-command behavior can be verified deterministically through the Tauri command bridge. +- Exact desktop accelerator delivery still requires real Tauri QA for commands flagged as needing manual native verification, especially browser-reserved macOS chords. diff --git a/docs/adr/0055-h1-is-the-only-editor-title-surface.md b/docs/adr/0055-h1-is-the-only-editor-title-surface.md new file mode 100644 index 0000000..02c2c70 --- /dev/null +++ b/docs/adr/0055-h1-is-the-only-editor-title-surface.md @@ -0,0 +1,43 @@ +--- +type: ADR +id: "0055" +title: "H1 is the only editor title surface" +status: superseded +date: 2026-04-11 +supersedes: "0044" +superseded_by: "0068" +--- + +## Context + +ADR-0044 moved Laputa to H1-as-title, but the frontend still carried a legacy fallback: when a note had no H1, `TitleField` and the old title section could reappear above the editor. That left two competing title surfaces in the product and made it possible for deleting an H1 to resurrect UI that was supposed to be gone. + +The result was both behavioral drift and stale tests: some code paths still treated the dedicated title row as a valid editing surface even though the product direction is now keyboard-first writing directly in the document body. + +## Decision + +**The editor body is now the only title surface. Laputa never renders a separate title section above the editor, regardless of whether a note currently has an H1.** + +Display-title behavior stays: +1. First H1 in the body +2. Legacy frontmatter `title:` +3. Filename-derived fallback + +But the UI no longer exposes a dedicated title field for cases 2 or 3. When a note has no H1, the editor simply shows normal body content or the empty-editor placeholder. + +Filename operations remain explicit: +- untitled notes still auto-rename from H1 on save +- manual filename rename/sync remains in the breadcrumb + +## Options considered + +- **Option A** (chosen): remove the fallback title section entirely. This makes the editor honest, removes a stale code path, and keeps title editing aligned with the keyboard-first document model. +- **Option B**: keep the fallback title field for non-H1 notes. This preserves an alternate rename path, but it reintroduces the exact dual-surface ambiguity that ADR-0044 tried to escape. +- **Option C**: hide the title section with CSS only. Low churn, but it leaves dead render/state paths in place and makes regressions like “delete H1 and old title row returns” easy to reintroduce. + +## Consequences + +- Deleting an H1 no longer reveals any legacy title UI; the user stays in the editor body. +- `TitleField` and the title-section render path are removed from the frontend. +- Breadcrumb filename controls are now the only explicit file-identifier editing surface outside the editor body. +- Older tests that asserted title editing through `TitleField` are obsolete and should be replaced by H1-title or breadcrumb-filename coverage. diff --git a/docs/adr/0056-system-git-cli-auth-no-provider-oauth.md b/docs/adr/0056-system-git-cli-auth-no-provider-oauth.md new file mode 100644 index 0000000..a1d0a8b --- /dev/null +++ b/docs/adr/0056-system-git-cli-auth-no-provider-oauth.md @@ -0,0 +1,50 @@ +--- +type: ADR +id: "0056" +title: "System git auth only — no provider-specific OAuth or repo APIs" +status: active +date: 2026-04-12 +supersedes: "0019" +--- + +## Context + +Tolaria already uses the system `git` executable for the core remote workflow: commit, pull, push, status, history, and conflict resolution. The only provider-specific part left was GitHub authentication and repository management: + +- GitHub Device Flow OAuth +- persisted `github_token` / `github_username` settings +- GitHub-only clone/create UI +- GitHub API calls for repo listing and creation + +That split made the product more complex than the actual user need. Tolaria's remote-sync users are developers who typically already have git configured via SSH keys, Git Credential Manager, Keychain helpers, or `gh auth`. The app was carrying a provider-specific auth stack even though the real transport path was already plain git CLI. + +## Decision + +**Tolaria does not implement provider-specific authentication or remote-repository APIs. All remote auth is delegated to the user's existing system git configuration, and cloning is a generic "paste any git URL" flow.** + +Concretely: + +- remove GitHub Device Flow commands and UI +- remove persisted GitHub auth fields from app settings +- remove GitHub repo list/create API integration +- keep `clone_repo`, but make it a generic system-git clone command +- keep commit / pull / push behavior unchanged apart from surfacing raw git errors directly + +## Options considered + +- **Option A — Keep GitHub Device Flow OAuth** (ADR-0019, now superseded): polished GitHub-specific onboarding, but it preserves provider lock-in, token storage, and an entire second auth model beside system git. +- **Option B — Replace OAuth with manual PAT entry**: smaller implementation than Device Flow, but still provider-specific, still stores credentials in app settings, and still teaches users the wrong abstraction. +- **Option C — Pure system git auth** (chosen): one auth path, less code, works with any git host, and aligns the clone flow with the rest of Tolaria's git stack. Downside: users must already have git auth configured outside the app. + +## Consequences + +- `CloneVaultModal` accepts any git URL and local destination path. +- `clone_repo` shells out to system git without injecting provider tokens. +- `git_push` / `git_pull` continue to rely on the same external git configuration; auth failures surface as raw git stderr. +- `SettingsPanel` no longer contains a GitHub connection section. +- Tolaria no longer stores git-provider credentials in `settings.json`. +- GitHub, GitLab, Bitbucket, Gitea, and self-hosted remotes all work through the same product path. +- Creating or listing remote repos from inside Tolaria is no longer supported; remote setup happens in the user's normal git tools. +- The Getting Started vault still clones from a public remote URL, but it now goes through the same generic git clone path as every other vault import. + +Re-evaluate if Tolaria later targets less technical users who cannot reasonably be expected to configure git outside the app. diff --git a/docs/adr/0057-alpha-stable-release-channels-and-beta-cohorts.md b/docs/adr/0057-alpha-stable-release-channels-and-beta-cohorts.md new file mode 100644 index 0000000..c309af8 --- /dev/null +++ b/docs/adr/0057-alpha-stable-release-channels-and-beta-cohorts.md @@ -0,0 +1,45 @@ +--- +type: ADR +id: "0057" +title: "Alpha/stable release channels with PostHog beta cohorts" +status: superseded +date: 2026-04-12 +superseded_by: "0066" +--- + +## Context + +Tolaria's updater and release docs still described a canary branch, a beta updater channel, and a single `latest.json` feed. That no longer matched the desired product model: + +- `main` should continuously publish **alpha** builds. +- **Stable** should be promoted manually by pushing `stable-vX.Y.Z` tags. +- "Beta" users should be modeled in PostHog for targeting and analysis, not as a separate binary or updater feed. + +The updater also needed semver-safe versioning when a user switches between Stable and Alpha. A date-based alpha version below the latest stable release would cause the updater to ignore newer alpha builds after a stable promotion. + +This ADR supersedes ADR-0017's canary-branch updater model. + +## Decision + +**Tolaria exposes exactly two updater channels: `stable` and `alpha`. Stable is the default feed, while every push to `main` publishes a prerelease alpha build to `alpha/latest.json`, and manually promoted `stable-vX.Y.Z` tags publish stable builds to `stable/latest.json`. Beta audiences are handled in PostHog and are not a third updater channel.** + +## Options considered + +- **Option A** (chosen): Two updater channels (`stable`, `alpha`) plus PostHog beta cohorts. Pros: matches the product requirement, keeps CI simple, keeps updater semantics understandable, and separates release distribution from experimentation audiences. Cons: requires semver-aware alpha versioning and a small migration for legacy channel settings. +- **Option B**: Keep the canary branch / canary channel model. Pros: no workflow redesign. Cons: no longer matches how releases are actually promoted and forces distribution strategy to depend on a long-lived branch. +- **Option C**: Add a third updater channel for beta builds. Pros: direct binary segmentation. Cons: extra CI complexity, extra updater endpoints, and unnecessary duplication because beta targeting is already better handled by PostHog. + +## Consequences + +- `release.yml` now publishes alpha prereleases from every push to `main`. +- `release-stable.yml` publishes stable releases only from `stable-v*` tags. +- `src-tauri/src/app_updater.rs` selects `alpha/latest.json` or `stable/latest.json` at runtime. +- `release_channel` stays an app setting, but only `alpha` is stored explicitly; Stable serializes to the default `null` value. +- Legacy or invalid persisted channel values fall back to Stable. +- Alpha versions are prereleases of the next stable patch version (for example `1.2.4-alpha.202604122135.7` after stable `1.2.3`) so semver ordering remains valid across channel switches. +- The legacy GitHub Pages aliases `latest.json` and `latest-canary.json` continue to mirror alpha for backward compatibility. +- Beta rollouts and internal-user targeting are done in PostHog using person properties or cohorts rather than updater manifests. + +## Advice + +If a future release process needs more than two binary distribution rings, re-evaluate this decision only when PostHog cohorting is no longer sufficient and the extra operational cost of another updater feed is justified. diff --git a/docs/adr/0058-claude-code-first-launch-onboarding-gate.md b/docs/adr/0058-claude-code-first-launch-onboarding-gate.md new file mode 100644 index 0000000..368c19a --- /dev/null +++ b/docs/adr/0058-claude-code-first-launch-onboarding-gate.md @@ -0,0 +1,36 @@ +--- +type: ADR +id: "0058" +title: "Claude Code first-launch onboarding gate" +status: superseded +superseded_by: "0062" +date: 2026-04-12 +--- + +## Context + +Tolaria's AI features depend on the `claude` CLI being installed on the user's machine. New users arriving with no prior context could open the app, try AI-powered workflows, and get silent failures with no explanation. + +A dedicated first-launch prompt was needed to: +- Surface whether the `claude` CLI is already present. +- Guide users to the install page if it is missing. +- Not block experienced users who want to skip the check. + +The existing `useOnboarding` hook already handles vault setup and resolves to a `ready` state, but it had no mechanism for a post-vault, pre-app step. + +## Decision + +**A one-time `ClaudeCodeOnboardingPrompt` is shown immediately after vault onboarding resolves to `ready`, before the main app shell renders. Dismissal is persisted in `localStorage` via `useClaudeCodeOnboarding`, so the gate appears exactly once per install.** + +## Options considered + +- **Option A** (chosen): Full-screen gate after vault onboarding, dismissed once and persisted in `localStorage`. Pros: cannot be missed on first launch, reuses `useClaudeCodeStatus` for live detection, zero impact on returning users. Cons: adds one extra render phase to the boot sequence. +- **Option B**: Inline banner inside the main app. Pros: less intrusive. Cons: easy to ignore, harder to surface install link prominently. +- **Option C**: Check at feature use time (show error when AI action fails). Pros: no new screen. Cons: poor UX — silent failure or cryptic error at the moment the user needs AI. + +## Consequences + +- The app boot sequence now has four phases: loading → welcome (if needed) → Claude Code check (once) → main shell. +- `useClaudeCodeOnboarding(enabled)` takes a boolean so the gate is skipped entirely in note windows and before vault onboarding completes. +- The dismissal key (`tolaria:claude-code-onboarding-dismissed`) must be pre-set in Playwright storage state so smoke tests bypass the gate. +- Re-evaluation warranted if the `claude` CLI gains an in-app auto-install path, making the manual prompt unnecessary. diff --git a/docs/adr/0059-local-only-git-commits-without-remote.md b/docs/adr/0059-local-only-git-commits-without-remote.md new file mode 100644 index 0000000..9bd946c --- /dev/null +++ b/docs/adr/0059-local-only-git-commits-without-remote.md @@ -0,0 +1,33 @@ +--- +type: ADR +id: "0059" +title: "Local-only git commits for vaults without a remote" +status: active +date: 2026-04-12 +--- + +## Context + +ADR-0034 mandates a git repo for every vault, but never required a remote. In practice, the commit flow always attempted a `git push` after staging and committing. Users with purely local vaults (no remote configured) would hit a push error on every commit. + +The fix required distinguishing between two commit modes at the point of user action: +- **Push mode**: repo has a remote → commit then push (existing behavior). +- **Local mode**: repo has no remote → commit only, no push attempted. + +## Decision + +**`useCommitFlow` detects the vault's remote status before opening the commit dialog and at commit time. When `hasRemote === false`, it commits locally and skips the push step entirely, showing "Committed locally (no remote configured)" as the confirmation toast.** + +## Options considered + +- **Option A** (chosen): Runtime detection via a new `useGitRemoteStatus` hook + `CommitMode` type (`push` | `local`). Pros: transparent to the user, no configuration needed, adapts if a remote is added later. Cons: adds an async remote-status check to the commit open flow. +- **Option B**: Require all vaults to have a remote (keep blocking behavior). Pros: simpler model. Cons: breaks the valid use case of a local-only knowledge base; contradicts ADR-0056 which removed provider-specific OAuth. +- **Option C**: Let the push fail silently and always show success. Pros: no new logic. Cons: misleading feedback; users wouldn't know the push was skipped vs. succeeded. + +## Consequences + +- `useGitRemoteStatus` is a new hook that exposes `remoteStatus` and `refreshRemoteStatus`; it is called both when opening the commit dialog and after each commit. +- `CommitDialog` now receives a `commitMode` prop and adjusts its CTA label accordingly (`Commit & Push` vs `Commit`). +- The `commitAndPush` callback in `CommitFlowConfig` is replaced by `resolveRemoteStatus` + `vaultPath`; the actual git operations (`git_commit`, `git_push`) are invoked directly inside `useCommitFlow`. +- Local-only commits fire `trackEvent('commit_made')` the same as push commits for analytics continuity. +- Re-evaluation warranted if a remote is later added to a previously-local vault and the UX should prompt the user to push accumulated commits. diff --git a/docs/adr/0060-network-aware-ui-gating-for-remote-features.md b/docs/adr/0060-network-aware-ui-gating-for-remote-features.md new file mode 100644 index 0000000..bcc9292 --- /dev/null +++ b/docs/adr/0060-network-aware-ui-gating-for-remote-features.md @@ -0,0 +1,28 @@ +--- +type: ADR +id: "0060" +title: "Network-aware UI gating for remote-dependent features" +status: active +date: 2026-04-13 +--- + +## Context + +Some app features require an active internet connection (e.g., cloning the Getting Started vault template from GitHub). Prior to this decision, the UI would attempt the operation and surface a generic error only after failure. Users on first launch in offline environments got a confusing error when trying to use the template. + +## Decision + +**Introduce a `useNetworkStatus` hook that tracks `navigator.onLine` via `online`/`offline` DOM events, and use it to proactively gate UI surfaces that require a network.** Features that depend on remote access (clone, sync) show an explanatory message and disable their action button when the device is offline, rather than failing silently at execution time. + +## Options considered + +- **Option A** (chosen): `useNetworkStatus` hook + proactive UI disable — disables the action before the user tries it, with inline copy explaining the offline state. +- **Option B**: Attempt and catch — let the operation run and surface the error in a toast. Simpler, but poor UX for first-launch users who don't know what went wrong. +- **Option C**: Check connectivity with a ping on demand — more accurate but adds latency and complexity; `navigator.onLine` is sufficient for the use case. + +## Consequences + +- Positive: cleaner first-run experience for offline users; no misleading error messages. +- Positive: `useNetworkStatus` is a reusable hook for future remote-gated features. +- Negative: `navigator.onLine` can return `true` on a captive-portal / no-internet network — the hook reflects OS-level connectivity, not end-to-end reachability. The operation may still fail with a network error, which must still be handled. +- Re-evaluate if the app adds more remote features that need finer-grained reachability checks. diff --git a/docs/adr/0061-ai-prompt-bridge-event-bus.md b/docs/adr/0061-ai-prompt-bridge-event-bus.md new file mode 100644 index 0000000..1f22d3d --- /dev/null +++ b/docs/adr/0061-ai-prompt-bridge-event-bus.md @@ -0,0 +1,29 @@ +--- +type: ADR +id: "0061" +title: "AI prompt bridge — module-level event bus for cross-component prompt routing" +status: active +date: 2026-04-13 +--- + +## Context + +The AI panel is a sibling subtree to the command palette in the component tree. When the user submits a prompt from the command palette's AI mode, the AI panel (mounted elsewhere) needs to receive it and start processing. Props-down / callbacks-up wiring between the two would require threading state through multiple layers of unrelated components. + +## Decision + +**Introduce `aiPromptBridge.ts` as a module-level singleton event bus.** The bridge exposes `queueAiPrompt(text, references)` (write path) and `takeQueuedAiPrompt()` (consume path), backed by a module variable and a `CustomEvent` on `window` (`tolaria:ai-prompt-queued`). The command palette enqueues a prompt; the AI panel listens for the event, consumes the prompt via `takeQueuedAiPrompt`, and dispatches it to the agent. A companion `requestOpenAiChat()` function fires a separate `tolaria:open-ai-chat` event to open the panel before the prompt is sent. + +## Options considered + +- **Option A** (chosen): module-level singleton + `window` events — zero dependencies, no new global state manager, consistent with the existing `window.dispatchEvent` pattern already used for menu-command bridging. +- **Option B**: Lift AI panel state to a shared ancestor (e.g., `App.tsx`) and pass `onPrompt` callback down — would require `App.tsx` to own AI agent state, bloating it further; conflicts with ADR-0026 (props-down principle). +- **Option C**: Zustand / Jotai global store atom — adds a dependency and architecture overhead for a narrow, two-participant channel. + +## Consequences + +- Positive: decouples command palette from AI panel with no shared ancestor coupling. +- Positive: any future surface (e.g., wikilink context menu, note action bar) can call `queueAiPrompt` without tree-level wiring. +- Negative: module-level mutable state is harder to test in isolation; tests must call `takeQueuedAiPrompt` to drain state between runs. +- Negative: the event is fire-and-forget — if the AI panel is not mounted when the event fires, the prompt is silently dropped (currently not an issue as the panel is always mounted). +- Re-evaluate if the number of AI entry points grows large enough to warrant a proper state management solution. diff --git a/docs/adr/0062-selectable-cli-ai-agents.md b/docs/adr/0062-selectable-cli-ai-agents.md new file mode 100644 index 0000000..bd334a8 --- /dev/null +++ b/docs/adr/0062-selectable-cli-ai-agents.md @@ -0,0 +1,32 @@ +--- +type: ADR +id: "0062" +title: "Selectable CLI AI agents with a shared panel architecture" +status: active +date: 2026-04-13 +--- + +## Context + +Tolaria's AI panel, onboarding flow, and status surfaces were built around a single CLI dependency: Claude Code. That worked for the first release, but it made every UI and backend seam agent-specific. Adding Codex as a second supported CLI agent would have duplicated large parts of the app: separate availability checks, a second onboarding path, another status badge, and yet another streaming hook. + +The product direction is broader than a single vendor. Tolaria needs one AI panel that can target multiple local CLI agents while preserving the same MCP-backed vault tooling, the same note-context assembly, and a single install-local preference for which agent should be used by default. + +## Decision + +**Introduce a shared CLI-agent abstraction for Tolaria's AI surfaces.** The frontend now treats agents as a small registry (`claude_code`, `codex`) with labels, install URLs, availability state, and a persisted `default_ai_agent` setting. The AI panel, onboarding gate, command palette, and status bar all read from that shared model. On the backend, `ai_agents.rs` owns agent detection and streaming, dispatching to per-agent adapters: Claude still flows through `claude_cli.rs`, while Codex is launched through `codex exec --json` with Tolaria's MCP server injected via transient config flags. + +## Options considered + +- **Option A** (chosen): shared agent registry + backend adapter layer — one panel, one preference, one onboarding path, and a clear place to add future CLI agents. +- **Option B**: keep the UI Claude-specific and bolt on Codex as a second special case — lowest short-term cost, but every new agent multiplies the number of bespoke checks, prompts, and command handlers. +- **Option C**: split the product into separate per-agent panels — clearer ownership per integration, but fragments the UX and makes command-palette / status-bar interactions inconsistent. + +## Consequences + +- Positive: new CLI agents can be added by implementing one backend adapter and registering one frontend definition. +- Positive: onboarding and settings now explain the AI capability of the app at the product level rather than assuming Claude Code is the only valid path. +- Positive: the default agent is installation-local, matching ADR-0004's rule that machine-specific tool preferences belong in app settings rather than the vault. +- Negative: event normalization is now Tolaria-owned; backend adapters must translate each CLI's stream format into a common event model. +- Negative: some user guidance becomes agent-specific again at the edge, such as install links and authentication errors (`claude` login vs `codex login`). +- Re-evaluate if one agent needs capabilities the shared panel cannot express cleanly, or if Tolaria ever moves from CLI subprocesses to a dedicated local SDK/runtime. diff --git a/docs/adr/0063-blocknote-code-block-package-for-editor-highlighting.md b/docs/adr/0063-blocknote-code-block-package-for-editor-highlighting.md new file mode 100644 index 0000000..7d843b5 --- /dev/null +++ b/docs/adr/0063-blocknote-code-block-package-for-editor-highlighting.md @@ -0,0 +1,29 @@ +--- +type: ADR +id: "0063" +title: "BlockNote code-block package for editor syntax highlighting" +status: active +date: 2026-04-13 +--- + +## Context + +Tolaria uses BlockNote for rich-text editing. Fenced code blocks already render on BlockNote's dark `pre > code` surface, but they were missing syntax highlighting and inherited the muted inline-code chip background from the global `code` selector in `EditorTheme.css`. The QA expectation is a dark code block with highlighted tokens and light code text, without regressing inline-code styling elsewhere in the editor. + +BlockNote documents syntax highlighting as a schema concern: replace the default `codeBlock` spec with `createCodeBlockSpec(...)` and provide a Shiki highlighter. Tolaria also needs to preserve the existing default behavior for unlabeled code blocks, which should stay plain text instead of defaulting to JavaScript. + +## Decision + +**Tolaria overrides the default BlockNote `codeBlock` spec with `@blocknote/code-block`, keeps `defaultLanguage: "text"`, and scopes the muted inline-code chip styling away from fenced code blocks.** + +## Options considered + +- **Use `@blocknote/code-block`** (chosen): first-party BlockNote path, ships supported language aliases and a bundled Shiki highlighter, renders `.shiki` token spans in-editor, and avoids maintaining a parallel ProseMirror plugin integration. +- **Use a custom `createCodeBlockSpec({ createHighlighter })` bundle**: also valid, but Tolaria does not need a custom language/theme bundle beyond BlockNote's packaged setup right now. +- **Keep BlockNote defaults and only fix CSS**: removes the nested gray chip bug, but leaves fenced code blocks unhighlighted and fails the product requirement. + +## Consequences + +Tolaria's highlighting now lives in the editor schema instead of an editor-side plugin hook. `src/components/editorSchema.tsx` swaps in `createCodeBlockSpec({ ...codeBlockOptions, defaultLanguage: "text" })`, which adds BlockNote's language selector plus Shiki token spans for supported fenced blocks. `EditorTheme.css` continues to keep the `pre > code` background transparent so BlockNote's dark code-block shell remains intact. + +The tradeoff is one new first-party dependency and BlockNote's bundled language menu inside code blocks. If Tolaria later needs a narrower bundle, custom themes, or export-time highlighting parity, this ADR should be superseded with a custom Shiki bundle decision. diff --git a/docs/adr/0064-ratcheted-codescene-thresholds.md b/docs/adr/0064-ratcheted-codescene-thresholds.md new file mode 100644 index 0000000..6f63379 --- /dev/null +++ b/docs/adr/0064-ratcheted-codescene-thresholds.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0064" +title: "Ratcheted CodeScene thresholds as the quality gate baseline" +status: active +date: 2026-04-14 +--- + +## Context + +ADR-0018 established CodeScene code-health gates so Tolaria could block regressions before code reached `main`. Since then, the codebase has improved materially and the tracked baseline in `.codescene-thresholds` has been ratcheted above the original 9.50 / 9.31 minimums. + +Leaving ADR-0018 active would make the architecture record stale: the enforced thresholds are now stricter than the decision document says, and the current workflow intentionally tightens them as the project's sustained health improves. + +## Decision + +**Supersede ADR-0018 and treat `.codescene-thresholds` as the ratcheted policy baseline for Tolaria's CodeScene gate.** The current required minimums are `HOTSPOT_THRESHOLD=9.84` and `AVERAGE_THRESHOLD=9.45`. Thresholds move upward only when the repository can sustain a stricter baseline without immediately regressing. + +## Options considered + +- **Ratchet the enforced thresholds and document the new baseline** (chosen): keeps the ADRs aligned with the real gate, preserves the Boy Scout Rule, and makes code-health expectations stricter as the codebase improves. +- **Keep ADR-0018 active and treat higher thresholds as an implementation detail**: lower documentation churn, but the active ADR would no longer describe the actual CI and hook policy. +- **Remove numeric thresholds from ADRs entirely**: more durable on paper, but loses the explicit quality bar that developers are expected to maintain. + +## Consequences + +- `.codescene-thresholds` is now the authoritative location for the current numeric gate values. +- ADRs must be superseded again if Tolaria makes another meaningful policy jump in CodeScene thresholds. +- Pre-push and related quality checks now enforce a stricter floor than ADR-0018 described. +- The quality gate remains intentionally one-way: relaxing thresholds would require an explicit architectural reversal, not a quiet config edit. diff --git a/docs/adr/0065-root-managed-ai-guidance-files.md b/docs/adr/0065-root-managed-ai-guidance-files.md new file mode 100644 index 0000000..9955c91 --- /dev/null +++ b/docs/adr/0065-root-managed-ai-guidance-files.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0065" +title: "Root-managed AI guidance files with Claude shim" +status: active +date: 2026-04-14 +--- + +## Context + +Tolaria now supports multiple local CLI agents, but vault-level guidance still carried legacy assumptions. Existing vault bootstrap and repair flows centered on `config/agents.md`, while modern coding agents expect instructions at the vault root. That mismatch made managed guidance harder to reason about, left Claude Code compatibility implicit, and gave the UI no reliable way to distinguish between Tolaria-managed files that can be repaired and user-authored custom guidance that must be preserved. + +## Decision + +**Tolaria manages vault AI guidance at the vault root.** `AGENTS.md` is the canonical shared guidance file, `CLAUDE.md` is a compatibility shim that points Claude Code back to `AGENTS.md`, and Tolaria classifies both files as `managed`, `missing`, `broken`, or `custom` so repair flows restore only Tolaria-managed guidance without overwriting custom user files. + +## Options considered + +- **Root `AGENTS.md` as canonical plus a root `CLAUDE.md` shim** (chosen): matches current agent expectations, keeps one source of truth for shared instructions, and makes repair status explicit. +- **Keep managed guidance under `config/agents.md`**: preserves the older structure, but hides a user-facing integration contract behind legacy config paths and keeps Claude compatibility indirect. +- **Maintain separate full instruction files for each agent**: simple per tool, but duplicates instructions and increases drift risk whenever guidance changes. + +## Consequences + +- New and repaired vaults now seed `AGENTS.md` and `CLAUDE.md` at the vault root. +- Legacy `config/agents.md` content is migrated forward when safe, then the obsolete file is removed. +- The status bar and command palette can expose a first-class restore action because backend guidance state is normalized. +- Custom root guidance files are preserved instead of being silently overwritten by repair flows. +- Tolaria keeps a single shared guidance document even while supporting multiple CLI agents. +- Re-evaluate if supported agents stop relying on root-level files or if future agent integrations require materially different vault instructions instead of a shared source of truth. diff --git a/docs/adr/0066-calendar-semver-versioning-for-alpha-and-stable-releases.md b/docs/adr/0066-calendar-semver-versioning-for-alpha-and-stable-releases.md new file mode 100644 index 0000000..b18b1a5 --- /dev/null +++ b/docs/adr/0066-calendar-semver-versioning-for-alpha-and-stable-releases.md @@ -0,0 +1,37 @@ +--- +type: ADR +id: "0066" +title: "Calendar-semver versioning for alpha and stable releases" +status: active +date: 2026-04-16 +supersedes: "0057" +--- + +## Context + +ADR-0057 kept Tolaria on two updater channels and used "next stable patch" semver for alpha builds. That preserved ordering, but it no longer matched the agreed product naming: + +- Alpha should display as `Alpha YYYY.M.D.N` +- Alpha should ship the technical version `YYYY.M.D-alpha.N` +- Stable should ship and display as `YYYY.M.D` + +The naming change still needs to stay semver-safe when users switch between Stable and Alpha. A pure same-day calendar alpha would become older than a same-day stable promotion, so the workflow needs a monotonicity guard in addition to cleaner display strings. + +## Decision + +**Tolaria keeps exactly two updater channels (`stable` and `alpha`), but both now use calendar-semver release numbers.** Stable promotions use `stable-vYYYY.M.D` tags and stamp the technical version `YYYY.M.D`. Every push to `main` publishes an alpha build with technical version `YYYY.M.D-alpha.N` and display label `Alpha YYYY.M.D.N`. + +If the latest stable tag already uses the current UTC calendar date, the alpha workflow advances to the next calendar day before assigning `-alpha.N`. That keeps alpha semver-newer than the most recent stable build even after a same-day promotion. + +## Options considered + +- **Calendar semver with a next-day safeguard** (chosen): matches the agreed naming, keeps user-facing labels clean, and preserves updater ordering across channel switches. +- **Calendar semver without a safeguard**: simplest display model, but alpha can become semver-older than Stable after a same-day promotion. +- **Keep ADR-0057's next-patch prerelease numbering**: semver-safe, but it does not match the agreed release naming or the product surfaces that should show calendar-based versions. + +## Consequences + +- Release workflows now compute both a technical version and a display version. +- User-facing version surfaces strip technical prerelease noise into clean labels (`Alpha YYYY.M.D.N` or `YYYY.M.D`). +- Stable promotions must use `stable-vYYYY.M.D` tags instead of patch-based semver tags. +- Alpha sequence numbers are scoped to a calendar core date and remain compatible with the updater manifests. diff --git a/docs/adr/0067-autogit-idle-and-inactive-checkpoints.md b/docs/adr/0067-autogit-idle-and-inactive-checkpoints.md new file mode 100644 index 0000000..1d71f96 --- /dev/null +++ b/docs/adr/0067-autogit-idle-and-inactive-checkpoints.md @@ -0,0 +1,39 @@ +--- +type: ADR +id: "0067" +title: "AutoGit idle and inactive checkpoints" +status: active +date: 2026-04-17 +--- + +## Context + +Tolaria already had explicit git actions in the status bar (ADR-0032) and a remote-aware manual commit flow (ADR-0059), but git-backed vaults still depended on the user remembering to create checkpoints. That worked for deliberate commits, yet it left a gap for ordinary writing sessions where the app had already saved all note content but no git checkpoint had been recorded. + +The new checkpointing behavior needed to stay conservative: + +- never run for non-git vaults +- never commit unsaved editor buffers +- reuse the same remote detection and local-only fallback as the manual commit flow +- avoid drift between timer-driven checkpoints and the status-bar quick commit action + +## Decision + +**Tolaria introduces installation-local AutoGit settings plus a dedicated `useAutoGit` hook that triggers a shared `useCommitFlow.runAutomaticCheckpoint()` path after configurable idle or inactive thresholds.** The checkpoint runs only when the current vault is git-backed, there are pending saved changes (or local commits waiting to push), and no unsaved edits remain. + +`useCommitFlow.runAutomaticCheckpoint()` is now the single checkpoint runner for both AutoGit and the status-bar quick commit action. That shared path generates deterministic automatic commit messages (`Updated N note(s)` / `Updated N file(s)`), commits locally when no remote exists, and can also do a push-only retry when commits already exist locally. + +## Options considered + +- **Option A** (chosen): A shared checkpoint runner used by both AutoGit timers and the quick commit action. Pros: one git policy, one message generator, one remote-handling path. Cons: adds another cross-cutting settings-driven hook. +- **Option B**: A separate background AutoGit implementation. Pros: could evolve independently from the manual commit flow. Cons: high risk of drift in commit messages, push behavior, and remote handling. +- **Option C**: Commit on every save. Pros: simplest trigger model. Cons: far too noisy for git history, especially with Tolaria's autosave model. + +## Consequences + +- App settings now persist `autogit_enabled`, `autogit_idle_threshold_seconds`, and `autogit_inactive_threshold_seconds` in installation-local settings storage. +- `useAutoGit` tracks editor activity plus app focus/visibility state and triggers checkpoints after the configured thresholds. +- Automatic checkpoints are blocked while unsaved edits exist, so AutoGit only records content that is already flushed through the normal save pipeline. +- The bottom-bar quick commit action now reuses the same checkpoint runner after forcing a save, keeping manual and automatic checkpoint behavior aligned. +- Vaults without a remote still benefit: AutoGit uses the existing local-only commit behavior from ADR-0059 instead of treating missing remotes as an error. +- Re-evaluate if users need per-vault policy instead of installation-local policy, or if timer-driven checkpoints create too much git noise in real-world use. diff --git a/docs/adr/0068-h1-only-title-surface-with-optional-untitled-auto-rename.md b/docs/adr/0068-h1-only-title-surface-with-optional-untitled-auto-rename.md new file mode 100644 index 0000000..e507ec1 --- /dev/null +++ b/docs/adr/0068-h1-only-title-surface-with-optional-untitled-auto-rename.md @@ -0,0 +1,35 @@ +--- +type: ADR +id: "0068" +title: "H1-only title surface with optional untitled auto-rename" +status: active +date: 2026-04-17 +supersedes: "0055" +--- + +## Context + +ADR-0055 removed the legacy title row and made the editor body the only title surface. That ADR also kept one strong filename behavior from ADR-0044: untitled notes would auto-rename from their first H1 on save. + +That always-on rename rule turned out to be too rigid. Some users want the H1 to drive the displayed title immediately, but prefer to keep the synthetic `untitled-*` filename stable until they explicitly rename it from the breadcrumb bar. The product needed to preserve the H1-only editing model without forcing every installation into automatic filename changes. + +## Decision + +**Tolaria keeps the editor body as the only title surface, but untitled-note auto-rename from the first H1 becomes an installation-local setting (`initial_h1_auto_rename_enabled`) that defaults to enabled.** + +When the setting is enabled, untitled notes continue to auto-rename on save as soon as a real H1 title exists. When disabled, Tolaria still treats the H1 as the canonical display title, but it leaves the filename unchanged until the user explicitly renames it through the breadcrumb controls. + +## Options considered + +- **Option A** (chosen): Keep the current auto-rename behavior as the default, but make it an installation-local preference. Pros: preserves the fast path for most users while allowing opt-out for users who want stable temporary filenames. Cons: different installs can behave differently. +- **Option B**: Keep auto-rename mandatory, as assumed by ADR-0055. Pros: one simple filename policy. Cons: surprises users who want title editing without immediate file renames. +- **Option C**: Turn auto-rename off for everyone. Pros: filenames only change on explicit user action. Cons: leaves more `untitled-*` files around and adds friction to the common case. + +## Consequences + +- App settings now persist `initial_h1_auto_rename_enabled` in installation-local settings storage. +- The save pipeline consults that setting before scheduling untitled-file renames. +- Disabling untitled auto-rename does not restore any legacy title field or alternate title UI. H1 remains the only editor title surface. +- When the setting is off, display title and filename can diverge for longer: the note may show a human H1 while the file remains `untitled-*` until explicit rename. +- Settings UI and command/search affordances now expose this filename policy as a user preference rather than a hardcoded rule. +- Re-evaluate if users later need this policy to be per-vault instead of installation-local, or if longer-lived untitled filenames create too much Finder/git noise. diff --git a/docs/adr/0069-neighborhood-mode-for-note-list-relationship-browsing.md b/docs/adr/0069-neighborhood-mode-for-note-list-relationship-browsing.md new file mode 100644 index 0000000..a7d9988 --- /dev/null +++ b/docs/adr/0069-neighborhood-mode-for-note-list-relationship-browsing.md @@ -0,0 +1,33 @@ +--- +type: ADR +id: "0069" +title: "Neighborhood mode for note-list relationship browsing" +status: active +date: 2026-04-19 +--- + +## Context + +Tolaria already had a relationship-browsing state behind `SidebarSelection.kind === 'entity'`, but the product language and interaction model were still fuzzy. The pinned source note rendered as a special card instead of a normal note row, grouped relationship results were deduplicated across sections, and Cmd-click behaved like a legacy "open separately" affordance rather than a clear graph-navigation action. + +The new note-list flow needed an explicit product concept for browsing related notes around a source note, plus keyboard semantics that matched the mouse flow. The team also wanted the list to preserve graph truth instead of collapsing overlapping relationships away when a note legitimately belonged to multiple groups. + +## Decision + +**Tolaria formalizes `SidebarSelection.kind === 'entity'` as Neighborhood mode.** The note list now treats the selected note as the neighborhood source, pins it at the top using the standard active note-row styling, shows outgoing relationship groups first and inverse/backlink groups after, keeps empty groups visible with count `0`, and allows the same note to appear in multiple groups when multiple relationships are true. + +**Neighborhood navigation is a distinct pivot action.** Plain click and plain `Enter` open the focused note without replacing the current neighborhood. Cmd/Ctrl-click and Cmd/Ctrl-`Enter` open the note and pivot the note list into that note's Neighborhood. + +## Options considered + +- **Reuse the existing `entity` selection as Neighborhood mode** (chosen): keeps the state model localized, avoids a second nearly-identical note-list mode, and lets sidebar navigation exit Neighborhood by selecting any other sidebar target. Cons: code still uses the historical `entity` name internally. +- **Add a new `neighborhood` selection variant**: clearer internal naming, but it duplicates the same source-note payload and would force wider selection-handling churn across the app for little product gain. +- **Keep the old implicit entity-browsing behavior**: lowest short-term engineering effort, but it leaves the product terminology inconsistent and preserves interaction mismatches like deduped groups and non-pivot Cmd-click behavior. + +## Consequences + +- Product, tests, and docs now refer to Neighborhood as a first-class note-list browsing mode. +- The note list preserves overlapping graph evidence: one note can appear in multiple groups when multiple relationships are true. +- Keyboard-only browsing now matches the pointer flow: arrow keys/open keep the current neighborhood, while Cmd/Ctrl-`Enter` pivots it. +- Sidebar navigation remains the exit path from Neighborhood because the app still models the mode through the existing selection union. +- Internal code still uses the `entity` discriminator, so future refactors should treat "entity selection" and "Neighborhood mode" as the same concept unless a broader navigation redesign justifies a new selection shape. diff --git a/docs/adr/0070-starter-vaults-local-first-with-explicit-remote-connection.md b/docs/adr/0070-starter-vaults-local-first-with-explicit-remote-connection.md new file mode 100644 index 0000000..d53a67f --- /dev/null +++ b/docs/adr/0070-starter-vaults-local-first-with-explicit-remote-connection.md @@ -0,0 +1,33 @@ +--- +type: ADR +id: "0070" +title: "Starter vaults are local-first with explicit remote connection" +status: active +date: 2026-04-19 +--- + +## Context + +ADR-0046 moved the Getting Started vault to a public GitHub repo cloned at runtime, and ADR-0059 established that Tolaria should support valid local-only vaults without treating a missing remote as an error. + +That still left one mismatch: a freshly cloned starter vault inherited the template repo's `origin` remote. New users therefore landed in a vault that looked remote-backed by default, even though the intended workflow was to explore locally first and only connect a personal remote later. Keeping the starter remote also risked accidental pushes to the public template repo and gave Tolaria no safe place to reject incompatible remotes before tracking started. + +## Decision + +**After cloning the public starter vault, Tolaria removes every configured git remote so the vault opens local-only by default.** Users connect a remote later through an explicit Add Remote flow exposed from the `No remote` status-bar chip and the command palette. + +**The new `git_add_remote` backend is the only path for attaching a remote to an existing local-only vault.** It adds `origin`, fetches the remote, rejects incompatible or ahead histories, and only starts tracking when the remote is safe for the current local repo. + +## Options considered + +- **Strip starter-vault remotes and add an explicit connect flow** (chosen): preserves a local-first onboarding experience, matches ADR-0059's local-only model, and prevents accidental coupling to the public template repo. Cons: users who want sync must do one extra explicit step. +- **Keep the starter repo's remote attached**: simplest implementation, but it makes the template repo look like the user's real sync target and increases the risk of accidental pushes or confusing remote state. +- **Force remote replacement during onboarding**: guarantees a personal remote up front, but adds too much setup friction to the Getting Started path and weakens Tolaria's offline/local-first story. + +## Consequences + +- Fresh Getting Started vaults now behave like any other local-only vault: commit locally first, then opt into sync later. +- The app gains a dedicated Add Remote UX (`AddRemoteModal`) plus a backend connection path (`git_add_remote`) instead of overloading clone or commit flows. +- Remote attachment is safer: Tolaria can reject unrelated or incompatible histories before the vault starts tracking a remote. +- The starter repo remains a distribution source only, not an ongoing sync destination. +- Re-evaluate if Tolaria later needs a faster "publish this local starter vault to my own repo" flow that should prefill or streamline the Add Remote step. diff --git a/docs/adr/0071-external-vault-refresh-and-clean-tab-reopen.md b/docs/adr/0071-external-vault-refresh-and-clean-tab-reopen.md new file mode 100644 index 0000000..1a9ef9f --- /dev/null +++ b/docs/adr/0071-external-vault-refresh-and-clean-tab-reopen.md @@ -0,0 +1,49 @@ +--- +type: ADR +id: "0071" +title: "External vault updates reload derived state and reopen the clean active note" +status: superseded +date: 2026-04-21 +superseded_by: "0111" +--- + +## Context + +ADR-0002 makes the filesystem the source of truth, and ADR-0043 keeps locally edited frontmatter reactive inside the running UI. But external vault mutations still had a gap. A `git pull` or an AI agent edit could change notes on disk while the app kept showing stale note-list state, stale derived relationships, or an editor surface that still rendered the pre-refresh BlockNote document. + +The fix needed to satisfy a few constraints at once: + +- refresh all vault-derived UI, not just the main note list +- preserve unsaved local edits instead of clobbering them with disk state +- reopen the active note from disk when it is safe, even if another file changed, because backlinks, inverse relationships, and other derived surfaces can depend on the whole vault +- handle the native editor case where an in-place file update requires a full tab reopen to show the fresh document reliably + +## Decision + +**All external vault mutations now reconcile through one shared refresh path that reloads vault-derived state and then conditionally reopens the active note from disk.** + +Tolaria now routes post-pull refreshes and AI-agent file modifications through the same `refreshPulledVaultState()` helper. + +That shared path does the following: + +1. Reload `vault.entries`, folders, and saved views together. +2. If there is no active note, stop after the reload. +3. If the active note has unsaved local edits, keep the current editor buffer and do not replace it from disk. +4. Otherwise, find the refreshed `VaultEntry` for the active note and replace the active tab with freshly loaded disk content. +5. If the active file itself changed in place during the external update, close the tab before reopening it so BlockNote fully remounts onto the new document. +6. If the active file no longer exists after the reload, close the open tab state instead of leaving a stale editor behind. + +## Options considered + +- **Shared external-refresh reconciler** (chosen): one policy for pulls and agent edits, consistent vault-derived UI, and explicit protection for unsaved local edits. Cons: more coupling between sync flows and tab management. +- **Patch only the changed surfaces ad hoc**: smaller individual fixes, but high risk of drift between pull handling, agent handling, and future external-write paths. +- **Always force a full app-level reload**: simplest correctness story, but too disruptive and more likely to throw away user context unnecessarily. + +## Consequences + +- Any workflow that mutates the vault externally, such as git pulls or agent writes, should go through the shared refresh reconciler rather than reloading a single surface in isolation. +- Clean active notes now converge back to on-disk truth automatically after external updates. +- Unsaved local edits remain protected from external refreshes, even when the rest of the vault reloads. +- Folder, saved-view, backlink, and inverse-relationship surfaces stay aligned with the refreshed vault, not just the editor tab. +- Tolaria now treats "refresh after external mutation" as a first-class synchronization concern rather than a per-feature fix. +- Re-evaluate if the editor gains a reliable in-place document reset API, because that could remove the need for the close-and-reopen step when the active file itself changed. diff --git a/docs/adr/0072-confirmed-vault-paths-gate-startup-state.md b/docs/adr/0072-confirmed-vault-paths-gate-startup-state.md new file mode 100644 index 0000000..2d97fbd --- /dev/null +++ b/docs/adr/0072-confirmed-vault-paths-gate-startup-state.md @@ -0,0 +1,33 @@ +--- +type: ADR +id: "0072" +title: "Confirmed vault paths gate startup state" +status: active +date: 2026-04-22 +--- + +## Context + +Tolaria's startup path was assuming that any incoming `vaultPath` was authoritative immediately. In practice, boot can pass through transient empty paths and stale paths that no longer correspond to the persisted active vault. That produced two classes of regressions: + +1. `useVaultLoader` fired `reload_vault` and `get_modified_files` before a real vault path existed, generating avoidable warnings and backend calls for `""`. +2. On fresh install or other non-persisted startup cases, a missing path could incorrectly render `vault-missing` instead of the intended welcome flow. + +Tolaria's onboarding and vault-loading surfaces need the same invariant: only a confirmed vault identity should drive startup side effects or missing-vault error UI. + +## Decision + +**Tolaria now treats a vault path as authoritative at startup only after it is confirmed.** Vault-loading side effects no-op until the path is non-empty, and the `vault-missing` onboarding state is shown only when the missing path was the persisted active vault recorded in `load_vault_list`. Otherwise, startup falls back to `welcome`. + +## Options considered + +- **Option A** (chosen): gate startup effects and missing-vault UI on confirmed vault identity. This keeps boot deterministic, avoids empty-path backend calls, and preserves the product rule that fresh installs should land in Welcome rather than an error state. +- **Option B**: treat any startup `vaultPath` as authoritative immediately. Simpler branching, but it keeps the existing race where transient or stale paths trigger warnings and the wrong onboarding state. +- **Option C**: special-case each startup surface independently. Lower immediate churn, but it would duplicate boot logic and let `useOnboarding` and `useVaultLoader` drift again. + +## Consequences + +- `useVaultLoader` must guard all startup work behind a real non-empty vault path. +- `useOnboarding` must consult persisted vault state before deciding that a missing path represents a deleted active vault. +- Fresh installs, cleared vault lists, and other startup flows without a confirmed active vault should resolve to `welcome`, even if an initial path probe fails. +- Re-evaluate if Tolaria introduces deeper startup routing (for example multiple launch intents or restored workspaces) that needs a richer boot-state model than the current confirmed-path gate. diff --git a/docs/adr/0073-persistent-linkify-protocol-registry-across-editor-remounts.md b/docs/adr/0073-persistent-linkify-protocol-registry-across-editor-remounts.md new file mode 100644 index 0000000..e0bd6b6 --- /dev/null +++ b/docs/adr/0073-persistent-linkify-protocol-registry-across-editor-remounts.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0073" +title: "Persistent linkify protocol registry across editor remounts" +status: active +date: 2026-04-22 +--- + +## Context + +Tolaria keeps a single editor shell alive while users swap notes and toggle between BlockNote and raw mode. The upstream BlockNote/Tiptap link stack assumes linkify protocol registration is effectively one-shot per editor lifetime, and Tiptap's link extension resets that registry on destroy. + +In Tolaria's lifecycle, that behavior caused duplicate `linkifyjs: already initialized` warnings during note-open and editor-remount flows. The problem is cross-cutting: BlockNote and Tiptap both participate, and the failure only disappears when protocol registration survives teardown/remount cycles instead of being repeated opportunistically. + +## Decision + +**Tolaria patches the upstream BlockNote and Tiptap link packages so custom linkify protocols are pre-registered once per app runtime and are not reset on editor teardown.** The patched packages coordinate through `globalThis` flags, and Tolaria tracks them via `pnpm` patched dependencies rather than ad hoc runtime monkey-patching inside app code. + +## Options considered + +- **Option A** (chosen): maintain explicit `pnpm` patches for the affected upstream packages, pre-register the needed protocols once, and preserve the registry across remounts. This matches Tolaria's persistent editor shell and keeps the behavior deterministic in both dev and packaged builds. +- **Option B**: keep upstream behavior and tolerate or suppress the warnings locally. Lower maintenance, but it leaves editor lifecycle correctness dependent on noisy duplicate initialization and makes future regressions harder to reason about. +- **Option C**: add Tolaria-side runtime monkey-patches around editor mount/unmount. Avoids vendoring patches, but spreads dependency-specific lifecycle logic into application code and is more fragile across package upgrades. + +## Consequences + +- `pnpm-workspace.yaml` now treats the relevant BlockNote and Tiptap link packages as patched dependencies, so upgrades must preserve or consciously replace those patches. +- Editor teardown in Tolaria must not assume ownership of the global linkify protocol registry. +- Smoke coverage for note open, editor remount, and raw-mode toggling must stay in place because the failure mode is lifecycle-specific rather than feature-specific. +- Re-evaluate this ADR if upstream BlockNote/Tiptap gains a supported lifecycle-safe protocol-registration model that makes the Tolaria patches unnecessary. diff --git a/docs/adr/0074-explicit-external-ai-tool-setup-and-least-privilege-desktop-scope.md b/docs/adr/0074-explicit-external-ai-tool-setup-and-least-privilege-desktop-scope.md new file mode 100644 index 0000000..8591d81 --- /dev/null +++ b/docs/adr/0074-explicit-external-ai-tool-setup-and-least-privilege-desktop-scope.md @@ -0,0 +1,36 @@ +--- +type: ADR +id: "0074" +title: "Explicit external AI tool setup and least-privilege desktop scope" +status: active +date: 2026-04-22 +--- + +## Context + +Tolaria's first MCP integration optimized for zero setup: desktop startup auto-registered the Tolaria MCP server in Claude Code and Cursor config files, the Tauri asset protocol allowed every local path, and app-managed Codex sessions launched with the CLI's dangerous bypass flag. That made the product feel convenient, but it also widened trust by default in places that users could not see or consent to clearly. + +The product direction now favors least-privilege defaults. Fresh installs should not silently edit third-party config files, external AI tool setup must be intentional and reversible, and the desktop shell should only expose the filesystem paths that the active vault actually needs. + +## Decision + +**Tolaria now treats external AI tool wiring as an explicit user action and keeps the desktop shell scoped to the active vault.** + +- The app still spawns its local MCP WebSocket bridge on desktop startup, but it no longer auto-registers third-party MCP config files. +- External MCP registration is exposed through a keyboard-accessible setup flow reachable from the command palette and status surfaces. Confirming the flow upserts Tolaria's MCP entry for the current vault; cancel leaves external config untouched; disconnect removes Tolaria's entry again. +- The Tauri asset protocol remains enabled for local vault images, but its static config scope is empty. Tolaria grants recursive asset access only to the active vault at runtime when that vault is reloaded. +- App-managed Codex sessions use the CLI's normal approval and sandbox path by default instead of opting into the dangerous bypass mode automatically. + +## Options considered + +- **Explicit setup + runtime vault-only scope** (chosen): aligns with least-privilege defaults, keeps command-palette discoverability, preserves image loading and external-tool support, and makes every privileged step visible and reversible. +- **Keep startup auto-registration and global asset scope**: lowest friction, but it silently mutates third-party config and leaves the desktop shell effectively open to every local file path. +- **Disable external MCP registration entirely**: safest on paper, but it removes a valuable workflow for Claude Code, Cursor, and other MCP-compatible tools that Tolaria intentionally supports. + +## Consequences + +- Fresh installs no longer modify `~/.claude/mcp.json` or `~/.cursor/mcp.json` until the user confirms setup. +- Switching vaults does not silently retarget external MCP clients; users reconnect explicitly when they want a different vault exposed. +- Desktop asset access is constrained to the active vault instead of all filesystem paths, while note images and attachments continue to load normally. +- The command palette and status bar now expose an explicit external AI tools setup/remove flow that supports keyboard-only QA. +- Codex agent sessions are safer by default, at the cost of relying on the CLI's normal approval path instead of bypassing it automatically. diff --git a/docs/adr/0075-crash-safe-note-rename-transactions.md b/docs/adr/0075-crash-safe-note-rename-transactions.md new file mode 100644 index 0000000..649e430 --- /dev/null +++ b/docs/adr/0075-crash-safe-note-rename-transactions.md @@ -0,0 +1,36 @@ +--- +type: ADR +id: "0075" +title: "Crash-safe note rename transactions" +status: active +date: 2026-04-22 +--- + +## Context + +Tolaria's note rename path used a simple "write new file, then delete old file" flow. That was easy to implement, but it had three integrity problems called out by issue #205: it could leave a visible duplicate note if the app crashed between those steps, destination-path selection depended on check-then-use races, and backlink rewrite failures were collapsed into a generic updated-files count that made partial success look clean. + +Rename is a core vault integrity operation. The app needs a flow that preserves a trustworthy visible state even when the process dies mid-rename, and it needs to surface any partial backlink rewrite failures clearly enough that users are not told everything updated when some linked files still need manual repair. + +## Decision + +**Tolaria now stages note renames through a hidden per-vault transaction directory and recovers unfinished transactions on the next vault scan.** + +- A rename that changes the file path first writes a transaction manifest plus a hidden backup path inside `/.tolaria-rename-txn/`. +- Tolaria moves the old note into that hidden backup, persists the new file with a no-clobber destination write, and then deletes the backup/manifest only after the new note exists. +- If the process crashes before the new note is committed, the next `scan_vault` restores the hidden backup back to the original path before listing entries. +- Manual filename renames keep their explicit conflict semantics, but the final destination is now claimed with a no-clobber write instead of relying on an existence check. +- Backlink rewrites now return both the number of successful updates and the number of failed updates so the UI can warn about partial success instead of reporting a clean rename. + +## Options considered + +- **Hidden transaction directory + scan-time recovery** (chosen): keeps in-flight rename artifacts out of the visible vault model, gives Tolaria a deterministic recovery point after crashes, and lets the final destination use no-clobber persistence. +- **Rename in place without transaction metadata**: simpler, but it cannot recover a half-finished rename reliably after process death and still leaves either duplicate or missing-note windows. +- **Best-effort duplicate cleanup with no recovery path**: lowest implementation cost, but it leaves the user-visible vault state dependent on exact crash timing and does not meet the trustworthiness goal for rename operations. + +## Consequences + +- Every vault can now contain a hidden `.tolaria-rename-txn/` directory managed by Tolaria; scan and folder UI continue to ignore it because hidden directories are already excluded. +- Rename results are richer: the frontend must treat `failed_updates > 0` as a warning state even when the rename itself succeeded. +- Future changes to vault scanning or note rename behavior must preserve transaction recovery before entry listing, otherwise crash safety regresses. +- The rename path no longer silently overwrites a destination discovered via a stale existence check; title-driven renames retry with suffixed filenames, while explicit filename renames fail cleanly on collision. diff --git a/docs/adr/0076-note-retargeting-separates-type-and-folder-moves.md b/docs/adr/0076-note-retargeting-separates-type-and-folder-moves.md new file mode 100644 index 0000000..256751d --- /dev/null +++ b/docs/adr/0076-note-retargeting-separates-type-and-folder-moves.md @@ -0,0 +1,34 @@ +--- +type: ADR +id: "0076" +title: "Note retargeting separates type changes from folder moves" +status: active +date: 2026-04-22 +--- + +## Context + +ADR-0025 made `type:` the canonical classification field, and ADR-0033 reopened subfolders as a valid way to organize files in the vault. Once Tolaria exposed both type sections and the folder tree in the sidebar, note reorganization had an unresolved ambiguity: does retargeting a note mean changing its semantic type, moving its file, or both? + +Without an explicit model, drag-and-drop and command-palette flows would need to duplicate their own validation and persistence logic, and Tolaria could easily drift back toward the old type-folder coupling that ADR-0006 deliberately removed. + +## Decision + +**Tolaria treats note retargeting as one shared interaction model with two distinct mutation paths: types change metadata, folders change file paths.** + +- Retargeting a note to a type updates only the note's `type:` frontmatter. The file stays where it is. +- Retargeting a note to a folder preserves the current filename and `type:` value, and moves the file through the same crash-safe rename transaction pipeline used for backend rename commands. +- Drag/drop targets and command-palette actions both route through the same frontend retargeting abstraction so validation, dialogs, collision handling, and success/error behavior stay consistent. + +## Options considered + +- **Shared retargeting model with separate type-vs-folder semantics** (chosen): preserves ADR-0025/ADR-0006's decoupling of type from path, lets folder moves reuse ADR-0075's crash-safe rename guarantees, and keeps multiple UI surfaces behaviorally aligned. +- **Treat folders as the source of truth for note type**: simpler mental model for some vaults, but it reintroduces path-based type inference and makes type changes depend on file moves again. +- **Implement drag/drop and command-palette retargeting as separate flows**: lower short-term coordination cost, but it duplicates mutation rules and makes consistency regressions likely. + +## Consequences + +- Type sections are semantic targets only; they must never imply a filesystem move. +- Folder targets are physical move operations; they must preserve filename/title behavior, reject collisions, and rewrite path-based wikilinks through the shared rename pipeline. +- Future note-retargeting surfaces should reuse the shared retargeting abstraction instead of introducing another mutation path. +- Re-evaluate this ADR if Tolaria later supports bulk retargeting, folder rules that intentionally infer type, or another organization primitive that needs different semantics. diff --git a/docs/adr/0077-concurrent-safe-vault-cache-replacement.md b/docs/adr/0077-concurrent-safe-vault-cache-replacement.md new file mode 100644 index 0000000..5eef747 --- /dev/null +++ b/docs/adr/0077-concurrent-safe-vault-cache-replacement.md @@ -0,0 +1,37 @@ +--- +type: ADR +id: "0077" +title: "Concurrent-safe vault cache replacement" +status: active +date: 2026-04-24 +--- + +## Context + +ADR-0014 and ADR-0024 established Tolaria's git-based persistent vault cache and moved it outside the vault directory. That cache was still being rewritten with a simple temp-file-and-rename flow. + +Once Tolaria started reopening the same vault from multiple windows/processes more often, that write model became too optimistic: two scans could both build valid cache payloads from different moments in time, and the slower writer could still atomically replace a fresher cache written by another window. The cache needed cross-window safety without introducing a long-lived coordinator process or making vault open dependent on heavyweight IPC. + +## Decision + +**Tolaria now treats vault-cache replacement as a best-effort compare-and-swap operation instead of an unconditional atomic overwrite.** + +- Each scan still builds the next cache payload in memory and writes it to a temp file first. +- Before replacing the real cache file, Tolaria acquires a short-lived lock file for that vault cache path. +- After the lock is acquired, Tolaria rechecks the on-disk cache fingerprint and only renames the temp file into place if another window/process has not already refreshed the cache. +- If the cache changed underneath the current scan, Tolaria skips the replace and keeps the newer on-disk cache. +- Stale cache-write locks are garbage-collected after a short timeout so a crashed writer does not block future refreshes. + +## Options considered + +- **Lock + fingerprint guarded replacement** (chosen): keeps the cache file external and file-based, avoids overwriting fresher cache state from another Tolaria window, and preserves graceful fallback to filesystem rescans. Cons: cache writes become best-effort rather than guaranteed after every scan. +- **Keep unconditional temp-file + rename**: simplest implementation, but concurrent windows can regress the cache to an older view even though each individual replace is atomic. +- **Centralized cache service or long-lived process mutex**: strongest coordination story, but too much operational complexity for a local desktop app and would create new failure modes around boot, process lifetime, and IPC. + +## Consequences + +- Tolaria's cache correctness model is now "latest successful guarded replace wins," not "every scan must write a cache file." +- Cache refreshes must tolerate a skipped write when another window/process already produced a fresher cache. +- Temp-file writes and renames still provide corruption resistance, but freshness is protected separately by the writer lock and fingerprint check. +- Cache-write failures remain non-fatal: Tolaria logs them and falls back to rebuilding from the filesystem when needed. +- Re-evaluate if Tolaria later needs stronger cross-process coordination than lock-file + fingerprint checks can provide. diff --git a/docs/adr/0078-scoped-unsigned-fallback-for-app-managed-git-commits.md b/docs/adr/0078-scoped-unsigned-fallback-for-app-managed-git-commits.md new file mode 100644 index 0000000..c9d2e90 --- /dev/null +++ b/docs/adr/0078-scoped-unsigned-fallback-for-app-managed-git-commits.md @@ -0,0 +1,36 @@ +--- +type: ADR +id: "0078" +title: "Scoped unsigned fallback for app-managed git commits" +status: active +date: 2026-04-24 +--- + +## Context + +ADR-0021, ADR-0059, and ADR-0070 all assume Tolaria can create and advance a local git-backed vault without asking users to debug git internals first. In practice, inherited `commit.gpgsign` settings were breaking that promise: a missing or misconfigured GPG/SSH signing helper could block the initial `Initial vault setup` commit during onboarding and could also strand later app-triggered commits behind opaque signing failures. + +Tolaria needed a policy that kept signed workflows intact when the user's signing setup actually works, while still ensuring app-managed git operations do not become unusable just because a desktop environment cannot reach the signing helper. + +## Decision + +**Tolaria uses a scoped unsigned fallback for app-managed commits instead of requiring signing to succeed unconditionally.** + +- The onboarding/setup commit (`Initial vault setup`) always runs with `commit.gpgsign=false` for that single git invocation. +- Normal app-managed `git_commit` calls still honor the user's existing git signing configuration first. +- If a commit fails and Git's error matches a signing-helper failure, Tolaria retries that same app-managed commit once with signing disabled. +- Tolaria does not rewrite the user's git config and does not broaden the retry to unrelated commit failures. + +## Options considered + +- **Scoped unsigned fallback for app-managed commits** (chosen): keeps onboarding and local commit flows resilient while still preserving signed commits when the user's environment supports them. Cons: some Tolaria-created commits may be unsigned on machines with broken signing setups. +- **Require signing to succeed for every commit**: simplest policy, but it turns missing desktop GPG/SSH helpers into app-breaking failures during onboarding and normal use. +- **Disable signing for all Tolaria-triggered commits**: maximally robust, but it would silently bypass working signing setups and weaken users' expected git security posture. + +## Consequences + +- New vault creation is no longer blocked by inherited signing settings that only fail in Tolaria's app context. +- Users with healthy signing setups still get signed Tolaria commits after the first normal attempt succeeds. +- Signing-failure detection must stay narrow so Tolaria does not mask unrelated git errors behind an unsigned retry. +- Tolaria's git integration now explicitly prefers "complete the app-managed commit safely" over "preserve signing at all costs" when the signing helper is unavailable. +- Re-evaluate if Tolaria later exposes per-vault git policy controls or needs a richer user-facing explanation for when a fallback unsigned commit was used. diff --git a/docs/adr/0079-linux-window-chrome-and-menu-reuse.md b/docs/adr/0079-linux-window-chrome-and-menu-reuse.md new file mode 100644 index 0000000..7a07577 --- /dev/null +++ b/docs/adr/0079-linux-window-chrome-and-menu-reuse.md @@ -0,0 +1,37 @@ +--- +type: ADR +id: "0079" +title: "Linux window chrome and menu reuse" +status: active +date: 2026-04-24 +--- + +## Context + +Tolaria's desktop shell was designed around macOS window chrome. `titleBarStyle: "Overlay"` and `hiddenTitle: true` give the app a clean single-surface titlebar on macOS, but Linux ignores those flags and draws native GTK decorations and a native menu bar on top of the React UI. That creates a double-titlebar effect, mismatched theming, and inconsistent behavior between the main window and detached note windows. + +We still need Linux to reuse Tolaria's existing command palette, shortcut manifest, and deterministic menu-command routing instead of inventing a Linux-only command path. + +## Decision + +**Tolaria uses custom React-rendered window chrome on Linux and routes its menu through the existing shared command IDs.** + +- The main Tauri window disables server-side decorations on Linux during app setup. +- Detached note windows set `decorations: false` when Linux chrome is active. +- `LinuxTitlebar` renders the drag region, resize handles, and window controls for Linux windows. +- `LinuxMenuButton` mirrors the app's File/Edit/View/Go/Note/Vault/Window menus, but dispatches the existing command IDs through `trigger_menu_command`. +- The native Tauri menu bar is not mounted on Linux; macOS and other existing desktop targets keep the native menu. +- Shared shortcuts remain defined in `appCommandCatalog.ts`, including `Cmd+Shift+L` on macOS and `Ctrl+Shift+L` on Linux through the same command manifest. + +## Options considered + +- **React-rendered Linux chrome with shared command IDs** (chosen): keeps Linux visually aligned with Tolaria's existing shell and preserves one command-routing model across keyboard shortcuts, menu clicks, and QA helpers. Cons: Tolaria now owns Linux window chrome behavior directly. +- **Keep native GTK decorations and menu bar on Linux**: cheaper to ship, but it breaks visual consistency and produces overlapping titlebar/menu surfaces that do not match the rest of the app. +- **Introduce Linux-only command wiring for the custom menu**: would allow a Linux-specific implementation, but it would fork the shortcut/menu architecture and weaken deterministic QA. + +## Consequences + +- Linux main windows and detached note windows now present one consistent titlebar surface controlled by Tolaria. +- Menu commands, command palette actions, and deterministic QA still share the same command IDs, which limits platform-specific drift. +- Linux packaging and CI must install WebKit2GTK 4.1 dependencies and produce Linux bundles explicitly. +- Tolaria now owns Linux resize handles, maximize/minimize/close behavior, and titlebar drag-region behavior in the renderer, so regressions in those surfaces require direct tests. diff --git a/docs/adr/0080-cross-platform-desktop-release-artifacts-and-portable-vault-names.md b/docs/adr/0080-cross-platform-desktop-release-artifacts-and-portable-vault-names.md new file mode 100644 index 0000000..d0f73cc --- /dev/null +++ b/docs/adr/0080-cross-platform-desktop-release-artifacts-and-portable-vault-names.md @@ -0,0 +1,36 @@ +--- +type: ADR +id: "0080" +title: "Cross-platform desktop release artifacts and portable vault names" +status: active +date: 2026-04-24 +--- + +## Context + +Tolaria's release pipeline and file validation rules were still biased toward macOS. Alpha/stable releases only produced first-class macOS artifacts, stable download redirects assumed a DMG-only world, and vault file/folder validation allowed names that work on macOS/Linux but break on Windows clones and sync targets. + +Shipping Windows as a supported desktop target requires both distribution and data portability to become explicit. A Windows installer is not enough if shared vault content can still produce invalid filenames on that platform, and cross-platform updater manifests must keep Tauri's signed updater artifact separate from the user-facing installer download. + +## Decision + +**Tolaria ships first-class macOS, Windows x64, and Linux x64 desktop artifacts, and its vault-facing filename rules are portable across those platforms by default.** + +- Alpha and stable release workflows build and publish macOS, Windows x64, and Linux x64 artifacts from the same release tag/version computation. +- `latest.json` manifests continue to point Tauri updater clients at signed updater artifacts through `url`, while manual installer/download links are exposed separately via platform-specific fields such as `dmg_url` and `download_url`. +- The stable download page resolves the best current platform download from that manifest plus release assets, instead of assuming macOS-only DMG delivery. +- Note filename renames, folder creation/rename flows, and custom view filenames all share one portable validation rule set that rejects Windows reserved device names, invalid characters, and trailing dot/space suffixes. +- Shortcut labels shown in the UI are derived from the shared command manifest so non-macOS builds display `Ctrl`-style accelerators instead of macOS glyphs. + +## Options considered + +- **Cross-platform artifacts + portable filename rules** (chosen): makes Windows support real instead of nominal, keeps updater behavior compatible with Tauri, and prevents cross-OS vault breakage at the point of write. Cons: more CI matrix surface area and more platform-specific packaging constraints. +- **Ship Windows installers but keep existing filename validation**: lowers immediate implementation cost, but Windows users would still hit invalid vault content created elsewhere and trust in sync portability would stay weak. +- **Keep macOS-first updater/download metadata and infer other platforms from release assets only**: cheaper in the short term, but it weakens in-app update guarantees and makes the public download page depend on ad hoc asset naming rather than an explicit manifest contract. + +## Consequences + +- Tolaria's release CI now owns packaging and artifact validation on three desktop platforms instead of one. +- The public stable download page can redirect Windows/Linux users to real installers without special-case manual curation. +- Vault content created through Tolaria stays portable across macOS, Linux, and Windows, which reduces sync-time surprises and broken clones. +- Any future platform addition now needs both a release-artifact contract and an explicit portable-filename review instead of piggybacking on macOS assumptions. diff --git a/docs/adr/0081-internal-light-dark-theme-runtime.md b/docs/adr/0081-internal-light-dark-theme-runtime.md new file mode 100644 index 0000000..1162092 --- /dev/null +++ b/docs/adr/0081-internal-light-dark-theme-runtime.md @@ -0,0 +1,43 @@ +--- +type: ADR +id: "0081" +title: "Internal light and dark theme runtime" +status: active +date: 2026-04-24 +supersedes: "0013" +--- + +## Context + +ADR-0013 removed the vault-authored theming system and made Tolaria light-only. That kept the app simpler, but dark mode has become a product requirement for long writing sessions and accessibility. + +The previous theming system should not return in its old form: vault notes, live user-authored themes, and broad runtime editing created too much maintenance burden. Tolaria still needs a small app-owned theme architecture because the UI spans Tailwind/shadcn variables, BlockNote/Mantine surfaces, CodeMirror raw editing, syntax highlighting, and product-specific states such as selected rows, badges, warnings, and diff lines. + +## Decision + +**Tolaria will support internal app-owned light and dark themes through a semantic CSS-variable contract, with the user's theme mode persisted as installation-local app settings.** + +The v1 theme runtime is deliberately smaller than a general theming system: + +- Themes are defined by the app, not by vault-authored notes. +- CSS custom properties remain the public runtime contract for product components, Tailwind v4, and shadcn/ui. +- Typed TypeScript helpers may derive values for consumers that cannot read CSS variables directly, such as CodeMirror extensions. +- Existing CSS variables stay available as compatibility aliases while the UI migrates toward semantic names. +- The first persisted choices are `light` and `dark`; system-follow, high-contrast variants, custom themes, and per-vault themes are deferred. + +## Options considered + +- **Internal light/dark runtime with semantic tokens** (chosen): ships dark mode while keeping the product-owned theme surface small, testable, and compatible with existing CSS-variable usage. +- **Reintroduce vault-authored theme notes**: flexible, but repeats the complexity removed by ADR-0013 and makes dark mode dependent on user-editable data. +- **Ad hoc `.dark` overrides in components**: fastest initially, but would scatter color logic across the app and make future theme variants expensive. +- **Single TypeScript theme object as source of truth**: attractive for validation, but the current app already relies on CSS variables for Tailwind, shadcn/ui, BlockNote CSS overrides, and many product components. + +## Consequences + +- `src/index.css` owns the stable CSS custom-property contract for app chrome and shared states. +- `src/theme.json` continues to describe editor typography, but editor-facing colors should resolve through the same semantic CSS variables used by the app shell. +- `useTheme` remains responsible for editor theme flattening and can grow into the bridge between app theme mode and editor consumers. +- App settings, not vault frontmatter, store the selected theme mode because it is an installation-local comfort preference. +- Startup must avoid a light-mode flash when dark mode is selected, so the runtime needs a pre-React localStorage mirror and a minimal `index.html` prepaint style in addition to persisted Tauri settings. +- Domain tokens should be introduced only when a surface needs a role that generic semantic tokens cannot express clearly. +- Re-evaluate if Tolaria decides to support user-authored custom themes, per-vault themes, or system-synchronized mode as a first-class product requirement. diff --git a/docs/adr/0082-markdown-durable-math-notes.md b/docs/adr/0082-markdown-durable-math-notes.md new file mode 100644 index 0000000..bd556ca --- /dev/null +++ b/docs/adr/0082-markdown-durable-math-notes.md @@ -0,0 +1,40 @@ +--- +type: ADR +id: "0082" +title: "Markdown-durable math in notes" +status: active +date: 2026-04-26 +--- + +## Context + +Tolaria notes are durable Markdown files, while the main editor uses BlockNote and raw mode uses CodeMirror. Users coming from technical note-taking tools expect inline math such as `$E=mc^2$` and display math such as `$$ ... $$` to render inside notes without turning the note into an app-only document format. + +BlockNote does not currently ship a first-party math block in the local editor package. Tiptap now offers an official Mathematics extension that renders KaTeX nodes, but Tolaria's save path still depends on BlockNote's Markdown parser and `blocksToMarkdownLossy()` serializer. Adding opaque ProseMirror math nodes without an explicit Tolaria serializer would risk losing or rewriting the original Markdown source. + +## Decision + +**Tolaria will support note math through a Markdown placeholder round-trip owned by the editor pipeline.** + +The initial implementation: + +- Treats `$...$` as inline math and line-owned `$$...$$` / multiline `$$` blocks as display math. +- Converts math source to temporary placeholders before BlockNote parses Markdown. +- Replaces placeholders with Tolaria schema nodes that render via the existing `katex` dependency. +- Serializes those schema nodes back to the original Markdown delimiters before saving or entering raw mode. +- Uses KaTeX with `throwOnError: false` and `trust: false` so malformed or untrusted formulas remain visible rather than breaking the note. + +## Options considered + +- **Tolaria-owned placeholder round-trip with KaTeX rendering** (chosen): matches the existing wikilink architecture, preserves plain-text source, and avoids depending on BlockNote support for non-default ProseMirror math nodes. +- **Tiptap Mathematics extension directly in BlockNote**: attractive because it is official Tiptap and KaTeX-backed, but it does not by itself solve Tolaria's BlockNote Markdown serializer contract. +- **Raw-mode-only math support**: preserves source but fails the rich editor reading experience users expect. +- **Store formulas as custom JSON/frontmatter metadata**: richer structured editing later, but violates the Markdown-first durability requirement. + +## Consequences + +- `src/utils/mathMarkdown.ts` is the canonical parser/serializer bridge for note math. +- The rich editor renders math as schema nodes; raw mode remains the most direct way to edit exact math source. +- CodeMirror raw editing keeps the literal Markdown delimiters, so imported Obsidian-style notes remain understandable outside Tolaria. +- Future equation editing helpers can be added on top of the same Markdown source contract instead of changing the storage model. +- Re-evaluate direct Tiptap Mathematics integration only if it can be proven to preserve Tolaria's Markdown save path without custom lossy behavior. diff --git a/docs/adr/0083-dual-architecture-macos-release-artifacts.md b/docs/adr/0083-dual-architecture-macos-release-artifacts.md new file mode 100644 index 0000000..3d4a047 --- /dev/null +++ b/docs/adr/0083-dual-architecture-macos-release-artifacts.md @@ -0,0 +1,38 @@ +--- +type: ADR +id: "0083" +title: "Dual-architecture macOS release artifacts" +status: active +date: 2026-04-26 +supersedes: "0080" +--- + +## Context + +ADR-0080 made Tolaria's desktop release pipeline cross-platform, but the macOS leg still shipped only Apple Silicon artifacts. That left Intel Mac users without a compatible build in both the alpha feed and stable releases, even though Tauri and Rust can produce `x86_64-apple-darwin` bundles from the same release workflow. + +The updater manifest also needs to distinguish macOS CPU architectures. A generic `darwin` or macOS-only entry would make it too easy for an Intel installation to see an Apple Silicon updater bundle, and browser user agents cannot reliably tell Apple Silicon Macs apart from Intel Macs. + +## Decision + +**Tolaria publishes macOS release artifacts for both Apple Silicon (`darwin-aarch64`) and Intel (`darwin-x86_64`) in every alpha and stable release.** + +- Alpha and stable workflows build the macOS matrix for `aarch64-apple-darwin` and `x86_64-apple-darwin`. +- Alpha manifests include signed updater tarballs for `darwin-aarch64` and `darwin-x86_64`. +- Stable manifests include both macOS updater tarballs and both manual DMG downloads, alongside the existing Windows x64 and Linux x86_64 entries. +- Release jobs normalize macOS artifact filenames with the architecture suffix before publishing so GitHub release assets stay unambiguous. +- The stable download page exposes separate Apple Silicon and Intel Mac links. When both Mac links exist, a generic macOS browser is not auto-redirected because user-agent architecture detection is unreliable. +- The cross-platform filename portability decisions from ADR-0080 remain in force. + +## Options considered + +- **Publish separate Apple Silicon and Intel Mac artifacts** (chosen): gives each updater client an architecture-specific manifest key and gives users explicit manual download links. Cons: doubles the macOS release matrix and signing/notarization surface. +- **Publish a universal macOS binary**: gives users one download, but requires lipo/re-sign/notarize coordination and reintroduces the artifact-combining complexity the release pipeline intentionally avoided. +- **Keep Apple Silicon-only macOS releases**: keeps CI cheaper, but leaves Intel Mac users unsupported and makes the release artifacts inconsistent with Tolaria's desktop support goals. + +## Consequences + +- macOS release jobs now run one matrix entry per CPU architecture. +- Release manifest consumers must treat `darwin-aarch64` and `darwin-x86_64` as distinct platform keys. +- Stable manual downloads show two Mac choices instead of pretending browser detection can select the right CPU architecture. +- Future macOS release changes must validate both updater and manual-download artifacts for both architectures. diff --git a/docs/adr/0084-app-localization-foundation.md b/docs/adr/0084-app-localization-foundation.md new file mode 100644 index 0000000..e606f4c --- /dev/null +++ b/docs/adr/0084-app-localization-foundation.md @@ -0,0 +1,35 @@ +--- +type: ADR +id: "0084" +title: "App-owned localization foundation" +status: active +date: 2026-04-26 +--- + +## Context + +Tolaria was effectively English-only. Users requested a general i18n foundation and Chinese-language support. We need a path that lets the UI adopt additional locales without pushing UI-language preferences into vault files or making every partially translated string a runtime failure. + +## Decision + +Tolaria owns a dependency-free frontend localization layer in `src/lib/i18n.ts`. + +- English is the canonical fallback locale. +- Simplified Chinese (`zh-Hans`) is the first additional locale. +- `ui_language` is an installation-local app setting in `~/.config/com.tolaria.app/settings.json`; `null` means "follow system language when supported, otherwise English". +- Missing translation keys fall back to English. +- App-level chrome receives locale through props from `App.tsx`, following the existing props-down/callbacks-up architecture instead of introducing global React context. +- Language switching is exposed in Settings and through command-palette actions. + +## Alternatives considered + +- **Add an i18n dependency now**: useful long term, but unnecessary for the first locale and would add framework surface before we know Tolaria's locale workflow. +- **Store language in the vault**: rejected because UI language is an installation preference, not content structure. +- **Translate ad hoc strings inline**: rejected because it would make fallback behavior inconsistent and future locales expensive. + +## Consequences + +- New UI strings should be added to `src/lib/i18n.ts` first and rendered through `translate()` / `createTranslator()` where the surface already receives locale. +- Partially translated locales remain usable because English is the fallback for missing keys. +- Locale choice changes UI chrome immediately after settings save or command-palette language commands without reopening the vault. +- Larger feature surfaces can migrate to the shared localization module incrementally. diff --git a/docs/adr/0085-non-git-vault-support.md b/docs/adr/0085-non-git-vault-support.md new file mode 100644 index 0000000..ce96fc4 --- /dev/null +++ b/docs/adr/0085-non-git-vault-support.md @@ -0,0 +1,39 @@ +--- +type: ADR +id: "0085" +title: "Non-git vaults open with explicit later Git initialization" +status: active +date: 2026-04-26 +supersedes: "0034" +--- + +## Context + +ADR-0034 made Git a hard prerequisite for opening a vault because Git-backed cache, history, change, and sync flows failed invisibly when users opened plain Markdown folders. That protected Git features, but it blocked the common adoption path of opening an existing folder from Obsidian, iCloud, Dropbox, or a manually maintained notes directory. + +Tolaria now needs the opposite default: browsing and editing Markdown should work immediately, while Git remains an explicit capability users can enable when they want history, sync, commits, or collaboration. + +## Decision + +**Open existing Markdown folders even when they are not Git repositories.** A non-git vault is a supported state, not an error state. On open, Tolaria asks whether to initialize Git; if the user dismisses the prompt, the app keeps working and the status bar permanently shows a `Git disabled` warning. Clicking that warning, or running `Initialize Git for Current Vault` from the command palette, reopens the setup action. + +While a vault is not Git-backed: + +- Git history, change, commit, sync, conflict, and remote actions are hidden or disabled. +- Background auto-sync and AutoGit checkpoints do not run. +- Markdown scanning, note browsing, note editing, search, and non-Git vault features continue normally. + +`init_git_repo` remains the single backend command for enabling Git later. It creates the repository, writes Tolaria's default `.gitignore`, stages the vault, and creates the unsigned setup commit. + +## Options considered + +- **Option A (chosen): Supported non-git mode with explicit later initialization.** Best adoption path; keeps Git capabilities visible without blocking the basic notes workflow. +- **Option B: Keep the ADR-0034 blocking modal.** Prevents Git feature ambiguity, but rejects valid plain-folder workflows. +- **Option C: Auto-initialize Git when opening a plain folder.** Low friction, but surprising for users who do not want Tolaria to mutate folder metadata. + +## Consequences + +- Existing Git-backed vaults keep the same history, commit, sync, and remote behavior. +- UI surfaces must treat Git capability as stateful per vault, not as an app-wide invariant. +- Tests need to cover both Git-backed and non-git vaults in browser mocks and native QA. +- Future Git-dependent features must check the current vault's Git state before registering commands or running background work. diff --git a/docs/adr/0086-in-app-image-file-preview.md b/docs/adr/0086-in-app-image-file-preview.md new file mode 100644 index 0000000..672032c --- /dev/null +++ b/docs/adr/0086-in-app-image-file-preview.md @@ -0,0 +1,32 @@ +--- +type: ADR +id: "0086" +title: "In-app image previews for binary vault files" +status: active +date: 2026-04-26 +supersedes: "0041" +--- + +## Context + +ADR-0041 made the vault scanner index all visible files and introduced `fileKind` as `"markdown"`, `"text"`, or `"binary"`. Binary files were deliberately shown as inert entries until Tolaria had a dedicated preview model. + +That made image references visible in folder views, but opening an image still felt outside the normal Tolaria workflow. Users need to inspect screenshots, diagrams, and other image assets while keeping their place in the vault. + +## Decision + +**Tolaria previews supported image files in the editor pane while keeping them as ordinary binary `VaultEntry` files.** + +- The scanner keeps the existing `fileKind: "binary"` representation. Image previewability is inferred in the renderer from the file extension, not by introducing a proprietary image document type. +- Opening a binary entry creates the same single active-tab state used for notes, but with empty content and no `get_note_content` text read. +- `FilePreview` renders supported image extensions through Tauri's asset protocol (`convertFileSrc`) so the original file remains on disk. +- Broken images and unsupported binary files render an explicit fallback state with an intentional "Open in default app" action instead of launching another app automatically. +- Note-list rows use an image indicator for previewable image binaries. Unsupported binary rows remain muted and non-clickable in the normal list surface. +- The preview surface is keyboard focusable and `Escape` returns focus to the note list, matching the app's keyboard-first navigation model. + +## Consequences + +- The existing `VaultEntry` model and cache version do not need to change. +- Supported image files can participate in normal selection/navigation context without being converted into Markdown notes. +- Unsupported/broken binary files have a clear in-app state when reached through navigation paths that can select them. +- Any future PDF, audio, or video preview should extend the same file-preview renderer rather than adding new vault-owned document representations. diff --git a/docs/adr/0087-json-catalogs-and-lara-cli-localization.md b/docs/adr/0087-json-catalogs-and-lara-cli-localization.md new file mode 100644 index 0000000..44b73d2 --- /dev/null +++ b/docs/adr/0087-json-catalogs-and-lara-cli-localization.md @@ -0,0 +1,39 @@ +--- +type: ADR +id: "0087" +title: "JSON locale catalogs with Lara CLI synchronization" +status: active +date: 2026-04-27 +supersedes: + - "0084" +--- + +## Context + +ADR-0084 established an app-owned localization layer in `src/lib/i18n.ts` with English fallback and hand-maintained TypeScript dictionaries. That was enough for the first localized UI surface, but it does not scale well to a broader locale matrix or machine-assisted translation workflows. + +We now want Tolaria to support a wider set of locales and to automate translation updates with Lara CLI while keeping the runtime dependency-light and preserving the existing English fallback behavior. + +## Decision + +Tolaria will keep its app-owned runtime localization layer, but the translation source-of-truth moves to flat JSON catalogs in `src/lib/locales/`. + +- `src/lib/locales/en.json` is the canonical source catalog. +- Additional locale files use one JSON file per locale code (for example `zh-CN.json`, `fr-FR.json`). +- `src/lib/i18n.ts` keeps fallback, interpolation, locale resolution, and props-down locale wiring, but it now loads locale catalogs from JSON files instead of TypeScript objects. +- Lara CLI configuration lives in `lara.yaml`, and translation runs happen through repo scripts (`pnpm l10n:translate`, `pnpm l10n:translate:force`). +- `scripts/validate-locales.mjs` verifies that every locale catalog present in the repo matches the English keyset and only contains flat string values. +- Legacy stored preferences such as `zh-Hans` are normalized to the canonical `zh-CN` locale. + +## Alternatives considered + +- **Keep TypeScript dictionaries and point Lara at `.ts` files**: possible, but JSON is the more standard interchange format for translation tooling and keeps diffs simpler for translators and reviewers. +- **Adopt a full frontend i18n framework now**: rejected because Tolaria already has working locale propagation and fallback behavior, and the immediate need is better content management plus translation automation. +- **Store translated strings outside the app repo**: rejected because Tolaria's chrome localization should stay versioned with the app code that consumes it. + +## Consequences + +- Translators and automation tools now work against plain JSON catalogs instead of editing source code. +- The runtime keeps English fallback behavior, so a missing locale file or missing key does not break app chrome. +- Locale additions become a data/config change first: add the locale metadata, run Lara, review JSON output, then ship. +- Localization work now has a dedicated validation step that can run in CI or before commit. diff --git a/docs/adr/0088-markdown-durable-mermaid-diagrams.md b/docs/adr/0088-markdown-durable-mermaid-diagrams.md new file mode 100644 index 0000000..6aaeeb6 --- /dev/null +++ b/docs/adr/0088-markdown-durable-mermaid-diagrams.md @@ -0,0 +1,40 @@ +--- +type: ADR +id: "0088" +title: "Markdown-durable Mermaid diagrams in notes" +status: active +date: 2026-04-27 +--- + +## Context + +Tolaria notes are plain Markdown files, while the rich editor uses BlockNote and raw mode uses CodeMirror. Users need fenced `mermaid` blocks to render as diagrams in the note surface without changing the canonical file format or hiding the source from raw editing. + +BlockNote can parse fenced code blocks, but a generic highlighted code block does not provide diagram rendering. Rendering Mermaid directly from the Markdown fence also has to preserve the original fence source when notes are saved, copied through raw mode, closed, and reopened. + +## Decision + +**Tolaria will support Mermaid diagrams through a Markdown placeholder round-trip owned by the editor pipeline and rendered with the `mermaid` package.** + +The implementation: + +- Converts fenced `mermaid` blocks to temporary placeholders before BlockNote parses Markdown. +- Replaces placeholders with a `mermaidBlock` schema block that stores both the original fenced source and the diagram body. +- Renders the block through Mermaid in the rich editor. +- Serializes `mermaidBlock` nodes back to their stored fenced Markdown before save, raw-mode entry, and editor-position snapshots. +- Shows the original source as an inline fallback when Mermaid cannot render a diagram. + +## Options considered + +- **Tolaria-owned placeholder round-trip with Mermaid rendering** (chosen): matches the existing wikilink and math architecture, keeps Markdown as the source of truth, and gives Tolaria explicit control over serialization. +- **Render all `mermaid` code blocks by overriding the generic code-block renderer**: smaller surface, but it couples diagram behavior to the code-highlighting package and makes exact source preservation harder. +- **Raw-mode-only Mermaid support**: preserves source but fails the enhanced note reading experience users expect. +- **Store parsed diagram metadata outside the Markdown body**: enables richer future editing, but violates the files-first model. + +## Consequences + +- `src/utils/mermaidMarkdown.ts` is the canonical parser/serializer bridge for note diagrams. +- Rich mode renders diagrams as schema-backed blocks; raw mode remains the direct source editor. +- Invalid Mermaid source remains visible instead of breaking the editor surface. +- `mermaid` is now a runtime dependency and should be upgraded deliberately with rendering regression coverage. +- Future diagram controls, such as copy source or expand, can attach to the same `mermaidBlock` without changing storage. diff --git a/docs/adr/0089-active-vault-filesystem-watcher.md b/docs/adr/0089-active-vault-filesystem-watcher.md new file mode 100644 index 0000000..e8f607a --- /dev/null +++ b/docs/adr/0089-active-vault-filesystem-watcher.md @@ -0,0 +1,36 @@ +--- +type: ADR +id: "0089" +title: "Active vault filesystem watcher" +status: active +date: 2026-04-27 +--- + +## Context + +Tolaria treats the filesystem as the source of truth, but before this decision the running app only noticed external file changes after a manual Reload Vault, a Git pull, or an AI-agent-specific refresh callback. Edits from another editor, terminal commands, another Tolaria window, or a non-pull Git operation could leave React state and the editor surface stale. + +ADR-0071 already defines the safe reconciliation policy for external vault mutations: reload vault-derived state, protect unsaved local edits, and reopen the clean active note from disk when needed. Filesystem watching needed to reuse that policy instead of adding another ad hoc reload path. + +## Decision + +**Tolaria watches the active desktop vault with a native filesystem watcher and routes external change batches through the shared external-refresh reconciler.** + +The desktop backend exposes `start_vault_watcher` and `stop_vault_watcher` commands backed by Rust `notify`. It watches the active vault recursively, ignores known non-content churn such as `.git/`, `node_modules/`, temp files, and `.tolaria-rename-txn`, then emits `vault-changed` events with the active vault path and changed paths. + +The renderer owns batching and reconciliation. `useVaultWatcher` starts the backend watcher for the active main-window vault, debounces native events into one refresh, filters out recent app-owned saves, and calls `refreshPulledVaultState()`. Manual Reload Vault still uses `reload_vault` directly, but now exposes visible reload feedback. + +## Options considered + +- **Native active-vault watcher plus shared reconciliation** (chosen): keeps external changes visible without polling and preserves ADR-0071 behavior for clean and unsaved tabs. Cons: adds one native dependency and a long-lived watcher state. +- **Frontend-only polling**: simpler backend surface, but wastes work on idle vaults and still needs careful active-note reconciliation. +- **Direct `reloadVault()` on every native event**: easy to implement, but bypasses clean-tab reopen handling and can clobber the user experience around unsaved edits. +- **Watch every configured vault**: could pre-warm state, but burns resources for inactive vaults and complicates event ownership across windows. + +## Consequences + +- External writes converge automatically into the visible vault state after a short debounce. +- Active clean notes are refreshed through the same path as pull and AI-agent updates; unsaved local edits remain protected. +- Tolaria app-owned saves are suppressed briefly so autosave does not trigger a full external refresh loop. +- The status bar can show reload progress for manual and automatic refreshes. +- The watcher is a desktop-only integration; mobile builds keep no-op command stubs until a mobile-specific filesystem strategy exists. diff --git a/docs/adr/0090-pi-cli-agent-adapter.md b/docs/adr/0090-pi-cli-agent-adapter.md new file mode 100644 index 0000000..f98151a --- /dev/null +++ b/docs/adr/0090-pi-cli-agent-adapter.md @@ -0,0 +1,35 @@ +--- +type: ADR +id: "0090" +title: "Pi CLI agent adapter" +status: active +date: 2026-04-28 +--- + +## Context + +Tolaria already supports Claude Code, Codex, and OpenCode as local CLI agents in the AI panel. The next provider request is Pi Coding Agent support with the same first-class availability, settings, status, streaming, and MCP vault access. + +Pi exposes non-interactive JSON events through `pi --mode json`, but Pi core intentionally does not include built-in MCP. Its supported MCP path is the `pi-mcp-adapter` extension, which reads MCP server definitions from Pi-compatible config files. + +## Decision + +Tolaria adds Pi as a supported CLI agent id (`pi`) and launches app-managed Pi sessions through a dedicated adapter module. + +The adapter runs `pi --mode json --no-session` from the active vault cwd, closes stdin, and points `PI_CODING_AGENT_DIR` at a temporary directory containing Tolaria's `mcp.json`. That config loads the Tolaria MCP server through `pi-mcp-adapter`, pins `VAULT_PATH` to the selected vault, sets `WS_UI_PORT=9711`, uses lazy server lifecycle, and exposes the small Tolaria tool set directly. + +Pi availability follows the existing desktop pattern: check the inherited `PATH`, the user's login shell, and common local/toolchain install locations. Pi authentication remains owned by the Pi CLI; Tolaria only surfaces setup errors and does not store provider API keys. + +## Options Considered + +- **Transient Pi adapter config** (chosen): gives app-launched Pi sessions Tolaria MCP access without mutating user or vault config files. Cons: requires the Pi MCP adapter extension path to be available to Pi. +- **Write `.mcp.json` into the active vault**: simple for Pi discovery, but creates project files as a side effect of a chat session and can dirty user vaults. +- **Rely on global `~/.pi/agent/mcp.json`**: matches Pi documentation, but silently retargets or depends on user-global state and conflicts with Tolaria's explicit integration boundary. +- **Skip MCP for Pi**: easier to implement, but creates a weaker agent than the existing providers and violates the requirement for first-class MCP support. + +## Consequences + +- The AI panel, settings, command palette, status bar, onboarding, and stream normalization now treat Pi as a first-class supported agent. +- App-launched Pi sessions can use Tolaria vault MCP tools while remaining scoped to the selected vault. +- Pi support stays isolated in Pi-specific modules instead of expanding the shared `ai_agents.rs` hotspot. +- Users still need Pi itself, a configured Pi model/provider, and the Pi MCP adapter extension path available to the Pi CLI. diff --git a/docs/adr/0091-gemini-cli-external-ai-setup.md b/docs/adr/0091-gemini-cli-external-ai-setup.md new file mode 100644 index 0000000..db3c56e --- /dev/null +++ b/docs/adr/0091-gemini-cli-external-ai-setup.md @@ -0,0 +1,35 @@ +--- +type: ADR +id: "0091" +title: "Gemini CLI external AI setup" +status: active +date: 2026-04-28 +--- + +## Context + +Tolaria already supports explicit MCP setup for external desktop AI tools. Users asked for Gemini CLI support so the same active-vault MCP server can be registered where Gemini reads tool configuration, with optional Gemini-specific vault guidance. + +Gemini CLI reads MCP server definitions from `~/.gemini/settings.json` and can load project guidance from `GEMINI.md`. Tolaria must support those conventions without silently rewriting global user settings or overwriting user-authored vault instructions. + +## Decision + +Tolaria adds `~/.gemini/settings.json` to the explicit external AI setup path list. The existing MCP entry shape is reused: `mcpServers.tolaria` runs the packaged stdio server through Node.js, pins `VAULT_PATH` to the selected vault, and sets `WS_UI_PORT=9711`. + +Vault guidance keeps `AGENTS.md` as the canonical shared source. `restore_vault_ai_guidance` can create or repair a managed `GEMINI.md` shim that imports `AGENTS.md`, while bootstrap and repair flows continue to seed only required Tolaria guidance (`AGENTS.md` and `CLAUDE.md`) plus type scaffolding. Custom `GEMINI.md` files are classified as custom and are not overwritten. + +The setup dialog documents that Gemini CLI still needs its own install and sign-in. Tolaria does not store Gemini credentials or model-provider API keys. + +## Options Considered + +- **Add Gemini to explicit MCP setup and optional guidance restore** (chosen): matches the existing consent boundary, preserves user settings, and gives Gemini the shared vault context. +- **Generate `GEMINI.md` automatically for every vault**: simpler discovery, but turns an optional third-party compatibility file into startup side effect and dirties vaults for users who do not use Gemini. +- **Document manual Gemini setup only**: avoids code changes, but leaves users to transpose paths and loses the active-vault safety already available for other MCP clients. +- **Create a Gemini-specific MCP entry shape**: unnecessary because Gemini accepts the same `mcpServers` entry structure used by the existing Tolaria MCP server. + +## Consequences + +- External AI setup now writes/removes Tolaria's MCP entry in Claude, Gemini, Cursor, and generic MCP config files. +- Gemini users can create a managed `GEMINI.md` shim without duplicating the canonical `AGENTS.md` guidance. +- Existing vault bootstrap and repair remain non-invasive for users who do not use Gemini. +- Native QA requires a local Gemini CLI install and authentication for an end-to-end Gemini prompt; otherwise the app can still verify the generated config and documentation paths. diff --git a/docs/adr/0092-vault-ai-agent-permission-modes.md b/docs/adr/0092-vault-ai-agent-permission-modes.md new file mode 100644 index 0000000..aba5464 --- /dev/null +++ b/docs/adr/0092-vault-ai-agent-permission-modes.md @@ -0,0 +1,39 @@ +--- +type: ADR +id: "0092" +title: "Vault-scoped AI agent permission modes" +status: active +date: 2026-04-28 +--- + +## Context + +ADR-0074 established explicit setup and least-privilege defaults for desktop AI tools. The in-app AI panel now supports multiple local CLI agents, and users need a clear per-vault way to choose whether an agent should stay in the narrow vault-safe profile or use broader local-work tools for that vault. + +The mode must be visible at the point of use, must not mutate global CLI settings, and must not silently restore dangerous bypass flags. Existing transcripts should remain intact when the mode changes because a change applies to the next agent run, not to a process that is already streaming. + +## Decision + +**Tolaria stores an `ai_agent_permission_mode` per vault with values `safe` and `power_user`, defaulting missing or null values to `safe`, and passes that normalized mode through the AI panel stream request into each CLI adapter.** + +The AI panel header displays the current mode and offers a compact Vault Safe / Power User control that is disabled while an agent run is active. Changing the mode preserves the transcript and inserts a local transcript marker. + +Adapter mappings remain conservative: +- Claude Code Safe keeps `acceptEdits`, strict Tolaria MCP config, and file/search/edit tools only; Power User adds Bash to the allowed tool list without using `--dangerously-skip-permissions`. +- Codex keeps the active-vault `workspace-write` sandbox and `--ask-for-approval never` in both modes. +- OpenCode uses transient `OPENCODE_CONFIG_CONTENT`; Safe denies bash and external directories, while Power User allows bash but still denies external directories. +- Pi receives the mode on the adapter request path; both modes currently use the same transient MCP adapter config. + +## Options Considered + +- **Per-vault Safe / Power User modes** (chosen): makes the permission surface explicit where the agent is used and preserves least-privilege defaults for each vault. +- **Global app setting**: simpler storage, but a single toggle can over-apply a power-user profile to unrelated vaults. +- **Dangerous bypass mode**: maximizes CLI freedom, but violates ADR-0074's least-privilege boundary and needs a separate explicit security decision. +- **Adapter-specific UI switches**: exposes too much implementation detail and makes cross-agent behavior harder to reason about. + +## Consequences + +- Vault config normalization owns the safe default for old vaults and malformed values. +- Agent requests now carry a permission mode through frontend and Rust boundaries, so new adapters must choose an explicit mapping. +- Power User is intentionally not equivalent across agents; where an adapter lacks a safe broader local-work switch, both modes may map to the same conservative behavior and must document that with tests. +- Any future dangerous mode requires a new ADR and separate UI language. diff --git a/docs/adr/0093-shared-cli-agent-runtime-adapters.md b/docs/adr/0093-shared-cli-agent-runtime-adapters.md new file mode 100644 index 0000000..274625f --- /dev/null +++ b/docs/adr/0093-shared-cli-agent-runtime-adapters.md @@ -0,0 +1,35 @@ +--- +type: ADR +id: "0093" +title: "Shared CLI agent runtime adapters" +status: active +date: 2026-04-29 +--- + +## Context + +Tolaria supports Claude Code, Codex, OpenCode, and Pi as local CLI agents in the AI panel. Each agent has different command-line arguments, configuration shape, and JSON event schema, but the Rust backend had grown repeated runtime plumbing around those differences: request shapes, prompt wrapping, subprocess launch, stdout JSON reading, stderr capture, exit handling, done events, version probing, and Tolaria MCP server path resolution. + +That duplication made small runtime fixes expensive because they had to be repeated across several adapter files. It also kept Codex-specific command and event mapping inside `ai_agents.rs`, making the top-level module both an orchestrator and a bespoke adapter. + +## Decision + +**Tolaria uses `cli_agent_runtime.rs` as the shared runtime scaffold for app-managed CLI agents, while `ai_agents.rs` only normalizes and dispatches requests to per-agent adapter modules.** + +The shared scaffold owns the common agent request shape, system/user prompt wrapping, JSON-line process lifecycle, normalized error/done handling for `AiAgentStreamEvent` adapters, version probing, and Tolaria MCP server path resolution. Per-agent modules keep the provider-specific pieces: binary discovery candidates, command arguments, transient config shape, authentication error wording, and JSON event mapping. + +Codex now lives in `codex_cli.rs`, matching the Claude, OpenCode, and Pi adapter boundary. `ai_agents.rs` remains the Tauri-facing orchestrator that chooses an adapter and maps Claude's legacy event enum into the normalized event stream. + +## Options Considered + +- **Shared runtime scaffold with thin adapters** (chosen): reduces repeated process lifecycle code without hiding provider-specific command/config/event behavior. +- **One trait object per agent**: more uniform on paper, but adds indirection without removing much current complexity. +- **Leave each adapter self-contained**: keeps local readability for a single file, but new process, prompt, and MCP fixes continue to land in parallel. +- **Fully generic event mapping**: over-abstracts the JSON schemas and makes provider-specific edge cases harder to test. + +## Consequences + +- Runtime lifecycle fixes should usually start in `cli_agent_runtime.rs`. +- New agent adapters should use the shared request/prompt/process helpers and keep only command, config, discovery, and event mapping local. +- `ai_agents.rs` should not grow provider-specific runtime code again; it should normalize the frontend request, dispatch to an adapter, and map any legacy event shape. +- The shared scaffold deliberately does not erase provider differences. Authentication messages, permission semantics, and transient config formats remain adapter-owned and must stay covered by adapter tests. diff --git a/docs/adr/0094-gitignored-content-visibility-boundary-filter.md b/docs/adr/0094-gitignored-content-visibility-boundary-filter.md new file mode 100644 index 0000000..af06e5e --- /dev/null +++ b/docs/adr/0094-gitignored-content-visibility-boundary-filter.md @@ -0,0 +1,37 @@ +--- +type: ADR +id: "0094" +title: "Gitignored content visibility as a command-boundary filter" +status: active +date: 2026-04-29 +--- + +## Context + +Tolaria's vault scanner now indexes more of the real filesystem so Folder views, search, and reload flows can reflect what is actually in the vault. In Git-backed vaults, that includes generated, local-only, or machine-specific content that users intentionally hide through `.gitignore`. + +Always showing Gitignored files makes Folder lists and search noisy, especially in vaults that contain exports, build artifacts, or personal local scratch files. But removing those files during scanning would make visibility dependent on cache shape, complicate toggling, and blur the distinction between "what exists in the vault" and "what this installation chooses to surface." + +## Decision + +**Tolaria keeps the vault scan and cache complete, then applies Gitignored-content visibility at the command boundary before entries, folders, or search results reach React.** + +- `hide_gitignored_files` is an installation-local app setting and defaults to `true`. +- Visibility checks use batched `git check-ignore --no-index --stdin` so Tolaria follows normal Git ignore and negation semantics as closely as practical. +- `list_vault`, `reload_vault`, `list_vault_folders`, and keyword search all apply the same filter when the setting is enabled. +- Toggling the setting reloads the current vault surfaces instead of rebuilding a different cache format. +- If a vault has no `.gitignore`, or Gitignored visibility is turned off, Tolaria shows the full scanned result. + +## Options considered + +- **Complete scan/cache + boundary filter** (chosen): keeps the filesystem model authoritative, makes toggling cheap and consistent, and avoids cache divergence. +- **Skip Gitignored content during scan/cache**: reduces later filtering work, but makes visibility part of the persisted cache shape and complicates instant toggling. +- **Always show Gitignored content**: simplest implementation, but too noisy for real Git-backed vaults and undermines users' existing ignore rules. + +## Consequences + +- Gitignored visibility is a per-installation comfort preference, not vault-authored shared metadata. +- Search, folder lists, and note reloads stay aligned because they all consult the same boundary filter. +- The cache can still support future visibility changes without a data migration. +- Users can reveal ignored content again immediately by disabling the setting. +- Future features that expose vault file lists should apply the same boundary filter unless they intentionally need raw filesystem output. diff --git a/docs/adr/0095-saved-view-order-field.md b/docs/adr/0095-saved-view-order-field.md new file mode 100644 index 0000000..8b69c41 --- /dev/null +++ b/docs/adr/0095-saved-view-order-field.md @@ -0,0 +1,35 @@ +--- +type: ADR +id: "0095" +title: "Saved views use an explicit YAML order field" +status: active +date: 2026-04-29 +--- + +## Context + +Saved Views already persist as user-editable YAML files in the vault and sync through Git. Filename ordering was stable, but it forced users to rename files just to change sidebar order and gave Tolaria no durable way to support drag reordering, move actions, or keyboard-first ordering controls. + +The ordering choice also needs to travel with the view definition itself. Saved Views are part of the vault's shared information architecture, not a machine-local preference. + +## Decision + +**Each Saved View may store an optional top-level `order` number in its YAML definition, and Tolaria sorts views by that value before falling back to filename.** + +- Lower `order` values render earlier in the sidebar and other Saved View lists. +- Views without `order` sort after ordered views and then fall back to filename ordering for stability. +- Reordering actions rewrite affected view files with a dense sequence of order values instead of encoding position in filenames. +- The same persisted order supports drag handles, explicit move buttons, and command-palette ordering actions. + +## Options considered + +- **Explicit `order` field in the view YAML** (chosen): portable, Git-syncable, easy to inspect by hand, and consistent with the existing file-first view model. +- **Filename-based ordering only**: no schema change, but makes reordering clumsy and couples user-visible structure to file naming. +- **App-local ordering state**: easy to prototype, but breaks cross-device consistency and separates ordering from the view artifact users already version. + +## Consequences + +- Saved View ordering becomes part of the vault and syncs naturally through Git. +- Existing views remain valid; unordered files keep a stable fallback sort until reordered. +- Reordering can touch multiple view files in one action because Tolaria normalizes the sequence. +- Future Saved View features should treat `order` as part of the shared YAML schema rather than introducing a parallel ordering store. diff --git a/docs/adr/0096-root-created-type-documents.md b/docs/adr/0096-root-created-type-documents.md new file mode 100644 index 0000000..49d43ac --- /dev/null +++ b/docs/adr/0096-root-created-type-documents.md @@ -0,0 +1,37 @@ +--- +type: ADR +id: "0096" +title: "Root-created type documents" +status: active +date: 2026-04-29 +--- + +## Context + +Tolaria identifies type definitions by markdown frontmatter (`type: Type`), not by filesystem location. Older documentation and UI creation flows still treated `type/` as the canonical destination for new type documents, with a compatibility fallback for vaults that already used `types/`. + +That folder-based creation policy conflicted with the broader vault model: notes are scanned from all non-hidden folders, type identity comes from metadata, and root `type.md` / `note.md` definitions are already used by repair and bootstrap flows. + +## Decision + +**Tolaria creates new type documents at the vault root.** + +- A type document is any markdown note with `type: Type` in frontmatter. +- New UI-created type documents use `{vault}/{slug}.md`. +- Existing type documents in `type/`, `types/`, or other scanned folders remain valid and continue to drive templates, icons, colors, visibility, sorting, and sidebar grouping. +- Creation does not silently migrate or move existing type documents. +- Root filename collisions are handled as file collisions; Tolaria must not overwrite an existing note when creating a type document. + +## Options considered + +- **Root-created type documents** (chosen): matches the metadata-first model, removes special casing from creation, and aligns new types with root-managed default type scaffolding. +- **Canonical `type/` folder**: avoids root filename collisions, but makes path special even though type identity is already defined by frontmatter. +- **Preserve existing folder convention dynamically**: minimizes change for plural `types/` vaults, but creates inconsistent behavior across vaults and leaves `types/` only partially supported. + +## Consequences + +- Users can inspect and edit type documents as ordinary root notes by default. +- Existing vaults with `type/` or `types/` type documents remain readable because vault scanning already includes non-hidden subdirectories. +- If a root note with the same slug already exists, type creation fails with a collision message instead of writing into a fallback folder. +- Legacy `type/` may remain hidden from the folder tree so old type documents do not duplicate the Types sidebar section. +- Re-evaluate if users need a guided migration from folder-based type documents to root type documents. diff --git a/docs/adr/0097-gemini-cli-agent-adapter.md b/docs/adr/0097-gemini-cli-agent-adapter.md new file mode 100644 index 0000000..ff7e1b9 --- /dev/null +++ b/docs/adr/0097-gemini-cli-agent-adapter.md @@ -0,0 +1,42 @@ +--- +type: ADR +id: "0097" +title: "Gemini CLI agent adapter" +status: active +date: 2026-04-29 +--- + +## Context + +ADR 0091 added Gemini CLI to explicit external MCP setup, but Gemini was still absent from Tolaria's selectable app-managed AI agents. That left the AI panel able to generate Gemini-compatible MCP configuration while the actual agent picker, availability checks, install links, and streaming dispatch did not treat Gemini like Claude Code, Codex, OpenCode, or Pi. + +Gemini CLI supports headless `--prompt` execution with JSON output, configurable approval modes, tool exclusion, and settings-file overrides through `GEMINI_CLI_SYSTEM_SETTINGS_PATH`. Those features are enough to launch Gemini from Tolaria without mutating the user's durable `~/.gemini/settings.json` during app-managed sessions. + +## Decision + +Tolaria adds Gemini CLI as a first-class `AiAgentId`. The frontend agent definitions, onboarding prompt, install links, default-agent normalization, status badge, command registry, settings persistence, and mock Tauri status payloads include `gemini`. + +The desktop backend adds a Gemini adapter that: + +- discovers `gemini` through the process path, login shell, and common local/toolchain install locations +- runs `gemini --output-format json --approval-mode --prompt ` from the active vault +- supplies Tolaria MCP through a temporary settings file referenced by `GEMINI_CLI_SYSTEM_SETTINGS_PATH` +- uses Safe mode with `auto_edit`, an untrusted MCP entry, and `tools.exclude=["run_shell_command"]` +- uses Power User mode with `yolo` and a trusted Tolaria MCP entry +- maps Gemini JSON responses into Tolaria's existing AI panel stream events + +The existing external MCP setup remains explicit and durable. The app-managed Gemini adapter uses transient settings so selecting Gemini in Tolaria does not rewrite the user's global Gemini config. + +## Options Considered + +- **Add Gemini as a first-class app-managed agent** (chosen): matches the existing agent picker and onboarding UI, uses Gemini's headless JSON mode, and keeps MCP setup vault-scoped. +- **Keep Gemini as external MCP setup only**: avoids another adapter, but keeps the interface inconsistent and requires users to leave Tolaria for a flow that other agents support in-panel. +- **Write app-managed Gemini config into `~/.gemini/settings.json`**: reuses the external setup path, but would blur the consent boundary and risk overwriting user preferences during normal AI panel usage. +- **Use interactive Gemini sessions**: could preserve richer CLI state, but does not fit Tolaria's current one-request stream lifecycle and would make cleanup/auth/error handling harder. + +## Consequences + +- Gemini appears anywhere users can choose, install, or switch local AI agents. +- End-to-end native Gemini QA requires the Gemini CLI to be installed and authenticated, but missing/auth failures now produce agent-specific guidance. +- Safe and Power User behavior is limited by Gemini's own approval/tool semantics; if Gemini changes those names, the adapter tests and docs need updating. +- The durable MCP setup path and optional `GEMINI.md` shim continue to serve external Gemini usage outside Tolaria's AI panel. diff --git a/docs/adr/0098-in-app-image-and-pdf-file-previews.md b/docs/adr/0098-in-app-image-and-pdf-file-previews.md new file mode 100644 index 0000000..d6ee947 --- /dev/null +++ b/docs/adr/0098-in-app-image-and-pdf-file-previews.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0098" +title: "In-app image and PDF previews for binary vault files" +status: superseded +date: 2026-04-29 +supersedes: "0086" +superseded_by: "0110" +--- + +## Context + +ADR-0086 introduced the `FilePreview` path for image binaries while keeping binary files as ordinary `VaultEntry` records. The same file-first model should now cover PDFs, because asset-heavy vaults often mix screenshots, diagrams, and document exports that users need to inspect without leaving Tolaria. + +## Decision + +**Tolaria previews supported image and PDF files in the editor pane while keeping them as ordinary binary vault files.** + +- The scanner keeps the coarse `fileKind: "binary"` representation. Previewability stays a renderer concern inferred from the file extension in `src/utils/filePreview.ts`. +- Supported images render with `` and supported PDFs render with the webview PDF object renderer, both using Tauri asset URLs from `convertFileSrc`. +- The Tauri CSP permits scoped asset URLs in `object-src` so PDF objects can load vault-backed files without broadening script, connect, or image policy. +- PDF preview fallback content lives inside the PDF object so unsupported or failed renderers still expose an explicit "Open in default app" escape hatch. +- Note-list rows for previewable images and PDFs remain clickable and carry file-specific indicators; unsupported binary rows stay muted and non-clickable. +- `Escape` on the preview surface returns keyboard focus to the note list, matching the existing image-preview keyboard behavior. + +## Consequences + +- PDFs do not become notes and do not get Markdown editor semantics. +- The asset preview surface can keep growing to additional safe binary formats without changing the vault scanner or persisted cache shape. +- Broken PDFs may rely on the webview's own renderer failure state, but the surrounding Tolaria preview chrome still provides reveal, copy path, and default-app actions. diff --git a/docs/adr/0099-cumulative-vault-asset-scope.md b/docs/adr/0099-cumulative-vault-asset-scope.md new file mode 100644 index 0000000..bbd96fb --- /dev/null +++ b/docs/adr/0099-cumulative-vault-asset-scope.md @@ -0,0 +1,37 @@ +--- +type: ADR +id: "0099" +title: "Cumulative vault asset scope for previews" +status: active +date: 2026-04-29 +supersedes: "0074 asset-protocol runtime scoping" +--- + +## Context + +ADR-0074 moved the desktop asset protocol away from broad filesystem access and toward runtime vault scoping. The implementation tried to keep only the active vault in scope by calling Tauri's `forbid_directory` for vault roots that were no longer active. + +Tauri's filesystem scope treats forbidden paths as permanent precedence rules: a forbidden path is denied even if it is later allowed again. After a user switched away from a vault and back, image and PDF previews could keep producing `403 Forbidden` responses for valid vault files until the app restarted. + +## Decision + +**Tolaria accumulates Tauri asset protocol access for vault roots loaded during the current app session and never forbids a previously loaded vault root at runtime.** + +- `sync_vault_asset_scope` adds the canonical vault root and requested vault root when they are missing from the runtime asset scope. +- The runtime asset scope remains narrower than global filesystem access because only vault roots that Tolaria has loaded are added. +- Command paths still enforce the active vault boundary through the Rust command layer before reads, writes, external opens, and attachment imports. +- Asset scope revocation is deferred to process exit, because Tauri does not expose a safe runtime unallow operation for directories. + +## Options considered + +- **Cumulative runtime vault scope** (chosen): keeps previews reliable after vault switches while preserving vault-only access in the current process. +- **Continue forbidding previous vaults**: appears stricter, but Tauri forbids are not reversible and valid previews fail after switching back. +- **Allow all filesystem paths**: avoids preview failures but returns to the broad asset protocol access that ADR-0074 intentionally removed. +- **Replace `convertFileSrc` with a custom protocol**: could support exact active-vault revocation, but it would be a larger cross-cutting migration for editor images, file previews, and PDF rendering. + +## Consequences + +- Images and PDFs from any vault loaded in the current session can keep rendering after vault switches. +- The app process, not each vault switch, is the revocation boundary for asset URL access. +- Active-vault command validation remains the primary guard for mutations and default-app opens. +- Re-evaluate this if Tauri adds a public runtime unallow operation for asset protocol directories. diff --git a/docs/adr/0100-synthetic-vault-root-folder-row.md b/docs/adr/0100-synthetic-vault-root-folder-row.md new file mode 100644 index 0000000..8dfe5e8 --- /dev/null +++ b/docs/adr/0100-synthetic-vault-root-folder-row.md @@ -0,0 +1,35 @@ +--- +type: ADR +id: "0100" +title: "Synthetic vault-root row in folder navigation" +status: active +date: 2026-04-30 +--- + +## Context + +ADR-0033 introduced subfolder scanning and a collapsible folder tree backed by `list_vault_folders`, but the sidebar still had no first-class way to select the vault root itself. That left root-level files outside the folder-navigation model and pushed the UI toward one-off handling for the opened vault path. + +The new sidebar behavior needs to show root-level files when the user clicks the vault name, while preserving the existing folder rename/delete model for real folders only. + +## Decision + +**Represent the vault root in the sidebar as a synthetic frontend-owned folder row rather than as a mutable backend folder.** + +- `FolderTree` wraps backend folder nodes in a root row with `path: ""` and `rootPath` set to the opened vault path. +- `SidebarSelection` keeps using `kind: 'folder'`, but root selection is encoded as the empty folder path plus `rootPath` metadata. +- Root-level file filtering is handled in note-list helpers as a dedicated root case instead of pretending the vault root is an ordinary folder. +- Rename/delete remain available only for real folders; the vault root row is navigable, not mutable. + +## Options considered + +- **Option A** (chosen): Synthetic vault-root row in the renderer — keeps `list_vault_folders` focused on real folders, avoids backend schema churn, and reuses the existing folder-selection mental model. +- **Option B**: Add a pseudo-folder to backend folder results — would couple presentation-only root behavior to command data and blur the distinction between the vault itself and mutable folders. +- **Option C**: Keep root files outside folder navigation entirely — simpler, but leaves the sidebar with an incomplete navigation model and special cases elsewhere in the UI. + +## Consequences + +- Folder navigation now has a single model for root and nested folder browsing. +- Backend folder APIs stay unchanged: they describe actual folders, not UI-only rows. +- Selection handling must treat `path: ""` as the vault-root case and use `rootPath` when computing direct-root file membership. +- Re-evaluate if folder actions ever need to operate on the vault root itself, because that would likely require a separate command model instead of extending the synthetic row. diff --git a/docs/adr/0101-categorical-product-analytics-events.md b/docs/adr/0101-categorical-product-analytics-events.md new file mode 100644 index 0000000..ec0f992 --- /dev/null +++ b/docs/adr/0101-categorical-product-analytics-events.md @@ -0,0 +1,35 @@ +--- +type: ADR +id: "0101" +title: "Categorical product analytics events" +status: active +date: 2026-04-30 +--- + +## Context + +ADR-0016 established opt-in PostHog analytics and Sentry crash reporting, but new feature telemetry now spans file previews, inline image lightbox opens, AI agent session lifecycle events, and All Notes visibility toggles. Instrumenting those flows ad hoc would make it easy to leak vault-specific data such as note paths, filenames, prompt text, or image URLs. + +Tolaria needs richer product telemetry for feature behavior while preserving the privacy bar expected from a local-first notes app. + +## Decision + +**Route product analytics through dedicated helper functions that emit only coarse categorical metadata.** + +- Product events live behind `src/lib/productAnalytics.ts` instead of scattering raw `trackEvent()` payloads across feature code. +- Allowed payloads are constrained to categories and counts such as preview kind, action kind, AI agent id, permission mode, response/tool counts, status, and All Notes visibility category/enabled state. +- Product events must not include vault content, note titles, file paths, filenames, image URLs, prompt text, or other user-authored data. +- When a feature needs telemetry, it should add or extend a typed helper rather than sending free-form payloads inline. + +## Options considered + +- **Option A** (chosen): Typed categorical wrappers for product events — preserves useful product signals while keeping privacy constraints explicit and reviewable in one place. +- **Option B**: Let each feature call `trackEvent()` directly with whatever fields seem useful — faster short term, but inconsistent and too easy to let sensitive data leak into analytics. +- **Option C**: Avoid new product events entirely — safest from a privacy standpoint, but leaves the team blind to whether preview, AI, and list-visibility features are actually being used or failing. + +## Consequences + +- Telemetry reviews become easier because the allowed product-event surface is centralized. +- Future product analytics work should start by defining categorical event helpers, not by attaching raw note/file context. +- Some debugging detail is intentionally sacrificed; deep diagnosis should rely on consented crash reporting and local reproduction rather than richer analytics payloads. +- Re-evaluate if Tolaria ever needs a broader telemetry taxonomy or stronger compile-time guarantees around event schemas. diff --git a/docs/adr/0102-low-end-safe-autosave-idle-window.md b/docs/adr/0102-low-end-safe-autosave-idle-window.md new file mode 100644 index 0000000..5094793 --- /dev/null +++ b/docs/adr/0102-low-end-safe-autosave-idle-window.md @@ -0,0 +1,29 @@ +--- +type: ADR +id: "0102" +title: "Low-end-safe autosave idle window" +status: active +date: 2026-04-30 +supersedes: "0015" +--- + +## Context + +ADR-0015 chose a 500ms autosave debounce so normal edits would persist quickly without writing on every keystroke. GitHub issue #443 showed that this window is too aggressive on very weak Windows CPUs: if typing intervals exceed 500ms or a save takes long enough to overlap continued typing, Tolaria can start disk and derived-state work while the user is still entering text. + +## Decision + +**Tolaria autosaves after a 1.5s idle window and treats stale in-flight autosaves as obsolete when newer content arrives before they resolve.** Manual saves, note switches, raw-mode entry, and destructive actions still flush pending editor content immediately. + +## Options Considered + +- **Option A — 1.5s idle window plus stale-save protection** (chosen): reduces mid-typing saves on slow CPUs while keeping ordinary autosave behavior fast enough for reliability. It also prevents an older slow save from clearing or repainting over newer pending text. +- **Option B — Keep 500ms and only fix stale saves**: preserves the previous timing but still triggers repeated saves for slower typists and weaker machines. +- **Option C — Save only on blur or navigation**: minimizes background work but increases crash-loss risk during longer writing sessions. + +## Consequences + +- Autosave is less likely to compete with active typing on low-end Windows hardware. +- The unsaved window grows from roughly 500ms to roughly 1.5s after Tolaria receives the latest content change. +- Explicit flush paths remain immediate, so navigation, manual save, raw-mode transitions, and destructive actions still preserve pending edits before proceeding. +- Future changes to autosave timing should keep weak-CPU responsiveness and stale in-flight save behavior in the same test surface. diff --git a/docs/adr/0103-adapter-specific-ai-permission-semantics.md b/docs/adr/0103-adapter-specific-ai-permission-semantics.md new file mode 100644 index 0000000..4368c2b --- /dev/null +++ b/docs/adr/0103-adapter-specific-ai-permission-semantics.md @@ -0,0 +1,32 @@ +--- +type: ADR +id: "0103" +title: "Adapter-specific AI permission semantics" +status: active +date: 2026-04-30 +supersedes: "0092" +--- + +## Context + +ADR-0092 introduced per-vault Vault Safe / Power User modes, but the first implementation left too much room for adapter drift. Some agents can directly deny or allow Bash, some expose only a sandbox/approval profile, and Pi currently has no narrower app-managed switch beyond Tolaria's transient MCP configuration. The shared UI still needs a consistent product contract: Vault Safe must not encourage shell execution, while Power User should keep shell execution available across repeated agent turns where the selected adapter supports it. + +## Decision + +**Tolaria treats the permission mode as a product contract first and maps it conservatively per adapter.** + +- Shared AI system prompts are mode-aware on every turn, including turns with note context snapshots. +- Vault Safe tells agents not to use or advertise shell, terminal, Bash, script execution, git, or command-line tools. +- Power User tells shell-capable agents that local shell commands are available for the active vault and should remain scoped to that vault. +- Claude Code Safe excludes Bash; Power User includes and pre-approves Bash without dangerous bypass flags. +- Codex Safe uses the CLI's read-only sandbox plus untrusted approval policy; Power User uses workspace-write plus never-ask approval so shell-capable Codex turns remain low-friction across the session. +- OpenCode Safe denies bash and external directories; Power User allows bash while still denying external directories. +- Pi keeps the same conservative transient MCP config in both modes until the Pi CLI exposes a reliable app-managed shell permission switch. The prompt must not promise shell for Pi Power User. +- Gemini Safe excludes `run_shell_command`; Gemini Power User intentionally uses `yolo` with trusted transient Tolaria MCP settings. + +## Consequences + +- Mode behavior is no longer described solely by generic UI copy; adapter docs and tests define the exact mapping. +- Codex Vault Safe remains a best-effort safe profile rather than a true built-in-tools-off mode, because Codex CLI currently exposes sandbox and approval controls but not a dedicated switch to remove shell tooling while preserving MCP. +- Future adapters must either implement both modes explicitly or document that Power User maps to the same conservative behavior. +- If Tolaria adds a stronger warning or dangerous mode later, it needs a separate ADR and UI language. diff --git a/docs/adr/0104-tauri-frontend-readiness-watchdog.md b/docs/adr/0104-tauri-frontend-readiness-watchdog.md new file mode 100644 index 0000000..d64956f --- /dev/null +++ b/docs/adr/0104-tauri-frontend-readiness-watchdog.md @@ -0,0 +1,48 @@ +--- +type: ADR +id: "0104" +title: "Tauri frontend readiness watchdog" +status: active +date: 2026-05-01 +--- + +## Context + +Tolaria already keeps heavy filesystem and subprocess work off the Tauri window-creation path, but that alone does not protect against a different startup failure mode: the desktop WebView can render the static HTML shell while the React app never becomes interactive. + +On macOS this showed up as an inert window that looked launched but never finished mounting the real app. The failure boundary is cross-layer: + +- `index.html` can paint before React commits +- React root errors can happen before the app reports itself ready +- a plain reload is acceptable as a one-time recovery, but an automatic reload loop is not +- browser/mock runs should not inherit desktop-only recovery behavior + +Tolaria needs a startup contract that distinguishes “HTML painted” from “frontend actually became interactive”, and a bounded recovery path when that contract is not satisfied. + +## Decision + +**Tolaria uses a Tauri-only frontend-readiness watchdog that reloads the WebView at most once if React never reports startup readiness.** + +Concretely: + +- `index.html` installs a Tauri-only startup timer before React loads +- React dispatches a readiness signal from a mounted effect after the app shell commits +- if readiness never arrives before the timeout, the WebView reloads once +- the same one-shot reload path is available to React root error handling before readiness is marked +- `sessionStorage` tracks whether the startup reload was already attempted so Tolaria does not loop forever +- browser/mock environments keep using ordinary browser clipboard/storage behavior and do not enable this desktop startup recovery path + +## Options considered + +- **Tauri-only readiness watchdog with one-shot reload** (chosen): directly addresses the inert-startup failure mode, keeps recovery local to the frontend, and avoids permanent reload loops. Cost: startup now depends on a small cross-layer contract between HTML bootstrap and React. +- **Do nothing and rely on manual relaunch**: simplest implementation, but leaves users stranded in a broken-looking app state with no automatic recovery. +- **Reload on any React root error without a readiness gate**: more aggressive, but too noisy; post-startup runtime errors should not trigger surprise reloads. +- **Move recovery entirely into native Rust window/bootstrap logic**: possible, but the failure signal lives in the frontend lifecycle, so native code would still need a readiness handshake. + +## Consequences + +- Tolaria now distinguishes successful frontend startup from merely rendering the HTML shell. +- Desktop startup recovery is bounded to a single retry per session, reducing the chance of trapping users in reload loops. +- `index.html`, `src/main.tsx`, and `src/utils/frontendReady.ts` form a shared startup contract that future bootstrap refactors must preserve. +- Any future change that delays app-shell mount beyond the watchdog timeout must re-evaluate the timeout and readiness trigger. +- If a startup failure persists after one retry, Tolaria still surfaces the broken state instead of hiding a deeper bug behind repeated reloads. diff --git a/docs/adr/0105-editor-correctness-and-responsiveness-contract.md b/docs/adr/0105-editor-correctness-and-responsiveness-contract.md new file mode 100644 index 0000000..2cde082 --- /dev/null +++ b/docs/adr/0105-editor-correctness-and-responsiveness-contract.md @@ -0,0 +1,39 @@ +--- +type: ADR +id: "0105" +title: "Editor correctness and responsiveness contract" +status: active +date: 2026-05-01 +--- + +## Context + +Tolaria notes are durable Markdown files on disk, but the rich editor renders them through BlockNote blocks. Large notes exposed a tempting optimization: show a fast Markdown preview first, then hydrate BlockNote later. In practice, that creates two renderers for the same document, visible flicker, delayed click-time lag, and more places for stale async work to race with the currently selected note. + +The editor must optimize for the product priorities in this order: + +1. no crashes +2. no stale content, race-condition overwrites, or lost edits +3. responsive typing and cursor movement +4. fast note-list-to-editor loading + +## Decision + +**Tolaria keeps a single direct editor surface for Markdown notes and treats editor content swaps as generation-checked, source-content-checked operations.** Fast loading may use raw file-content prefetching and a bounded parsed-block cache, but it must not show a separate preview that later swaps into the editor. Parsed BlockNote blocks are reusable only when their source Markdown exactly matches the content being opened, and background parsing must run only after recent typing/navigation has gone idle. + +## Options considered + +- **Single direct editor surface with guarded swaps plus bounded caches** (chosen): preserves one visual representation of the document, rejects stale async parse results, validates or identity-checks cached disk content before opening, and keeps typing work debounced. Cons: very large notes can still wait on BlockNote conversion when they were not warmed. +- **Fast Markdown preview followed by hidden BlockNote hydration**: improves first paint but creates flicker, delayed edit-time stalls, and duplicated rendering semantics. +- **Unbounded or eager background BlockNote parsing for likely next notes**: can make some opens faster, but competes with typing/navigation and introduces stale parse-result hazards unless heavily scheduled, bounded, and invalidated. +- **Always raw mode for large notes**: strongest responsiveness for huge files, but changes the editing experience abruptly and should be an explicit fallback rather than the default. + +## Consequences + +- Async editor work must prove it still matches both the latest swap generation and the latest tab source content before touching BlockNote. +- Cached raw note content must be validated against disk before it is shown unless it carries the same `modifiedAt` and `fileSize` identity as the current `VaultEntry`, or the content was just authored by Tolaria in the current process. +- Cached parsed BlockNote blocks must be keyed by vault, path, and exact source content, cloned on read/write, and bounded by entry count plus source byte budget. +- Background parsed-block warming is allowed only for likely next large Markdown notes after a foreground idle window; active typing, raw mode, and editor mount state must defer it. +- Dirty local editor content remains authoritative. External filesystem refreshes may replace clean notes, but must not overwrite unsaved local edits. +- Per-keystroke editor work must stay minimal. Serialization, metadata derivation, autosave, and cache updates should be debounced, coalesced, or scheduled away from active typing. +- Future large-note optimizations should target true progressive/chunked conversion or explicit raw/read-only fallback states, not a visually different preview that morphs into BlockNote. diff --git a/docs/adr/0106-shared-app-command-manifest.md b/docs/adr/0106-shared-app-command-manifest.md new file mode 100644 index 0000000..b5093a2 --- /dev/null +++ b/docs/adr/0106-shared-app-command-manifest.md @@ -0,0 +1,31 @@ +--- +type: ADR +id: "0106" +title: "Shared app command manifest" +status: active +date: 2026-05-02 +--- + +## Context + +Tolaria command metadata was split across several runtime surfaces: TypeScript owned shortcut lookup and command-palette shortcut display, Rust owned native menu IDs, labels, accelerators, aliases, and enablement groups, and the Linux titlebar fallback menu duplicated another command list. Adding or changing a command required carefully editing multiple files that could drift while still compiling. + +The existing renderer-first shortcut model and native-menu dedupe remain correct, but they need a single source for metadata that must be identical across those surfaces. + +## Decision + +**Tolaria stores cross-runtime app command metadata in `src/shared/appCommandManifest.json`, and both the renderer and Tauri native menu derive their command/menu IDs, accelerators, menu labels, menu aliases, enablement groups, and deterministic QA metadata from it.** Context-sensitive command-palette builders still own availability and execution callbacks, and OS-native menu entries remain local to the native menu implementation. + +## Options considered + +- **Shared JSON manifest included by TypeScript and Rust** (chosen): works in both runtimes without code generation, keeps menu metadata reviewable, and lets tests validate drift directly. +- **Generate TypeScript and Rust constants from a schema**: gives stronger compile-time types but adds a build step and a generated-file maintenance burden for a small manifest. +- **Keep duplicated constants with more tests**: reduces immediate refactor scope, but still forces every command change through parallel manual edits. + +## Consequences + +- New app commands that appear in native menus or shortcut QA must be added to `src/shared/appCommandManifest.json`. +- `appCommandCatalog.ts` is responsible for turning the manifest into typed renderer helpers such as `APP_COMMAND_IDS`, shortcut lookup maps, Linux menu sections, and deterministic QA definitions. +- `src-tauri/src/menu.rs` includes the same manifest JSON, builds custom menu items from it, maps overridden menu item IDs such as `file-quick-open-alias` back to their primary command IDs, and resolves state-dependent enablement groups from manifest entries. +- Platform-native menu items such as Undo, Redo, Copy, Paste, Select All, Services, Quit, and Window controls stay in Rust because they are OS affordances, not Tolaria app commands. +- Command-palette builders continue to own dynamic labels, filtering, enabled state, and callbacks where those depend on current app state. diff --git a/docs/adr/0107-markdown-durable-tldraw-whiteboards.md b/docs/adr/0107-markdown-durable-tldraw-whiteboards.md new file mode 100644 index 0000000..703f9ca --- /dev/null +++ b/docs/adr/0107-markdown-durable-tldraw-whiteboards.md @@ -0,0 +1,43 @@ +--- +type: ADR +id: "0107" +title: "Markdown-durable tldraw whiteboards in notes" +status: active +date: 2026-05-03 +--- + +## Context + +Tolaria notes are durable Markdown files, while whiteboard editing needs an interactive canvas with structured shape data. tldraw provides a mature React whiteboard runtime and exposes snapshot APIs that can persist the document without using browser-local `persistenceKey` storage. + +The storage decision needs to preserve Tolaria's filesystem source-of-truth rule: deleting local caches must not lose a board, raw mode must expose the canonical source, and Git should track the board together with the surrounding note. + +## Decision + +**Tolaria will support whiteboards as Markdown-durable fenced `tldraw` blocks backed by tldraw document snapshots.** + +The implementation: + +- Converts fenced `tldraw` blocks into temporary placeholders before BlockNote parses Markdown. +- Replaces those placeholders with `tldrawBlock` schema blocks that store a stable board id and tldraw document snapshot JSON. +- Renders each block with the `tldraw` package inside the rich editor. +- Debounces tldraw document changes into BlockNote block props so Tolaria's normal autosave writes the snapshot into the `.md` file. +- Serializes `tldrawBlock` nodes back to fenced Markdown before save, raw-mode entry, and editor-position snapshots. +- Adds a `/whiteboard` slash command that inserts the same block format. + +Session state such as camera position, selected shapes, and selected tools is not persisted into the note. Preview images are intentionally omitted from the initial design; they may be introduced later as derived cache artifacts for note lists, search results, or graph cards. + +## Options considered + +- **Markdown fenced block with tldraw document snapshot** (chosen): keeps the board in the note file, matches Tolaria's Mermaid/math round-trip model, and works for both full whiteboard notes and embedded boards inside larger notes. +- **tldraw `persistenceKey` / IndexedDB**: simplest app integration, but violates the vault-as-source-of-truth rule and would lose boards when browser storage is cleared. +- **Separate `.tldr` files embedded from Markdown**: keeps JSON out of prose notes, but fragments note ownership, makes embedded boards harder to move with their parent note, and complicates Git history for small board edits. +- **Preview-image-first storage**: useful for thumbnails, but not editable source. It would make the PNG an attractive but stale source of truth. + +## Consequences + +- `src/utils/tldrawMarkdown.ts` is the canonical parser/serializer bridge for whiteboard blocks. +- `src/components/TldrawWhiteboard.tsx` owns the tldraw runtime integration and only persists document snapshots. +- Raw mode remains a direct source editor for the fenced JSON. +- Embedded whiteboards and future full-note whiteboard templates share the same storage format. +- Asset support is deferred; when added, asset bytes should live in vault-relative attachment paths referenced from the tldraw asset records. diff --git a/docs/adr/0107-pointer-owned-editor-block-reordering.md b/docs/adr/0107-pointer-owned-editor-block-reordering.md new file mode 100644 index 0000000..03663fc --- /dev/null +++ b/docs/adr/0107-pointer-owned-editor-block-reordering.md @@ -0,0 +1,32 @@ +--- +type: ADR +id: "0107" +title: "Pointer-owned editor block reordering" +status: active +date: 2026-05-02 +--- + +## Context + +Tolaria uses BlockNote for rich Markdown editing, Tauri for native desktop file drops, and custom editor drop handlers for images and wikilinks. BlockNote's default block drag path depends on HTML5 drag events and `DataTransfer`. On macOS inside the Tauri webview, that makes block reordering fragile because the same browser-level drag system also carries native file/image drops into the editor. + +Regressions tended to oscillate: fixes that restored block dragging could break image drops, and fixes that protected native drops could make block reordering fail or lose visual feedback. The drag handle also needs editor-specific affordances: a moving block preview, an insertion separator, stale-block protection, and typography-aware positioning for the side-menu controls. + +## Decision + +**Tolaria owns editor block reordering as a pointer gesture that directly moves BlockNote blocks, and leaves HTML5/native drag data paths for file, image, and external drops.** The drag side menu is responsible for resolving live BlockNote blocks, computing pointer hit targets, rendering drag affordances, and aligning the hover controls to the rendered text range of the hovered block. + +## Options considered + +- **Pointer-owned block reordering** (chosen): separates internal block moves from native drop payloads, works without `DataTransfer`, gives Tolaria deterministic visual affordances, and can be tested with Playwright pointer/mouse actions. Cons: Tolaria now owns a small amount of hit-testing, block-move, and affordance code around BlockNote. +- **Continue using BlockNote's HTML5 drag handler**: keeps less local code, but ties internal block moves to the same unstable drag channel used by native file drops in Tauri. +- **Patch native file drops around BlockNote drag events**: might repair individual regressions, but preserves the root coupling between editor-internal reordering and external drag payload handling. +- **Disable block dragging on macOS/Tauri**: avoids the conflict, but removes an important editing workflow. + +## Consequences + +- Internal block reordering must not depend on `DataTransfer` or `draggable=true`. +- File/image/wikilink drop behavior remains owned by the existing editor drop abstractions and native Tauri file-drop bridge. +- Block reordering tests should use pointer or mouse movement in Playwright, and should assert the moving preview and insertion separator while the drag is in progress. +- The side menu should align to measured rendered content rather than heading-specific pixel offsets, so future theme font-size and line-height changes do not need drag-control retuning. +- Changes to BlockNote DOM structure around `.bn-block-content`, `.bn-inline-content`, `.bn-side-menu`, or block container IDs require rechecking the pointer hit-testing and side-menu alignment tests. diff --git a/docs/adr/0108-direct-model-ai-targets.md b/docs/adr/0108-direct-model-ai-targets.md new file mode 100644 index 0000000..81da914 --- /dev/null +++ b/docs/adr/0108-direct-model-ai-targets.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0108" +title: "Direct model AI targets alongside coding agents" +status: active +date: 2026-05-03 +--- + +## Context + +Tolaria's AI panel originally targeted desktop coding-agent CLIs only. That works well for tool-capable vault editing, but it excludes users who run local model servers, users who prefer OpenAI or Anthropic APIs, and future mobile builds where desktop subprocesses cannot run. + +## Decision + +**Tolaria models AI selection as an AI target.** Targets can be desktop coding agents or direct model endpoints. Coding agents keep the existing Safe / Power User permission modes and tool access. Direct model targets run in Chat mode: they receive note context and conversation history, but they do not get vault-write tools or shell access. + +Direct model provider metadata is stored in app settings. Provider API secrets are not stored in settings; hosted providers can either save a key in Tolaria's local app-data secrets file or read a key from a named environment variable. The local secrets file is outside the vault, outside project worktrees, and written with owner-only file permissions on Unix platforms. Local providers such as Ollama and LM Studio can run without a key. + +## Options Considered + +- **AI target abstraction** (chosen): supports agents, local models, hosted APIs, and mobile-compatible model runtimes without pretending all AI backends have the same capabilities. +- **Treat custom providers as OpenCode configuration only**: lower implementation cost on desktop, but does not help mobile and keeps API users dependent on a coding-agent install. +- **Direct API runtime with write tools immediately**: powerful, but would require Tolaria-owned tool loops, confirmations, retries, and safety semantics before the basic chat value is proven. + +## Consequences + +- Settings owns durable provider setup and default target selection. +- The status bar becomes a quick target switcher across agents and configured model targets. +- The AI panel explains capability differences: agents show Safe / Power User; direct model targets show Chat mode. +- Future work can migrate local secrets to OS keychain storage and add read/write tool loops without changing the top-level target model. diff --git a/docs/adr/0108-sanitized-rendered-markup-and-safe-regex.md b/docs/adr/0108-sanitized-rendered-markup-and-safe-regex.md new file mode 100644 index 0000000..9ff30d3 --- /dev/null +++ b/docs/adr/0108-sanitized-rendered-markup-and-safe-regex.md @@ -0,0 +1,25 @@ +# 0108. Sanitized Rendered Markup and Safe User Regex + +Date: 2026-05-03 + +## Status + +Accepted + +## Context + +Tolaria renders generated SVG/HTML from trusted libraries such as Mermaid and KaTeX, and it allows users to opt into regex matching in filters and editor find/replace. Codacy SRM flagged the raw markup insertion and direct regex construction as Critical XSS/DoS risks. + +## Decision + +Add direct runtime dependencies on `dompurify` and `safe-regex2`. + +Rendered Mermaid SVG and KaTeX HTML must be sanitized before insertion and mounted through DOM nodes rather than React `dangerouslySetInnerHTML`. User-provided regex sources must be length-bounded and checked with `safe-regex2` before compilation. + +SCA-reported vulnerable transitive dependencies are pinned through package-manager overrides so Codacy resolves patched floors until upstream dependencies adopt them naturally. This includes `protobufjs`, the MCP SDK web-server stack, and Vite's parser/build transitive stack. Rust lockfile-only updates keep OpenSSL, rustls-webpki, and tar on patched versions without changing public Tauri APIs. + +## Consequences + +Markup rendering now has an explicit sanitizer boundary that is shared by Mermaid and math rendering. User regex features remain available, but unsafe or overly large expressions fail validation instead of running in the UI thread. + +The overrides should be removed once the dependency graph no longer pulls the vulnerable versions. diff --git a/docs/adr/0109-debounced-worker-derived-editor-indexes.md b/docs/adr/0109-debounced-worker-derived-editor-indexes.md new file mode 100644 index 0000000..c52504e --- /dev/null +++ b/docs/adr/0109-debounced-worker-derived-editor-indexes.md @@ -0,0 +1,32 @@ +--- +type: ADR +id: "0109" +title: "Debounced worker-derived editor indexes" +status: active +date: 2026-05-04 +--- + +## Context + +Right side panels can need derived indexes of the active note, such as the Table of Contents hierarchy. These indexes are useful while editing, but rebuilding them synchronously during note opening or on every keystroke competes with the editor's main-thread work and violates ADR-0105's responsiveness contract. + +The Table of Contents also needs live BlockNote block IDs for navigation, while the fastest and most stable source for the outline itself is the active note's Markdown content. Binding the outline rebuild directly to BlockNote document mutations makes typing and note swaps more expensive than necessary. + +## Decision + +**Derived editor indexes that are not required for the editor surface itself must be lazy, debounced, and built off the main thread when they can be derived from Markdown.** The Table of Contents does not build while its panel is closed; once opened, it uses a Web Worker to build its Markdown-derived H1/H2/H3 tree after a debounce, while live BlockNote block IDs are resolved only at click time for navigation. + +## Options considered + +- **Lazy debounced Web Worker for Markdown-derived indexes** (chosen): avoids any TOC work while the panel is closed, keeps outline parsing away from typing and note-opening work once opened, cancels stale panel updates, and lets the rendered editor remain the only editor surface. Cons: adds a small worker/client path and a title-only interim state. +- **Main-thread deferred rebuild with `setTimeout`**: avoids blocking the first render, but still runs on the UI thread and can still rebuild too often during active edits. +- **Synchronous rebuild from the BlockNote document**: simplest and gives immediate block IDs, but makes every BlockNote document update a potential side-panel rebuild. +- **Never update the TOC while editing**: safest for typing performance, but stale outlines make the panel misleading for active authoring. + +## Consequences + +- TOC tree state is driven by note identity plus debounced Markdown content, not by BlockNote document churn. +- Closing the TOC panel unmounts the panel and cancels pending debounce callbacks; no worker request is scheduled while the panel is closed. +- The TOC may briefly show only the note title after a note switch or edit burst; the full tree appears when the debounced worker result returns. +- Navigation remains tied to the live editor: block IDs are resolved from the current BlockNote document at click time and scrolled/focused then. +- Future derived side-panel indexes should follow the same pattern when they parse or scan note content and are not needed to render the editor itself. diff --git a/docs/adr/0110-in-app-media-and-pdf-file-previews.md b/docs/adr/0110-in-app-media-and-pdf-file-previews.md new file mode 100644 index 0000000..2a5fe6a --- /dev/null +++ b/docs/adr/0110-in-app-media-and-pdf-file-previews.md @@ -0,0 +1,44 @@ +--- +type: ADR +id: "0110" +title: "In-app media and PDF previews for binary vault files" +status: superseded +date: 2026-05-05 +supersedes: "0098" +superseded_by: "0121" +--- + +## Context + +ADR-0098 extended Tolaria's file-first preview model from images to PDFs while keeping binary files as ordinary `VaultEntry` records. In practice, vaults also carry voice notes, interview recordings, screen captures, and short clips that users need to inspect in context without round-tripping through another app. + +The existing binary preview architecture already had the important constraints in place: + +- previewability should stay a renderer concern inferred from filename extension rather than a persisted schema field +- preview access should stay inside Tauri's scoped asset protocol instead of broad filesystem reads +- external-open actions must still re-enter the active-vault command boundary before delegating to the OS + +Audio and video support should extend that same model rather than introducing a separate asset or media subsystem. + +## Decision + +**Tolaria previews supported image, audio, video, and PDF files in the editor pane while keeping them as ordinary binary vault files.** + +- The scanner keeps the coarse `fileKind: "binary"` representation; `src/utils/filePreview.ts` infers preview support from safe extension allow-lists. +- `FilePreview` remains the single renderer-owned preview surface for supported binary files. +- Images continue to render through ``, PDFs through the webview PDF object renderer, and audio/video through native HTML media controls, all backed by Tauri asset URLs from `convertFileSrc`. +- The Tauri CSP must allow scoped asset URLs in `media-src` for audio/video and in `object-src` for PDFs without broadening script or network permissions. +- Note-list rows for previewable media stay clickable and use file-specific affordances; unsupported binaries remain ordinary files with explicit fallback/open-external paths. + +## Alternatives considered + +- **Extend the existing FilePreview model to media** (chosen): keeps one binary-preview surface, reuses scoped asset access, and avoids new persisted file categories. Cons: native media controls are intentionally minimal. +- **Open audio and video only in the default app**: simpler implementation, but breaks in-context review for media-heavy vaults. +- **Introduce dedicated persisted media file kinds or a separate media library**: could support richer metadata later, but adds schema and scanner complexity for files that should remain normal vault entries. + +## Consequences + +- Audio and video do not become notes and do not get special persistence semantics. +- Tolaria's binary preview surface now covers the common safe media formats without changing cache shape, scanner output, or the filesystem-first model. +- Scoped runtime asset access and active-vault command validation remain the security boundary for binary previews and external-open actions. +- Re-evaluate this decision if Tolaria later needs editing, waveform/timeline tooling, subtitles, or transcoding, because those would justify a richer media-specific subsystem. diff --git a/docs/adr/0111-path-aware-external-vault-refresh-with-focused-editor-preservation.md b/docs/adr/0111-path-aware-external-vault-refresh-with-focused-editor-preservation.md new file mode 100644 index 0000000..98dda08 --- /dev/null +++ b/docs/adr/0111-path-aware-external-vault-refresh-with-focused-editor-preservation.md @@ -0,0 +1,49 @@ +--- +type: ADR +id: "0111" +title: "Path-aware external vault refresh with focused-editor preservation" +status: active +date: 2026-05-05 +supersedes: "0071" +--- + +## Context + +ADR-0071 established a shared reconciliation path for external vault mutations so git pulls, AI-agent writes, and other non-local edits would reload vault-derived state and protect unsaved local changes. That policy was directionally right, but the original "reopen the clean active note" rule was too broad once Tolaria added a native filesystem watcher and more editor-mounted integrations. + +Two problems emerged: + +- unrelated external updates could remount a clean active editor even when the active file itself did not change +- remounting while focus was inside the rich or raw editor surface could drop cursor/focus state and disrupt native input flows even when the vault refresh itself was otherwise safe + +Tolaria still needs refreshed entries, folders, views, backlinks, and other derived state after external writes. But it should only pay the cost of an active-editor remount when the changed-path batch actually requires one and the user is not actively focused inside the editor. + +## Decision + +**External vault refreshes now reload shared vault-derived state eagerly, but only remount the active editor when the active file itself changed and the editor is clean and unfocused.** + +The shared `refreshPulledVaultState()` path now applies these rules: + +1. Reload vault entries, folders, and saved views together for every external change batch. +2. If there is no active note, stop after the shared reload. +3. If the active note changed during the async reload, stop rather than reopening stale context. +4. If the active note has unsaved local edits, keep the current editor buffer mounted. +5. If focus is currently inside the rich or raw editor surface, keep that editor mounted even for otherwise clean notes. +6. If the active file disappeared, close the tab instead of leaving a stale editor behind. +7. Only close and reopen the active tab when the changed-path batch includes that active file and the previous guards did not apply. + +Git pulls, AI-agent refresh callbacks, and filesystem-watcher batches should continue to converge through this single reconciliation helper instead of inventing separate reload policies. + +## Alternatives considered + +- **Path-aware refresh with focused-editor preservation** (chosen): keeps derived vault state fresh while avoiding unnecessary remounts and focus loss. Cons: a focused clean editor can temporarily lag on-disk content until a later safe remount. +- **Always reopen the clean active note after every external refresh**: strongest immediate convergence, but causes visible churn and drops editor focus for unrelated changes. +- **Skip shared reloads whenever the editor is focused**: preserves focus, but leaves folders, views, backlinks, and other derived state stale. + +## Consequences + +- Unrelated external vault updates no longer remount the active editor just because the note is clean. +- Focused rich/raw editor sessions preserve cursor and native input state across watcher- or agent-driven vault refreshes. +- The changed-path batch is now part of the external-refresh contract; callers should pass the best available file list instead of treating all refreshes as full active-note invalidations. +- A focused clean editor may intentionally continue showing pre-refresh content until a later safe reopen, trading immediate active-note convergence for editing continuity. +- `refreshPulledVaultState()` remains the single place to evolve external-refresh policy; future features should extend that helper rather than layering ad hoc editor reload behavior. diff --git a/docs/adr/0112-system-theme-mode.md b/docs/adr/0112-system-theme-mode.md new file mode 100644 index 0000000..06bba85 --- /dev/null +++ b/docs/adr/0112-system-theme-mode.md @@ -0,0 +1,41 @@ +--- +type: ADR +id: "0112" +title: "System theme mode" +status: active +date: 2026-05-05 +--- + +## Context + +ADR-0081 introduced Tolaria's internal app-owned light and dark theme runtime and deliberately deferred system-follow mode. That kept the first dark-mode release small, but users now need Tolaria to match the operating system appearance automatically, including scheduled macOS light/dark changes. + +The previous constraints still apply: themes are app-owned, not vault-authored; the renderer must avoid startup flashes; shadcn/ui, Tailwind variables, editor chrome, and secondary windows must keep sharing the same resolved light/dark contract. + +## Decision + +**Tolaria now treats `system` as a persisted theme preference that resolves to the current OS light/dark appearance at runtime.** + +The selected preference can be `light`, `dark`, or `system`: + +1. `settings.theme_mode` remains the source of truth for the installation-local preference. +2. The localStorage mirror stores the selected preference, including `system`, so the `index.html` prepaint script can resolve the correct appearance before React mounts. +3. `data-theme` and the shadcn `.dark` class always receive the resolved app theme, `light` or `dark`; they never receive `system`. +4. When `system` is selected, the renderer subscribes to `prefers-color-scheme` changes and reapplies the resolved theme without reopening the app. +5. Explicit `light` and `dark` choices remain overrides and do not follow OS changes. + +Command-palette theme actions and the Settings panel both save the same preference path. Product analytics record preference changes with the selected mode only, without sending vault or note content. + +## Alternatives considered + +- **Persist `system` and resolve it into the existing light/dark runtime** (chosen): keeps ADR-0081's small app-owned theme surface while adding OS-follow behavior. +- **Store the resolved OS theme in settings**: avoids a third stored value, but silently converts System users into explicit Light/Dark users after every save. +- **Set `data-theme="system"` and branch in CSS**: would require every theme consumer to understand a third state and would break existing Tailwind/shadcn dark-mode assumptions. +- **Rely only on CSS `prefers-color-scheme` media queries**: helps static CSS, but does not update JavaScript consumers, command state, editor integrations, or the localStorage startup mirror consistently. + +## Consequences + +- Startup still avoids a light flash when the stored preference is `system` and the OS is dark. +- Secondary windows that mount the shared theme hook receive the same resolved appearance and update on OS changes. +- Code that reads `document.documentElement.dataset.theme` must treat it as a resolved `light` or `dark` value, not as the stored user preference. +- Future theme variants should preserve this split between selected preference and resolved app theme rather than widening `data-theme` to non-renderable preference values. diff --git a/docs/adr/0113-shared-renderer-attachment-path-normalization.md b/docs/adr/0113-shared-renderer-attachment-path-normalization.md new file mode 100644 index 0000000..12cdf11 --- /dev/null +++ b/docs/adr/0113-shared-renderer-attachment-path-normalization.md @@ -0,0 +1,43 @@ +--- +type: ADR +id: "0113" +title: "Shared renderer attachment path normalization" +status: active +date: 2026-05-07 +--- + +## Context + +Tolaria already treats vault attachments as ordinary files under `attachments/`, and ADRs around previews and asset scoping rely on Tauri asset URLs to render them safely. In practice, attachment handling had started to fragment across the renderer: some flows joined `vaultPath + attachments/...`, some decoded `convertFileSrc` URLs directly, some handled Windows separators ad hoc, and some only worked for one editor surface. + +That duplication turned attachment behavior into a drift risk. Opening file blocks, following editor links, serializing raw-mode Markdown, rewriting image URLs after vault switches, and copying dropped files into the vault all needed the same three representations to stay in sync: + +- portable markdown references such as `attachments/report.pdf` +- Tauri asset URLs used by the renderer +- absolute filesystem paths inside the active vault + +## Decision + +**Tolaria centralizes attachment path conversion in a single renderer-owned primitive and keeps portable `attachments/...` references as the canonical cross-surface representation.** + +Specifically: + +1. `src/utils/vaultAttachments.ts` is the single owner for converting between portable attachment references, Tauri asset URLs, and active-vault filesystem paths. +2. Editor rendering, raw-mode serialization, image upload/drop flows, file-block open actions, and parsed-media cleanup must call that shared primitive instead of carrying local path/URL conversion logic. +3. Renderer code may derive absolute paths only relative to the current active vault and must reject asset URLs or relative paths that fall outside that boundary. +4. Tauri asset URLs remain a transport/rendering detail, not a persisted vault format. + +## Alternatives considered + +- **Shared renderer attachment-path primitive with portable persisted refs** (chosen): keeps behavior consistent across media rendering, editor actions, and vault switching while preserving Markdown portability. +- **Per-feature helpers for each attachment surface**: simpler locally, but repeats Windows/path-normalization rules and lets editor actions drift apart. +- **Persist absolute paths or Tauri asset URLs in Markdown/editor state**: would couple notes to one machine or one runtime session and make vault content less portable. +- **Push all attachment conversion into backend commands**: could reduce renderer logic, but the renderer still needs a shared local model for in-memory markdown rewriting, link activation, and preview URL handling. + +## Consequences + +- Attachment behavior becomes consistent across previews, editor links, toolbar/file-block opens, drag-drop imports, and markdown serialization. +- Vault content stays portable because persisted references remain `attachments/...` paths rather than machine-specific absolute paths or session-specific asset URLs. +- Cross-platform edge cases such as Windows separators, encoded asset URLs, and vault-switch rewrites now have one place to harden and test. +- The Rust command layer remains the write/read security boundary; this ADR only centralizes renderer-side normalization before those commands are called or asset URLs are rendered. +- Future attachment/media features should extend `vaultAttachments.ts` rather than introducing new ad hoc conversion helpers. diff --git a/docs/adr/0114-mounted-workspaces-unified-graph.md b/docs/adr/0114-mounted-workspaces-unified-graph.md new file mode 100644 index 0000000..e6d4d69 --- /dev/null +++ b/docs/adr/0114-mounted-workspaces-unified-graph.md @@ -0,0 +1,40 @@ +--- +type: ADR +id: "0114" +title: "Mounted workspaces unified graph" +status: active +date: 2026-05-07 +--- + +## Context + +Tolaria users can already register multiple vaults, but switching vaults historically replaced the active graph. That model breaks down when separate Git repositories represent different workspaces that still need to reference each other: search, quick-open, wikilink navigation, and note lists should see one graph, while Git status, folders, saved views, and sync controls remain scoped to the repository currently in focus. + +The app also needs a stable way to disambiguate same-named notes across repositories without writing machine-specific paths into Markdown. A full storage migration or database-backed graph would conflict with Tolaria's filesystem-first model and make separate Git histories harder to reason about. + +## Decision + +**Tolaria treats the registered vault list as an installation-local mounted-workspace set and annotates loaded entries with workspace provenance.** + +Specifically: + +1. `vaults.json` persists workspace identity (`label`, stable `alias`, color, mount flag) and the default workspace path for newly created notes. +2. `useVaultLoader` loads entries from every available mounted workspace and attaches `WorkspaceIdentity` to each `VaultEntry` before React consumes the combined graph. +3. Active-vault switching remains the focus control for Git, folder tree, saved views, watchers, repair, and other per-repository operations. +4. Wikilinks stay Markdown-first. Same-workspace links remain vault-relative; cross-workspace canonical links are prefixed with the target workspace alias. +5. Note reads and writes for absolute paths can resolve the deepest registered vault root at the Tauri boundary when no explicit `vaultPath` is supplied, preserving path-containment validation across mounted workspaces. + +## Alternatives considered + +- **Mounted workspace provenance on `VaultEntry` with alias-prefixed links** (chosen): preserves filesystem/Git independence while letting UI graph surfaces operate across repositories. +- **Merge separate repositories into one vault**: avoids cross-root resolution, but forces users to collapse unrelated Git histories and permissions into one repo. +- **Persist absolute paths in wikilinks**: disambiguates locally, but makes notes non-portable and leaks machine paths into user data. +- **Store a global graph database**: could make cross-workspace queries faster, but violates the cache-is-disposable rule and adds a new source of truth. + +## Consequences + +- Search, quick-open, note lists, and wikilink navigation can operate across mounted workspaces. +- UI surfaces that show ambiguous note names should display compact workspace provenance only when more than one workspace is present. +- New notes and Type files are created in the configured default workspace, falling back to the active workspace if the default is unavailable or unmounted. +- Backend command boundaries must continue validating every disk operation against a registered root; mounted workspaces do not loosen filesystem access. +- Future per-workspace features should distinguish graph-wide behavior from active-repository behavior before adding state or commands. diff --git a/docs/adr/0115-scoped-react-context-for-shared-ui-preferences.md b/docs/adr/0115-scoped-react-context-for-shared-ui-preferences.md new file mode 100644 index 0000000..6a41967 --- /dev/null +++ b/docs/adr/0115-scoped-react-context-for-shared-ui-preferences.md @@ -0,0 +1,28 @@ +--- +type: ADR +id: "0115" +title: "Scoped React Context for shared UI preferences" +status: active +date: 2026-05-12 +--- + +## Context + +Laputa has relied on props-down callbacks-up state flow since ADR-0026 because most renderer state is orchestrated in `App.tsx` and the component tree stays understandable. Today's `date_display_format` refactor exposed a narrow exception: the same installation-local rendering preference now needs to reach note rows, property chips and cells, inspector surfaces, table-of-contents metadata, search subtitles, and date-editing controls across multiple branches of the tree. Continuing to thread that value through intermediate components would add noisy prop plumbing to components that do not conceptually own the preference. + +## Decision + +**Use a scoped React context for shared UI preferences that are read in many renderer leaves but still sourced from `App.tsx`. `AppPreferencesProvider` publishes the current installation-local preference values, and leaf components consume them through focused hooks such as `useDateDisplayFormat`; writes still flow through the existing settings/update path rather than through context mutations.** + +## Alternatives considered +- **Scoped app-preferences context** (chosen): removes prop forwarding for cross-cutting rendering preferences while keeping the source of truth in `App.tsx` and avoiding a general-purpose global store. +- **Continue prop drilling from `App.tsx`**: preserves the old rule literally, but keeps widening component signatures and couples intermediate components to preferences they do not use. +- **Adopt a broader global state/store solution**: centralizes access, but introduces more indirection and policy surface than this renderer-only preference case needs. + +## Consequences + +Leaf components can read shared formatting preferences directly, so `date_display_format` stays consistent across note-list, inspector, search, and metadata surfaces without forwarding props through unrelated layers. + +This narrows ADR-0026's blanket "no Context for data" rule. The replacement rule is: mutable application/domain state still lives in `App.tsx` plus focused hooks, while React context is allowed only for tightly scoped, cross-cutting UI preferences whose canonical value still originates from that same top-level state. + +Future additions to `AppPreferencesProvider` should stay small, renderer-local, and read-focused. If Laputa starts moving writable domain state, async workflows, or large derived objects into context, that needs a new ADR rather than quietly expanding this pattern. diff --git a/docs/adr/0116-rich-raw-transition-and-serialization-ownership.md b/docs/adr/0116-rich-raw-transition-and-serialization-ownership.md new file mode 100644 index 0000000..081aa6e --- /dev/null +++ b/docs/adr/0116-rich-raw-transition-and-serialization-ownership.md @@ -0,0 +1,44 @@ +--- +type: ADR +id: "0116" +title: "Rich/raw transition and serialization ownership" +status: active +date: 2026-05-13 +--- + +## Context + +Tolaria already relies on BlockNote as the rich editor and on a Markdown-first save path, but the rich/raw boundary had started to split that contract across multiple local helpers. Autosave and tab-swap logic serialized rich-editor content in one place, raw-mode entry rebuilt Markdown in another, and raw-mode toggles carried pending content and pending cursor/scroll restore state through separate refs. + +That fragmentation created two drift risks in one of the most correctness-sensitive parts of the app: + +- rich-editor Markdown output could diverge across autosave, tab switches, and raw-mode entry, especially around wikilink restoration, durable schema-node serialization, frontmatter preservation, and portable attachment paths +- raw/rich mode switches could desynchronize pending content from pending position restoration, making stale transition state harder to reason about and harder to harden + +Tolaria's editor contract already prioritizes no crashes, no stale overwrites, and minimal per-keystroke work. The rich/raw boundary needs the same single-owner discipline. + +## Decision + +**Tolaria centralizes BlockNote-to-Markdown serialization for editor flows in one shared owner and treats raw/rich mode handoff as explicit transition state with a single owner per concern.** + +Specifically: + +1. `src/utils/richEditorMarkdown.ts` is the canonical owner for rich-editor body/document serialization used by autosave, tab-swap, and raw-mode entry. +2. Raw-mode content handoff is modeled as one content transition object, so pending raw-exit content and raw-mode overrides move together. +3. Cursor/scroll restoration across rich/raw toggles is modeled as one restore-transition ref consumed by the editor-mode position sync path. +4. Editor surfaces should not keep independent ad hoc pending-content or pending-position refs outside those shared owners. + +## Options considered + +- **Shared serialization owner plus explicit transition owners** (chosen): keeps the Markdown contract and mode-switch lifecycle consistent across editor flows, while still allowing debounced work and focused testing. +- **Per-flow local serializers and pending refs**: simpler inside each hook, but lets autosave, tab-swap, and raw-mode entry drift apart over time. +- **Separate raw-mode-specific serialization and restore logic**: would preserve local autonomy, but duplicates correctness-sensitive rules at the exact boundary where stale state bugs are hardest to diagnose. +- **Always rebuild all content/position state synchronously on every toggle**: reduces retained transition state, but increases work at toggle time and does not solve ownership drift in shared serialization logic. + +## Consequences + +- Autosave, tab-switch flushing, and raw-mode entry now share one Markdown serialization contract. +- Wikilink restoration, durable editor-node serialization, frontmatter preservation, and portable attachment-path rewriting have one place to evolve. +- Rich/raw mode toggles become easier to reason about because content transition state and restore transition state each have a single owner. +- Future editor features that need rich-editor Markdown output or mode-transition bookkeeping should extend these shared owners rather than introducing local one-off refs or serializers. +- Re-evaluation is warranted if Tolaria adopts a different editor runtime or if rich/raw mode stops being a first-class bidirectional workflow. diff --git a/docs/adr/0117-appimage-fcitx-gtk3-frontend-bundle.md b/docs/adr/0117-appimage-fcitx-gtk3-frontend-bundle.md new file mode 100644 index 0000000..f779352 --- /dev/null +++ b/docs/adr/0117-appimage-fcitx-gtk3-frontend-bundle.md @@ -0,0 +1,24 @@ +# ADR-0117: Bundle the fcitx GTK3 frontend in Linux AppImages + +## Status + +Accepted + +## Context + +Linux AppImages run WebKitGTK through the GTK3 input-method stack. Users with fcitx5 can export `GTK_IM_MODULE=fcitx`, `QT_IM_MODULE=fcitx`, and `XMODIFIERS=@im=fcitx`, but the AppImage still cannot load the host GTK immodule reliably because the GTK module cache and library paths are isolated from the mounted AppDir. + +The previous AppImage startup fallback set `GTK_IM_MODULE=fcitx` when fcitx was detected, but it did not make the `im-fcitx5.so` module available inside the AppImage. That left Chinese/Japanese/Korean input dependent on host paths that GTK may not search from a sealed AppImage. + +## Decision + +Linux release jobs install `fcitx5-frontend-gtk3` and the AppImage output-plugin shim copies the GTK3 fcitx immodule plus its client library into the AppDir before the AppImage is sealed. At runtime, AppImage startup writes a mount-path-specific `GTK_IM_MODULE_FILE` cache that points GTK at the bundled module whenever fcitx is configured explicitly or through common fcitx environment hints. + +The sealed AppImage validation step extracts every produced AppImage and fails the release if the symlink-safe AppRun resolver, the bundled fcitx immodule, or the fcitx client library is missing. + +## Consequences + +- fcitx5 input works in AppImage launches without relying on the host GTK immodule cache path. +- X11 fallback launches with explicit `GTK_IM_MODULE=fcitx` use the same bundled module path as Wayland launches. +- Linux AppImage builds now depend on the distro package that provides the GTK3 fcitx frontend. +- If the Ubuntu package path changes, the AppImage validation step fails before publishing a broken bundle. diff --git a/docs/adr/0118-entry-scoped-note-windows-without-vault-index-scans.md b/docs/adr/0118-entry-scoped-note-windows-without-vault-index-scans.md new file mode 100644 index 0000000..a58f411 --- /dev/null +++ b/docs/adr/0118-entry-scoped-note-windows-without-vault-index-scans.md @@ -0,0 +1,35 @@ +--- +type: ADR +id: "0118" +title: "Entry-scoped note windows without vault index scans" +status: active +date: 2026-05-14 +--- + +## Context + +ADR-0031 kept secondary note windows on the full `App` shell so they would inherit the same editor capabilities as the primary window. That decision also accepted a full vault load per secondary window as the simpler trade-off. + +In practice, repeated note-window opens were paying that full-vault scan cost even when the window only needed one known note path. The startup path loaded the vault index and passed related entries into the editor even though note-window mode renders a single-note surface. That extra work increased window-open cost and made every secondary window depend on repository-wide entry hydration for a workflow that is intentionally scoped to one note. + +Tolaria still wants note windows to reuse the main App architecture rather than reviving a separate `NoteWindow` shell. The missing decision was how far the shared vault loader should go when the window contract is already narrowed to a single entry. + +## Decision + +**Secondary note windows continue to render the full `App` shell, but they no longer load the full vault index during startup.** In note-window mode, Tolaria skips the shared vault-entry scan, reloads only the requested note entry, and scopes editor entry props to that active note instead of passing repository-wide visible entries. + +This keeps the architectural benefit of ADR-0031 (one window architecture, one editor surface) while changing the data-loading contract for secondary windows from vault-scoped to entry-scoped. + +## Alternatives considered + +- **Full `App` shell with entry-scoped loading** (chosen): preserves feature parity in the shared shell while removing unnecessary full-vault scans for a one-note window. Trade-off: note windows should not assume vault-index-derived context is available by default. +- **Full `App` shell with full vault scan**: simplest continuation of ADR-0031, but repeats avoidable repository-wide I/O every time a note window opens. +- **Dedicated `NoteWindow` shell**: could be lighter still, but reintroduces the architectural drift and duplicated feature work that ADR-0031 intentionally removed. + +## Consequences + +- Opening a secondary note window no longer requires `list_vault`/full entry hydration before the editor can render the requested note. +- Repeated note-window opens avoid redundant vault scans and stay aligned with the product contract that these windows are single-note work surfaces. +- Features inside note-window mode must treat vault-index-derived data as opt-in; they cannot assume related entries are already loaded just because the full `App` shell is mounted. +- ADR-0031 remains directionally valid for shared window architecture, but its original "full vault load per secondary window" trade-off is no longer the operating model. +- Re-evaluate if note windows later need immediate repository-wide browsing context, or if future profiling shows the remaining single-entry reload path is still too expensive. diff --git a/docs/adr/0119-vault-neutral-mcp-registration-with-mounted-workspace-guidance.md b/docs/adr/0119-vault-neutral-mcp-registration-with-mounted-workspace-guidance.md new file mode 100644 index 0000000..a239285 --- /dev/null +++ b/docs/adr/0119-vault-neutral-mcp-registration-with-mounted-workspace-guidance.md @@ -0,0 +1,35 @@ +# 0119. Vault-Neutral MCP Registration With Mounted Workspace Guidance + +Status: active + +Date: 2026-05-14 + +## Context + +Tolaria used to register external MCP clients with a durable `VAULT_PATH` environment variable. That made the copied config easy to inspect, but it also meant Claude Code, Cursor, Gemini CLI, and generic MCP clients stayed pinned to whichever vault was active at setup time. + +Domenico Lupinetti's dynamic-vault MCP proposal in PR #603 pointed in the right direction: MCP clients should follow Tolaria's current workspace state instead of requiring users to reconnect after each vault change. The current app model has also moved from one selected vault toward mounted workspaces, so the MCP server needs to operate on every active mounted vault and load local agent guidance from each vault. + +## Decision + +Durable external MCP registration is vault-neutral. Tolaria still writes an explicit stdio MCP entry, but that entry contains only the Node command, `mcp-server/index.js`, and `WS_UI_PORT=9711`. It no longer writes `VAULT_PATH`. + +The Node MCP entrypoints resolve vaults at tool-call time: + +- Explicit `VAULT_PATH` and `VAULT_PATHS` environment variables continue to win for app-owned bridge launches and legacy/manual launches. +- When those env vars are absent, the MCP server reads Tolaria's `vaults.json`. +- `active_vault` is returned first. +- Every workspace in `vaults[]` is included unless it is explicitly marked `mounted: false`. +- Paths are deduplicated and blank paths are ignored. + +Vault context now checks each active mounted workspace root for `AGENTS.md` and returns those instructions with the vault summary. The MCP server also exposes `list_vaults` so agents can discover the active workspace set and whether each vault has root guidance. + +We are not adding a session-local `switch_vault` tool. A switch tool would create a second source of truth inside the MCP process, while Tolaria already owns mounted workspace state. + +## Consequences + +External MCP config survives vault switches and mounted-workspace changes without reconnecting. + +Agents can work across all active mounted vaults and receive the per-vault `AGENTS.md` instructions needed to respect local rules. + +Manual users can still override the resolved workspace set with `VAULT_PATH` or `VAULT_PATHS` when they intentionally want a static or scripted MCP session. diff --git a/docs/adr/0120-stable-appimage-mcp-server-path-with-opencode-registration.md b/docs/adr/0120-stable-appimage-mcp-server-path-with-opencode-registration.md new file mode 100644 index 0000000..b1bab70 --- /dev/null +++ b/docs/adr/0120-stable-appimage-mcp-server-path-with-opencode-registration.md @@ -0,0 +1,37 @@ +# 0120. Stable AppImage MCP Server Path With OpenCode Registration + +Status: active + +Date: 2026-05-14 + +## Context + +Domenico Lupinetti's PR #600 identified two gaps in durable external MCP setup: + +- Linux AppImage launches expose bundled resources through a mount path that can change between app starts, so external clients can keep a stale `mcp-server/index.js` path. +- OpenCode uses `~/.config/opencode/opencode.json` with a different MCP schema from Claude Code, Cursor, Gemini, and generic `mcpServers` clients. + +ADR-0119 made durable MCP registration vault-neutral, so PR #600 could not be merged directly: its registered entries still carried `VAULT_PATH`. The stable-path and OpenCode work is still valid, but it has to preserve the current mounted-workspace resolution model. + +## Decision + +On Linux AppImage startup, Tolaria extracts the bundled `mcp-server/` directory to `~/.local/share/tolaria/mcp-server/`. The extracted directory is version-gated by a `.tolaria-version` marker. Extraction runs on first launch or after an app version change, uses a staging directory plus rename, and uses a process lock so concurrent app launches do not write the stable directory at the same time. + +Durable external registration prefers the stable extracted server directory when it is ready. Otherwise it falls back to the packaged resource resolver. + +OpenCode is added to durable MCP registration and removal. Tolaria writes an OpenCode-specific entry under the top-level `mcp` key using: + +- `type: "local"` +- `command: [node, index.js]` +- `enabled: true` +- `environment.WS_UI_PORT = "9711"` + +OpenCode registration remains vault-neutral. It does not write `VAULT_PATH`; the Node MCP server resolves active mounted workspaces from Tolaria state per ADR-0119. + +## Consequences + +Linux AppImage users can register external MCP clients once and keep a valid `index.js` path across restarts and updates. + +OpenCode participates in the same connect, disconnect, and status flow as Claude Code, Cursor, Gemini, and generic MCP clients while preserving its own config schema. + +The stable path fixes the packaging lifecycle without reintroducing static vault pinning. diff --git a/docs/adr/0121-appimage-external-fallback-for-audio-and-video-previews.md b/docs/adr/0121-appimage-external-fallback-for-audio-and-video-previews.md new file mode 100644 index 0000000..b327f73 --- /dev/null +++ b/docs/adr/0121-appimage-external-fallback-for-audio-and-video-previews.md @@ -0,0 +1,34 @@ +--- +type: ADR +id: "0121" +title: "AppImage external fallback for audio and video previews" +status: active +date: 2026-05-15 +supersedes: "0110" +--- + +## Context + +ADR-0110 standardized in-app previews for image, audio, video, and PDF vault files through the shared `FilePreview` surface and Tauri asset URLs. In practice, Linux AppImage builds run audio and video playback through WebKitGTK, and that runtime has proven unstable enough that mounting the same in-webview media controls is not a reliable default for packaged Linux releases. + +Tolaria still needs one binary-preview model across platforms: previewability should remain renderer-inferred from filename extensions, binary files should remain ordinary vault entries, and external-open actions must continue to re-enter the active-vault command boundary before the OS opens a file. + +## Decision + +**Tolaria keeps in-app image and PDF previews everywhere, but Linux AppImage builds fall back to external-open controls for audio and video instead of mounting in-webview media playback.** + +- `FilePreview` remains the single renderer-owned surface for supported binary vault files. +- The preview policy is runtime-owned: the renderer asks the native runtime whether external media fallback is required before rendering audio or video elements. +- Linux AppImage builds return `true` for that runtime check and suppress in-webview audio/video previews; other targets keep the existing native HTML media controls. +- Editor-embedded BlockNote audio/video blocks follow the same runtime gate so binary preview behavior stays consistent between note bodies and file previews. + +## Alternatives considered +- **Runtime-gated external fallback on Linux AppImage** (chosen): keeps one preview architecture while containing a platform-specific runtime instability. Cons: AppImage users lose inline playback for audio/video. +- **Keep in-app audio/video previews on every platform**: preserves feature parity, but continues shipping a known unstable playback path on AppImage. +- **Disable all binary previews on Linux**: simpler policy, but unnecessarily removes stable image/PDF previews and weakens the file-first editor experience. + +## Consequences + +Tolaria now treats audio/video preview as a runtime capability decision rather than a universal guarantee of the binary preview system. Linux AppImage users see explicit external-open fallback controls for audio and video, while other platforms keep the richer in-app playback path. + +This keeps the filesystem-first binary model, scoped asset access, and active-vault validation boundary intact without introducing persisted media types or a separate media subsystem. Re-evaluate this decision if AppImage media playback becomes stable enough to restore inline playback without special handling, or if other packaged runtimes need their own preview capability gates. diff --git a/docs/adr/0122-scalar-array-frontmatter-properties.md b/docs/adr/0122-scalar-array-frontmatter-properties.md new file mode 100644 index 0000000..48c2c57 --- /dev/null +++ b/docs/adr/0122-scalar-array-frontmatter-properties.md @@ -0,0 +1,33 @@ +# 0122. Scalar Array Frontmatter Properties + +Status: active + +Date: 2026-05-15 + +## Context + +Saved Views filter against `VaultEntry.properties` in the renderer and in the Rust view evaluator. Before this decision, Tolaria preserved custom scalar frontmatter values as properties but dropped multi-element non-wikilink arrays during a full vault scan. That made a view such as `tags / contains / blues` unstable: optimistic renderer state could see a changed array for a while, but reload, view switch, or restart rebuilt the entry without the array-backed property. + +Relationship arrays already have separate semantics because wikilink-bearing fields are stored in `VaultEntry.relationships`. Plain scalar arrays need to stay queryable as custom properties without being treated as relationship fields. + +## Decision + +**Tolaria preserves custom scalar-array frontmatter fields in `VaultEntry.properties`, while wikilink-bearing arrays remain relationships.** Single-item scalar arrays continue to normalize to a scalar value for compatibility; multi-item scalar arrays remain arrays. + +Saved View filters evaluate scalar-array properties with set semantics. `contains` and `any_of` match exact case-insensitive elements, not substrings inside an element. Scalar properties keep their existing case-insensitive text matching behavior. + +The vault cache version is bumped so existing caches that dropped array properties are rebuilt from disk. + +## Options considered + +- **Option A (chosen): Preserve scalar arrays as properties** - keeps YAML frontmatter expressive, fixes reload/restart behavior, and avoids hardcoded fields such as `tags`. The cost is widening `VaultEntry.properties` from scalar-only to scalar-or-array. +- **Option B: Flatten arrays to comma-delimited strings** - keeps the old property type, but cannot distinguish exact elements from substrings and makes filters ambiguous. +- **Option C: Treat every array as a relationship** - reuses existing relationship matching, but non-wikilink values such as tags are not graph edges and should not appear as relationships. + +## Consequences + +Views can filter custom scalar arrays consistently across save, reload, view switches, and app restart. + +Property chips and sorting must tolerate property arrays. The note-list chip resolver already expands array values; custom-property sorting falls back to string comparison for arrays. + +Any future custom-property logic must handle `VaultPropertyValue` rather than assuming every property is a scalar. diff --git a/docs/adr/0123-full-vault-graph-for-secondary-note-windows.md b/docs/adr/0123-full-vault-graph-for-secondary-note-windows.md new file mode 100644 index 0000000..6ad8754 --- /dev/null +++ b/docs/adr/0123-full-vault-graph-for-secondary-note-windows.md @@ -0,0 +1,32 @@ +--- +type: ADR +id: "0123" +title: "Full vault graph for secondary note windows" +status: active +date: 2026-05-25 +supersedes: "0118" +--- + +## Context + +ADR-0118 made secondary note windows entry-scoped to avoid repeated full-vault scans. That reduced startup work, but it also removed the vault-index context that normal Tolaria capabilities depend on: properties, view actions, quick open/search, workspace-aware navigation, and other command surfaces no longer behaved like the main window. + +The product expectation is that opening a note in a separate window creates another capable Tolaria window, not a reduced editor shell. Performance remains important, but capability parity is the stronger contract. + +## Decision + +**Secondary note windows load the same active vault/workspace graph as a normal Tolaria window.** They still start in editor-only view mode and auto-open the requested note from the URL parameters, but the app keeps the shared vault loader, mounted-workspace filtering, watcher scope, editor entry list, and workspace-aware note actions enabled. + +`main.tsx` always mounts `App`; note-window mode is handled inside `App` through `getNoteWindowParams()` and `useNoteWindowLifecycle`. + +## Alternatives considered + +- **Entry-scoped note windows**: faster startup, but loses app-level features that require repository-wide context. Rejected because it breaks the secondary-window product contract. +- **Full app with full active graph** (chosen): keeps one window architecture and restores feature parity. Trade-off: each secondary window performs its own vault load. +- **Shared state from the main window over IPC**: could provide parity without duplicate scans, but adds synchronization complexity and failure modes. Deferred until profiling proves the duplicate load is a real bottleneck. + +## Consequences + +- Secondary windows can use normal Tolaria capabilities such as Properties, view actions, quick open/search, wikilink navigation, and workspace-aware note operations. +- Opening several note windows can repeat vault-loading work. This is acceptable for now because correctness and parity are more important than avoiding the scan. +- ADR-0118 is superseded. If secondary-window startup becomes too slow, optimize with shared state or incremental loading without removing app capabilities. diff --git a/docs/adr/0124-cached-secondary-note-window-startup.md b/docs/adr/0124-cached-secondary-note-window-startup.md new file mode 100644 index 0000000..a37f70d --- /dev/null +++ b/docs/adr/0124-cached-secondary-note-window-startup.md @@ -0,0 +1,27 @@ +--- +type: ADR +id: "0124" +title: "Cached secondary note window startup" +status: active +date: 2026-05-26 +supersedes: "0123" +--- + +## Context + +ADR-0123 restored secondary note windows to the normal `App` path so they retain the full vault/workspace graph required by Properties, quick open/search, wikilinks, and workspace-aware note actions. + +That parity is still the product contract, but forcing a fresh Tauri `reload_vault` during every secondary-window mount invalidates the backend cache. Opening several note windows can therefore repeat expensive full-vault scans even when the main window has already warmed the cache. + +## Decision + +**Secondary note windows keep the full vault graph, but their initial vault load uses the cached/incremental `list_vault` path instead of the forced `reload_vault` path.** + +Normal main-window startup continues to force a fresh initial reload. Explicit refresh paths, watcher-driven refreshes, and user-initiated reloads still use reload commands where they need disk freshness. + +## Consequences + +- Secondary note windows remain capable full app windows rather than reduced editor shells. +- Repeated note-window opens can reuse the backend vault cache instead of invalidating it on every startup. +- First open after a cold cache still scans the vault, then warms the shared backend cache for later windows. +- If the cached scan path is stale, the existing backend cache update logic remains responsible for incremental freshness. diff --git a/docs/adr/0126-renderer-action-history.md b/docs/adr/0126-renderer-action-history.md new file mode 100644 index 0000000..bce527f --- /dev/null +++ b/docs/adr/0126-renderer-action-history.md @@ -0,0 +1,29 @@ +--- +id: "0126" +title: "Renderer action history for app-level undo and redo" +status: "active" +date: "2026-05-26" +supersedes: + - "0106" +--- + +# ADR-0126: Renderer action history for app-level undo and redo + +## Context + +Tolaria already lets native text surfaces keep their own undo stacks, but app-level state changes such as frontmatter edits, archive toggles, favorite toggles, and organization toggles did not share a consistent undo/redo model. Routing all Undo and Redo through the native menu items left these app actions one-way while also making command-palette discoverability inconsistent. + +## Decision + +Introduce a renderer-owned `useActionHistory` stack for app-level actions. Supported actions record explicit undo and redo callbacks only after the write succeeds, clear redo after new user actions, and suppress nested recordings while a history entry is replaying. + +The Edit menu and command manifest now route Undo and Redo to renderer commands. Focused text-editing controls still receive native text history first through `document.execCommand('undo' | 'redo')`, so editor/input undo behavior remains separate from the app action stack. + +Destructive actions that are not safely reversible remain outside this stack and continue to rely on confirmation/destructive UX instead of pretending to be undoable. + +## Consequences + +- App-level history is scoped to the active renderer session and is not persisted across launches. +- Undo/redo labels can be surfaced in the command palette because the top stack entries expose labels. +- Menu accelerators and keyboard shortcuts use the shared command manifest instead of Tauri's native Undo/Redo menu builders. +- ADR-0106 remains valid for the broader menu ownership model, but its native Undo/Redo exception is superseded by this renderer action-history route. diff --git a/docs/adr/0127-native-ai-workspace-window.md b/docs/adr/0127-native-ai-workspace-window.md new file mode 100644 index 0000000..18aeeb5 --- /dev/null +++ b/docs/adr/0127-native-ai-workspace-window.md @@ -0,0 +1,32 @@ +--- +type: ADR +id: "0127" +title: "Native AI workspace window" +status: active +date: 2026-05-26 +--- + +## Context + +The AI panel used to behave like another right-side editor panel. That kept the agent UI inside the main Tolaria window even when the user undocked it, so the "floating" surface could not be moved to another macOS space or placed beside Tolaria as a real window. + +The AI surface also needed to support multiple chat sessions, per-chat target selection, and a single header that does not duplicate the old panel title and permission controls. + +## Decision + +**The AI surface is a renderer-owned `AiWorkspace` that can run either docked in the main app or in a dedicated native Tauri webview window labeled `ai-workspace`.** + +The docked and native-window modes share the same React workspace component. The native window boots the normal `App` path with `?window=ai-workspace`, skips main-window size constraints, and uses macOS overlay traffic lights. Close and minimize requests from that window emit a dock request back to the main window before destroying the pop-out window. + +## Options considered + +- **Native Tauri window** (chosen): gives macOS users real window movement, traffic lights, and normal desktop window management; requires route/window-mode plumbing and explicit dock events. +- **CSS floating panel inside the main window**: simple and preserves component state in one renderer, but it cannot leave the main window bounds and fails the expected macOS behavior. +- **Separate full AI app shell**: isolates the workspace, but would duplicate vault loading and settings flows more than necessary. + +## Consequences + +- The status-bar AI affordance opens the workspace; target selection now belongs in the workspace header. +- `AiWorkspace` owns multi-chat sidebar state and filters target choices to installed local agents plus configured local/API model providers. +- The old `AiPanel` remains the reusable transcript/composer surface, but its header and prompt/focus effects can be disabled when mounted inside workspace sessions. +- Pop-out/dock currently transfers the workspace at the window level; future persistence can promote active conversations into a shared store if users need exact in-flight chat reparenting across renderer instances. diff --git a/docs/adr/0128-lightweight-ai-workspace-window.md b/docs/adr/0128-lightweight-ai-workspace-window.md new file mode 100644 index 0000000..1c842d5 --- /dev/null +++ b/docs/adr/0128-lightweight-ai-workspace-window.md @@ -0,0 +1,35 @@ +--- +type: ADR +id: "0128" +title: "Lightweight AI workspace window" +status: active +date: 2026-05-26 +supersedes: "0127" +--- + +## Context + +ADR-0127 moved the AI workspace into a native Tauri window, but the first version booted the full `App` shell and used macOS overlay traffic lights. That made pop-out feel slow, duplicated startup work, and left the chat route dependent on main-window vault loading before agent turns could run. + +The AI workspace also needs installation-local chat metadata so user-facing chat titles and archive state survive dock/pop-out transitions without writing to a vault. + +## Decision + +**The AI workspace pop-out uses a lightweight renderer route backed by app settings metadata.** + +`openAiWorkspaceWindow()` opens the `ai-workspace` Tauri webview with `?window=ai-workspace` plus active vault context in URL params. `App` routes that window directly to `AiWorkspaceWindowApp`, which loads settings, AI agent status, and vault guidance without mounting the full vault/editor shell. The window is undecorated and transparent, and it relies on `AiWorkspace` headers for drag regions plus separate close and dock controls. Close only closes the pop-out; dock emits the main-window dock request before closing the pop-out. + +`settings.ai_workspace_conversations` stores only chat sidebar metadata: conversation id, title, archive state, and explicit target override. Prompt text, transcripts, note content, model credentials, and vault-local configuration stay out of app settings. + +## Options considered + +- **Lightweight AI route** (chosen): keeps pop-out startup focused on AI state and passes explicit vault context to the agent controller. +- **Full `App` route**: preserves maximum feature parity by default, but repeats vault/editor startup work and delays a window that should contain only the AI workspace. +- **Vault-stored chat metadata**: would travel with a vault, but chat labels and archive state are installation UI preferences rather than vault content. + +## Consequences + +- Pop-out startup avoids full note graph loading and should be close to instant after the Tauri webview is created. +- The dedicated AI window has no native traffic lights; users close or redock it through separate workspace header controls, and the rounded workspace shell defines the visible floating-window corners. +- Chat titles, archived state, and target overrides persist at the installation level in `settings.json`. +- Future transcript persistence must use a separate storage decision; `ai_workspace_conversations` is intentionally metadata-only. diff --git a/docs/adr/0129-tolaria-vault-item-deep-links.md b/docs/adr/0129-tolaria-vault-item-deep-links.md new file mode 100644 index 0000000..bbf44a0 --- /dev/null +++ b/docs/adr/0129-tolaria-vault-item-deep-links.md @@ -0,0 +1,45 @@ +--- +type: ADR +id: "0129" +title: "Tolaria vault item deep links" +status: active +date: 2026-05-27 +--- + +# ADR-0129: Tolaria vault item deep links + +## Context + +Users need durable links they can paste into calendars, task managers, chats, and other apps to return to a Tolaria vault item. The link needs to identify a registered vault, preserve the file extension so non-Markdown files can be opened, and fail clearly when the vault or item is unavailable. Links must not create or import files implicitly. + +Mounted workspaces make vault naming non-trivial. A readable slug is useful, but two vaults can share a label, alias, or folder name. URL parsing also cannot rely only on the browser URL implementation because dot-segment normalization can hide path traversal attempts before validation runs. + +## Decision + +Tolaria deep links use this shape: + +```text +tolaria:/// +``` + +The vault slug is generated from the registered workspace alias, then label, then path basename. When two vaults would share the same base slug, generated links append a stable short hash derived from the normalized vault path. A handwritten ambiguous base slug is rejected instead of picking an arbitrary vault. + +The path component is encoded per segment with `encodeURIComponent`, so spaces, Unicode, and reserved characters are preserved while `/` remains the path separator. Parsing rejects empty path segments, `.`, `..`, decoded separators inside a segment, unsafe Windows separators, and resolved paths outside the selected vault root. + +`src/utils/deepLinks.ts` owns URL building, parsing, and vault resolution. `src/hooks/useDeepLinks.ts` owns renderer integration: it receives Tauri deep-link events, validates them against the registered vault list, switches vaults when needed, reloads once if the target file is not in the current index yet, opens existing Markdown/text/binary entries, reports localized errors, and emits safe PostHog outcomes. Deep links are navigation-only; they never create missing files, import external content, or infer a fallback vault. + +The desktop shell registers the `tolaria` scheme through `tauri-plugin-deep-link` and keeps second launches focused through `tauri-plugin-single-instance`. Windows and Linux also call runtime `register_all()` as a repair step. macOS uses bundle scheme registration. Linux runtime registration is best-effort and is not part of the verified v1 support target. + +Copy surfaces are shared actions: + +- Breadcrumb overflow: `Copy note deeplink` +- Command palette: `Copy deep link to current item` +- File preview header: copy action for non-Markdown vault files + +## Consequences + +Path-based links are understandable and support every vault file kind, but a file rename changes the old link. A future stable item-id layer could supersede this URL shape while preserving path links as a readable fallback. + +Collision handling keeps generated links deterministic without exposing full local paths. Ambiguous handwritten slugs fail clearly, which is safer than opening the wrong vault. + +Renderer-owned resolution keeps the navigation logic close to mounted-workspace state and note selection. Native plugins stay responsible only for scheme registration, event delivery, and focusing the existing app instance. diff --git a/docs/adr/0130-windows-authenticode-release-signing.md b/docs/adr/0130-windows-authenticode-release-signing.md new file mode 100644 index 0000000..6b9507d --- /dev/null +++ b/docs/adr/0130-windows-authenticode-release-signing.md @@ -0,0 +1,36 @@ +--- +type: ADR +id: "0130" +title: "Windows Authenticode signing for release installers" +status: active +date: 2026-05-27 +--- + +## Context + +Tolaria's Windows release job already produced Tauri updater signatures, but those signatures are not the Windows trust signal used by SmartScreen, Smart App Control, Defender, or WDAC policies when a user downloads and runs an installer from the browser. A managed Windows 11 user reported that the stable NSIS installer was blocked by Windows Security with no bypass option. + +Microsoft's current guidance is that unsigned public installers can be fully blocked by enterprise policy, while signed installers at least carry a publisher identity and can build reputation across releases. Store distribution would provide the strongest SmartScreen outcome, but Tolaria does not currently publish a Microsoft Store package. + +## Decision + +**Tolaria release CI must Authenticode-sign Windows app executables and installers before publishing them.** + +- Alpha and stable Windows release jobs import a CI-provided code-signing certificate from GitHub secrets. +- The workflow generates a temporary Tauri config that sets `bundle.windows.certificateThumbprint`, `digestAlgorithm`, and `timestampUrl`, then passes that config to `pnpm tauri build`. +- The Windows job verifies the produced app executable and installer artifacts with `Get-AuthenticodeSignature` and fails before upload if any signature is missing, invalid, or signed by an unexpected certificate. +- The public stable download page requires an explicit Windows installer click and tells managed-device users that IT may need to approve the Tolaria publisher before first install. + +## Options considered + +- **CI-enforced Authenticode signing** (chosen): gives Windows users and enterprise admins a real publisher identity, lets certificate reputation transfer across releases, and blocks accidental publication of unsigned installers. Cons: release jobs now depend on code-signing secrets and a valid certificate. +- **Documentation-only SmartScreen warning**: cheaper, but it leaves managed-device users with no supported path when policy removes the bypass option. +- **Microsoft Store distribution only**: strongest SmartScreen behavior, but it requires a separate packaging, submission, and release-management path that Tolaria does not yet own. +- **Portable ZIP fallback**: still downloads executable content from the browser and can remain subject to SmartScreen, Mark-of-the-Web, Smart App Control, or WDAC policy. + +## Consequences + +- Windows release failures caused by missing or expired code-signing credentials are intentional release blockers. +- Tauri updater signatures remain required for in-app updates, but they are treated as separate from Windows Authenticode trust. +- Enterprise-managed Windows installs can be documented around a stable Tolaria publisher identity instead of asking users to disable security policy. +- A future Microsoft Store/MSIX distribution path can supersede or supplement this policy if Tolaria decides to support Store-managed installs. diff --git a/docs/adr/0131-reusable-release-artifact-build-workflow.md b/docs/adr/0131-reusable-release-artifact-build-workflow.md new file mode 100644 index 0000000..59ce0a9 --- /dev/null +++ b/docs/adr/0131-reusable-release-artifact-build-workflow.md @@ -0,0 +1,29 @@ +--- +type: ADR +id: "0131" +title: "Reusable release artifact build workflow" +status: active +date: 2026-05-28 +--- + +## Context + +Tolaria's alpha and stable release workflows both need to build the same platform artifact set: dual-architecture macOS updater bundles, optional stable macOS DMGs, Linux bundles, and signed Windows installers/updater bundles. Keeping those build jobs copied into both release workflows made platform fixes and validation changes easy to apply in one channel while accidentally leaving the other channel behind. + +The release workflows still differ in how they compute versions, create releases, and publish alpha vs. stable metadata, but the artifact build contract is shared. + +## Decision + +**Tolaria centralizes release artifact production in `.github/workflows/release-build-artifacts.yml`, invoked by alpha and stable release workflows through `workflow_call`.** Channel-specific workflows own versioning and publishing; the shared workflow owns platform build, signing, validation, and artifact upload behavior. + +## Alternatives considered + +- **Reusable artifact workflow** (chosen): keeps alpha and stable artifact behavior aligned while preserving separate channel-specific release orchestration. Cons: release behavior is split across one caller workflow and one called workflow, so debugging requires following both files. +- **Keep duplicated jobs in each release workflow**: makes each workflow self-contained, but every platform build fix must be applied twice and drift is likely. +- **Merge alpha and stable releases into one workflow**: reduces workflow count, but couples different trigger/version/publishing semantics and makes the release pipeline harder to reason about. + +## Consequences + +Alpha and stable releases now share one platform artifact contract, including macOS, Linux, and Windows validation. Changes to signing, cache keys, bundle validation, or platform matrices should happen in the reusable artifact workflow unless they are genuinely channel-specific. + +The architecture documentation should describe the release pipeline as channel orchestration plus shared artifact production, not as independent duplicated build job sets. diff --git a/docs/adr/0132-alpha-authenticode-soft-gate.md b/docs/adr/0132-alpha-authenticode-soft-gate.md new file mode 100644 index 0000000..b9b9925 --- /dev/null +++ b/docs/adr/0132-alpha-authenticode-soft-gate.md @@ -0,0 +1,25 @@ +--- +type: ADR +id: "0132" +title: "Alpha Authenticode soft gate" +status: active +date: 2026-05-28 +amends: "0130" +--- + +## Context + +ADR 0130 made Windows Authenticode signing mandatory for release installers. That is still the right requirement for stable promotions, but the repository does not yet have the Windows code-signing certificate secrets needed by CI. Because alpha releases run on every push to `main`, requiring those secrets there broke the continuous alpha channel before the certificate provisioning work was complete. + +## Decision + +**Alpha Windows artifacts keep building when Authenticode certificate secrets are absent; stable Windows artifacts still require Authenticode signing.** + +- The shared release artifact workflow accepts `require_windows_authenticode`. +- Alpha passes `false`, emits a workflow warning when certificate secrets are absent, and still requires Tauri updater signatures. +- Stable passes `true` and fails before building Windows artifacts unless certificate and password secrets are configured. +- When certificate secrets are present, both channels use the generated Tauri Authenticode config and verify Windows executable/installer signatures before upload. + +## Consequences + +The alpha updater channel remains live while Windows certificate provisioning is underway. Stable releases continue to enforce the Windows trust policy from ADR 0130 before public promotion. Once the certificate secrets are configured, alpha builds automatically regain Authenticode signing without another workflow change. diff --git a/docs/adr/0133-request-scoped-ai-stream-events.md b/docs/adr/0133-request-scoped-ai-stream-events.md new file mode 100644 index 0000000..12b9f6d --- /dev/null +++ b/docs/adr/0133-request-scoped-ai-stream-events.md @@ -0,0 +1,27 @@ +--- +type: ADR +id: "0133" +title: "Request-scoped AI stream event channels" +status: active +date: 2026-05-29 +--- + +## Context + +AI agent streams and direct model streams used shared Tauri event names (`ai-agent-stream` and `ai-model-stream`). That worked while only one stream of each kind was active, but the side AI workspace and pop-out window can make concurrent or rapidly reused AI sessions more likely. Shared channels risk routing deltas, tool events, or completion events to the wrong renderer listener. + +## Decision + +**Each native AI stream uses a request-scoped Tauri event channel generated by the renderer and validated by the backend.** + +The renderer creates a unique event name with the stream's stable base prefix, listens on that channel, and passes it to the `stream_ai_agent` or `stream_ai_model` command as `event_name`. The Rust command accepts only scoped names that match the expected base prefix and safe character set; invalid or missing names fall back to the legacy shared channel. + +## Alternatives considered + +- **Request-scoped renderer channel** (chosen): isolates simultaneous streams without changing the stream event payload shape, while preserving backward-compatible command defaults. +- **Keep shared static event names**: simpler, but concurrent agent/model sessions can cross-deliver stream events between workspaces or chats. +- **Backend-generated channel names**: centralizes validation, but requires a setup handshake before the renderer can subscribe and complicates the current fire-and-stream command flow. + +## Consequences + +Concurrent AI streams can run without contaminating each other's renderer callbacks. The backend keeps a narrow validation boundary for externally supplied event names. Any future AI streaming command should follow the same base-prefix plus scoped-suffix convention instead of adding another process-wide static channel. diff --git a/docs/adr/0134-direct-shiki-language-registrations.md b/docs/adr/0134-direct-shiki-language-registrations.md new file mode 100644 index 0000000..5eeb8d1 --- /dev/null +++ b/docs/adr/0134-direct-shiki-language-registrations.md @@ -0,0 +1,27 @@ +--- +type: ADR +id: "0134" +title: "Direct Shiki language registrations for code blocks" +status: active +date: 2026-05-29 +--- + +## Context + +Tolaria uses `@blocknote/code-block` for rich-editor fenced-code highlighting. The bundled BlockNote highlighter covers the common web and systems languages already in the editor menu, but it does not include several common Shiki grammars such as PowerShell, VBScript, Dart, Dockerfile, Terraform/HCL, and TOML. Users still expect imported fences like `powershell`, `ps1`, `vb`, and `vbscript` to highlight, show a valid language picker state, and serialize back to a stable Markdown fence. + +## Decision + +**Tolaria keeps BlockNote's code-block integration and adds direct, lazy `@shikijs/langs` registrations for missing common languages and aliases.** + +## Options considered + +- **Keep only the BlockNote bundle**: simplest, but leaves PowerShell/VBScript and other common fences unsupported. +- **Register selected `@shikijs/langs` grammars lazily** (chosen): preserves BlockNote's schema and parser path while adding only the extra grammars users need. +- **Replace BlockNote's highlighter with a full custom Shiki bundle**: more control, but a larger structural change than the current requirement needs. + +## Consequences + +`src/components/codeBlockOptions.ts` remains the owner of the BlockNote highlighter configuration, but extra grammar modules are now imported directly from `@shikijs/langs` only when a matching fence or picker value needs highlighting. `src/utils/codeBlockLanguageCatalog.ts` owns the supported extra language labels and aliases, and `src/utils/codeBlockLanguage.ts` normalizes known imported aliases such as `ps1` and `vb` to the canonical picker language. + +The language menu grows, but unsupported aliases still fail safely by staying as plain explicit fence names. If Tolaria later needs a generated language bundle, export-time highlighting, or a substantially smaller menu, this ADR should be superseded by a custom Shiki packaging decision. diff --git a/docs/adr/0134-sheet-nodes-with-plain-text-workbook-storage.md b/docs/adr/0134-sheet-nodes-with-plain-text-workbook-storage.md new file mode 100644 index 0000000..b82e541 --- /dev/null +++ b/docs/adr/0134-sheet-nodes-with-plain-text-workbook-storage.md @@ -0,0 +1,49 @@ +# ADR 0134: Sheet Nodes With Plain-Text Workbook Storage + +## Status + +Experimental + +## Context + +Tolaria should support spreadsheet-style content without losing its file-first, local-first, offline-first, and Git-friendly model. A sheet must remain inspectable and editable as plain text, while the interactive editor should behave like a spreadsheet and avoid a custom grid implementation owned by Tolaria. + +## Decision + +Notes whose frontmatter resolves to `_display: sheet` are displayed with a dedicated sheet editor instead of the block note editor. `type` stays semantic and organizational metadata, so sheets can belong to any Tolaria type. The body of the note is CSV-like plain text containing cell inputs and formulas. Sheet presentation metadata is stored in the same note frontmatter under `_sheet`. + +Example: + +```yaml +--- +type: Project +_display: sheet +_sheet: + frozen_rows: 1 + frozen_columns: 1 + columns: + B: + width: 180 + cells: + C6: + num_fmt: "0.00%" + bold: true + font_size: 15 + border_top: "thin #ff0000" +--- +Metric,January,February +Revenue,1200,=B2*1.1 +``` + +The prototype uses IronCalc's workbook package for the spreadsheet UI and formula engine. Tolaria adapts between the plain-text note representation and the IronCalc workbook model on load/save. + +## Consequences + +- Spreadsheet data remains legible and diffable in Git. +- Common workbook UI behavior, including selection, keyboard navigation, formatting controls, and copy/paste, is delegated to IronCalc. +- Tolaria-owned code is limited to format routing, the plain-text adapter, persistence safeguards, product-specific control hiding, and formula autocomplete. It should not grow into a custom spreadsheet grid. +- The current prototype is intentionally single-sheet. Cross-note cell references remain a future Tolaria extension, not an IronCalc multi-sheet feature. +- `_sheet` stores common presentation state as plain YAML, including column widths, row heights, grid-line visibility, frozen rows/columns, number formats, borders, and basic cell styling. +- Metadata extraction is bounded and save serialization is debounced, with an idle-time pass when available, to avoid unbounded autosave scans. Larger workbooks may need incremental dirty-range tracking before this becomes production-ready. +- Formula autocomplete is a small Tolaria-side enhancement over IronCalc's input surface; broader spreadsheet UI behavior should stay delegated to the workbook package. +- Simple Markdown wrappers in imported non-formula CSV cells can seed initial bold, italic, and strike cell styles, but saved sheet styling is represented in `_sheet` metadata rather than inline Markdown markers. diff --git a/docs/adr/0135-clean-active-note-refresh-after-external-edit.md b/docs/adr/0135-clean-active-note-refresh-after-external-edit.md new file mode 100644 index 0000000..ca134be --- /dev/null +++ b/docs/adr/0135-clean-active-note-refresh-after-external-edit.md @@ -0,0 +1,44 @@ +--- +type: ADR +id: "0135" +title: "Clean active notes refresh immediately after external edits" +status: active +date: 2026-05-30 +supersedes: "0111" +--- + +## Context + +ADR-0111 made external vault refreshes path-aware and preserved focused editor mounts so unrelated watcher events would not disrupt cursor state. That avoided needless remount churn, but it also meant a clean active note edited by Codex or another external process could remain visibly stale while the editor stayed focused. Because no later safe-remount trigger was guaranteed, users could see the old content until a full app restart. + +Tolaria's filesystem-first model requires clean in-memory editor state to converge to the file on disk during the current session. Unsaved local editor buffers still need protection, but editor focus alone is not enough reason to keep showing stale content when the changed-path batch identifies the active file. + +## Decision + +**External vault refreshes now remount a clean active note immediately when the external changed-path batch includes that note, regardless of editor focus.** + +The shared `refreshPulledVaultState()` path applies these rules: + +1. Reload vault entries, folders, and saved views together for every external change batch. +2. If there is no active note, stop after the shared reload. +3. If the active note changed during the async reload, stop rather than reopening stale context. +4. If the active note has unsaved local edits, keep the current editor buffer mounted. +5. If the active file disappeared, close the tab instead of leaving a stale editor behind. +6. If the changed-path batch includes the clean active file, close and reopen the active tab from disk even when focus is inside the rich or raw editor. +7. Unknown or unrelated change batches refresh vault-derived state without remounting the active editor. + +Git pulls, AI-agent refresh callbacks, and filesystem-watcher batches continue to converge through this single reconciliation helper instead of adding separate reload policies. + +## Alternatives considered + +- **Immediate clean active-note remount** (chosen): restores filesystem convergence for Codex and other external note edits while preserving unsaved local edits. Cons: a focused clean editor can lose cursor state when its own file changes externally. +- **Keep focused-editor preservation from ADR-0111**: avoids cursor disruption, but can leave the active editor stale indefinitely. +- **Defer active-note reload until blur**: reduces focus disruption, but adds another pending-refresh state machine and still allows the active editor to show stale disk content for an unbounded editing session. + +## Consequences + +- External edits to the currently open clean note become visible without restarting Tolaria. +- Unsaved local content remains authoritative and is not replaced by watcher, pull, or agent refreshes. +- The changed-path batch remains part of the external-refresh contract; callers should pass specific file paths whenever available. +- Unrelated watcher events still avoid active-editor remounts, so broad vault churn does not disturb the editor unless the active file itself changed. +- ADR-0111 is superseded by this stronger filesystem-convergence rule. diff --git a/docs/adr/0136-macos-webview-pdf-export.md b/docs/adr/0136-macos-webview-pdf-export.md new file mode 100644 index 0000000..cb329a2 --- /dev/null +++ b/docs/adr/0136-macos-webview-pdf-export.md @@ -0,0 +1,31 @@ +# ADR-0136: macOS Webview PDF Export + +## Status + +Accepted + +## Context + +The first note PDF export implementation reused the native webview print command. On macOS that opens the full printer dialog, which is not the product behavior expected from "Export note as PDF"; users should choose a filesystem destination and get a PDF directly. + +Tolaria already renders the exportable note in the live BlockNote DOM and applies print-only CSS so math, Mermaid, images, code blocks, tables, links, and custom blocks follow the same rendering path users see in the editor. Introducing a second Markdown-to-PDF renderer would duplicate that rendering logic and create drift. + +## Decision + +Use the existing Tauri webview and WebKit's own print operation to save the current webview directly to a chosen PDF path. The renderer remains responsible for export preparation: + +- exit raw/diff modes +- apply the PDF export body class +- ask the user for a `.pdf` destination +- invoke the native `export_current_webview_pdf` command + +The native command uses direct `objc2`, `objc2-app-kit`, `objc2-foundation`, and `objc2-web-kit` dependencies on macOS only. These crates are already part of Tauri's platform stack; declaring them directly lets Tolaria ask `WKWebView` for a WebKit-aware `NSPrintOperation` without adding a separate PDF rendering engine. + +Windows, Linux, and browser mode keep print-dialog fallback behavior because they do not have the macOS WebKit/AppKit direct PDF save path yet. The renderer checks the native capability before opening a filesystem save dialog, so unsupported platforms do not ask for a destination that cannot be used. + +## Consequences + +- The macOS path opens a save-file dialog, not the printer dialog. +- The exported PDF keeps using the rendered note DOM, so frontmatter stays excluded by the existing rich-editor body extraction. +- The feature depends on macOS WebKit/AppKit behavior for direct PDF output. Other desktop platforms use the existing native print dialog until they get a platform-specific direct PDF path. +- The new direct dependencies must stay target-scoped to macOS so Linux and Windows builds do not compile AppKit crates. diff --git a/docs/adr/0137-shared-rich-editor-input-transforms.md b/docs/adr/0137-shared-rich-editor-input-transforms.md new file mode 100644 index 0000000..d3c3a0d --- /dev/null +++ b/docs/adr/0137-shared-rich-editor-input-transforms.md @@ -0,0 +1,58 @@ +--- +type: ADR +id: "0137" +title: "Shared rich-editor input transforms" +status: active +date: 2026-06-07 +--- + +## Context + +Tolaria has several Markdown-style conveniences in the rich BlockNote editor: +typed arrows become ligatures, completed inline math becomes a math node, and +completed `==highlight==` syntax becomes the durable highlight mark. + +These features were added incrementally as separate `beforeinput` extensions. +Each extension repeated the same lifecycle work: reading the live ProseMirror +view, skipping IME composition, guarding stale views, dispatching transactions, +preventing native input only after a successful transform, and recovering known +BlockNote/ProseMirror transform failures. The syntax matchers differed, but the +execution shell was parallel enough that each new Markdown affordance risked a +slightly different edge-case policy. + +## Decision + +**Tolaria routes rich-editor Markdown input transforms through one shared +`beforeinput` execution path.** + +`src/components/richEditorInputTransform.ts` owns the common lifecycle, +dispatch, and recoverable-error behavior. Feature files such as +`arrowLigaturesExtension.ts`, `mathInputExtension.ts`, and +`markdownHighlightInputExtension.ts` expose small transform objects that only +decide whether the current input event should produce a transaction. + +`src/components/richEditorInputTransformExtension.ts` composes the Markdown +transform set used by the main editor and hidden editor probe. + +## Options Considered + +- **Shared transform primitive with feature-owned matchers** (chosen): removes + duplicate listener, dispatch, composition, and recovery code while keeping each + syntax rule local and testable. +- **One monolithic Markdown input extension**: reduces listener count, but mixes + unrelated syntax rules in one file and makes each future input affordance + harder to test independently. +- **Keep one extension per feature**: preserves local ownership, but keeps the + repeated edge-case shell and invites divergence as more transforms are added. + +## Consequences + +- `Editor` mounts one Markdown input-transform extension rather than separate + arrow, math, and highlight `beforeinput` listeners. +- Feature-specific files remain responsible for their syntax matching and + transaction construction only. +- Recoverable transform errors use the same telemetry event and fallback policy + across Markdown input transforms. +- New rich-editor Markdown input affordances should plug into the shared + transform primitive instead of adding another capture-phase `beforeinput` + extension. diff --git a/docs/adr/0138-authenticode-required-for-all-release-channels.md b/docs/adr/0138-authenticode-required-for-all-release-channels.md new file mode 100644 index 0000000..a1f9bf0 --- /dev/null +++ b/docs/adr/0138-authenticode-required-for-all-release-channels.md @@ -0,0 +1,32 @@ +--- +type: ADR +id: "0138" +title: "Require Authenticode signing for all Windows release channels" +status: active +date: 2026-06-09 +supersedes: "0132" +amends: "0130" +--- + +## Context + +ADR 0132 temporarily allowed alpha Windows artifacts to build without Authenticode when the repository did not yet have Windows code-signing certificate secrets. That kept the alpha channel moving during certificate provisioning, but it also normalized unsigned Windows artifacts and made stable promotion exceptions easy to repeat. + +At the same time, CI coverage uploads started failing because the pinned Codecov action still fetched Codecov's retired Keybase public-key account while the current Codecov CLI signatures use the original key from `codecovsecops`. That failure is unrelated to Windows Authenticode, but it blocked the same mainline quality lane and made release-readiness harder to reason about. + +## Decision + +**All Windows release artifacts must be Authenticode-signed before upload, for both alpha and stable channels.** + +- The reusable release artifact workflow no longer accepts a soft-gate input for Windows Authenticode. +- The Windows build validates that Tauri updater signing secrets and Windows Authenticode certificate/password secrets are present before packaging starts. +- The Windows build always passes the generated Authenticode config to `pnpm tauri build`. +- The Windows build always verifies the app executable and installer signatures with `Get-AuthenticodeSignature` before artifact upload. +- Codecov uploads use the patched `codecov-action` release that imports Codecov's current public key source, preserving CLI integrity validation instead of skipping it. + +## Consequences + +- Missing, expired, partial, or invalid Windows code-signing credentials fail alpha and stable release artifact builds. +- The repository cannot publish unsigned Windows installers as a convenience fallback. +- A trusted Windows code-signing certificate still has to come from a certificate authority or signing service; generating a local self-signed certificate is not an acceptable substitute for release artifacts. +- If Tolaria later adopts Microsoft Trusted Signing, Store packaging, or another signing provider, that integration should replace the PFX secret import path while preserving mandatory verification before upload. diff --git a/docs/adr/0139-temporary-windows-authenticode-soft-gate.md b/docs/adr/0139-temporary-windows-authenticode-soft-gate.md new file mode 100644 index 0000000..1fa4150 --- /dev/null +++ b/docs/adr/0139-temporary-windows-authenticode-soft-gate.md @@ -0,0 +1,26 @@ +--- +type: ADR +id: "0139" +title: "Temporary Windows Authenticode soft gate" +status: active +date: 2026-06-09 +supersedes: "0138" +amends: "0130" +--- + +## Context + +ADR 0138 made Authenticode mandatory for every Windows release artifact. That is the desired end state, but the repository still does not have a trusted Windows code-signing certificate or signing service configured. Enforcing Authenticode before provisioning is complete blocks the alpha and stable release pipelines even though the Tauri updater signatures are available and still protect updater integrity. + +## Decision + +**Windows Authenticode signing is temporarily optional for both alpha and stable release builds.** + +- Windows builds still require `TAURI_SIGNING_PRIVATE_KEY` and `TAURI_KEY_PASSWORD`, so updater artifacts remain signed. +- When `WINDOWS_CODE_SIGNING_CERTIFICATE`/`WINDOWS_CERTIFICATE` and the matching password secret are present, CI imports the certificate, builds with the generated Authenticode config, and verifies executable and installer signatures with `Get-AuthenticodeSignature`. +- When both Windows Authenticode certificate and password secrets are absent, CI emits a warning and builds Windows artifacts without Authenticode. +- Partial Authenticode configuration remains a hard error because it is ambiguous and easy to misread as a signed release. + +## Consequences + +Windows releases can continue while certificate provisioning is pending. This does not provide the Windows publisher identity needed by SmartScreen, Smart App Control, Defender, or WDAC-managed environments; those policies can still block the installer until a trusted Authenticode certificate or signing service is configured. Reinstating a hard gate should be a small workflow change once the certificate path exists. diff --git a/docs/adr/0140-extension-based-raw-text-syntax-highlighting.md b/docs/adr/0140-extension-based-raw-text-syntax-highlighting.md new file mode 100644 index 0000000..637f891 --- /dev/null +++ b/docs/adr/0140-extension-based-raw-text-syntax-highlighting.md @@ -0,0 +1,35 @@ +--- +type: ADR +id: "0140" +title: "Extension-based raw text syntax highlighting" +status: active +date: 2026-06-17 +--- + +## Context + +Tolaria already indexes UTF-8 non-Markdown vault files as `fileKind: "text"` and opens them in the raw CodeMirror editor. Discussion #872 asked for those files to receive syntax highlighting by file extension, especially `.sql`, `.json`, `.py`, and `.yaml`, matching the expectation set by highlighted fenced code blocks in Markdown notes. + +The raw editor previously installed the Markdown language extension for every raw file. That made Markdown notes work, but `.sql`, `.json`, `.py`, and `.yaml` files rendered as effectively plain text or Markdown-shaped text instead of using their own grammars. + +## Decision + +Tolaria maps raw editor file extensions to CodeMirror language packages at editor creation time: + +- Markdown files keep the existing frontmatter-aware Markdown path. +- YAML, JSON, Python, SQL, JavaScript, and TypeScript-like files use the official CodeMirror language packages for those grammars. +- Unknown text files stay plain instead of inheriting Markdown highlighting. +- Markdown-only frontmatter decorations and warnings stay scoped to Markdown files. + +## Options considered + +- **Use official CodeMirror language packages** (chosen): keeps raw editing inside CodeMirror, provides maintained incremental parsers, and avoids a second rendering surface. +- **Reuse Shiki from BlockNote code blocks**: visually closer to rich-editor code blocks, but Shiki is a static highlighter and would require a parallel CodeMirror decoration pipeline. +- **Keep Markdown highlighting for every raw file**: no new dependencies, but fails the requested behavior and treats non-Markdown files as Markdown. + +## Consequences + +- New runtime dependencies: `@codemirror/lang-javascript`, `@codemirror/lang-json`, `@codemirror/lang-python`, and `@codemirror/lang-sql`. +- `src/utils/rawEditorLanguage.ts` owns extension-to-language mapping. +- `src/extensions/rawEditorLanguage.ts` owns the CodeMirror extension selection and keeps Markdown frontmatter highlighting scoped to Markdown files. +- Additional languages should be added by extending the mapping and using official CodeMirror language packages where available. diff --git a/docs/adr/0141-scoped-linux-webkit-rendering-safeguards.md b/docs/adr/0141-scoped-linux-webkit-rendering-safeguards.md new file mode 100644 index 0000000..d9f8242 --- /dev/null +++ b/docs/adr/0141-scoped-linux-webkit-rendering-safeguards.md @@ -0,0 +1,33 @@ +--- +type: ADR +id: "0141" +title: "Scoped Linux WebKit rendering safeguards" +status: active +date: 2026-06-18 +--- + +## Context + +Tolaria needs Linux WebKitGTK startup safeguards because some Wayland/AppImage environments crash before the app can render. The existing startup path treated native Linux Wayland launches and sealed Linux AppImage launches the same way by setting both `WEBKIT_DISABLE_DMABUF_RENDERER=1` and `WEBKIT_DISABLE_COMPOSITING_MODE=1` unless the user had already provided either variable. + +That broad fallback protected unstable AppImage launches, but it also applied the last-resort compositing disablement to native Wayland sessions. Native Wayland still needs the DMABUF crash workaround, but disabling WebKit compositing there can make windows feel unresponsive. The sealed AppImage runtime remains the verified environment that needs both rendering overrides. + +## Decision + +Tolaria scopes Linux WebKit rendering safeguards by launch environment: + +- Native Linux Wayland launches set `WEBKIT_DISABLE_DMABUF_RENDERER=1` by default, while preserving WebKit compositing unless the user explicitly disables it. +- Linux AppImage launches continue to set both `WEBKIT_DISABLE_DMABUF_RENDERER=1` and `WEBKIT_DISABLE_COMPOSITING_MODE=1` by default because the sealed AppImage path has the verified rendering failure this fallback protects. +- User-provided environment values remain authoritative per variable, so advanced users and distro-specific workarounds can override either safeguard. + +## Alternatives considered + +- **Scope the fallback by launch environment** (chosen): keeps the proven AppImage protection while avoiding a heavier native Wayland workaround that hurts responsiveness. Cons: the startup policy now distinguishes AppImage from native Linux sessions. +- **Keep both overrides for all Wayland and AppImage launches**: simplest and maximally conservative for crash avoidance, but applies the last-resort compositing workaround beyond the environment that actually needs it. +- **Remove the WebKit rendering overrides entirely**: restores default WebKitGTK behavior, but would reopen known Linux startup crashes in AppImage/Wayland environments. + +## Consequences + +Native Linux Wayland users keep the broad DMABUF crash workaround without losing WebKit compositing by default. AppImage users keep the sealed-runtime fallback that has been validated against the startup crash class. + +Future Linux rendering work should treat `linux_appimage.rs` startup overrides as a capability/policy boundary, not as a single global Linux switch. Re-evaluate this decision if WebKitGTK or the AppImage runtime no longer requires these environment safeguards, or if another packaged Linux runtime develops a distinct rendering failure mode. diff --git a/docs/adr/0142-rich-editor-prosemirror-decoration-dependency.md b/docs/adr/0142-rich-editor-prosemirror-decoration-dependency.md new file mode 100644 index 0000000..9cac056 --- /dev/null +++ b/docs/adr/0142-rich-editor-prosemirror-decoration-dependency.md @@ -0,0 +1,29 @@ +--- +type: ADR +id: "0142" +title: "Rich editor ProseMirror decoration dependency" +status: active +date: 2026-06-22 +--- + +## Context + +Tolaria's rich editor needs per-node RTL rendering for quote blocks whose Markdown begins with an Obsidian callout marker such as `[!note]`. Browser `dir="auto"` sees the Latin marker first and resolves the quote as LTR, leaving the quote rail on the left even when the title/body are Hebrew or Arabic. + +External DOM patching is not reliable here because BlockNote/ProseMirror owns those nodes and can replace them after mutations. The styling decision must be expressed through the editor render pipeline. + +## Decision + +Add `@tiptap/pm` as a direct dependency and use ProseMirror decorations from a BlockNote extension for rich-editor text-direction overrides. + +The extension decorates RTL quote nodes with Tolaria-specific direction attributes/classes. CSS then uses those stable decoration attributes to move quote rails to the logical start side. + +## Alternatives considered + +- **ProseMirror decorations via `@tiptap/pm`** (chosen): uses the editor's supported render layer and avoids DOM reconciliation fights. Cons: makes a transitive ProseMirror facade dependency direct. +- **MutationObserver DOM patching**: avoids a direct dependency, but ProseMirror strips or replaces externally mutated editor nodes. +- **Pure CSS logical properties only**: works when the element's direction is already correct, but cannot ignore leading callout marker syntax when computing direction. + +## Consequences + +Rich-editor RTL quote and callout-marker rendering can be tested deterministically through the existing BlockNote render path. Future per-node editor presentation rules should prefer ProseMirror decorations over post-render DOM mutation when the target node is owned by BlockNote. diff --git a/docs/adr/0143-shared-focus-ownership-guard.md b/docs/adr/0143-shared-focus-ownership-guard.md new file mode 100644 index 0000000..47a3e06 --- /dev/null +++ b/docs/adr/0143-shared-focus-ownership-guard.md @@ -0,0 +1,32 @@ +--- +type: ADR +id: "0143" +title: "Shared focus ownership guard" +status: active +date: 2026-06-23 +--- + +## Context + +Tolaria has multiple renderer surfaces that intentionally reject programmatic focus while another surface owns keyboard input. The rich editor must not steal focus back from the Properties panel, and the sheet editor must not let IronCalc autofocus reclaim keyboard capture after focus moves to app chrome or dialogs. + +The editor and sheet implementations previously installed separate `HTMLElement.prototype.focus` patches and separate document focus listeners. That duplicated lifecycle made unmount order matter: removing one surface guard could restore the native focus method while another guard was still mounted. + +## Decision + +Use one shared focus ownership registry for global focus interception. + +`src/hooks/focusOwnershipGuard.ts` owns the single `HTMLElement.prototype.focus` patch, document focus/pointer listeners, outside-target memory, blocked-focus restoration, and cleanup. Surface modules register scoped ownership policy: + +- `src/hooks/editorFocusOwnership.ts` decides when rich-editor focus is suspended or resumed. +- `src/components/sheet-editor/useGuardedWorkbookFocus.ts` decides when workbook focus requires active sheet keyboard capture and no external focus surface. + +## Alternatives considered + +- **Shared global registry with surface-owned policy** (chosen): removes duplicate patch/listener lifecycle while preserving editor and sheet behavior locally. +- **Keep stacked surface-specific patches**: minimizes immediate movement but keeps cleanup-order bugs and duplicated outside-focus restoration. +- **Move all editor and sheet focus policy into one module**: centralizes more code, but mixes unrelated surface rules and makes future policy changes harder to review. + +## Consequences + +Only the shared guard may patch `HTMLElement.prototype.focus` or install document-level focus ownership listeners. New editor-like surfaces should register a scoped policy through the shared guard instead of adding another prototype patch. diff --git a/docs/adr/0144-collections-and-presentations.md b/docs/adr/0144-collections-and-presentations.md new file mode 100644 index 0000000..86e689a --- /dev/null +++ b/docs/adr/0144-collections-and-presentations.md @@ -0,0 +1,48 @@ +--- +type: ADR +id: "0144" +title: "Collections carry presentation configuration" +status: active +date: 2026-06-24 +--- + +## Context + +Tolaria already has several ways to select a group of notes: built-in sidebar filters such as All Notes and Inbox, type sections, folder rows, saved Views, and Neighborhood mode around one note. Product-wise, these are all collections of notes. The current implementation still routes most of them through a component named `NoteList`, which makes the list presentation look like the domain model. + +Spreadsheets also introduced a parallel single-note concern: a note can keep the same durable identity while choosing a different display mode through `_display`. Collections need the same separation between the notes being selected and how those notes are presented. Future presentations such as boards, calendars, tables, timelines, and graphs need presentation-specific field mappings, such as board column field, calendar start/end fields, or table columns, without inventing a parallel data model. + +## Decision + +**Tolaria treats a Collection as the internal representation of a selected group of notes plus its presentation configuration.** A collection can be built from a saved View YAML file, a type section, a built-in sidebar filter, a folder, or Neighborhood mode. The first supported presentation is `list`, preserving current behavior. + +Saved Views remain the most configurable persisted collection artifact. Existing top-level saved-view fields (`sort`, `listPropertiesDisplay`, `filters`, `order`, `name`, `icon`, `color`) remain valid. The renderer normalizes them into an in-memory collection presentation: + +```yaml +presentation: + type: list + sort: modified:desc + properties: + - status + - owner +``` + +Future saved-view YAML may store nested `presentation` configuration. For compatibility, Tolaria reads legacy top-level list fields and lets nested `presentation.type: list` override them in memory. The current implementation does not rewrite existing YAML into the nested shape. + +`SidebarSelection` remains the navigation input for now. Renderer code adapts it to `CollectionDefinition` through `src/collections/collectionFromSelection.ts`, and resolves visible entries through `src/collections/resolveCollectionEntries.ts`. This is an implementation bridge, not a new user-visible concept. + +## Options considered + +- **Use one Collection concept with nested presentation config** (chosen): keeps the product model small, matches saved-view YAML, and lets built-in sections and type sections behave like generated collections without exposing separate "source" terminology. +- **Separate CollectionSource and CollectionPresentation concepts everywhere**: precise internally, but adds vocabulary and wiring before Tolaria has multiple presentations. It remains a possible implementation detail later, not a product concept now. +- **Keep adding one-off branches to NoteList/App.tsx**: fastest for the next feature, but makes boards, calendars, and graph-like surfaces compete with list-specific assumptions. +- **Use `kind` on saved views**: already explored in prior Kanban work, but `kind` is vague and "view" is overloaded across saved Views, app view mode, and note display. `presentation.type` is clearer and leaves room for presentation-specific config. + +## Consequences + +- Current UI behavior remains unchanged: every existing collection still renders as the list presentation. +- New collection presentations should consume resolved notes and presentation config instead of reimplementing sidebar filtering. +- Presentation config maps existing note properties; it must not create a separate data store. A board groups notes by a property, a calendar maps notes to date properties, and a table chooses visible property columns. +- Saved-view YAML remains portable and Git-syncable. Nested presentation config should be added compatibly and only written when a user edits presentation settings. +- Note display modes remain a separate per-note concern owned by `_display` and ADR-0134. Collection presentations operate across notes; note display modes interpret one note. +- Community plugin surfaces should wait until at least two internal collection presentations exist, so the host API can be based on proven capabilities rather than speculative extension points. diff --git a/docs/adr/0145-xdg-backed-app-config-path.md b/docs/adr/0145-xdg-backed-app-config-path.md new file mode 100644 index 0000000..eb557d5 --- /dev/null +++ b/docs/adr/0145-xdg-backed-app-config-path.md @@ -0,0 +1,44 @@ +--- +type: ADR +id: "0145" +title: "XDG-backed app config path" +status: active +date: 2026-06-27 +--- + +## Context + +Tolaria stores installation-local state such as settings, registered workspaces, window state, AI workspace session metadata, and local AI provider secrets outside the vault. The product rule is still that vault-shaped content belongs in the vault, while machine- and installation-specific preferences stay in app config. + +Users also want this app config to be portable through dotfile backup workflows. The existing implementation documented `~/.config/com.tolaria.app`, but the Rust path resolver used the platform config directory directly and duplicated that decision in settings and vault-list code. That made `$XDG_CONFIG_HOME` support unclear and increased the risk that new app config files would pick a different path. + +## Decision + +**Tolaria resolves app-owned config files through one Rust helper that follows the XDG config location on Unix platforms and keeps the platform config directory as a read fallback.** + +All app-owned config JSON remains under the `com.tolaria.app` namespace: + +```text +${XDG_CONFIG_HOME:-$HOME/.config}/com.tolaria.app/settings.json +${XDG_CONFIG_HOME:-$HOME/.config}/com.tolaria.app/vaults.json +${XDG_CONFIG_HOME:-$HOME/.config}/com.tolaria.app/window-state.json +${XDG_CONFIG_HOME:-$HOME/.config}/com.tolaria.app/ai-provider-secrets.json +``` + +If `XDG_CONFIG_HOME` is missing on Unix platforms, Tolaria uses `$HOME/.config`. If `XDG_CONFIG_HOME` is relative, Tolaria ignores it and falls back to the next valid config root. Windows keeps using the platform config directory unless the user sets an absolute `XDG_CONFIG_HOME`. + +Reads check the preferred Tolaria namespace first, then the legacy `com.laputa.app` namespace, and then the previous platform config directory when it differs from the XDG location. Writes always go to the Tolaria namespace under the preferred config root. The helper lives in `src-tauri/src/app_config.rs`, and app config consumers should call it instead of joining their own config root. + +## Options considered + +- **Use the XDG config home on Unix and preserve platform read fallback** (chosen): makes dotfile-backed config explicit without changing the JSON file formats or stranding existing platform-config installs. +- **Always force `~/.config` on every platform**: maximizes visible dotfile portability, but ignores OS conventions for users who have not opted into XDG config. +- **Keep per-module path helpers**: preserves the status quo, but makes future config files easy to place inconsistently. +- **Move installation-local state into the vault**: would sync more data, but would mix device preferences, credentials, and window state into user content. + +## Consequences + +- Users can back up Tolaria's app config JSON alongside other dotfile-managed tools. +- Existing installs keep working because reads still check `com.laputa.app` and the previous platform config directory when no preferred XDG/Tolaria file exists. +- Relative `XDG_CONFIG_HOME` values are ignored so Tolaria does not write config relative to an arbitrary process working directory. +- New app-owned config files should use the shared Rust helper and document whether they are safe to back up. Secrets stay outside vaults and worktrees, but users who back up their XDG directory need to treat those files as sensitive. diff --git a/docs/adr/0146-cached-main-window-startup-with-empty-reload-recovery.md b/docs/adr/0146-cached-main-window-startup-with-empty-reload-recovery.md new file mode 100644 index 0000000..56b0e83 --- /dev/null +++ b/docs/adr/0146-cached-main-window-startup-with-empty-reload-recovery.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0146" +title: "Cached main-window startup with empty reload recovery" +status: active +date: 2026-06-28 +supersedes: "0124" +--- + +## Context + +ADR-0124 allowed secondary note windows to use the cached/incremental `list_vault` path, but kept normal main-window startup on a forced `reload_vault`. + +That forced reload invalidates the cache and runs a full filesystem scan before the main window finishes indexing. On large local macOS vaults this can make startup look hung for tens of seconds, with the status bar stuck in the vault reloading state even when a healthy cached index is available. + +Tolaria still needs the recovery behavior that motivated the fresh reload path: if a startup cache returns an empty result for a vault that should contain notes, the app should recover with a fresh scan instead of leaving the user with an empty graph. + +## Decision + +**Main-window startup uses the cached/incremental `list_vault` path first, just like secondary note windows. The main window performs a `reload_vault` fallback only when that initial cached result is empty.** + +Explicit user reloads, watcher/external-edit refreshes, Git pull refreshes, and other freshness-critical paths continue to call `reload_vault` through the existing reload abstractions. + +## Consequences + +- Healthy cached starts avoid invalidating the vault cache and do not pay a full rescan just to mount the main window. +- Empty-cache or stale-empty startup regressions still recover through one fresh `reload_vault` pass in the main window. +- Secondary note windows keep the ADR-0124 behavior and do not use startup empty-result recovery, avoiding surprise full scans when opening many note windows. +- The backend cache remains responsible for incremental freshness when `list_vault` returns non-empty cached entries. +- Future startup performance changes must preserve the distinction between initial cached hydration and explicit freshness reloads. diff --git a/docs/adr/0147-antigravity-cli-agent-adapter.md b/docs/adr/0147-antigravity-cli-agent-adapter.md new file mode 100644 index 0000000..6723b42 --- /dev/null +++ b/docs/adr/0147-antigravity-cli-agent-adapter.md @@ -0,0 +1,42 @@ +--- +type: ADR +id: "0147" +title: "Antigravity CLI agent adapter" +status: active +date: 2026-06-28 +supersedes: + - "0091" + - "0097" +--- + +## Context + +ADR-0091 and ADR-0097 made Gemini CLI the Google-backed CLI agent path for durable external MCP setup and app-managed AI panel sessions. Antigravity CLI is now the maintained successor path, with official one-shot execution through `agy -p ... --cwd`, dedicated Antigravity MCP configuration in `~/.gemini/config/mcp_config.json` or workspace `.agents/mcp_config.json`, and continued support for `GEMINI.md` / `AGENTS.md` workspace rules during Gemini CLI migration. + +Keeping `gemini` as Tolaria's product-facing local-agent id would leave users pointed at deprecated setup docs and the old `~/.gemini/settings.json` MCP shape. + +## Decision + +Tolaria replaces the app-managed Gemini CLI agent with an Antigravity CLI adapter. + +The product-facing local agent id is `antigravity`. Existing stored `gemini` default-agent values and legacy status payloads normalize to `antigravity` so older settings do not get dropped. + +The desktop backend: + +- discovers `agy` through PATH, login-shell lookup, and common local install locations, including `~/.local/bin/agy` +- runs `agy -p --cwd ` from the active vault +- writes transient workspace MCP config to `.agents/mcp_config.json` for app-managed sessions +- maps Safe mode to `--sandbox=true --toolPermission=proceed-in-sandbox` +- maps Power User mode to `--sandbox=false --toolPermission=always-proceed` +- avoids `--dangerously-skip-permissions` +- streams line-oriented stdout into Tolaria AI panel text events and reports setup/auth failures with Antigravity-specific recovery copy + +Durable external MCP setup now writes the standard Tolaria MCP entry to `~/.gemini/config/mcp_config.json` for Antigravity CLI instead of the legacy Gemini CLI `~/.gemini/settings.json` path. The optional vault-root `GEMINI.md` compatibility shim remains valid because Antigravity CLI continues to parse Gemini-compatible context files during migration. + +## Consequences + +- Users see Antigravity CLI in agent pickers, onboarding, command registry, and install links. +- Older `default_ai_agent: "gemini"` settings continue to select the Google-backed CLI path after migration. +- Existing standalone Gemini CLI global config is no longer mutated by Tolaria's durable MCP setup. +- Antigravity end-to-end native QA requires `agy` to be installed and authenticated; local tests use fake executables for deterministic command/config coverage. +- Future Antigravity CLI flag changes must update the adapter tests and this ADR rather than silently falling back to dangerous bypass flags. diff --git a/docs/adr/0148-cancellable-ai-agent-streams.md b/docs/adr/0148-cancellable-ai-agent-streams.md new file mode 100644 index 0000000..8010d52 --- /dev/null +++ b/docs/adr/0148-cancellable-ai-agent-streams.md @@ -0,0 +1,29 @@ +--- +type: ADR +id: "0148" +title: "Cancellable AI agent streams" +status: active +date: 2026-06-28 +supersedes: +--- + +## Context + +ADR-0133 gave every AI stream a request-scoped event name so concurrent or rapidly reused renderer sessions do not share Tauri event channels. Users can still start long-running CLI-agent work that spawns local subprocesses, and closing or ignoring the frontend stream only suppresses stale renderer callbacks. Without a backend cancellation path, the selected CLI can keep running after the user has decided to stop the response. + +The stop control needs to work across the shared JSON-line runtime and line-oriented adapters such as Antigravity, Kiro, and Hermes without giving the renderer access to arbitrary process ids. + +## Decision + +Tolaria treats the request-scoped AI-agent event name as the cancellation handle for app-managed CLI-agent streams. + +The desktop backend wraps `stream_ai_agent` execution in an AI-agent stream scope. When an adapter spawns a child process, it registers that child under the current scoped stream id through a shared process registry. The renderer can then call `abort_ai_agent_stream(event_name)` with the same scoped id used for event delivery. The command validates the `ai-agent-stream-*` prefix and safe character set before killing the registered child, and returns `false` when no active child is present. + +Direct AI-model streams are not registered in this process registry because they do not spawn local CLI children. + +## Consequences + +- The stop button can abort an in-flight app-managed CLI process instead of only ignoring late stream events. +- New CLI-agent adapters that spawn subprocesses must register their child after taking stdout/stderr handles and wait through the registered wrapper. +- The renderer never receives or supplies OS process ids; cancellation remains scoped to validated AI-agent stream names. +- A stop can race with process startup or natural completion, so both frontend and backend stop paths are idempotent. diff --git a/docs/adr/0149-shared-app-config-policy-manifest.md b/docs/adr/0149-shared-app-config-policy-manifest.md new file mode 100644 index 0000000..b9ed178 --- /dev/null +++ b/docs/adr/0149-shared-app-config-policy-manifest.md @@ -0,0 +1,32 @@ +--- +type: ADR +id: "0149" +title: "Shared app config policy manifest" +status: active +date: 2026-07-02 +--- + +## Context + +Tolaria resolves app-owned configuration such as settings, vault registries, window state, AI workspace sessions, and local AI provider secrets outside vault content. ADR 0145 established the XDG-backed app config location and a Rust helper for app-owned config paths, while the external Node MCP server also needs to resolve durable mounted workspace state from the same app config files when launched vault-neutrally. + +Keeping the namespace names, readable files, migration fallback order, and write target duplicated in Rust and Node makes the durable MCP registration path fragile. A new config file such as `settings.json` or `vaults.json` can silently diverge between the app and the external MCP server, especially across the current `com.tolaria.app` namespace, legacy `com.laputa.app` namespace, and previous platform config directory fallback. + +## Decision + +**Tolaria declares app config path policy once in `mcp-server/app-config-policy.json`, and both the Rust app helper and Node MCP server consume that manifest for namespace and file resolution.** + +The policy manifest names the current and legacy app namespaces, lists shared app config files, defines the read order across preferred and platform config roots, and keeps writes targeting the current Tolaria namespace in the preferred config root. The Rust helper still owns platform/XDG root discovery, but it reads the manifest for namespace order. The MCP server uses the same manifest for settings and vault registry lookup, so durable external agent registrations resolve mounted workspaces the same way the app resolves installation-local settings. + +## Alternatives considered + +- **Shared JSON policy manifest consumed by Rust and Node** (chosen): keeps the cross-runtime app config contract explicit and testable while preserving the existing XDG and legacy fallback behavior. +- **Keep separate Rust and Node constants**: avoids introducing a manifest, but invites drift between the app and MCP server whenever a namespace, file name, or fallback rule changes. +- **Move all MCP path resolution behind a Rust command**: centralizes logic in the app process, but external MCP clients must work when launched outside the running desktop app. +- **Expose only environment variables to external MCP servers**: preserves static launch configuration, but reintroduces stale vault/workspace state after mounted workspace changes. + +## Consequences + +The app and external MCP server now share one durable source for app config namespace and file naming policy. Mounted workspace resolution, settings resolution, and future app-owned config files are less likely to drift across runtimes. + +Changes to app config namespace or fallback order must update the manifest and keep both Rust and Node tests passing. The manifest remains a policy contract, not a replacement for platform root discovery: Rust still determines the preferred and platform config roots, and Node still resolves files from its runtime environment when launched externally. diff --git a/docs/adr/0150-github-copilot-cli-agent-adapter.md b/docs/adr/0150-github-copilot-cli-agent-adapter.md new file mode 100644 index 0000000..db2ad83 --- /dev/null +++ b/docs/adr/0150-github-copilot-cli-agent-adapter.md @@ -0,0 +1,39 @@ +--- +type: ADR +id: "0150" +title: "GitHub Copilot CLI agent adapter" +status: active +date: 2026-07-02 +supersedes: +--- + +## Context + +Tolaria already exposes several app-managed local AI agent adapters through one normalized AI panel contract. GitHub Copilot CLI is a mainstream local agent with programmatic `copilot -p ...` execution, per-session tool permissions, current-directory path scoping, custom instructions support including `AGENTS.md`, and MCP server support. + +Users who prefer Copilot should not have to leave Tolaria or manually configure a generic/custom agent to get vault-aware assistance. + +## Decision + +Tolaria adds GitHub Copilot as a first-class local agent with product-facing id `copilot`. + +The desktop backend: + +- discovers `copilot` through PATH, login-shell lookup, and common local install locations including npm, pnpm, Bun, Mise, asdf, nvm-managed Node versions, Homebrew, and Windows npm/pnpm/Scoop shims +- runs `copilot -p -s --no-ask-user` from the active vault directory +- passes Tolaria MCP as a per-session `--additional-mcp-config` JSON payload instead of mutating `~/.copilot/mcp-config.json` +- uses Safe mode with `--available-tools=write,tolaria`, `--allow-tool=write,tolaria`, and `--deny-tool=shell` +- maps Power User mode to `--allow-all-tools` +- intentionally avoids `--allow-all`, `--yolo`, `--allow-all-paths`, and `--allow-all-urls` +- streams line-oriented stdout through the shared agent event contract and registers the child process under the request-scoped stream id for cancellation +- maps authentication, organization policy, subscription, and directory-trust stderr into Copilot-specific setup guidance + +The renderer adds Copilot to the shared AI-agent registry so the onboarding prompt, Settings target picker, command palette switch command, AI workspace target groups, and persisted `agent:copilot` target all use the same definition. + +## Consequences + +- Users can select GitHub Copilot anywhere Tolaria lists local/app-managed AI agents. +- App-managed Copilot sessions stay vault-scoped by current working directory and Copilot's default path verification unless the user chooses Tolaria Power User, which still does not bypass all paths or URLs. +- Tolaria does not persist Copilot credentials, model-provider secrets, or durable third-party MCP files. +- Native end-to-end QA requires a local Copilot CLI install and authentication; automated tests use fake executables for deterministic streaming and error coverage. +- Future Copilot CLI permission or MCP flag changes must update this adapter and ADR rather than falling back to broad `--allow-all` / `--yolo` behavior. diff --git a/docs/adr/0151-antigravity-add-dir-workspace-flag.md b/docs/adr/0151-antigravity-add-dir-workspace-flag.md new file mode 100644 index 0000000..3f2f2f9 --- /dev/null +++ b/docs/adr/0151-antigravity-add-dir-workspace-flag.md @@ -0,0 +1,36 @@ +--- +type: ADR +id: "0151" +title: "Antigravity add-dir workspace flag" +status: active +date: 2026-07-02 +supersedes: "0147" +--- + +## Context + +ADR-0147 introduced the Antigravity CLI adapter with `agy -p --cwd `. +Newer Antigravity CLI builds reject that workspace flag with `flags provided but not defined: -cwd` and list `--add-dir` as the supported workspace-directory argument. + +Tolaria still needs app-managed Antigravity runs to start in the active vault, expose that vault as the only intended workspace, and preserve the Safe / Power User permission mapping from ADR-0103. + +## Decision + +Tolaria launches app-managed Antigravity sessions with: + +```text +agy -p --add-dir +``` + +The subprocess `current_dir` remains the active vault path, and Tolaria still writes the transient MCP config to `/.agents/mcp_config.json` before launch. + +Safe mode continues to pass `--sandbox=true --toolPermission=proceed-in-sandbox`. +Power User continues to pass `--sandbox=false --toolPermission=always-proceed`. +Tolaria still avoids `--dangerously-skip-permissions`. + +## Consequences + +- Antigravity CLI versions that reject `--cwd` can start from the Tolaria AI panel. +- The active vault remains both the process working directory and the explicit Antigravity workspace directory. +- Adapter tests must reject regressions that reintroduce `--cwd` or drop `--add-dir`. +- Future Antigravity CLI workspace flag changes should supersede this ADR with a new adapter decision and test update. diff --git a/docs/adr/0152-wsl2-git-provider.md b/docs/adr/0152-wsl2-git-provider.md new file mode 100644 index 0000000..69d14f4 --- /dev/null +++ b/docs/adr/0152-wsl2-git-provider.md @@ -0,0 +1,29 @@ +--- +type: ADR +id: "0152" +title: "WSL2 Git provider" +status: active +date: 2026-07-03 +supersedes: +--- + +## Context + +Windows users may keep their vaults and Git credentials inside WSL2 even when running Tolaria as a native desktop app. Requiring native Windows Git blocks that setup, but silently switching to WSL based on detection would make Git operations difficult to reason about and could send the wrong filesystem paths to the selected executable. + +Tolaria already treats Git as a per-vault capability and app settings as installation-local. The executable provider is also machine-specific: one Windows installation may use native Git while another uses Git from an Ubuntu WSL distribution for the same vault content. + +## Decision + +Tolaria adds an explicit Git provider setting with native Git as the default and WSL2 Git as an opt-in provider on Windows. + +The Rust Git module resolves a `GitLaunchConfig` for each Git command from app settings. Native launches keep the existing `git`/configured path behavior. WSL launches use `wsl.exe`, optional `--distribution `, and `--exec git`, while repository and clone destination paths are translated to Linux-style paths before they cross into WSL. Provider probes and tests run on blocking worker threads with timeouts so unavailable WSL distributions cannot freeze the renderer. + +The Settings Git section shows the provider, WSL distribution, and a test action before save. Detection populates available WSL distributions, but selection remains explicit; Tolaria does not automatically switch from native Git to WSL2 Git. + +## Consequences + +- Windows users can run Tolaria with Git from WSL2 without installing or configuring native Windows Git. +- Native Git remains supported and remains the default provider. +- Git command paths must go through the Git provider abstraction rather than passing Windows repository paths directly to subprocess arguments. +- WSL-specific QA requires a Windows host with WSL2 and at least one distribution containing Git; non-Windows automated coverage verifies path translation, distro parsing, settings persistence, and renderer controls. diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000..b343ccf --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,211 @@ +# Architecture Decision Records + +This folder contains Architecture Decision Records (ADRs) for the Laputa app. + +## Format + +Each ADR is a markdown note with YAML frontmatter. Template: + +```markdown +--- +type: ADR +id: "0001" +title: "Short decision title" +status: proposed # proposed | active | superseded | retired +date: YYYY-MM-DD +superseded_by: "0007" # only if status: superseded +--- + +## Context +What situation led to this decision? What forces and constraints are at play? + +## Decision +**What was decided.** State it clearly in one or two sentences — bold so it stands out. + +## Options considered +- **Option A** (chosen): brief description — pros / cons +- **Option B**: brief description — pros / cons +- **Option C**: brief description — pros / cons + +## Consequences +What becomes easier or harder as a result? +What are the positive and negative ramifications? +What would trigger re-evaluation of this decision? + +## Advice +*(optional)* Input received before making this decision — who was consulted, what they said, when. +Omit if the decision was made unilaterally with no external input. +``` + +### Status lifecycle + +``` +proposed → active → superseded + ↘ retired (decision no longer relevant, not replaced) +``` + +## Rules + +- One decision per file +- Files named `NNNN-short-title.md` (monotonic numbering) +- Once `active`, never edit — supersede instead +- When superseded: update `status: superseded` and add `superseded_by: "NNNN"` +- ARCHITECTURE.md reflects the current state (active decisions only) + +## Index + +| ID | Title | Status | +|----|-------|--------| +| [0001](0001-tauri-react-stack.md) | Tauri v2 + React as application stack | active | +| [0002](0002-filesystem-source-of-truth.md) | Filesystem as the single source of truth | active | +| [0003](0003-single-note-model.md) | Single note open at a time (no tabs) | active | +| [0004](0004-vault-vs-app-settings-storage.md) | Vault vs app settings for state storage | active | +| [0005](0005-tauri-ios-for-ipad.md) | Tauri v2 iOS for iPad support (vs SwiftUI rewrite) | active | +| [0006](0006-flat-vault-structure.md) | Flat vault structure (no type-based folders) | active | +| [0007](0007-title-filename-sync.md) | Title equals filename (slug sync) | active | +| [0008](0008-underscore-system-properties.md) | Underscore convention for system properties | active | +| [0009](0009-keyword-only-search.md) | Keyword-only search (remove semantic indexing) | active | +| [0010](0010-dynamic-wikilink-relationship-detection.md) | Dynamic wikilink relationship detection | active | +| [0011](0011-mcp-server-for-ai-integration.md) | MCP server for AI tool integration | superseded → [0074](0074-explicit-external-ai-tool-setup-and-least-privilege-desktop-scope.md) | +| [0012](0012-claude-cli-for-ai-agent.md) | Claude CLI subprocess for AI agent | active | +| [0013](0013-remove-theming-system.md) | Remove vault-based theming system | superseded -> [0081](0081-internal-light-dark-theme-runtime.md) | +| [0014](0014-git-based-vault-cache.md) | Git-based incremental vault cache | active | +| [0015](0015-auto-save-with-debounce.md) | Auto-save with 500ms debounce | superseded → [0102](0102-low-end-safe-autosave-idle-window.md) | +| [0016](0016-sentry-posthog-telemetry.md) | Sentry + PostHog telemetry with consent | active | +| [0017](canary-release-channel-and-local-feature-flags.md) | Canary release channel and feature flags | superseded → [0057](0057-alpha-stable-release-channels-and-beta-cohorts.md) | +| [0018](0018-codescene-code-health-gates.md) | CodeScene code health gates in CI | superseded → [0064](0064-ratcheted-codescene-thresholds.md) | +| [0019](0019-github-device-flow-oauth.md) | GitHub device flow OAuth for vault sync | superseded → [0056](0056-system-git-cli-auth-no-provider-oauth.md) | +| [0020](0020-keyboard-first-design.md) | Keyboard-first design principle | active | +| [0021](0021-push-to-main-workflow.md) | Push directly to main (no PRs) | active | +| [0022](0022-blocknote-rich-text-editor.md) | BlockNote as the rich text editor | active | +| [0023](0023-repair-vault-auto-bootstrap.md) | Repair Vault auto-bootstrap pattern | active | +| [0024](0024-cache-outside-vault.md) | Vault cache stored outside vault directory | active | +| [0025](0025-type-field-canonical.md) | type: as canonical field (replacing Is A:) | active | +| [0026](0026-props-down-no-global-state.md) | Props-down callbacks-up (no global state) | superseded → [0115](0115-scoped-react-context-for-shared-ui-preferences.md) | +| [0027](0027-dual-ai-architecture.md) | Dual AI architecture (API chat + CLI agent) | superseded | +| [0028](0028-cli-agent-only-no-api-key.md) | CLI agent only — no direct Anthropic API key | active | +| [0029](0029-domain-command-builder-pattern.md) | Domain command builder pattern for useCommandRegistry | active | +| [0030](0030-rust-commands-module-split.md) | Rust commands/ module split by domain | active | +| [0031](0031-full-app-for-note-windows.md) | Full App instance for secondary note windows | active | +| [0032](0032-status-bar-for-git-actions.md) | Git actions (Changes, Pulse, Commit) in status bar, not sidebar | active | +| [0033](0033-subfolder-scanning-and-folder-tree.md) | Subfolder scanning and folder tree navigation | active | +| [0034](0034-git-repo-required-for-vault.md) | Git repo required — blocking modal enforces vault prerequisite | superseded → [0085](0085-non-git-vault-support.md) | +| [0035](0035-path-suffix-wikilink-resolution.md) | Path-suffix wikilink resolution for subfolder vaults | active | +| [0036](0036-external-rename-detection-via-git-diff.md) | External rename detection via git diff on focus regain | active | +| [0037](0037-codemirror-language-markdown-highlighting.md) | Language-based markdown syntax highlighting in raw editor | active | +| [0038](0038-frontmatter-backed-favorites.md) | Frontmatter-backed favorites (_favorite, _favorite_index) | active | +| [0039](0039-git-history-for-note-dates.md) | Git history as source of truth for note creation/modification dates | active | +| [0040](0040-custom-views-yml-filter-engine.md) | Custom Views — .laputa/views/*.yml with YAML filter engine | active | +| [0041](0041-filekind-all-files-in-vault-scanner.md) | fileKind field — scan all vault files, not just markdown | active | +| [0042](0042-posthog-release-channels-feature-flags.md) | PostHog-based release channels and feature flags | superseded → [0057](0057-alpha-stable-release-channels-and-beta-cohorts.md) | +| [0042](0042-trash-auto-purge-safety-model.md) | Trash auto-purge safety model | superseded → [0045](0045-permanent-delete-no-trash.md) | +| [0043](0043-reactive-vault-state-on-save.md) | Reactive vault state: editor changes propagate immediately to all UI | active | +| [0044](0044-h1-as-title-primary-source.md) | H1 as primary title source — filename as stable identifier | superseded → [0055](0055-h1-is-the-only-editor-title-surface.md) | +| [0045](0045-permanent-delete-no-trash.md) | Permanent delete with confirm modal — no Trash system | active | +| [0046](0046-starter-vault-cloned-from-github.md) | Starter vault cloned from GitHub at runtime — no bundled content | active | +| [0047](0047-regex-mode-for-view-filter-conditions.md) | Regex mode for view filter conditions | active | +| [0048](0048-relative-date-expressions-in-view-filters.md) | Relative date expressions in view filter conditions | active | +| [0049](0049-per-note-icon-property.md) | Per-note icon property (_icon on individual notes) | active | +| [0050](0050-deterministic-shortcut-command-routing.md) | Deterministic shortcut command routing | superseded → [0051](0051-shared-shortcut-manifest-for-testable-routing.md) | +| [0051](0051-shared-shortcut-manifest-for-testable-routing.md) | Shared shortcut manifest for testable routing | superseded → [0052](0052-renderer-first-shortcut-execution-with-native-menu-dedupe.md) | +| [0052](0052-renderer-first-shortcut-execution-with-native-menu-dedupe.md) | Renderer-first shortcut execution with native-menu dedupe | active | +| [0053](0053-webview-init-prevention-for-browser-reserved-shortcuts.md) | Webview-init prevention for browser-reserved shortcuts | active | +| [0054](0054-deterministic-shortcut-qa-matrix.md) | Deterministic shortcut QA matrix | active | +| [0055](0055-h1-is-the-only-editor-title-surface.md) | H1 is the only editor title surface | superseded → [0068](0068-h1-only-title-surface-with-optional-untitled-auto-rename.md) | +| [0056](0056-system-git-cli-auth-no-provider-oauth.md) | System git auth only — no provider-specific OAuth or repo APIs | active | +| [0057](0057-alpha-stable-release-channels-and-beta-cohorts.md) | Alpha/stable release channels with PostHog beta cohorts | superseded → [0066](0066-calendar-semver-versioning-for-alpha-and-stable-releases.md) | +| [0058](0058-claude-code-first-launch-onboarding-gate.md) | Claude Code first-launch onboarding gate | superseded → [0062](0062-selectable-cli-ai-agents.md) | +| [0059](0059-local-only-git-commits-without-remote.md) | Local-only git commits for vaults without a remote | active | +| [0060](0060-network-aware-ui-gating-for-remote-features.md) | Network-aware UI gating for remote-dependent features | active | +| [0061](0061-ai-prompt-bridge-event-bus.md) | AI prompt bridge — module-level event bus for cross-component prompt routing | active | +| [0062](0062-selectable-cli-ai-agents.md) | Selectable CLI AI agents with a shared panel architecture | active | +| [0063](0063-blocknote-code-block-package-for-editor-highlighting.md) | BlockNote code-block package for editor syntax highlighting | active | +| [0064](0064-ratcheted-codescene-thresholds.md) | Ratcheted CodeScene thresholds as the quality gate baseline | active | +| [0065](0065-root-managed-ai-guidance-files.md) | Root-managed AI guidance files with Claude shim | active | +| [0066](0066-calendar-semver-versioning-for-alpha-and-stable-releases.md) | Calendar-semver versioning for alpha and stable releases | active | +| [0067](0067-autogit-idle-and-inactive-checkpoints.md) | AutoGit idle and inactive checkpoints | active | +| [0068](0068-h1-only-title-surface-with-optional-untitled-auto-rename.md) | H1-only title surface with optional untitled auto-rename | active | +| [0069](0069-neighborhood-mode-for-note-list-relationship-browsing.md) | Neighborhood mode for note-list relationship browsing | active | +| [0070](0070-starter-vaults-local-first-with-explicit-remote-connection.md) | Starter vaults are local-first with explicit remote connection | active | +| [0071](0071-external-vault-refresh-and-clean-tab-reopen.md) | External vault updates reload derived state and reopen the clean active note | superseded → [0111](0111-path-aware-external-vault-refresh-with-focused-editor-preservation.md) | +| [0072](0072-confirmed-vault-paths-gate-startup-state.md) | Confirmed vault paths gate startup state | active | +| [0073](0073-persistent-linkify-protocol-registry-across-editor-remounts.md) | Persistent linkify protocol registry across editor remounts | active | +| [0074](0074-explicit-external-ai-tool-setup-and-least-privilege-desktop-scope.md) | Explicit external AI tool setup and least-privilege desktop scope | active | +| [0075](0075-crash-safe-note-rename-transactions.md) | Crash-safe note rename transactions | active | +| [0076](0076-note-retargeting-separates-type-and-folder-moves.md) | Note retargeting separates type changes from folder moves | active | +| [0077](0077-concurrent-safe-vault-cache-replacement.md) | Concurrent-safe vault cache replacement | active | +| [0078](0078-scoped-unsigned-fallback-for-app-managed-git-commits.md) | Scoped unsigned fallback for app-managed git commits | active | +| [0079](0079-linux-window-chrome-and-menu-reuse.md) | Linux window chrome and menu reuse | active | +| [0080](0080-cross-platform-desktop-release-artifacts-and-portable-vault-names.md) | Cross-platform desktop release artifacts and portable vault names | superseded → [0083](0083-dual-architecture-macos-release-artifacts.md) | +| [0081](0081-internal-light-dark-theme-runtime.md) | Internal light and dark theme runtime | active | +| [0082](0082-markdown-durable-math-notes.md) | Markdown-durable math in notes | active | +| [0083](0083-dual-architecture-macos-release-artifacts.md) | Dual-architecture macOS release artifacts | active | +| [0084](0084-app-localization-foundation.md) | App-owned localization foundation | superseded → [0087](0087-json-catalogs-and-lara-cli-localization.md) | +| [0085](0085-non-git-vault-support.md) | Non-git vaults open with explicit later Git initialization | active | +| [0086](0086-in-app-image-file-preview.md) | In-app image previews for binary vault files | superseded → [0098](0098-in-app-image-and-pdf-file-previews.md) | +| [0087](0087-json-catalogs-and-lara-cli-localization.md) | JSON locale catalogs with Lara CLI synchronization | active | +| [0088](0088-markdown-durable-mermaid-diagrams.md) | Markdown-durable Mermaid diagrams in notes | active | +| [0089](0089-active-vault-filesystem-watcher.md) | Active vault filesystem watcher | active | +| [0090](0090-pi-cli-agent-adapter.md) | Pi CLI agent adapter | active | +| [0091](0091-gemini-cli-external-ai-setup.md) | Gemini CLI external AI setup | superseded -> [0147](0147-antigravity-cli-agent-adapter.md) | +| [0092](0092-vault-ai-agent-permission-modes.md) | Vault-scoped AI agent permission modes | superseded -> [0103](0103-adapter-specific-ai-permission-semantics.md) | +| [0093](0093-shared-cli-agent-runtime-adapters.md) | Shared CLI agent runtime adapters | active | +| [0094](0094-gitignored-content-visibility-boundary-filter.md) | Gitignored content visibility as a command-boundary filter | active | +| [0095](0095-saved-view-order-field.md) | Saved views use an explicit YAML order field | active | +| [0096](0096-root-created-type-documents.md) | Root-created type documents | active | +| [0097](0097-gemini-cli-agent-adapter.md) | Gemini CLI agent adapter | superseded -> [0147](0147-antigravity-cli-agent-adapter.md) | +| [0098](0098-in-app-image-and-pdf-file-previews.md) | In-app image and PDF previews for binary vault files | superseded → [0110](0110-in-app-media-and-pdf-file-previews.md) | +| [0099](0099-cumulative-vault-asset-scope.md) | Cumulative vault asset scope for previews | active | +| [0100](0100-synthetic-vault-root-folder-row.md) | Synthetic vault-root row in folder navigation | active | +| [0101](0101-categorical-product-analytics-events.md) | Categorical product analytics events | active | +| [0102](0102-low-end-safe-autosave-idle-window.md) | Low-end-safe autosave idle window | active | +| [0103](0103-adapter-specific-ai-permission-semantics.md) | Adapter-specific AI permission semantics | active | +| [0104](0104-tauri-frontend-readiness-watchdog.md) | Tauri frontend readiness watchdog | active | +| [0105](0105-editor-correctness-and-responsiveness-contract.md) | Editor correctness and responsiveness contract | active | +| [0106](0106-shared-app-command-manifest.md) | Shared app command manifest | active | +| [0107](0107-markdown-durable-tldraw-whiteboards.md) | Markdown-durable tldraw whiteboards in notes | active | +| [0107](0107-pointer-owned-editor-block-reordering.md) | Pointer-owned editor block reordering | active | +| [0108](0108-direct-model-ai-targets.md) | Direct model AI targets alongside coding agents | active | +| [0108](0108-sanitized-rendered-markup-and-safe-regex.md) | Sanitized rendered markup and safe user regex | active | +| [0109](0109-debounced-worker-derived-editor-indexes.md) | Debounced worker-derived editor indexes | active | +| [0110](0110-in-app-media-and-pdf-file-previews.md) | In-app media and PDF previews for binary vault files | superseded → [0121](0121-appimage-external-fallback-for-audio-and-video-previews.md) | +| [0111](0111-path-aware-external-vault-refresh-with-focused-editor-preservation.md) | Path-aware external vault refresh with focused-editor preservation | superseded → [0135](0135-clean-active-note-refresh-after-external-edit.md) | +| [0112](0112-system-theme-mode.md) | System theme mode | active | +| [0113](0113-shared-renderer-attachment-path-normalization.md) | Shared renderer attachment path normalization | active | +| [0114](0114-mounted-workspaces-unified-graph.md) | Mounted workspaces unified graph | active | +| [0115](0115-scoped-react-context-for-shared-ui-preferences.md) | Scoped React Context for shared UI preferences | active | +| [0116](0116-rich-raw-transition-and-serialization-ownership.md) | Rich/raw transition and serialization ownership | active | +| [0117](0117-appimage-fcitx-gtk3-frontend-bundle.md) | Bundle the fcitx GTK3 frontend in Linux AppImages | active | +| [0118](0118-entry-scoped-note-windows-without-vault-index-scans.md) | Entry-scoped note windows without vault index scans | superseded -> [0123](0123-full-vault-graph-for-secondary-note-windows.md) | +| [0119](0119-vault-neutral-mcp-registration-with-mounted-workspace-guidance.md) | Vault-neutral MCP registration with mounted workspace guidance | active | +| [0120](0120-stable-appimage-mcp-server-path-with-opencode-registration.md) | Stable AppImage MCP server path with OpenCode registration | active | +| [0121](0121-appimage-external-fallback-for-audio-and-video-previews.md) | AppImage external fallback for audio and video previews | active | +| [0122](0122-scalar-array-frontmatter-properties.md) | Scalar array frontmatter properties | active | +| [0123](0123-full-vault-graph-for-secondary-note-windows.md) | Full vault graph for secondary note windows | superseded -> [0124](0124-cached-secondary-note-window-startup.md) | +| [0124](0124-cached-secondary-note-window-startup.md) | Cached secondary note window startup | superseded -> [0146](0146-cached-main-window-startup-with-empty-reload-recovery.md) | +| [0126](0126-renderer-action-history.md) | Renderer action history for app-level undo and redo | active | +| [0127](0127-native-ai-workspace-window.md) | Native AI workspace window | superseded -> [0128](0128-lightweight-ai-workspace-window.md) | +| [0128](0128-lightweight-ai-workspace-window.md) | Lightweight AI workspace window | active | +| [0129](0129-tolaria-vault-item-deep-links.md) | Tolaria vault item deep links | active | +| [0130](0130-windows-authenticode-release-signing.md) | Windows Authenticode signing for release installers | amended -> [0139](0139-temporary-windows-authenticode-soft-gate.md) | +| [0131](0131-reusable-release-artifact-build-workflow.md) | Reusable release artifact build workflow | active | +| [0132](0132-alpha-authenticode-soft-gate.md) | Alpha Authenticode soft gate | superseded -> [0139](0139-temporary-windows-authenticode-soft-gate.md) | +| [0133](0133-request-scoped-ai-stream-events.md) | Request-scoped AI stream event channels | active | +| [0134](0134-sheet-nodes-with-plain-text-workbook-storage.md) | Sheet nodes with plain-text workbook storage | experimental | +| [0134](0134-direct-shiki-language-registrations.md) | Direct Shiki language registrations for code blocks | active | +| [0135](0135-clean-active-note-refresh-after-external-edit.md) | Clean active notes refresh immediately after external edits | active | +| [0136](0136-macos-webview-pdf-export.md) | macOS Webview PDF export | active | +| [0137](0137-shared-rich-editor-input-transforms.md) | Shared rich-editor input transforms | active | +| [0138](0138-authenticode-required-for-all-release-channels.md) | Require Authenticode signing for all Windows release channels | superseded -> [0139](0139-temporary-windows-authenticode-soft-gate.md) | +| [0139](0139-temporary-windows-authenticode-soft-gate.md) | Temporary Windows Authenticode soft gate | active | +| [0140](0140-extension-based-raw-text-syntax-highlighting.md) | Extension-based raw text syntax highlighting | active | +| [0141](0141-scoped-linux-webkit-rendering-safeguards.md) | Scoped Linux WebKit rendering safeguards | active | +| [0142](0142-rich-editor-prosemirror-decoration-dependency.md) | Rich editor ProseMirror decoration dependency | active | +| [0143](0143-shared-focus-ownership-guard.md) | Shared focus ownership guard | active | +| [0144](0144-collections-and-presentations.md) | Collections carry presentation configuration | active | +| [0145](0145-xdg-backed-app-config-path.md) | XDG-backed app config path | active | +| [0146](0146-cached-main-window-startup-with-empty-reload-recovery.md) | Cached main-window startup with empty reload recovery | active | +| [0147](0147-antigravity-cli-agent-adapter.md) | Antigravity CLI agent adapter | superseded -> [0151](0151-antigravity-add-dir-workspace-flag.md) | +| [0148](0148-cancellable-ai-agent-streams.md) | Cancellable AI agent streams | active | +| [0149](0149-shared-app-config-policy-manifest.md) | Shared app config policy manifest | active | +| [0151](0151-antigravity-add-dir-workspace-flag.md) | Antigravity add-dir workspace flag | active | diff --git a/docs/adr/canary-release-channel-and-local-feature-flags.md b/docs/adr/canary-release-channel-and-local-feature-flags.md new file mode 100644 index 0000000..a69c067 --- /dev/null +++ b/docs/adr/canary-release-channel-and-local-feature-flags.md @@ -0,0 +1,31 @@ +--- +type: ADR +id: "0017" +title: "Canary release channel and local feature flags" +status: active +date: 2026-03-25 +--- +## Context + +Shipping new features directly to all users is risky. A mechanism was needed to let early adopters test pre-release builds and to gate experimental features behind flags that can be toggled without a new release. + +[[Keyboard-first design principle]] + +## Decision + +**Add a canary release channel alongside stable, with builds from the **`canary`** branch. Feature flags are localStorage-based (**`ff_`**) with compile-time defaults, checked via **`useFeatureFlag(flag)`** hook. The update channel is configurable in Settings.** + +## Options considered + +* **Option A** (chosen): Canary branch + localStorage feature flags — simple, no server infrastructure, users opt in via Settings. Downside: no remote flag management, no gradual rollout percentages. +* **Option B**: Server-side feature flags (LaunchDarkly, Unleash) — gradual rollouts, A/B testing. Downside: external dependency, requires server infrastructure, adds latency. +* **Option C**: Single release channel with only feature flags — simpler CI. Downside: no way to test full pre-release builds. + +## Consequences + +* `release.yml` builds stable from `main`; `release-canary.yml` builds canary from `canary` branch. +* Canary releases produce `latest-canary.json` on GitHub Pages, marked as prerelease. +* `useUpdater(channel)` checks the appropriate update manifest. +* `useFeatureFlag(flag)` checks localStorage override, then compile-time default. Type-safe via `FeatureFlagName` union. +* `update_channel` stored in Settings as `"stable"` or `"canary"`. +* Re-evaluation trigger: if user base grows enough to warrant server-side gradual rollouts. diff --git a/e2e/ai-chat-screenshot.spec.ts b/e2e/ai-chat-screenshot.spec.ts new file mode 100644 index 0000000..e4f8a27 --- /dev/null +++ b/e2e/ai-chat-screenshot.spec.ts @@ -0,0 +1,57 @@ +import { type Page, test } from '@playwright/test' + +const SCREENSHOT_PATH = '/Users/luca/OpenClaw/ai-chat-final.jpg' + +async function clickNoteListItem(page: Page): Promise { + return page.evaluate(() => { + const items = document.querySelectorAll('[class*="cursor-pointer"]') + const noteListItem = Array.from(items).find((el) => { + const rect = el.getBoundingClientRect() + return rect.x > 249 && rect.x < 700 && rect.height > 40 && rect.width > 200 + }) + + if (!noteListItem) { + return 'Nothing found' + } + + const noteListElement = noteListItem as HTMLElement + noteListElement.click() + const rect = noteListItem.getBoundingClientRect() + return `Clicked: ${noteListItem.textContent?.trim().slice(0, 50)} at x=${rect.x}` + }) +} + +async function clickAiToolbarButton(page: Page): Promise { + return page.evaluate(() => { + const aiButton = Array.from(document.querySelectorAll('button')).find((btn) => + btn.title?.includes('AI'), + ) + + if (!aiButton) { + return 'AI button not found' + } + + aiButton.click() + return `Clicked: ${aiButton.title}` + }) +} + +test('screenshot AI chat panel', async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 900 }) + await page.goto('/', { waitUntil: 'networkidle' }) + await page.waitForTimeout(3000) + + // Click a note item in the NoteList (x > 243, past the sidebar) + // Note items have: cursor-pointer border-b border-[var(--border)] + const clicked = await clickNoteListItem(page) + console.log('Note click result:', clicked) + await page.waitForTimeout(1200) + + // Now find and click the AI button in the editor toolbar + const aiBtn = await clickAiToolbarButton(page) + console.log('AI btn result:', aiBtn) + await page.waitForTimeout(800) + + await page.screenshot({ path: SCREENSHOT_PATH, type: 'jpeg', quality: 90 }) + console.log('Done') +}) diff --git a/e2e/ai-chat.spec.ts b/e2e/ai-chat.spec.ts new file mode 100644 index 0000000..76b3967 --- /dev/null +++ b/e2e/ai-chat.spec.ts @@ -0,0 +1,69 @@ +import { test, expect } from '@playwright/test' + +test('AI chat panel: open, send message, close', async ({ page }) => { + await page.goto('http://localhost:5173') + await page.waitForTimeout(2000) + + // Click the first note in the list — note items are cursor-pointer divs inside .app__note-list + const noteItem = page.locator('.app__note-list .cursor-pointer').first() + await noteItem.click() + await page.waitForTimeout(800) + + // Screenshot before opening AI chat + await page.screenshot({ path: 'test-results/ai-chat-before.png', fullPage: true }) + + // Find the Sparkle button in the breadcrumb/info bar and click it + const sparkleButton = page.locator('button[title="Open AI Chat"]') + await expect(sparkleButton).toBeVisible({ timeout: 5000 }) + await sparkleButton.click() + await page.waitForTimeout(300) + + // AI Chat panel should be visible + const aiChatHeader = page.locator('text=AI Chat') + await expect(aiChatHeader).toBeVisible() + + // Screenshot with AI chat panel open + await page.screenshot({ path: 'test-results/ai-chat-open.png', fullPage: true }) + + // Context pills should be visible + await expect(page.locator('text=Frontmatter')).toBeVisible() + await expect(page.locator('text=Links')).toBeVisible() + + // Type a message and send + const textarea = page.locator('textarea[placeholder="Ask about this document..."]') + await textarea.fill('Summarize this note') + await page.locator('button[title="Send message"]').click() + + // Should see user message + await expect(page.locator('text=Summarize this note')).toBeVisible() + + // Wait for typing indicator to appear + await page.waitForTimeout(300) + await page.screenshot({ path: 'test-results/ai-chat-typing.png', fullPage: true }) + + // Wait for mock response + await page.waitForTimeout(1200) + await expect(page.locator('text=words and links to')).toBeVisible({ timeout: 5000 }) + + // Screenshot with response + await page.screenshot({ path: 'test-results/ai-chat-response.png', fullPage: true }) + + // Test quick action pill + const expandButton = page.locator('button', { hasText: 'Expand' }) + await expandButton.click() + await page.waitForTimeout(1800) + await expect(page.locator('text=Add more detail to the introduction')).toBeVisible({ timeout: 5000 }) + + // Screenshot with quick action response + await page.screenshot({ path: 'test-results/ai-chat-quick-action.png', fullPage: true }) + + // Close the panel using the X button in the AI Chat header + await page.locator('button[title="Close AI Chat"]').last().click() + await page.waitForTimeout(300) + + // Inspector should be back + await expect(page.locator('text=Properties')).toBeVisible() + + // Screenshot after closing + await page.screenshot({ path: 'test-results/ai-chat-closed.png', fullPage: true }) +}) diff --git a/e2e/app.spec.ts b/e2e/app.spec.ts new file mode 100644 index 0000000..f07efa2 --- /dev/null +++ b/e2e/app.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from '@playwright/test' + +test('app loads with four-panel layout', async ({ page }) => { + await page.goto('/') + + // Verify the four panels are present + await expect(page.locator('.sidebar')).toBeVisible() + await expect(page.locator('.note-list')).toBeVisible() + await expect(page.locator('.editor')).toBeVisible() + await expect(page.locator('.inspector')).toBeVisible() +}) + +test('sidebar shows filters and section groups', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) // Wait for mock data + + await expect(page.getByRole('heading', { name: 'Laputa' })).toBeVisible() + // Filters + await expect(page.locator('.sidebar__filter-item').filter({ hasText: 'All Notes' })).toBeVisible() + await expect(page.locator('.sidebar__filter-item').filter({ hasText: 'People' })).toBeVisible() + await expect(page.locator('.sidebar__filter-item').filter({ hasText: 'Events' })).toBeVisible() + // Section groups (use exact match to avoid collision with note list pills) + await expect(page.locator('.sidebar__section-label', { hasText: 'PROJECTS' })).toBeVisible() + await expect(page.locator('.sidebar__section-label', { hasText: 'EXPERIMENTS' })).toBeVisible() + await expect(page.locator('.sidebar__section-label', { hasText: 'RESPONSIBILITIES' })).toBeVisible() + await expect(page.locator('.sidebar__section-label', { hasText: 'PROCEDURES' })).toBeVisible() +}) diff --git a/e2e/auto-save.spec.ts b/e2e/auto-save.spec.ts new file mode 100644 index 0000000..9c4bb9e --- /dev/null +++ b/e2e/auto-save.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@playwright/test' + +test.use({ baseURL: 'http://localhost:5239' }) + +test.beforeEach(async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(2000) // Wait for vault data to load +}) + +test('editor loads and renders note content for editing', async ({ page }) => { + await page.screenshot({ path: 'test-results/save-01-initial.png', fullPage: true }) + + // 1. Click a note in the note list panel + const noteList = page.locator('.app__note-list') + await expect(noteList).toBeVisible({ timeout: 5000 }) + const firstNote = noteList.locator('div.cursor-pointer').first() + await expect(firstNote).toBeVisible({ timeout: 5000 }) + await firstNote.click() + await page.waitForTimeout(1000) + + // 2. Verify the BlockNote editor is visible with content + const editor = page.locator('.bn-editor') + await expect(editor).toBeVisible({ timeout: 5000 }) + + // Verify the editor is contenteditable (ready for editing) + const isEditable = await editor.getAttribute('contenteditable') + expect(isEditable).toBe('true') + + await page.screenshot({ path: 'test-results/save-02-note-open.png', fullPage: true }) + + // 3. Verify the editor has content (not empty) + const editorText = await page.evaluate(() => { + const el = document.querySelector('.bn-editor') + return el?.textContent ?? '' + }) + expect(editorText.length).toBeGreaterThan(10) + + // 4. Verify tab bar shows the active note + const tabBar = page.locator('.editor') + await expect(tabBar).toBeVisible() + + await page.screenshot({ path: 'test-results/save-03-editor-ready.png', fullPage: true }) +}) + +test('Cmd+S keyboard shortcut triggers save toast', async ({ page }) => { + // Open a note + const noteList = page.locator('.app__note-list') + await expect(noteList).toBeVisible({ timeout: 5000 }) + const firstNote = noteList.locator('div.cursor-pointer').first() + await firstNote.click() + await page.waitForTimeout(1000) + + // Press Cmd+S — shows either "Saved" or "Nothing to save" depending on + // whether BlockNote's onChange fired from prior interactions + await page.keyboard.press('Meta+s') + await page.waitForTimeout(500) + + // Verify a save-related toast appears (the shortcut was handled) + const toast = page.locator('text=/Saved|Nothing to save/') + await expect(toast).toBeVisible({ timeout: 3000 }) + + await page.screenshot({ path: 'test-results/save-04-cmd-s.png', fullPage: true }) +}) diff --git a/e2e/core-flows.spec.ts b/e2e/core-flows.spec.ts new file mode 100644 index 0000000..51ba787 --- /dev/null +++ b/e2e/core-flows.spec.ts @@ -0,0 +1,260 @@ +import { test, expect } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) // Wait for mock data +}) + +// --- Flow 1: Open app, click note, verify editor shows content --- + +test('clicking a note opens it in the editor with content', async ({ page }) => { + // Click "Build Laputa App" in the note list + await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(300) + + // Tab should appear and be active + await expect(page.locator('.editor__tab--active')).toHaveText(/Build Laputa App/) + + // Editor should show note content (heading in live preview) + await expect(page.locator('.cm-editor')).toBeVisible() + + // Inspector should show properties + await expect(page.locator('.inspector__prop-value', { hasText: 'Project' })).toBeVisible() + + await page.screenshot({ path: 'test-results/core-open-note.png', fullPage: true }) +}) + +test('editor shows markdown content with live preview', async ({ page }) => { + await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(300) + + // CodeMirror should render the content + const editor = page.locator('.cm-editor') + await expect(editor).toBeVisible() + + // Should contain the heading text (rendered by live preview) + await expect(page.locator('.cm-content')).toContainText('Build Laputa App') +}) + +// --- Flow 2: Sidebar filter changes note list --- + +test('sidebar filter: People shows only Person entries', async ({ page }) => { + await page.locator('.sidebar__filter-item', { hasText: 'People' }).click() + await page.waitForTimeout(200) + + await expect(page.locator('.note-list__count')).toHaveText('1') + await expect(page.locator('.note-list__title', { hasText: 'Matteo Cellini' })).toBeVisible() + await expect(page.locator('.note-list__title', { hasText: 'Build Laputa App' })).not.toBeVisible() +}) + +test('sidebar filter: Events shows only Event entries', async ({ page }) => { + await page.locator('.sidebar__filter-item', { hasText: 'Events' }).click() + await page.waitForTimeout(200) + + await expect(page.locator('.note-list__count')).toHaveText('1') + await expect(page.locator('.note-list__title', { hasText: 'Laputa App Design Session' })).toBeVisible() +}) + +test('sidebar filter: clicking back to All Notes restores full list', async ({ page }) => { + await page.locator('.sidebar__filter-item', { hasText: 'People' }).click() + await page.waitForTimeout(200) + await expect(page.locator('.note-list__count')).toHaveText('1') + + await page.locator('.sidebar__filter-item', { hasText: 'All Notes' }).click() + await page.waitForTimeout(200) + await expect(page.locator('.note-list__count')).toHaveText('12') +}) + +// --- Flow 3: Search for a note --- + +test('search filters notes by title', async ({ page }) => { + await page.fill('.note-list__search-input', 'stock') + await page.waitForTimeout(200) + + await expect(page.locator('.note-list__count')).toHaveText('1') + await expect(page.locator('.note-list__title', { hasText: 'Stock Screener' })).toBeVisible() + await expect(page.locator('.note-list__title', { hasText: 'Build Laputa App' })).not.toBeVisible() +}) + +test('clearing search restores all results', async ({ page }) => { + await page.fill('.note-list__search-input', 'stock') + await page.waitForTimeout(200) + await expect(page.locator('.note-list__count')).toHaveText('1') + + await page.fill('.note-list__search-input', '') + await page.waitForTimeout(200) + await expect(page.locator('.note-list__count')).toHaveText('12') +}) + +// --- Flow 4: Open multiple tabs and switch between them --- + +test('opening multiple notes creates multiple tabs', async ({ page }) => { + // Open first note + await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(300) + + // Open second note + await page.locator('.note-list__item', { hasText: 'Grow Newsletter' }).click() + await page.waitForTimeout(300) + + // Both tabs should exist + const tabs = page.locator('.editor__tab') + await expect(tabs).toHaveCount(2) + + // Second tab should be active + await expect(page.locator('.editor__tab--active')).toHaveText(/Grow Newsletter/) + + await page.screenshot({ path: 'test-results/core-multi-tabs.png', fullPage: true }) +}) + +test('clicking a tab switches to it', async ({ page }) => { + // Open two notes + await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(300) + await page.locator('.note-list__item', { hasText: 'Grow Newsletter' }).click() + await page.waitForTimeout(300) + + // Switch back to first tab + await page.locator('.editor__tab', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(200) + + // First tab should now be active + await expect(page.locator('.editor__tab--active')).toHaveText(/Build Laputa App/) + + // Editor should show first note's content + await expect(page.locator('.cm-content')).toContainText('Build Laputa App') +}) + +test('closing a tab removes it and switches to adjacent', async ({ page }) => { + // Open two notes + await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(300) + await page.locator('.note-list__item', { hasText: 'Grow Newsletter' }).click() + await page.waitForTimeout(300) + + // Close active tab (Grow Newsletter) + await page.locator('.editor__tab--active .editor__tab-close').click() + await page.waitForTimeout(200) + + // Should have 1 tab left + await expect(page.locator('.editor__tab')).toHaveCount(1) + await expect(page.locator('.editor__tab--active')).toHaveText(/Build Laputa App/) +}) + +// --- Flow 5: Inspector shows correct properties --- + +test('inspector shows properties for selected note', async ({ page }) => { + await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(300) + + // Type + await expect(page.locator('.inspector__prop-value', { hasText: 'Project' })).toBeVisible() + // Status + await expect(page.locator('.inspector__status-pill', { hasText: 'Active' })).toBeVisible() + // Owner + await expect(page.locator('.inspector__prop-value', { hasText: 'Luca Rossi' })).toBeVisible() + + await page.screenshot({ path: 'test-results/core-inspector.png', fullPage: true }) +}) + +test('inspector shows relationships', async ({ page }) => { + // Open a note with relationships + await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(300) + + // Should show "Related to" relationships + await expect(page.locator('.inspector__section', { hasText: 'Relationships' })).toBeVisible() +}) + +test('inspector shows backlinks', async ({ page }) => { + // Open a note that has backlinks (Build Laputa App is referenced by Facebook Ads and Budget Allocation) + await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(300) + + // Backlinks section should show + const backlinksSection = page.locator('.inspector__section', { hasText: 'Backlinks' }) + await expect(backlinksSection).toBeVisible() +}) + +test('inspector shows git history', async ({ page }) => { + await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(300) + + // Should show commits + await expect(page.locator('.inspector__commit-hash').first()).toBeVisible() + await expect(page.locator('.inspector__commit-msg').first()).toContainText('26q1-laputa-app') +}) + +test('inspector updates when switching tabs', async ({ page }) => { + // Open first note + await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(300) + await expect(page.locator('.inspector__prop-value', { hasText: 'Project' })).toBeVisible() + + // Open second note with different type — use title locator for precision + await page.locator('.note-list__item').filter({ has: page.locator('.note-list__title', { hasText: 'Matteo Cellini' }) }).click() + await page.waitForTimeout(300) + await expect(page.locator('.inspector__prop-value', { hasText: 'Person' })).toBeVisible() +}) + +// --- Flow 6: Note list preview snippets --- + +test('note list items show preview snippets', async ({ page }) => { + // Check that snippets are visible + const snippet = page.locator('.note-list__snippet').first() + await expect(snippet).toBeVisible() + // Snippet should have some text content + const text = await snippet.textContent() + expect(text!.length).toBeGreaterThan(10) +}) + +// --- Flow 7: Create note and verify it appears --- + +test('full create note flow', async ({ page }) => { + // Count before + const countBefore = await page.locator('.note-list__count').textContent() + + // Create new note + await page.click('.note-list__add-btn') + await page.waitForTimeout(200) + await page.fill('.create-dialog__input', 'E2E Test Note') + await page.click('.create-dialog__type-btn:text("Experiment")') + await page.click('.create-dialog__btn--create') + await page.waitForTimeout(500) + + // Count should increase + const countAfter = await page.locator('.note-list__count').textContent() + expect(parseInt(countAfter!, 10)).toBe(parseInt(countBefore!, 10) + 1) + + // Note should be opened in editor + await expect(page.locator('.editor__tab--active')).toHaveText(/E2E Test Note/) + + await page.screenshot({ path: 'test-results/core-create-note.png', fullPage: true }) +}) + +// --- Flow 8: Wiki-link navigation --- + +test('clicking a wikilink opens the target note in a new tab', async ({ page }) => { + // Open "Manage Sponsorships" which contains [[Matteo Cellini]] wikilink + await page.locator('.note-list__item', { hasText: 'Manage Sponsorships' }).click() + await page.waitForTimeout(300) + + // Verify we opened the right note + await expect(page.locator('.editor__tab--active')).toHaveText(/Manage Sponsorships/) + + // Click the wikilink — use mouse.click to fire real mousedown + const wikilink = page.locator('.cm-wikilink', { hasText: 'Matteo Cellini' }) + await expect(wikilink).toBeVisible() + const box = await wikilink.boundingBox() + expect(box).not.toBeNull() + await page.mouse.click(box!.x + box!.width / 2, box!.y + box!.height / 2) + await page.waitForTimeout(300) + + // New tab should open with the target note active + await expect(page.locator('.editor__tab--active')).toHaveText(/Matteo Cellini/) + + // Editor should show the target note's content + await expect(page.locator('.cm-content')).toContainText('Matteo Cellini') + + await page.screenshot({ path: 'test-results/core-wikilink-nav.png', fullPage: true }) +}) diff --git a/e2e/create-note.spec.ts b/e2e/create-note.spec.ts new file mode 100644 index 0000000..7d6aa61 --- /dev/null +++ b/e2e/create-note.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test' + +test('clicking + button opens create note dialog', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) + + // Click the + button + await page.click('.note-list__add-btn') + await page.waitForTimeout(200) + + // Dialog should be visible + await expect(page.locator('.create-dialog')).toBeVisible() + await expect(page.locator('.create-dialog__title')).toHaveText('Create New Note') + + await page.screenshot({ path: 'test-results/create-dialog.png', fullPage: true }) +}) + +test('create a new note via dialog', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) + + // Open dialog + await page.click('.note-list__add-btn') + await page.waitForTimeout(200) + + // Type a title + await page.fill('.create-dialog__input', 'My Test Note') + + // Select "Project" type + await page.click('.create-dialog__type-btn:text("Project")') + + // Click Create + await page.click('.create-dialog__btn--create') + await page.waitForTimeout(300) + + // Dialog should close + await expect(page.locator('.create-dialog')).not.toBeVisible() + + // New note should appear in the list and be opened in editor + await expect(page.locator('.note-list__item:has-text("My Test Note")').first()).toBeVisible() + await expect(page.locator('.editor__tab--active:has-text("My Test Note")')).toBeVisible() + + await page.screenshot({ path: 'test-results/create-note-result.png', fullPage: true }) +}) + +test('escape closes create dialog', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) + + await page.click('.note-list__add-btn') + await page.waitForTimeout(200) + await expect(page.locator('.create-dialog')).toBeVisible() + + await page.keyboard.press('Escape') + await page.waitForTimeout(100) + await expect(page.locator('.create-dialog')).not.toBeVisible() +}) diff --git a/e2e/create-type.spec.ts b/e2e/create-type.spec.ts new file mode 100644 index 0000000..3b62e52 --- /dev/null +++ b/e2e/create-type.spec.ts @@ -0,0 +1,120 @@ +import { test, expect } from '@playwright/test' + +test.describe('Create New Type Feature', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:5203') + await page.waitForSelector('text=All Notes') + }) + + test('clicking + on Types section opens Create Type dialog', async ({ page }) => { + // Hover over the Types section to reveal the + button + const typesSection = page.locator('text=Types').first() + await typesSection.hover() + + // Click the + button next to Types + const createTypeBtn = page.locator('[title="New Type"]') + await createTypeBtn.click() + + // Dialog should open with correct title and elements + await expect(page.locator('text=Create New Type')).toBeVisible() + await expect(page.locator('input[placeholder="e.g. Recipe, Book, Habit..."]')).toBeVisible() + await expect(page.locator('text=Creates a type document')).toBeVisible() + + await page.screenshot({ path: 'test-results/create-type-dialog.png', fullPage: true }) + }) + + test('Create button is disabled when name is empty', async ({ page }) => { + const typesSection = page.locator('text=Types').first() + await typesSection.hover() + await page.locator('[title="New Type"]').click() + + const createBtn = page.locator('button:has-text("Create")') + await expect(createBtn).toBeDisabled() + }) + + test('can create a new type and it appears in sidebar', async ({ page }) => { + // Open Create Type dialog + const typesSection = page.locator('text=Types').first() + await typesSection.hover() + await page.locator('[title="New Type"]').click() + + // Type a name and submit + await page.locator('input[placeholder="e.g. Recipe, Book, Habit..."]').fill('Workout') + await page.locator('button:has-text("Create")').click() + + // Dialog should close + await expect(page.locator('text=Create New Type')).not.toBeVisible() + + // New type should appear as a sidebar section (pluralized) + await expect(page.locator('text=Workouts')).toBeVisible({ timeout: 3000 }) + + // The type document should open in the editor + await expect(page.locator('text=Workout').first()).toBeVisible() + + await page.screenshot({ path: 'test-results/after-create-type.png', fullPage: true }) + }) + + test('newly created type appears in Create Note dialog type selector', async ({ page }) => { + // First, create a custom type + const typesSection = page.locator('text=Types').first() + await typesSection.hover() + await page.locator('[title="New Type"]').click() + await page.locator('input[placeholder="e.g. Recipe, Book, Habit..."]').fill('Workout') + await page.locator('button:has-text("Create")').click() + + // Now open Create Note dialog + await page.keyboard.press('Meta+n') + await page.waitForSelector('text=Create New Note') + + // Built-in types should be visible + await expect(page.locator('button:has-text("Note")')).toBeVisible() + await expect(page.locator('button:has-text("Project")')).toBeVisible() + + // Our custom type should also be visible + await expect(page.locator('button:has-text("Workout")')).toBeVisible() + + await page.screenshot({ path: 'test-results/create-note-with-custom-type.png', fullPage: true }) + }) + + test('can create an instance of a custom type', async ({ page }) => { + // First, create a custom type + const typesSection = page.locator('text=Types').first() + await typesSection.hover() + await page.locator('[title="New Type"]').click() + await page.locator('input[placeholder="e.g. Recipe, Book, Habit..."]').fill('Workout') + await page.locator('button:has-text("Create")').click() + await expect(page.locator('text=Workouts')).toBeVisible({ timeout: 3000 }) + + // Hover over the new Workouts section and click + + const workoutsSection = page.locator('text=Workouts').first() + await workoutsSection.hover() + await page.locator('[title="New Workout"]').click() + + // Create Note dialog should open + await expect(page.locator('text=Create New Note')).toBeVisible() + + // Type a title and create + await page.locator('input[placeholder="Enter note title..."]').fill('Morning Run') + await page.locator('button:has-text("Create")').last().click() + + // The note should open in editor + await expect(page.locator('text=Morning Run').first()).toBeVisible({ timeout: 3000 }) + + await page.screenshot({ path: 'test-results/custom-type-instance.png', fullPage: true }) + }) + + test('Cancel closes the dialog without creating', async ({ page }) => { + const typesSection = page.locator('text=Types').first() + await typesSection.hover() + await page.locator('[title="New Type"]').click() + + await page.locator('input[placeholder="e.g. Recipe, Book, Habit..."]').fill('ShouldNotExist') + await page.locator('button:has-text("Cancel")').click() + + // Dialog should close + await expect(page.locator('text=Create New Type')).not.toBeVisible() + + // Type should NOT appear in sidebar + await expect(page.locator('text=ShouldNotExists')).not.toBeVisible() + }) +}) diff --git a/e2e/debug-content-bug.spec.ts b/e2e/debug-content-bug.spec.ts new file mode 100644 index 0000000..0fa4649 --- /dev/null +++ b/e2e/debug-content-bug.spec.ts @@ -0,0 +1,38 @@ +import { test, expect } from '@playwright/test' + +test.use({ baseURL: 'http://localhost:5201' }) + +/** + * Regression test: editor content must appear after clicking a note. + * + * Root cause: BlockNote's replaceBlocks/insertBlocks internally calls flushSync, + * which fails silently when invoked from inside React's useEffect lifecycle. + * Fix: defer the content swap via queueMicrotask. + */ +test('editor content appears on first note click', async ({ page }) => { + const errors: string[] = [] + page.on('console', msg => { + if (msg.type() === 'error') errors.push(msg.text()) + }) + + await page.goto('/') + // Wait for note list to load + await page.waitForSelector('.app__note-list .cursor-pointer', { timeout: 15000 }) + await page.waitForTimeout(200) + + // Click the first note in the note list + const noteList = page.locator('.app__note-list') + const firstNote = noteList.locator('.cursor-pointer').first() + await firstNote.click() + + // Wait for ProseMirror editor to have content + const pm = page.locator('.ProseMirror') + await expect(async () => { + const text = await pm.textContent() + expect(text?.trim().length, 'Editor content should not be empty').toBeGreaterThan(5) + }).toPass({ timeout: 5000 }) + + // Verify no flushSync errors appeared + const flushSyncErrors = errors.filter(e => e.includes('flushSync')) + expect(flushSyncErrors, 'No flushSync-inside-lifecycle errors should occur').toHaveLength(0) +}) diff --git a/e2e/editor-min-width.spec.ts b/e2e/editor-min-width.spec.ts new file mode 100644 index 0000000..51b1365 --- /dev/null +++ b/e2e/editor-min-width.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from '@playwright/test' + +test('editor panel has proper width regardless of title length', async ({ page }) => { + await page.goto('http://localhost:5204') + await page.waitForTimeout(1000) + + // Click on a note with a short title from the note list + const noteItems = page.locator('.note-list__item') + const count = await noteItems.count() + + if (count > 0) { + // Click the first note + await noteItems.first().click() + await page.waitForTimeout(500) + + // Measure editor container width + const editorContainer = page.locator('.editor__blocknote-container') + const box = await editorContainer.boundingBox() + + // The editor should have a reasonable width (at least 300px) + expect(box).toBeTruthy() + expect(box!.width).toBeGreaterThan(300) + + // Screenshot for visual verification + await page.screenshot({ path: 'test-results/editor-min-width.png', fullPage: true }) + + // Also check that .bn-container fills the editor width + const bnContainer = page.locator('.editor__blocknote-container .bn-container') + const bnBox = await bnContainer.boundingBox() + expect(bnBox).toBeTruthy() + expect(bnBox!.width).toBeGreaterThan(300) + } +}) diff --git a/e2e/filtering.spec.ts b/e2e/filtering.spec.ts new file mode 100644 index 0000000..d2f76d7 --- /dev/null +++ b/e2e/filtering.spec.ts @@ -0,0 +1,82 @@ +import { test, expect } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) // Wait for mock data +}) + +test('All Notes shows all entries', async ({ page }) => { + const count = page.locator('.note-list__count') + await expect(count).toHaveText('12') +}) + +test('clicking People filter shows only people', async ({ page }) => { + await page.click('text=People') + await page.waitForTimeout(100) + const count = page.locator('.note-list__count') + await expect(count).toHaveText('1') + await expect(page.locator('.note-list__title', { hasText: 'Matteo Cellini' })).toBeVisible() + await page.screenshot({ path: 'test-results/filter-people.png' }) +}) + +test('clicking Events filter shows only events', async ({ page }) => { + await page.click('text=Events') + await page.waitForTimeout(100) + const count = page.locator('.note-list__count') + await expect(count).toHaveText('1') + await expect(page.locator('.note-list__title', { hasText: 'Laputa App Design Session' })).toBeVisible() + await page.screenshot({ path: 'test-results/filter-events.png' }) +}) + +test('clicking PROJECTS header shows all projects', async ({ page }) => { + await page.click('text=PROJECTS') + await page.waitForTimeout(100) + const count = page.locator('.note-list__count') + await expect(count).toHaveText('1') + await expect(page.locator('.note-list__title', { hasText: 'Build Laputa App' })).toBeVisible() + await page.screenshot({ path: 'test-results/filter-projects.png' }) +}) + +test('clicking specific entity shows it pinned with children', async ({ page }) => { + await page.locator('.sidebar__item', { hasText: 'Build Laputa App' }).click() + await page.waitForTimeout(100) + // Pinned entity + children that belongTo this project + await expect(page.locator('.note-list__title', { hasText: 'Build Laputa App' })).toBeVisible() + await expect(page.locator('.note-list__title', { hasText: 'Facebook Ads Strategy' })).toBeVisible() + await expect(page.locator('.note-list__title', { hasText: 'Budget Allocation' })).toBeVisible() + await expect(page.locator('.note-list__item--pinned')).toBeVisible() + await page.screenshot({ path: 'test-results/filter-entity.png' }) +}) + +test('clicking topic shows entries related to that topic', async ({ page }) => { + await page.locator('.sidebar__topic-item', { hasText: 'Software Development' }).click() + await page.waitForTimeout(100) + await expect(page.locator('.note-list__title', { hasText: 'Build Laputa App' })).toBeVisible() + await page.screenshot({ path: 'test-results/filter-topic.png' }) +}) + +test('search bar filters by title substring', async ({ page }) => { + await page.fill('.note-list__search-input', 'budget') + await page.waitForTimeout(100) + const count = page.locator('.note-list__count') + await expect(count).toHaveText('1') + await expect(page.locator('.note-list__title', { hasText: 'Budget Allocation' })).toBeVisible() + await expect(page.locator('.note-list__title', { hasText: 'Build Laputa App' })).not.toBeVisible() + await page.screenshot({ path: 'test-results/search-budget.png' }) +}) + +test('type filter pills narrow results', async ({ page }) => { + // Click "Projects" pill + await page.locator('.note-list__pill', { hasText: 'Projects' }).click() + await page.waitForTimeout(100) + const count = page.locator('.note-list__count') + await expect(count).toHaveText('1') + await expect(page.locator('.note-list__title', { hasText: 'Build Laputa App' })).toBeVisible() + await expect(page.locator('.note-list__title', { hasText: 'Matteo Cellini' })).not.toBeVisible() + await page.screenshot({ path: 'test-results/pill-projects.png' }) + + // Click "All" to reset + await page.locator('.note-list__pill', { hasText: 'All' }).click() + await page.waitForTimeout(100) + await expect(count).toHaveText('12') +}) diff --git a/e2e/find-selectors.spec.ts b/e2e/find-selectors.spec.ts new file mode 100644 index 0000000..4af5b42 --- /dev/null +++ b/e2e/find-selectors.spec.ts @@ -0,0 +1,19 @@ +import { test } from '@playwright/test' + +test('find note selectors', async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 900 }) + await page.goto('/', { waitUntil: 'networkidle' }) + await page.waitForTimeout(3000) + + // Get all clickable elements with their text + const info = await page.evaluate(() => { + const items = document.querySelectorAll('[class*="cursor-pointer"]') + return Array.from(items).slice(0, 20).map(el => ({ + tag: el.tagName, + text: el.textContent?.trim().slice(0, 50), + cls: el.className.toString().slice(0, 80), + rect: el.getBoundingClientRect().toJSON() + })) + }) + console.log(JSON.stringify(info, null, 2)) +}) diff --git a/e2e/image-drag-drop.spec.ts b/e2e/image-drag-drop.spec.ts new file mode 100644 index 0000000..b33d22b --- /dev/null +++ b/e2e/image-drag-drop.spec.ts @@ -0,0 +1,182 @@ +import { test, expect } from '@playwright/test' +import * as path from 'path' +import * as fs from 'fs' + +// Minimal valid PNG: 1x1 red pixel +const TEST_PNG_BASE64 = + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==' + +function createTestImage(filepath: string) { + fs.mkdirSync(path.dirname(filepath), { recursive: true }) + fs.writeFileSync(filepath, Buffer.from(TEST_PNG_BASE64, 'base64')) +} + +test('drag & drop image into editor inserts image block', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(1000) + + // Open a note + await page.locator('[data-testid="type-icon"]').first().click({ timeout: 10000 }) + await page.waitForTimeout(500) + + const editor = page.locator('.bn-editor') + await expect(editor).toBeVisible({ timeout: 10000 }) + await editor.click() + await page.waitForTimeout(200) + + // Create a test image file + const testImagePath = path.join(process.cwd(), 'test-results', 'drag-test-image.png') + createTestImage(testImagePath) + + // Screenshot before + await page.screenshot({ path: 'test-results/drag-drop-before.png', fullPage: true }) + + // Simulate drag-and-drop of a file into the editor + // Playwright supports dispatching drag events with DataTransfer + const editorContainer = page.locator('.editor__blocknote-container') + const box = await editorContainer.boundingBox() + expect(box).toBeTruthy() + + // Use Playwright's page.dispatchEvent with a custom script to simulate file drop + await page.evaluate(async ({ base64, x, y }) => { + const container = document.querySelector('.editor__blocknote-container') + if (!container) throw new Error('Editor container not found') + + // Convert base64 to Uint8Array + const binary = atob(base64) + const bytes = new Uint8Array(binary.length) + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i) + + // Create a File object + const file = new File([bytes], 'drag-test-image.png', { type: 'image/png' }) + + // Create DataTransfer with the file + const dt = new DataTransfer() + dt.items.add(file) + + // Dispatch dragover first (to set the drop effect) + const dragOverEvent = new DragEvent('dragover', { + dataTransfer: dt, + bubbles: true, + cancelable: true, + clientX: x, + clientY: y, + }) + container.dispatchEvent(dragOverEvent) + + // Then dispatch drop + const dropEvent = new DragEvent('drop', { + dataTransfer: dt, + bubbles: true, + cancelable: true, + clientX: x, + clientY: y, + }) + container.dispatchEvent(dropEvent) + }, { + base64: TEST_PNG_BASE64, + x: box!.x + box!.width / 2, + y: box!.y + box!.height / 2, + }) + + // Wait for the image to be uploaded and inserted + await page.waitForTimeout(2000) + + // Verify: image element exists in the editor + const images = page.locator('.bn-editor img') + await expect(images.first()).toBeVisible({ timeout: 5000 }) + + // Verify: image uses data URL (browser mode) + const src = await images.first().getAttribute('src') + expect(src).toMatch(/^data:/) + + await page.screenshot({ path: 'test-results/drag-drop-after.png', fullPage: true }) + + // Clean up + if (fs.existsSync(testImagePath)) fs.unlinkSync(testImagePath) +}) + +test('drop zone overlay appears during image drag', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(1000) + + // Open a note + await page.locator('[data-testid="type-icon"]').first().click({ timeout: 10000 }) + await page.waitForTimeout(500) + + const editor = page.locator('.bn-editor') + await expect(editor).toBeVisible({ timeout: 10000 }) + + const editorContainer = page.locator('.editor__blocknote-container') + const box = await editorContainer.boundingBox() + expect(box).toBeTruthy() + + // Simulate dragover with an image file to trigger overlay + await page.evaluate(({ x, y }) => { + const container = document.querySelector('.editor__blocknote-container') + if (!container) throw new Error('Editor container not found') + + const file = new File([new Uint8Array(1)], 'test.png', { type: 'image/png' }) + const dt = new DataTransfer() + dt.items.add(file) + + const event = new DragEvent('dragover', { + dataTransfer: dt, + bubbles: true, + cancelable: true, + clientX: x, + clientY: y, + }) + container.dispatchEvent(event) + }, { x: box!.x + box!.width / 2, y: box!.y + box!.height / 2 }) + + // The drop overlay should be visible + const overlay = page.locator('.editor__drop-overlay') + await expect(overlay).toBeVisible({ timeout: 2000 }) + await expect(overlay).toContainText('Drop image here') + + await page.screenshot({ path: 'test-results/drag-drop-overlay.png', fullPage: true }) +}) + +test('non-image file drop is ignored', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(1000) + + // Open a note + await page.locator('[data-testid="type-icon"]').first().click({ timeout: 10000 }) + await page.waitForTimeout(500) + + const editor = page.locator('.bn-editor') + await expect(editor).toBeVisible({ timeout: 10000 }) + + const editorContainer = page.locator('.editor__blocknote-container') + const box = await editorContainer.boundingBox() + expect(box).toBeTruthy() + + // Count images before + const imagesBefore = await page.locator('.bn-editor img').count() + + // Simulate dropping a text file + await page.evaluate(({ x, y }) => { + const container = document.querySelector('.editor__blocknote-container') + if (!container) throw new Error('Editor container not found') + + const file = new File(['not an image'], 'readme.txt', { type: 'text/plain' }) + const dt = new DataTransfer() + dt.items.add(file) + + container.dispatchEvent(new DragEvent('drop', { + dataTransfer: dt, + bubbles: true, + cancelable: true, + clientX: x, + clientY: y, + })) + }, { x: box!.x + box!.width / 2, y: box!.y + box!.height / 2 }) + + await page.waitForTimeout(500) + + // No new images should have been added + const imagesAfter = await page.locator('.bn-editor img').count() + expect(imagesAfter).toBe(imagesBefore) +}) diff --git a/e2e/image-upload.spec.ts b/e2e/image-upload.spec.ts new file mode 100644 index 0000000..82dfdb7 --- /dev/null +++ b/e2e/image-upload.spec.ts @@ -0,0 +1,134 @@ +import { test, expect, type Page } from '@playwright/test' +import * as path from 'path' +import * as fs from 'fs' +import { + createFixtureVaultCopy, + openFixtureVault, + removeFixtureVaultCopy, +} from '../tests/helpers/fixtureVault' + +// Minimal valid PNG: 1x1 red pixel +const TEST_PNG_BASE64 = + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==' + +function createTestPng(filepath: string) { + fs.mkdirSync(path.dirname(filepath), { recursive: true }) + fs.writeFileSync(filepath, Buffer.from(TEST_PNG_BASE64, 'base64')) +} + +let tempVaultDir: string + +async function openImageTestNote(page: Page) { + await page.locator('[data-testid="note-list-container"]').getByText('Alpha Project', { exact: true }).click() + + const editor = page.locator('.bn-editor') + await expect(editor).toBeVisible({ timeout: 10000 }) + return editor +} + +test.beforeEach(async ({ page }, testInfo) => { + testInfo.setTimeout(60_000) + tempVaultDir = createFixtureVaultCopy() + await openFixtureVault(page, tempVaultDir) +}) + +test.afterEach(async () => { + removeFixtureVaultCopy(tempVaultDir) +}) + +test('image upload via file picker displays image with data URL', async ({ page }) => { + const editor = await openImageTestNote(page) + await editor.click() + await page.waitForTimeout(200) + + // Insert image block via slash command + await page.keyboard.press('Enter') + await page.waitForTimeout(100) + await page.keyboard.type('/image', { delay: 80 }) + await page.waitForTimeout(500) + + // Select Image from slash menu (press Enter to pick first match) + await page.keyboard.press('Enter') + await page.waitForTimeout(500) + + // Verify Upload tab is available (uploadFile is configured) + const fileInput = page.locator('input[type="file"]') + expect(await fileInput.count()).toBeGreaterThan(0) + + // Upload a test image + const testImagePath = path.join(process.cwd(), 'test-results', 'test-image.png') + createTestPng(testImagePath) + + await fileInput.first().setInputFiles(testImagePath) + await page.waitForTimeout(2000) + + // Verify: image element exists in the editor + const images = page.locator('.bn-editor img') + const imageCount = await images.count() + expect(imageCount).toBeGreaterThan(0) + + // Verify: image uses data URL (stable, survives reload in dev mode) + const src = await images.first().getAttribute('src') + expect(src).toMatch(/^data:/) + + // Verify: no "Loading..." elements remain + const loadingEls = page.locator('.bn-file-loading-preview') + expect(await loadingEls.count()).toBe(0) + + await page.screenshot({ path: 'test-results/image-upload-after.png', fullPage: true }) + + if (fs.existsSync(testImagePath)) fs.unlinkSync(testImagePath) +}) + +test('image paste into editor inserts image block', async ({ page }) => { + const editor = await openImageTestNote(page) + await editor.click() + + await page.evaluate((base64) => { + const editorElement = document.querySelector('.bn-editor') + if (!editorElement) throw new Error('Editor not found') + + const binary = atob(base64) + const bytes = new Uint8Array(binary.length) + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i) + + const file = new File([bytes], 'pasted-image.png', { type: 'image/png' }) + const clipboardData = new DataTransfer() + clipboardData.items.add(file) + editorElement.dispatchEvent(new ClipboardEvent('paste', { + clipboardData, + bubbles: true, + cancelable: true, + })) + }, TEST_PNG_BASE64) + + const images = page.locator('.bn-editor img') + await expect(images.first()).toBeVisible({ timeout: 5000 }) + + const src = await images.first().getAttribute('src') + expect(src).toMatch(/^data:/) +}) + +test('editor has uploadFile configured (no error on image block insert)', async ({ page }) => { + const editor = await openImageTestNote(page) + + // Capture console errors + const errors: string[] = [] + page.on('pageerror', (err) => errors.push(err.message)) + + // Insert an image block via slash command + await editor.click() + await page.keyboard.press('Enter') + await page.keyboard.type('/image', { delay: 30 }) + await page.waitForTimeout(500) + + // Press Enter to select Image + await page.keyboard.press('Enter') + await page.waitForTimeout(500) + + await page.screenshot({ path: 'test-results/image-block-inserted.png', fullPage: true }) + + // No errors related to upload should have occurred + const uploadErrors = errors.filter(e => e.includes('upload')) + expect(uploadErrors).toHaveLength(0) +}) diff --git a/e2e/keyboard-shortcuts.spec.ts b/e2e/keyboard-shortcuts.spec.ts new file mode 100644 index 0000000..c0e92be --- /dev/null +++ b/e2e/keyboard-shortcuts.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from '@playwright/test' + +test('Cmd+N opens create note dialog', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) + + await page.keyboard.press('Meta+n') + await page.waitForTimeout(200) + + await expect(page.locator('.create-dialog')).toBeVisible() + await expect(page.locator('.create-dialog__title')).toHaveText('Create New Note') +}) + +test('Cmd+S shows save toast', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) + + await page.keyboard.press('Meta+s') + await page.waitForTimeout(200) + + await expect(page.locator('.toast')).toBeVisible() + await expect(page.locator('.toast')).toHaveText('Saved') + + await page.screenshot({ path: 'test-results/save-toast.png', fullPage: true }) +}) + +test('Cmd+W closes the active tab', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) + + // Open a note + await page.click('.note-list__item') + await page.waitForTimeout(300) + await expect(page.locator('.editor__tab--active')).toBeVisible() + + // Close it with Cmd+W + await page.keyboard.press('Meta+w') + await page.waitForTimeout(300) + + // Tab should be gone, placeholder should show + await expect(page.locator('.editor__tab')).not.toBeVisible() + await expect(page.locator('.editor__placeholder')).toBeVisible() +}) diff --git a/e2e/quick-open.spec.ts b/e2e/quick-open.spec.ts new file mode 100644 index 0000000..d038013 --- /dev/null +++ b/e2e/quick-open.spec.ts @@ -0,0 +1,88 @@ +import { test, expect } from '@playwright/test' + +test('Cmd+P opens quick open palette', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) + + // Open palette with keyboard shortcut + await page.keyboard.press('Meta+p') + await page.waitForTimeout(200) + + await expect(page.locator('.palette')).toBeVisible() + await expect(page.locator('.palette__input')).toBeFocused() + + await page.screenshot({ path: 'test-results/quick-open.png', fullPage: true }) +}) + +test('quick open: search and select a note', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) + + await page.keyboard.press('Meta+p') + await page.waitForTimeout(200) + + // Type to search + await page.fill('.palette__input', 'laputa') + await page.waitForTimeout(100) + + // Should show matching result + await expect(page.locator('.palette__item-title:has-text("Build Laputa App")')).toBeVisible() + + // Press Enter to select + await page.keyboard.press('Enter') + await page.waitForTimeout(500) + + // Palette should close and note should be opened + await expect(page.locator('.palette')).not.toBeVisible() + // The top result should have been opened (wait for async content load) + await expect(page.locator('.editor__tab--active')).toBeVisible({ timeout: 3000 }) + + await page.screenshot({ path: 'test-results/quick-open-selected.png', fullPage: true }) +}) + +test('quick open: arrow keys navigate results', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) + + await page.keyboard.press('Meta+p') + await page.waitForTimeout(200) + + // First item should be selected by default + await expect(page.locator('.palette__item--selected').first()).toBeVisible() + + // Arrow down to move selection + await page.keyboard.press('ArrowDown') + await page.waitForTimeout(50) + + // Second item should be selected + const items = page.locator('.palette__item') + const count = await items.count() + expect(count).toBeGreaterThan(1) +}) + +test('quick open: Escape closes palette', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) + + await page.keyboard.press('Meta+p') + await page.waitForTimeout(200) + await expect(page.locator('.palette')).toBeVisible() + + await page.keyboard.press('Escape') + await page.waitForTimeout(100) + await expect(page.locator('.palette')).not.toBeVisible() +}) + +test('quick open: clicking outside closes palette', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) + + await page.keyboard.press('Meta+p') + await page.waitForTimeout(200) + await expect(page.locator('.palette')).toBeVisible() + + // Click the overlay area (outside the palette) using mouse click at top-left + await page.mouse.click(10, 10) + await page.waitForTimeout(200) + await expect(page.locator('.palette')).not.toBeVisible() +}) diff --git a/e2e/rename-tab.spec.ts b/e2e/rename-tab.spec.ts new file mode 100644 index 0000000..10bcd37 --- /dev/null +++ b/e2e/rename-tab.spec.ts @@ -0,0 +1,80 @@ +import { test, expect } from '@playwright/test' + +test('rename tab by double-clicking', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(2000) + + // Screenshot initial state + await page.screenshot({ path: 'test-results/rename-01-initial.png', fullPage: true }) + + // Click a note in the list using text content + await page.getByText('Deprecated Workflow').first().click() + await page.waitForTimeout(500) + + // Screenshot: note opened as tab + await page.screenshot({ path: 'test-results/rename-02-note-opened.png', fullPage: true }) + + // Find the tab title in the tab bar and double-click it + const tabTitle = page.locator('.group span.truncate').first() + await expect(tabTitle).toBeVisible({ timeout: 5000 }) + const originalTitle = await tabTitle.textContent() + console.log(`Original title: "${originalTitle}"`) + + await tabTitle.dblclick() + await page.waitForTimeout(300) + + // Screenshot: editing mode + await page.screenshot({ path: 'test-results/rename-03-editing.png', fullPage: true }) + + // Verify input appeared + const editInput = page.locator('.group input') + await expect(editInput).toBeVisible({ timeout: 3000 }) + + // Type new name + await editInput.fill('Renamed Test Note') + await page.screenshot({ path: 'test-results/rename-04-typing.png', fullPage: true }) + + // Press Enter to save + await editInput.press('Enter') + await page.waitForTimeout(1000) + + // Screenshot: after rename + await page.screenshot({ path: 'test-results/rename-05-saved.png', fullPage: true }) + + // Verify tab title changed + const newTabTitle = page.locator('.group span.truncate').first() + await expect(newTabTitle).toHaveText('Renamed Test Note') +}) + +test('cancel rename with Escape', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(2000) + + // Open a note + await page.getByText('Deprecated Workflow').first().click() + await page.waitForTimeout(500) + + const tabTitle = page.locator('.group span.truncate').first() + const originalTitle = await tabTitle.textContent() + + // Double-click to edit + await tabTitle.dblclick() + await page.waitForTimeout(300) + + const editInput = page.locator('.group input') + await expect(editInput).toBeVisible({ timeout: 3000 }) + + // Type something different + await editInput.fill('Will Be Cancelled') + + // Press Escape to cancel + await editInput.press('Escape') + await page.waitForTimeout(300) + + // Screenshot: after cancel + await page.screenshot({ path: 'test-results/rename-06-cancelled.png', fullPage: true }) + + // Verify title unchanged + const afterTitle = page.locator('.group span.truncate').first() + await expect(afterTitle).toHaveText(originalTitle!) +}) diff --git a/e2e/screenshot.spec.ts b/e2e/screenshot.spec.ts new file mode 100644 index 0000000..0680b0f --- /dev/null +++ b/e2e/screenshot.spec.ts @@ -0,0 +1,94 @@ +import { test } from '@playwright/test' + +test('capture app screenshot for review', async ({ page }) => { + await page.goto('/') + // Wait for mock data to load + await page.waitForTimeout(500) + await page.screenshot({ path: 'test-results/app-screenshot.png', fullPage: true }) +}) + +test('capture editor with note selected', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) + + // Click the first note in the list + await page.click('.note-list__item') + await page.waitForTimeout(300) + + await page.screenshot({ path: 'test-results/editor-screenshot.png', fullPage: true }) +}) + +test('live preview: headings styled, syntax hidden', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) + + // Click a note to load it + await page.click('.note-list__item') + await page.waitForTimeout(500) + + // Screenshot showing live preview (headings styled, syntax hidden) + await page.screenshot({ path: 'test-results/live-preview.png', fullPage: true }) +}) + +test('tab bar: multiple tabs open and close', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) + + // Click first note + await page.click('.note-list__item') + await page.waitForTimeout(500) + + // Debug: screenshot after first click + await page.screenshot({ path: 'test-results/tabs-debug-1.png', fullPage: true }) + + // Check if other items are visible + const items = await page.locator('.note-list__item').count() + console.log(`Note list items visible after first click: ${items}`) + + // Click second item if available + if (items >= 2) { + await page.locator('.note-list__item').nth(1).click({ timeout: 5000 }) + await page.waitForTimeout(500) + } + + if (items >= 3) { + await page.locator('.note-list__item').nth(2).click({ timeout: 5000 }) + await page.waitForTimeout(500) + } + + await page.screenshot({ path: 'test-results/tabs-screenshot.png', fullPage: true }) +}) + +test('frontmatter hidden from editor view', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) + + // Click a note that has frontmatter + await page.click('.note-list__item') + await page.waitForTimeout(500) + + // Frontmatter should be hidden — editor starts with content, not --- + await page.screenshot({ path: 'test-results/frontmatter-hidden.png', fullPage: true }) +}) + +test('wikilinks: rendered as styled elements and clickable', async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) + + // Open "Manage Sponsorships" which contains [[Matteo Cellini]] wikilink + await page.locator('.note-list__item', { hasText: 'Manage Sponsorships' }).click() + await page.waitForTimeout(500) + + // Screenshot showing wikilink rendered as styled text + await page.screenshot({ path: 'test-results/wikilinks-styled.png', fullPage: true }) + + // Click the wikilink to navigate — use mouse.click to fire real mousedown + const wikilink = page.locator('.cm-wikilink', { hasText: 'Matteo Cellini' }) + const box = await wikilink.boundingBox() + if (box) { + await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2) + await page.waitForTimeout(500) + // Should now have a new tab for Matteo Cellini + await page.screenshot({ path: 'test-results/wikilinks-navigated.png', fullPage: true }) + } +}) diff --git a/e2e/settings-oauth.spec.ts b/e2e/settings-oauth.spec.ts new file mode 100644 index 0000000..1b81755 --- /dev/null +++ b/e2e/settings-oauth.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from '@playwright/test' + +test('settings shows connected GitHub state with username', async ({ page }) => { + await page.goto('http://localhost:5243/') + await page.waitForLoadState('networkidle') + await page.waitForTimeout(500) + + // Open settings + await page.keyboard.press('Meta+Comma') + await page.waitForTimeout(500) + + // Verify settings panel opened + await expect(page.getByTestId('settings-panel')).toBeVisible() + + // Mock data starts with github connected — verify connected state + const connected = page.getByTestId('github-connected') + await expect(connected).toBeVisible({ timeout: 5000 }) + await expect(connected).toContainText('lucaong') + await expect(connected).toContainText('Connected') + + // Verify disconnect button + await expect(page.getByTestId('github-disconnect')).toBeVisible() + + // Verify NO token input field + await expect(page.getByTestId('settings-key-github-token')).not.toBeVisible() + + await page.screenshot({ path: 'test-results/settings-oauth-connected.png' }) +}) + +test('disconnect shows Login with GitHub button', async ({ page }) => { + await page.goto('http://localhost:5243/') + await page.waitForLoadState('networkidle') + await page.waitForTimeout(500) + + // Open settings + await page.keyboard.press('Meta+Comma') + await page.waitForTimeout(500) + + // Click disconnect + const disconnectBtn = page.getByTestId('github-disconnect') + await expect(disconnectBtn).toBeVisible({ timeout: 5000 }) + await disconnectBtn.click() + await page.waitForTimeout(300) + + // Login button should now be visible + const loginBtn = page.getByTestId('github-login') + await expect(loginBtn).toBeVisible({ timeout: 3000 }) + await expect(loginBtn).toContainText('Login with GitHub') + + await page.screenshot({ path: 'test-results/settings-oauth-login.png' }) +}) diff --git a/e2e/sidebar-collapse.spec.ts b/e2e/sidebar-collapse.spec.ts new file mode 100644 index 0000000..c54528b --- /dev/null +++ b/e2e/sidebar-collapse.spec.ts @@ -0,0 +1,90 @@ +import { test, expect } from '@playwright/test' + +// On macOS, Alt+key produces special characters, so we dispatch events directly +function dispatchAltKey(page: import('@playwright/test').Page, key: string) { + return page.evaluate((k) => { + window.dispatchEvent(new KeyboardEvent('keydown', { + key: k, altKey: true, metaKey: false, ctrlKey: false, + bubbles: true, cancelable: true, + })) + }, key) +} + +async function loadApp(page: import('@playwright/test').Page) { + await page.goto('/') + // Clear stored view mode so we start fresh + await page.evaluate(() => localStorage.removeItem('laputa-view-mode')) + await page.reload() + await page.waitForTimeout(500) +} + +test.describe('Sidebar collapse', () => { + test('default: all three panels visible', async ({ page }) => { + await loadApp(page) + + await expect(page.locator('.app__sidebar')).toBeVisible() + await expect(page.locator('.app__note-list')).toBeVisible() + await expect(page.locator('.app__editor')).toBeVisible() + + await page.screenshot({ path: 'test-results/collapse-all-panels.png', fullPage: true }) + }) + + test('collapse button hides sidebar', async ({ page }) => { + await loadApp(page) + + const collapseBtn = page.locator('button[aria-label="Collapse sidebar"]') + await expect(collapseBtn).toBeVisible() + await collapseBtn.click() + await page.waitForTimeout(500) + + await expect(page.locator('.app__sidebar')).toHaveCount(0) + await expect(page.locator('.app__note-list')).toBeVisible() + await expect(page.locator('.app__editor')).toBeVisible() + + await page.screenshot({ path: 'test-results/collapse-sidebar-hidden.png', fullPage: true }) + }) + + test('Alt+1 shows editor only', async ({ page }) => { + await loadApp(page) + + await dispatchAltKey(page, '1') + await page.waitForTimeout(500) + + await expect(page.locator('.app__sidebar')).toHaveCount(0) + await expect(page.locator('.app__note-list')).toHaveCount(0) + await expect(page.locator('.app__editor')).toBeVisible() + + await page.screenshot({ path: 'test-results/collapse-editor-only.png', fullPage: true }) + }) + + test('Alt+2 shows editor + note list', async ({ page }) => { + await loadApp(page) + + await dispatchAltKey(page, '2') + await page.waitForTimeout(500) + + await expect(page.locator('.app__sidebar')).toHaveCount(0) + await expect(page.locator('.app__note-list')).toBeVisible() + await expect(page.locator('.app__editor')).toBeVisible() + + await page.screenshot({ path: 'test-results/collapse-editor-list.png', fullPage: true }) + }) + + test('Alt+3 restores all panels after collapse', async ({ page }) => { + await loadApp(page) + + // Collapse first + await dispatchAltKey(page, '1') + await page.waitForTimeout(500) + await expect(page.locator('.app__sidebar')).toHaveCount(0) + + // Restore + await dispatchAltKey(page, '3') + await page.waitForTimeout(500) + await expect(page.locator('.app__sidebar')).toBeVisible() + await expect(page.locator('.app__note-list')).toBeVisible() + await expect(page.locator('.app__editor')).toBeVisible() + + await page.screenshot({ path: 'test-results/collapse-restored.png', fullPage: true }) + }) +}) diff --git a/e2e/vault-picker-local.spec.ts b/e2e/vault-picker-local.spec.ts new file mode 100644 index 0000000..3ab0b54 --- /dev/null +++ b/e2e/vault-picker-local.spec.ts @@ -0,0 +1,39 @@ +import { test, expect } from '@playwright/test' + +test.describe('Vault Picker Local Options', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:5240') + await page.waitForSelector('text=notes', { timeout: 10000 }) + }) + + test('vault menu shows local folder options', async ({ page }) => { + // Screenshot before opening menu + await page.screenshot({ path: 'test-results/vault-picker-before.png', fullPage: true }) + + // Click the vault button in the status bar to open the menu + const vaultButton = page.locator('[title="Switch vault"]') + await expect(vaultButton).toBeVisible() + await vaultButton.click() + + // Wait for menu to appear + await page.waitForTimeout(300) + + // Verify all three options are visible + await expect(page.locator('text=Open local folder')).toBeVisible() + await expect(page.locator('text=Create new vault')).toBeVisible() + await expect(page.locator('text=Connect GitHub repo')).toBeVisible() + + // Screenshot with menu open showing all options + await page.screenshot({ path: 'test-results/vault-picker-menu-open.png', fullPage: true }) + }) + + test('vault menu options have correct test IDs', async ({ page }) => { + const vaultButton = page.locator('[title="Switch vault"]') + await vaultButton.click() + await page.waitForTimeout(200) + + await expect(page.locator('[data-testid="vault-menu-open-local"]')).toBeVisible() + await expect(page.locator('[data-testid="vault-menu-create-new"]')).toBeVisible() + await expect(page.locator('[data-testid="vault-menu-connect-github"]')).toBeVisible() + }) +}) diff --git a/e2e/visual-verify.spec.ts b/e2e/visual-verify.spec.ts new file mode 100644 index 0000000..2eb6779 --- /dev/null +++ b/e2e/visual-verify.spec.ts @@ -0,0 +1,103 @@ +import { test, expect } from '@playwright/test' + +test('visual verify: editor theme + list indentation', async ({ page }) => { + const consoleErrors: string[] = [] + page.on('console', msg => { + if (msg.type() === 'error') consoleErrors.push(msg.text()) + }) + + await page.goto('http://localhost:5173') + await page.waitForTimeout(1000) + + // Open "Write Weekly Essays" which has bullets, nested items, checkboxes + const noteItem = page.locator('.note-list__item', { hasText: 'Write Weekly Essays' }) + await noteItem.click() + await page.waitForTimeout(1000) + + const cmEditor = page.locator('.cm-editor') + await expect(cmEditor).toBeVisible() + + // No console errors + expect(consoleErrors).toHaveLength(0) + + // --- BUG 2: Verify list indentation --- + + // Level-0 bullets should have 40px padding-left + const level0Lines = page.locator('.cm-line.cm-live-list-level-0') + const level0Count = await level0Lines.count() + console.log(`Level-0 bullet lines: ${level0Count}`) + expect(level0Count).toBeGreaterThan(0) + + const paddingL0 = await level0Lines.first().evaluate(el => + window.getComputedStyle(el).paddingLeft + ) + console.log(`Level-0 padding-left: ${paddingL0}`) + expect(parseInt(paddingL0, 10)).toBe(40) + + // Bullet widgets and checkboxes are rendered + const bulletCount = await page.locator('.cm-live-bullet').count() + console.log(`Bullet widgets: ${bulletCount}`) + expect(bulletCount).toBeGreaterThan(0) + + const checkboxCount = await page.locator('.cm-live-checkbox').count() + console.log(`Checkbox widgets: ${checkboxCount}`) + expect(checkboxCount).toBeGreaterThan(0) + + // Screenshot dark mode (top) + await page.screenshot({ path: 'test-results/01-dark-mode-editor.png', fullPage: true }) + + // Scroll down to see nested items section + const scroller = page.locator('.cm-scroller') + await scroller.evaluate(el => el.scrollTop = el.scrollHeight) + await page.waitForTimeout(300) + await page.screenshot({ path: 'test-results/01b-dark-mode-nested.png', fullPage: true }) + + // --- BUG 1: Verify theme toggle --- + + // Dark mode: editor bg should be dark + const darkBg = await cmEditor.evaluate(el => + window.getComputedStyle(el).backgroundColor + ) + console.log(`Dark mode bg: ${darkBg}`) + expect(darkBg).toBe('rgb(15, 15, 26)') + + // Toggle to light mode + const themeToggle = page.locator('.sidebar__theme-toggle') + await themeToggle.click() + await page.waitForTimeout(500) + + // Scroll back to top for light mode screenshot + await scroller.evaluate(el => el.scrollTop = 0) + await page.waitForTimeout(200) + await page.screenshot({ path: 'test-results/02-light-mode-editor.png', fullPage: true }) + + // Light mode: editor bg should be white + const lightBg = await cmEditor.evaluate(el => + window.getComputedStyle(el).backgroundColor + ) + console.log(`Light mode bg: ${lightBg}`) + expect(lightBg).toBe('rgb(255, 255, 255)') + + // Heading color should be dark in light mode + const headingColor = await page.locator('.cm-live-heading').first().evaluate(el => + window.getComputedStyle(el).color + ) + console.log(`Light mode heading color: ${headingColor}`) + expect(headingColor).toBe('rgb(55, 53, 47)') + + // Scroll to nested items in light mode + await scroller.evaluate(el => el.scrollTop = el.scrollHeight) + await page.waitForTimeout(300) + await page.screenshot({ path: 'test-results/02b-light-mode-nested.png', fullPage: true }) + + // Toggle back to dark mode + await themeToggle.click() + await page.waitForTimeout(500) + await page.screenshot({ path: 'test-results/03-dark-mode-restored.png', fullPage: true }) + + const restoredBg = await cmEditor.evaluate(el => + window.getComputedStyle(el).backgroundColor + ) + console.log(`Restored dark mode bg: ${restoredBg}`) + expect(restoredBg).toBe('rgb(15, 15, 26)') +}) diff --git a/e2e/zero-shift-closeup.spec.ts b/e2e/zero-shift-closeup.spec.ts new file mode 100644 index 0000000..58ad875 --- /dev/null +++ b/e2e/zero-shift-closeup.spec.ts @@ -0,0 +1,77 @@ +import { test, expect } from '@playwright/test' + +test('closeup: heading marker in gutter', async ({ page }) => { + await page.goto('http://localhost:5173') + await page.waitForTimeout(800) + + const noteItem = page.locator('.note-list__item', { hasText: 'Build Laputa App' }) + await noteItem.click() + await page.waitForTimeout(800) + + // Find "Overview" heading and take a tight crop around it + const heading = page.locator('.cm-header-2', { hasText: 'Overview' }).first() + await expect(heading).toBeVisible() + const hBox = await heading.boundingBox() + if (!hBox) throw new Error('heading not found') + + // Crop: include gutter area to the left (extra 80px) and a small vertical band + const clip = { + x: Math.max(0, hBox.x - 80), + y: hBox.y - 10, + width: hBox.width + 120, + height: hBox.height + 20, + } + + // Before click — preview mode, no marker visible + await page.screenshot({ path: 'test-results/closeup-heading-inactive.png', clip }) + + // Click on heading to activate + await page.mouse.click(hBox.x + hBox.width / 2, hBox.y + hBox.height / 2) + await page.waitForTimeout(500) + + // After click — ## marker should appear in gutter + await page.screenshot({ path: 'test-results/closeup-heading-active.png', clip }) + + // Verify the marker is visible (opacity > 0) + const marker = page.locator('.cm-heading-line .cm-formatting-block.cm-formatting-block-visible').first() + if (await marker.count() > 0) { + const opacity = await marker.evaluate(el => window.getComputedStyle(el).opacity) + console.log(`Heading marker opacity when active: ${opacity}`) + expect(parseFloat(opacity)).toBeGreaterThan(0) + + const mBox = await marker.boundingBox() + console.log(`Heading marker bounding box: ${JSON.stringify(mBox)}`) + + // The marker should be to the LEFT of the heading text + if (mBox && hBox) { + console.log(`Marker right edge: ${mBox.x + mBox.width}, Heading left edge: ${hBox.x}`) + expect(mBox.x + mBox.width).toBeLessThanOrEqual(hBox.x + 5) // marker is left of heading + } + } + + // Now check bullet closeup + const bulletLine = page.locator('.cm-line', { hasText: 'Four-panel layout working' }).first() + const bBox = await bulletLine.boundingBox() + if (!bBox) throw new Error('bullet not found') + + // First click elsewhere to deactivate + await page.mouse.click(hBox.x + 10, hBox.y - 40) + await page.waitForTimeout(300) + + const bulletClip = { + x: Math.max(0, bBox.x - 20), + y: bBox.y - 5, + width: Math.min(400, bBox.width + 40), + height: bBox.height + 60, // include next line too + } + + // Inactive bullets + await page.screenshot({ path: 'test-results/closeup-bullet-inactive.png', clip: bulletClip }) + + // Click on bullet line + await page.mouse.click(bBox.x + bBox.width / 2, bBox.y + bBox.height / 2) + await page.waitForTimeout(500) + + // Active bullet + await page.screenshot({ path: 'test-results/closeup-bullet-active.png', clip: bulletClip }) +}) diff --git a/e2e/zero-shift-detail.spec.ts b/e2e/zero-shift-detail.spec.ts new file mode 100644 index 0000000..cd8c94e --- /dev/null +++ b/e2e/zero-shift-detail.spec.ts @@ -0,0 +1,83 @@ +import { test, expect } from '@playwright/test' + +test('zero-shift detail: focused editor screenshots', async ({ page }) => { + await page.goto('http://localhost:5173') + await page.waitForTimeout(800) + + // Open "Build Laputa App" which has headings and bullets + const noteItem = page.locator('.note-list__item', { hasText: 'Build Laputa App' }) + await noteItem.click() + await page.waitForTimeout(800) + + const cmEditor = page.locator('.cm-editor') + await expect(cmEditor).toBeVisible() + + // Screenshot just the editor content area + const editorBox = await cmEditor.boundingBox() + if (!editorBox) throw new Error('Editor not visible') + + // Crop to top portion of editor where headings and bullets are + const clip = { + x: editorBox.x, + y: editorBox.y, + width: editorBox.width, + height: Math.min(editorBox.height, 500), + } + + // 1. Initial preview state (cursor not on headings/bullets) + await page.screenshot({ path: 'test-results/detail-01-preview.png', clip }) + + // 2. Click on "Overview" heading + const headingSpan = page.locator('.cm-header-2', { hasText: 'Overview' }).first() + const hBox = await headingSpan.boundingBox() + if (hBox) { + await page.mouse.click(hBox.x + hBox.width / 2, hBox.y + hBox.height / 2) + await page.waitForTimeout(500) + } + await page.screenshot({ path: 'test-results/detail-02-heading-active.png', clip }) + + // 3. Click on a bullet line + const bulletLine = page.locator('.cm-line', { hasText: 'Four-panel layout working' }).first() + const bBox = await bulletLine.boundingBox() + if (bBox) { + await page.mouse.click(bBox.x + bBox.width / 2, bBox.y + bBox.height / 2) + await page.waitForTimeout(500) + } + await page.screenshot({ path: 'test-results/detail-03-bullet-active.png', clip }) + + // 4. Click on plain paragraph to go back to preview + const para = page.locator('.cm-line', { hasText: 'Custom desktop app' }).first() + const pBox = await para.boundingBox() + if (pBox) { + await page.mouse.click(pBox.x + 10, pBox.y + pBox.height / 2) + await page.waitForTimeout(500) + } + await page.screenshot({ path: 'test-results/detail-04-back-preview.png', clip }) + + // 5. Check that heading marker (.cm-formatting-block inside .cm-heading-line) is position:absolute + const headingMarkers = page.locator('.cm-heading-line .cm-formatting-block') + const markerCount = await headingMarkers.count() + console.log(`Heading markers found: ${markerCount}`) + + if (markerCount > 0) { + const position = await headingMarkers.first().evaluate(el => + window.getComputedStyle(el).position + ) + console.log(`Heading marker position: ${position}`) + expect(position).toBe('absolute') + } + + // 6. Check that non-heading markers always have font-size != 0.01em + const bulletMarkers = page.locator('.cm-line:not(.cm-heading-line) .cm-formatting-block') + const bmCount = await bulletMarkers.count() + console.log(`Non-heading block markers found: ${bmCount}`) + + if (bmCount > 0) { + const fontSize = await bulletMarkers.first().evaluate(el => + window.getComputedStyle(el).fontSize + ) + console.log(`Bullet marker font-size: ${fontSize}`) + // Should NOT be tiny (library default is 0.01em ≈ 0.15px) + expect(parseFloat(fontSize)).toBeGreaterThan(10) + } +}) diff --git a/e2e/zero-shift.spec.ts b/e2e/zero-shift.spec.ts new file mode 100644 index 0000000..2025271 --- /dev/null +++ b/e2e/zero-shift.spec.ts @@ -0,0 +1,98 @@ +import { test, expect } from '@playwright/test' + +test('zero horizontal shift: headings and bullets', async ({ page }) => { + await page.goto('http://localhost:5173') + await page.waitForTimeout(800) + + // Open "Build Laputa App" which has headings and bullets + const noteItem = page.locator('.note-list__item', { hasText: 'Build Laputa App' }) + await noteItem.click() + await page.waitForTimeout(800) + + const cmEditor = page.locator('.cm-editor') + await expect(cmEditor).toBeVisible() + + // Screenshot 1: initial state — cursor after frontmatter, headings/bullets in preview mode + await page.screenshot({ path: 'test-results/zero-shift-01-initial.png', fullPage: true }) + + // Find a heading line (## Overview) and measure its text position + const headingText = page.locator('.cm-header-2', { hasText: 'Overview' }).first() + await expect(headingText).toBeVisible() + + // Get bounding box BEFORE clicking on it (inactive state) + const beforeBox = await headingText.boundingBox() + console.log('Heading "Overview" BEFORE click:', JSON.stringify(beforeBox)) + + // Screenshot 2: before clicking heading + await page.screenshot({ path: 'test-results/zero-shift-02-before-heading-click.png', fullPage: true }) + + // Click on the heading to activate it + if (beforeBox) { + await page.mouse.click(beforeBox.x + beforeBox.width / 2, beforeBox.y + beforeBox.height / 2) + } + await page.waitForTimeout(500) + + // Screenshot 3: after clicking heading — ## should appear in gutter, text stays put + await page.screenshot({ path: 'test-results/zero-shift-03-after-heading-click.png', fullPage: true }) + + // Get bounding box AFTER clicking (active state) + const afterBox = await headingText.boundingBox() + console.log('Heading "Overview" AFTER click:', JSON.stringify(afterBox)) + + // CRITICAL: The heading text X position must not change + if (beforeBox && afterBox) { + const xShift = Math.abs(afterBox.x - beforeBox.x) + console.log(`Horizontal shift: ${xShift}px`) + expect(xShift).toBeLessThan(2) // Allow 1px tolerance for subpixel rendering + } + + // Now test bullet lines: click on a bullet item + const bulletText = page.locator('.cm-line', { hasText: 'Four-panel layout working' }).first() + await expect(bulletText).toBeVisible() + + const bulletBeforeBox = await bulletText.boundingBox() + console.log('Bullet line BEFORE click:', JSON.stringify(bulletBeforeBox)) + + // Screenshot 4: before clicking bullet + await page.screenshot({ path: 'test-results/zero-shift-04-before-bullet-click.png', fullPage: true }) + + // Click on the bullet line + if (bulletBeforeBox) { + await page.mouse.click(bulletBeforeBox.x + bulletBeforeBox.width / 2, bulletBeforeBox.y + bulletBeforeBox.height / 2) + } + await page.waitForTimeout(500) + + // Screenshot 5: after clicking bullet + await page.screenshot({ path: 'test-results/zero-shift-05-after-bullet-click.png', fullPage: true }) + + const bulletAfterBox = await bulletText.boundingBox() + console.log('Bullet line AFTER click:', JSON.stringify(bulletAfterBox)) + + // CRITICAL: Bullet line X position must not change + if (bulletBeforeBox && bulletAfterBox) { + const xShift = Math.abs(bulletAfterBox.x - bulletBeforeBox.x) + console.log(`Bullet horizontal shift: ${xShift}px`) + expect(xShift).toBeLessThan(2) + } + + // Screenshot 6: click somewhere else (a plain paragraph) to verify heading/bullets go back to preview + const paragraph = page.locator('.cm-line', { hasText: 'Custom desktop app' }).first() + if (await paragraph.isVisible()) { + const paraBox = await paragraph.boundingBox() + if (paraBox) { + await page.mouse.click(paraBox.x + 10, paraBox.y + paraBox.height / 2) + } + await page.waitForTimeout(500) + } + await page.screenshot({ path: 'test-results/zero-shift-06-back-to-preview.png', fullPage: true }) + + // Verify heading underline is removed (FIX 1) + const headingLine = page.locator('.cm-heading-line').first() + if (await headingLine.count() > 0) { + const borderBottom = await headingLine.evaluate(el => + window.getComputedStyle(el).borderBottom + ) + console.log(`Heading line border-bottom: ${borderBottom}`) + expect(borderBottom).toContain('none') + } +}) diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..2ecf567 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,32 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores([ + 'dist', + 'coverage', + 'site/.vitepress/cache/', + 'site/.vitepress/dist/', + 'src-tauri/resources/mcp-server/', + 'src-tauri/target/', + 'src-tauri/gen/', + 'tools/', + ]), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/index.html b/index.html new file mode 100644 index 0000000..34f6708 --- /dev/null +++ b/index.html @@ -0,0 +1,170 @@ + + + + + + + + + + + + Tolaria + + + + +
+ + + diff --git a/lara.lock b/lara.lock new file mode 100644 index 0000000..65aadbb --- /dev/null +++ b/lara.lock @@ -0,0 +1,1000 @@ +version: 1.1.0 +files: + e6ab9bf36c30b9d72bc97897e0d89a16: + command.noMatches: a2a97d96e196148e04c51c47d985e327 + command.palettePlaceholder: 6d6b61b4c08ed382ad7ba406a9ba395d + command.footerNavigate: e3808db59d29bcfc91532d6539c4f9ae + command.footerSelect: 6d73339906ed42e20f6be91bf7f5f4a9 + command.footerClose: dcf77460e548fe215ef28583f092a496 + command.footerSend: 6b19fc9694fd8b7b3f1aafaca83db864 + command.aiMode: 7197e3c5f9afd24022377176611f35cb + command.openSettings: 638c05f4f159a403e04aebe94b46a2a8 + command.openSettings.keywords: ab6b5e03395f4db955ed9cbfd4de33b7 + command.openLanguageSettings: e5a425aa6222fab3df66c0e1e0ca4183 + command.openLanguageSettings.keywords: c92bcfddcacf33ad7bdb6ccb88f0d4f4 + command.useSystemLanguage: b7f0315c47d4a59faa2a49571fcf1fca + command.openH1Setting: 05b4f6edc0e98b29670e8ee902cdb9bf + command.toggleGitignoredFilesVisibility: 0a2d3ea4e8ff8b00a801183caeb97a03 + command.contribute: 9887a4451812854f0f1b6f669a874307 + command.checkUpdates: f77f38f684c8b974dd4a56bb321cfd5f + menu.application: bc16b1fbe2e90ace4991cafc5149955c + menu.file: 0b27918290ff5323bea1e3b78a9cf04e + menu.edit: 7dce122004969d56ae2e0245cb754d35 + menu.view: 4351cfebe4b61d8aa5efa1d020710005 + menu.go: 5f075ae3e1f9d0382bb8c4632991f96f + menu.note: 3b0649c72650c313a357338dcdfb64ec + menu.vault: 5b70a213f150f01f0776fa9481ef2ddf + menu.window: c89686a387d2b12b3c729ce35a0bcb5b + menu.file.quickOpen: d79cba7ca4badcccc081bdcba22abd32 + menu.file.quickOpenCmdO: 2f1c2a963833b85008562cd615515234 + menu.file.quickOpenCtrlO: 8aaba3ce15c61798ce80bda311fc1e2d + menu.file.save: c9cc8cce247e49bae79f15173ce97354 + menu.edit.pasteWithoutFormatting: 0219dac5b3b9c457aa65eb19fdd3d22c + menu.edit.findInVault: 7dad10e092a9fe1bfbb4a96c377bc417 + menu.edit.toggleNoteListSearch: f6e0cd9529f571609033958a55f18d78 + menu.view.allPanels: d33bf99e0756e1d5008cb78902e38ca9 + menu.view.zoomIn: f5ca4abce85e2dddb0342d0bae3a7270 + menu.view.zoomOut: 30850b501f98539b1aabaa29fabe41ef + menu.view.actualSize: a5dac79ddad1cbdb8b60c5b347ee047e + menu.view.commandPalette: c0a2436cd8b2dc5085c869e4a052572f + menu.go.allNotes: cd76cf851b08a9d2feafaf255bc1f4d5 + menu.go.archived: 7d69b3cb4cada18ae61811304f8fbcb5 + menu.go.changes: c112bb3542e98308d12d5ecb10a67abc + menu.go.inbox: 3882d32c66e7e768145ecd8f104b0c08 + menu.note.toggleOrganized: 003f5648f585407c894e85543a5fda47 + menu.note.toggleTableOfContents: c464111f97490e65699fe18970fc00e1 + menu.vault.addRemote: 4d9d0f784920d6ef7f9f3b33266dd00e + feedback.title: 96ab5025e38aef43fdb73c9830795264 + feedback.description: e900af4efb9167033d4ecc8a02cfe463 + feedback.sponsor.title: 2f6a1db54812f51924c3324a178125ed + feedback.sponsor.description: 6517e0c5d4e4801814a6034dd54862b6 + feedback.sponsor.cta: a2c71ec076796ab6af0ba0ea882303fc + feedback.sponsor.linkLabel: 7fbe24a8f025219f4d165aeb75352125 + feedback.newsletter.title: ffb7e666a70151215b4c55c6268d7d72 + feedback.newsletter.description: 939ab3adc8c7ad0bca8b27a10462055d + feedback.newsletter.cta: a2c71ec076796ab6af0ba0ea882303fc + feedback.newsletter.linkLabel: 7fbe24a8f025219f4d165aeb75352125 + feedback.sponsors.title: e881053494ad211d02cdde59c3ab59d9 + feedback.sponsors.description: e748f640be55858af848a87e7ce59920 + feedback.sponsors.logoLinkLabel: e5744ab47880f7d488bbb06af085aa07 + feedback.featureRequests.title: 73ea5953d68b2c20a82bb94240f6fe17 + feedback.featureRequests.description: 9583575bdef16e12927b0a5acfa84307 + feedback.featureRequests.cta: c19ef0158349da249920b0d9f67d7d48 + feedback.featureRequests.linkLabel: d125f38d1dc8dbdc7dae3f1778df3a3a + feedback.discussions.title: 560f23c77d499c21038c0b4487f03cb2 + feedback.discussions.description: 2e4da4c5f54c7af3b8db9f3d93ed63c5 + feedback.discussions.cta: 61ccb6a90ec8037f2926c48c0a90caa3 + feedback.discussions.linkLabel: 4f85e3343c5e21d2b11812b2baa8ed83 + feedback.contributeCode.title: 8f93fd72b63620711cc118403b85133e + feedback.contributeCode.description: 53230626356951578842a8cd7233d1f1 + feedback.contributeCode.cta: c73dd5e7d432cd18c109b333e0b55299 + feedback.contributeCode.linkLabel: bfe9dd9816bb39f82fc003de794a075d + feedback.contributingGuide.cta: 6cc1fbcbbc69a70a70a2020c979d8e22 + feedback.contributingGuide.linkLabel: b1310938ecab4c502a8b3af2ca1ce188 + feedback.reportBug.title: 3406147c3ce729ce17ed76d45c2896f9 + feedback.reportBug.description: 6da172412f774e022b0f32ba17305491 + feedback.reportBug.cta: 1f5aa905b40956ff5ea8b4cc5ea29315 + feedback.reportBug.linkLabel: ffaa2eaa0f015cf037d2b5fe8af6b813 + feedback.linkFallback.title: c5396ec50cec23a09977180747737264 + feedback.linkFallback.description: 91d1e18130765119606f095ac473d9cd + feedback.copyDiagnostics: dedf5388697bba1fd53173b04823761a + feedback.diagnosticsCopied: 9c7f3f37df9c000fc67c1a6ee35f40e5 + feedback.diagnosticsCopiedSentence: 6f8ebe492dbb95827ccf7536965dc672 + feedback.clipboardUnavailable: 8e25a424420687ed8e0dc2b1be8c7406 + onboarding.welcome.title: 4e146a5fbd84f4282d608683de0a2d67 + onboarding.welcome.subtitle: 5e490bfb589bd74b043c330f90d806cc + onboarding.welcome.missingSubtitle: e0b59bce6bbcc8dd56722e25c2a60485 + onboarding.welcome.templateTitle: 30ddd36173006e5db734f878bf12b9ea + onboarding.welcome.templateDescription: a142a395648f742c6b39d8391e30ec9f + onboarding.welcome.templateOffline: cf717aaf6b6b9afeda233ce4bc86189f + onboarding.welcome.templateLoading: 2ab31cd86895006363298fe680e64e9c + onboarding.welcome.templateLoadingDescription: d246a8a2d8c53a65c34359ba503cbc7c + onboarding.welcome.templateStatus: 6a0789a3d9ba4dd326202663a74129fc + onboarding.welcome.createEmpty: f37c03f236fbf1516222360a936249e8 + onboarding.welcome.createEmptyDescription: 9fe50bc53b609c5f734abaa3a58b2a17 + onboarding.welcome.createEmptyLoading: 20e33d4be622b4eb0af517900937ee68 + onboarding.welcome.createEmptyLoadingDescription: 759757ef559c94a4c232e1093041a897 + onboarding.welcome.openExisting: 4cbce894cee19b239fd88ae2fafedaab + onboarding.welcome.openExistingDescription: 969a090bf259323830d733d0141b0b2a + onboarding.welcome.retryDownload: aa0d36f85911dde10fbe345ae1fa13de + onboarding.welcome.docsPrompt: 4a7dc2be7890babade71476e9910c58d + onboarding.welcome.docsLink: 1ae3906f2bb3da58cecf9db8375952e4 + onboarding.ai.checkingTitle: 62a4f35979899c0492a5be91f0172b98 + onboarding.ai.checkingDescription: 67e6fbe3aa24ec68e88d05def752b47a + onboarding.ai.missingTitle: 194d3777688e137188ad8949c097f441 + onboarding.ai.missingDescription: 217cf33df5cbcbcf4ce1d2808470903a + onboarding.ai.readyTitle: ced601bb8aae12a4a1a8dd9b6440c36a + onboarding.ai.readyDescription: 5d05f1ad0c8af7a9d78146b840cecb90 + onboarding.ai.checkingLocalTitle: d61e8c908156195767eb4729fe377471 + onboarding.ai.checkingLocalDescription: 1048df882350972acbad709dcdd8e6cd + onboarding.ai.detectedHeader: 52d48f0516070836e91a3e53a1c9b693 + onboarding.ai.installed: 2c8bb57a0b1dff255f3d6684a9fddda3 + onboarding.ai.installedVersion: 5e12a26fd64792c6798e5fa9580e2e3f + onboarding.ai.installedBadge: 98dd43dfae05b11befe1f140e0ec787a + onboarding.ai.otherOptionsTitle: cdbde4ccb0c9f57e955d3d3e2a02a2c4 + onboarding.ai.otherOptionsDescription: 57f4ccb3d3ab15693d7d8a047acab618 + onboarding.ai.supportedAgents: 88dcd8c5d8602ae7bd84b7d2463d1286 + onboarding.ai.continue: a0bfb8e59e6c13fc8d990781f77694fe + onboarding.ai.setUpLater: 31aa06160419d713441c155cd2ed1f90 + command.group.navigation: 846495f9ceed11accf8879f555936a7d + command.group.note: 3b0649c72650c313a357338dcdfb64ec + command.group.git: 0bcc70105ad279503e31fe7b3f47b665 + command.group.view: 4351cfebe4b61d8aa5efa1d020710005 + command.group.settings: f4f70727dc34561dfde1a3c529b6205c + command.navigation.searchNotes: d8002e888293774162e43b263658cbb5 + command.navigation.goAllNotes: 7030dcbbb85b6f01d3c8b2fdcb6a5cff + command.navigation.goArchived: b620b8cb8fef8a287b986ff83a992a2a + command.navigation.goChanges: c6dd094ce55f8a765b68cef40a1c6bd9 + command.navigation.goHistory: 39b86b661267053a240ffe89b9eca847 + command.navigation.goBack: 4f2f5e1d6e2a13469ad431474a68ab82 + command.navigation.goForward: d13c318438f67d749cfc0904e3fafcda + command.navigation.goInbox: 522af71c26bc7e3ce1dae6204c2fbe9f + command.navigation.renameFolder: a3d3b0f787c52a984af61085c577bff2 + command.navigation.deleteFolder: 991e322cd8225c369a963bb60b0a9688 + command.navigation.showOpenNotes: bd1905c340d0518187865f4a6694e14d + command.navigation.showArchivedNotes: 241399b908884adfe8608a3ab827ca74 + command.navigation.listType: 4f70c27c6826a37543c8ada561397c22 + command.note.newNote: 23c4edda8b815f750782f01870d27271 + command.note.newSheet: 644f6b6a5bcbe0c3b723ec22ad198f3e + command.note.newNoteInCurrentFolder: 167b765903c70ab1b645908ff8a899ab + command.note.newType: adce5108f18945cc502a06c02445e32d + command.note.newTypedNote: 1493eda772c2179cb6247169d00723b2 + command.note.saveNote: 2a309cda46e95b00d2a5a8afb3cc0047 + command.note.undo: 1cdc076b28f70afac5fcedadf99fa119 + command.note.undoAction: 83a1f43c314533161109d8d4daf032f2 + command.note.redo: 5afeaba074ef570dc720caaa855d49f6 + command.note.redoAction: 7556c9dd6845991933c56006bf7ffbac + command.note.pastePlainText: 09e8075dbd91e11878f8f4a138df760c + command.note.findInNote: f72f8f93d8e6119d5d08b60eb3a7b3bc + command.note.replaceInNote: b008e2e20c5e4cd202f4386271d6bed1 + command.note.deleteNote: 56f727a2ee11d15159f1aa6373314cb6 + command.note.archiveNote: 6043509fda6dd7f6f167e4108033f8e8 + command.note.unarchiveNote: 6937fb694d00b11d579c21cd4bf103a2 + command.note.addFavorite: 782d6b28dce4e5d0b2ac92142c6624ba + command.note.removeFavorite: 4f9b5fa08f2ef86fdc5d0ceefc271981 + command.note.markOrganized: 505ad63357f533fb39e4276a2bfb9496 + command.note.markUnorganized: 1c62f1bffcb793c63d729096d2794ced + command.note.restoreDeleted: d704f8283ca322a9b3713d2ccf9a6cd4 + command.note.setIcon: 99b81d2663266c53e45e4d9c62c87a29 + command.note.removeIcon: 4bccbad66025e506b37b4218498b299c + command.note.changeType: 4a54ffd47ed1f65ee97f34bfe8c3de14 + command.note.moveToFolder: 3a0f26c42b9168ad839960d134065905 + command.note.turnCurrentBlockInto: ffe5660e197b8cc6ea4220aef1cb8983 + command.note.copyDeepLink: 8de1795f59af124594dc4decf77909a1 + command.note.exportPdf: 7a39acf8742a59d1a4f7ae064b6219a0 + command.note.openNewWindow: 6f67b2857093fa0fd45b1122dff4865d + command.git.initialize: 54a57cdc7105db6fb22dca661ce01ad6 + command.git.commitPush: 8cb5faa4019716f43dd69c41496b1d46 + command.git.addRemote: 7eef951b3f909340a68dc9811be83e9e + command.git.generateCommitMessage: 87cc84d36697c0763d61dd63df6ce40f + command.git.pull: 3988d61f4a4546381f61a4eaf61315b4 + command.git.pullRepository: 931ad25176586fd4332dd671574d3185 + command.git.resolveConflicts: 871ac36968cfca3d2297a89b28b25dee + command.git.viewChanges: b2699edf69d8e1031afce2b2f94e5c8d + git.author.label: 7a0ca1a813e4518e479a7927890568ce + git.author.warning.localOverridesGlobal: 5ea1eb9d1b9d2a80662368ee9e1fba63 + git.commitMessage.failed: 61e323218034e7e8d2de3f869b268701 + git.commitMessage.generate: 32b919d18cfaca89383f6000dcc9c031 + git.commitMessage.generatedAi: f919c1ee7bc85e658b2dcd3d2cc5ae92 + git.commitMessage.generatedFallback: a78024340c98d9720ad07ab55cb8f951 + git.commitMessage.generateFromDiff: 9defded1246bfd62d8bd089d0a600e84 + git.commitMessage.generating: f93642ae2d90147d24e974a5649c6622 + git.commitMessage.noChanges: 12d5dae6c75361dfbfeaddd092aea647 + git.repository.select: 33fcf2b3ec4686d9cd06051c726d0ba2 + git.toast.autoGitFailed: 3e5d7e183a78e1f54c87ad0dba8a302b + git.toast.commitFailed: bb50986d29c30042bed1446f8adaa544 + git.toast.missingAuthor: 0d4e7a7d6b6a3f883146dd86c9cdca09 + command.view.editorOnly: 8355e056b32086b9190d639be290dda8 + command.view.editorNoteList: b9604b1609eb6f581cd9570dca72d3f0 + command.view.fullLayout: 1cebdf839eee2b1713828731aaa3b507 + command.view.toggleProperties: 54f12756a363401243d82cbd0466117e + command.view.toggleDiff: 148a10ef6f04f897e73289f529ecae86 + command.view.toggleRaw: 9b622257cd6345ceaf58e42b1410899b + command.view.noteWidthNormal: d29ab88a55ae178bf4b5257201e51801 + command.view.noteWidthWide: 403f3913ad71451a90787c235f1684de + command.view.defaultNoteWidthNormal: b045349e37329df23317db98cccd3263 + command.view.defaultNoteWidthWide: 41f326763e3e3954912dfd45d04bd87c + command.view.leftLayout: 5046a7166675e2d0175b9da3befdcaca + command.view.centerLayout: 3bc6759c005178aae519aa3dd227cb74 + command.view.toggleAiPanel: 4279cbf31767465f25feff6e7bdd336e + command.view.newAiChat: 1b0a886d6e77f628ec96c2d71cdb0a9b + command.view.toggleBacklinks: b3c4be2d2c07bb8df181355a2c3658a7 + command.view.moveViewUp: 76f92d3250e028609349d825c672f13e + command.view.moveViewDown: 9deea30162b8d3234f8d52795ad7393f + command.view.moveNamedViewUp: b4ac634d13a09eacb680ef12c69dd1f7 + command.view.moveNamedViewDown: 15302c02df26fe52def77636e9141711 + command.view.zoomIn: ae4a8e406b707636392b6673fca0e6a6 + command.view.zoomOut: 97326f8cd9df886a6b19af180fb7dcc9 + command.view.resetZoom: 193ee8c32391fc5cc303a51617cfd046 + command.settings.createEmptyVault: d7c9673bb6c62a2b8aef009546ce8b23 + command.settings.openVault: c40e74822da9417fdf52eb4a37bb23f2 + command.settings.removeVault: 390e2b2031cde0dd18381efee0923bea + command.settings.restoreGettingStarted: b3358dc1c7b873b2a02ac6f05473fe4d + command.settings.manageExternalAi: c36b9bd171f11a18a1e624365d2b57ac + command.settings.setupExternalAi: 24eea679eb699763daa2e3f2a67fcf9a + command.settings.reloadVault: f6e506dad6b6cf2be24d9438d458ae54 + command.settings.repairVault: cee560b1fd5ad9d1ff1c19a0a2afe77a + command.settings.useLightMode: a76d5b1c076983bc113d247f0520c166 + command.settings.useDarkMode: ba7873c42ae00542ba71db9be3b6761f + command.settings.useSystemTheme: 6d8c69d916d3df8ceed8dfc154196d07 + command.settings.toggleGitignoredFilesVisibility: 0a2d3ea4e8ff8b00a801183caeb97a03 + command.ai.openAgents: 612abe804edd0f5ae228a534f77f3d23 + command.ai.restoreGuidance: 23331fecc692d11d85cf24838ed273f2 + command.ai.switchToAgent: 5bb02187e653b360ab7ed661403271f2 + command.ai.switchDefault: 42724de254240d598fbcc40fdb5b18a3 + command.ai.switchDefaultWithAgent: d405f7914bb1cd86172f108f3c33d953 + settings.title: f4f70727dc34561dfde1a3c529b6205c + settings.close: 7812b3644f897c5d3efb0dd63440e751 + settings.sync.title: 36f62a237420dcdc7096ad3a9018c4d9 + settings.sync.description: 06a6957c40867be2c28783183dc1bf5f + settings.pullInterval: 4f95ca677398604bca241427261ed07b + settings.pullIntervalDescription: 4a75c15f44d59dc8cb100ec2d0693048 + settings.releaseChannel: 2da37055e15a217f18bd403922085c3f + settings.releaseChannelDescription: c5ae9b7f25376df383d36603366ac1fa + settings.releaseStable: fa3aff3c185c6dc7754235f397c2099a + settings.releaseAlpha: 6132295fcf5570fb8b0a944ef322a598 + settings.automaticUpdateChecks: 95e5c6aab9c42787930040ed1743a708 + settings.automaticUpdateChecksDescription: 1cecffe438411cac2f910946d0706ab1 + settings.workspaces.title: 10075fec14fcc45a489d02bedf972e97 + settings.workspaces.enable: f418b797fc4d13ca6b59333326ac3246 + settings.workspaces.enableDescription: 1487bd26586b773c77c01746399cc4cd + settings.workspaces.name: 49ee3087348e8d44e1feda1917443987 + settings.workspaces.label: b021df6aac4654c454f46c77646e745f + settings.workspaces.slug: 0c908588520b3ef787bce443fc2b507c + settings.workspaces.nameAria: a94a5bbfbc6bbcff81961501be487703 + settings.workspaces.labelAria: f6a0693020980910eaa8b168b30b33c3 + settings.workspaces.slugAria: 1d9e314005db037b4a83a220a45f3d77 + settings.workspaces.nameTooltip: 5c12e3507377eadae5c2340462dda553 + settings.workspaces.labelTooltip: b1b87a2affcd504e54bf97100e3d9c34 + settings.workspaces.slugTooltip: daa690f5855e35d1cfa9dbffe1870d40 + settings.workspaces.color: cb5feb1b7314637725a2e73bdc9f7295 + settings.workspaces.moveUpAria: 2ade43872ef9432d14af83056304c6d7 + settings.workspaces.moveDownAria: ed1ef4a310797ac84894a224bf4c77a6 + settings.workspaces.removeAria: a5d2ceeeb76bfcb99015fc780ced07c2 + settings.appearance.title: a1c58e94227389415de133efdf78ea6e + settings.appearance.description: ca7b75a4c10ab8a540707e3a96cd3592 + settings.theme.label: d721757161f7f70c5b0949fdb6ec2c30 + settings.theme.light: 9914a0ce04a7b7b6a8e39bec55064b82 + settings.theme.dark: a18366b217ebf811ad1886e4f4f865b2 + settings.theme.system: a45da96d0bf6575970f2d27af22be28a + settings.language.title: 4994a8ffeba4ac3140beb89e8d41f174 + settings.language.description: e2bb9b523067fdc32a494b1d6fd97906 + settings.language.label: 244c7b77926f077b863c33ff1244e1ec + settings.language.system: 41e2252344aa441e925589ddcb762e6d + settings.language.summary: edc83f1f48ce29bc80bf2f2f90e24997 + settings.autogit.title: 0bcc70105ad279503e31fe7b3f47b665 + settings.git.enable: c40503e1b0f483eecf3aff2fd01665c4 + settings.git.enableDescription: a96a43dc4dbf1490d665eef437807b07 + settings.git.provider: 6a38305c8010acae3c04706773e0546f + settings.git.providerDescription: 7f23d6e2c5e462daf774c700ad6af2b6 + settings.git.providerNative: 2098510f1f33aa3fa4c1ea19ff543813 + settings.git.providerWsl: c28d793001f0044df477bfdfe280f88c + settings.git.wslDistro: cd2dfc009491c44acaa7d1c0d62689bc + settings.git.wslDistroDescription: 3acc5588d7c36c4a4dff199048d20eb3 + settings.git.wslDefaultDistro: 32266d0a0c5b47f0c7593ab569f21a2b + settings.git.providerTest: 96467c8e8ddf142f09616c1dcdb4193d + settings.git.providerTestDescription: cd60ba8d1135b9de4b5cdf1a19a3a31b + settings.git.providerTesting: 1e9731b0f033754e9df1973d1fd77b0f + settings.git.providerTestOk: 572542bead66201e0e49fa7c6ff18555 + settings.git.providerTestFailed: 3d0b8c3ec6a5a258bb3d6af98652c979 + settings.autogit.description.enabled: e65bec70ca4d7921be56f84d10836018 + settings.autogit.description.disabled: 1143ef6ee00614816785e3c6fc299d51 + settings.autogit.description.gitDisabled: 55a1dd2d7f797062cde1374930221649 + settings.autogit.enable: 9b205a4ae0e3ad4e6708155880ce9c75 + settings.autogit.enableDescription: a7424a8f90c0b7f290a8144d12374554 + settings.autogit.idleThreshold: 28016cc6fccd951a904ee3020cd733b6 + settings.autogit.idleThresholdDescription: 521140491e8c9d5a1d7b7739bc333358 + settings.autogit.inactiveThreshold: d28cb60d1e70e020ae8d1294e32dd474 + settings.autogit.inactiveThresholdDescription: f9e43c90bd10bae937c2cfa5c2767f1c + settings.titles.title: 761443f70a5067ecf015c6fb3fae9cef + settings.titles.description: b94aeeca78dd376cc19b9aa019e53f6c + settings.titles.autoRename: 5383db8e790ebf5f956d9c59cb4c4f67 + settings.titles.autoRenameDescription: cc28c28d8817736e658082a2261d9a6e + settings.vaultContent.title: f15c1cae7882448b3fb0404682e17e61 + settings.vaultContent.description: a53487bf1c7898a1459dc1510e216f7d + settings.dateDisplay.default: 915e8ca934abe6625e17695ee7ae266a + settings.dateDisplay.defaultDescription: 502ed7ff7b9c0fe5971e8cb0576e91f6 + settings.dateDisplay.us: abe1c573145c4419ba8bb158cf0068d6 + settings.dateDisplay.european: 0b4f509278b2919973c5d1ae906c162b + settings.dateDisplay.friendly: 81e56440280f6dfaef0eb503ca301578 + settings.dateDisplay.iso: 89b4ef461b67eb141eadd0fc6caa8d8d + settings.noteWidth.default: 66be0f882801fd19468181f31f58dc20 + settings.noteWidth.defaultDescription: 2b5fb6d7dfa2b1da1e47bcac30f58e8d + settings.noteWidth.normal: 960b44c579bc2f6818d2daaf9e4c16f0 + settings.noteWidth.wide: e7c770a61dbdf81ca922ae0260e327c1 + settings.sidebarTypePluralization.label: 1e5bd615b1abbf39129ef241f5cf2249 + settings.sidebarTypePluralization.description: 8d7bb0649fd7e637552429ea91298de8 + settings.vaultContent.hideGitignored: 2c07ee6c8bfe746d356f44275d42f90e + settings.vaultContent.hideGitignoredDescription: e8b1ddfa416610f9d6eb299fa123a1d6 + settings.allNotesVisibility.title: 0e07c3d48b91e1a4c42c1ff3e5751ec3 + settings.allNotesVisibility.description: bc9caa48296281401aad694fed65a2d9 + settings.allNotesVisibility.pdfs: 27ce5e1e1761ff362dbf75c76c6b4941 + settings.allNotesVisibility.pdfsDescription: 8191ed4a095cdd60ef856ce0ff213a48 + settings.allNotesVisibility.images: ea15460e63b6e8e92f8c03a79eeb6e4e + settings.allNotesVisibility.imagesDescription: 1704e10d5e5cdc17d2d2a5fb7b8969bf + settings.allNotesVisibility.unsupported: 257fe04419c9f6543a43e58b1edf9eb5 + settings.allNotesVisibility.unsupportedDescription: a3b8aa66900c742f001cd4533b3df44a + settings.aiAgents.title: 27f08387b26e1ceeb1b60bd9c97e7a47 + settings.aiAgents.description: 768c138c3432fb159ac58c5a4471e786 + settings.aiFeatures.enable: af47a6b2bd32a7b7309bac7bef34f026 + settings.aiFeatures.enableDescription: 7d3916f7f9e1bf8b8797f50f4533b9da + settings.aiAgents.default: 6af573559fd05d8c35bc57b41cc78e24 + settings.aiAgents.defaultTarget: 5acfc74fd97a6dd2d67d15c66de5eed5 + settings.aiAgents.agentGroup: 11eaa7f817e3f24d54f3cf3025be49b1 + settings.aiAgents.localGroup: d34f4b997ce78da5aa57d657a5d6fcb5 + settings.aiAgents.apiGroup: 589a257cbd265eaa7de93de8b4084dff + settings.aiAgents.installed: 73329564760013a7824ff9d5d1af91ff + settings.aiAgents.missing: ea21841da70e6405af19fabc4ff8bdd9 + settings.aiAgents.ready: 909a648c713be25fcd050725d0a41e37 + settings.aiAgents.notInstalled: 0ac3f76ef78bfdbab6331731ee56ba22 + settings.aiAgents.apiReady: e5b06f6ef4ef5f4bc2848d46195e3f48 + settings.aiAgents.apiLocalKey: 119de2f31867b273a134f05bc56b9e57 + settings.aiAgents.apiEnv: e011db3dff54ed3deae37a9615e31ebf + settings.aiAgents.apiNoKey: 5bbccbfcd09b28902bbf5319dd6e6053 + settings.aiAgents.installedTitle: 0de6536215f76c44351ba4d809804c7a + settings.aiAgents.installedDescription: 27c8ed48439ac4a613409f5303fbca84 + settings.aiAgents.noVersion: 89831adb7114946e916b9c31d57a1e33 + settings.aiProviders.title: 63eb84d8510f9f086d282ec7d47adaa1 + settings.aiProviders.description: d29dfa440713d15de311d29a64a27767 + settings.aiProviders.localTitle: c7c20443d8aaf04271df9d713f5c02ce + settings.aiProviders.localDescription: 7abed40ef2e46741c64ff47e423cca39 + settings.aiProviders.apiTitle: 66537b9e5200ebbbfbd091144d8afbcc + settings.aiProviders.apiDescription: 6ec773352e05a805af3abcebeff4e756 + settings.aiProviders.kind: 27703c8f150ac4bb0a3a83a7857353af + settings.aiProviders.kind.ollama: c23e8db458397f37c8e8d98e94e55a18 + settings.aiProviders.kind.lmStudio: 7b9b319662715e90583e823a6c3cfdbe + settings.aiProviders.kind.openAi: 0523b13262b12c215d8009938f5c14f1 + settings.aiProviders.kind.anthropic: f431db65cea024a5f19eab835afefee2 + settings.aiProviders.kind.gemini: 766cc4dd4d5005652e8514e3513683f8 + settings.aiProviders.kind.openRouter: a0607683815b6585fb80e7cac72ac59a + settings.aiProviders.kind.compatible: 1a4dc90a183c26d9026c6ac7727aa4cd + settings.aiProviders.name: 49ee3087348e8d44e1feda1917443987 + settings.aiProviders.baseUrl: ade86bc4899761ad46c52e381b6228bb + settings.aiProviders.model: 6bd36c36869288188863bd53a9bc5ed1 + settings.aiProviders.key: 656a6828d7ef1bb791e42087c4b5ee6e + settings.aiProviders.keyPlaceholder: 57dbf3720af3f156018bfff7f5a99b23 + settings.aiProviders.keyStorage: 3e5b8cfbc66d3a33dad3fda058bf5851 + settings.aiProviders.keyStorage.local: 7c6189f734f3c66d162a3f06529af1d9 + settings.aiProviders.keyStorage.env: a71b071971d7c54e9af381c9bea093d9 + settings.aiProviders.keyStorage.none: 94c841331ccd5179a283487f96285c90 + settings.aiProviders.keyEnv: cb72671bd5cf65512d4c94e8ad6182ef + settings.aiProviders.keySafety: 0d59d7e4def35e51c4bb785d74180afc + settings.aiProviders.keySafetyLocal: 8ef2ba0192ac725fa1a3e737e852387c + settings.aiProviders.localSafety: 60fb65e4e0d35cec3135584d8219e8fd + settings.aiProviders.add: a9f8e8b4ca3cbc4fc3bff5d59d6a5419 + settings.aiProviders.addLocal: 681985f2e412f22fcd55a0553f6e4bee + settings.aiProviders.addApi: 767b46f2a51aa9dd050cb226cd86e192 + settings.aiProviders.test: 602009d5aea1c1ef06fd777968f7e80e + settings.aiProviders.testing: 9d770c909c2c69b09eae2372c4cf405d + settings.aiProviders.testSuccess: 51677ed54231e41a4fe40850d90b64b2 + settings.aiProviders.empty: 9378027dec7f93fcc5011c5e528bdf82 + settings.aiProviders.defaultEndpoint: 666b355c305dd4c7b4649d3d7b4c5d11 + settings.aiProviders.keyLocalSaved: f4a102d6a67212e21f0f31f0758aea81 + settings.aiProviders.keyEnvSaved: bfed2d745dd76dbea9341d27e06c39f2 + settings.aiProviders.noKey: 1ab089f80ef4dc6c1ce3e173f5b405a0 + settings.workflow.title: 24f47cdbe9ddba774a7cc53e51d9032e + settings.workflow.description: aa9a07539b87cf407c3edc8a22732d45 + settings.workflow.explicit: 54a5b5c27937d25271c3127d093e8361 + settings.workflow.explicitDescription: ce523427b5dd0f9895f63d1fbb90ad24 + settings.workflow.autoAdvance: 9ed337f5bc61603d19a1ef35f3a3a243 + settings.workflow.autoAdvanceDescription: ae58ad8abf03df7cbdae8c3a725258f7 + settings.privacy.title: aa96a21412def0d916f43b639424f8e4 + settings.privacy.description: 33d53059be620b58e38b8e5c5ab26b27 + settings.privacy.crashReporting: b4efc5b61526566d431e99242f5c21b4 + settings.privacy.crashReportingDescription: e9d7c7efce44adc08d0460be1aad5c42 + settings.privacy.analytics: 29d55bb222ea76e1a17396375dfab228 + settings.privacy.analyticsDescription: a0f8b61d555d6d3fd279de2f47aaca76 + settings.footerShortcut: 7dc840d6b0ef9f422a663ba41975871e + settings.cancel: ea4788705e6873b424c65e91c2846b19 + settings.save: c9cc8cce247e49bae79f15173ce97354 + common.cancel: ea4788705e6873b424c65e91c2846b19 + common.create: 686e697538050e4664636337cc3b834f + common.save: c9cc8cce247e49bae79f15173ce97354 + common.remove: 1063e38cb53d94d386f21227fcd84717 + customize.color: cb5feb1b7314637725a2e73bdc9f7295 + customize.icon: 817434295a673aed431435658b65d9a7 + customize.searchIcons: 0688a8098567785a05cb4cf937cbb880 + customize.noIconsFound: 89c25bea716a589b11599f2bbcf34054 + customize.template: 278c491bdd8a53618c149c4ac790da34 + customize.templatePlaceholder: 44482755791f96f37118f3a3bc66f668 + customize.done: f92965e2c8a7afb3c1b9a5c09a263636 + viewDialog.title.create: 01e53f288ebe0b7bdda19c8151aa7710 + viewDialog.title.edit: 6369f1c063cb2c22efa584a19e98d97f + viewDialog.description.create: a9600940be61edfcad2b47fb72965406 + viewDialog.description.edit: d4e6333950d5031c5f4ae766a1f1d886 + viewDialog.nameLabel: 49ee3087348e8d44e1feda1917443987 + viewDialog.namePlaceholder: dbb077d36957a33c4147eb24919192e1 + viewDialog.filtersLabel: f3f43e30c8c7d78c6ac0173515e57a00 + viewDialog.saveError: 80fe81f3ca09ea03af03fdb9ae809eda + viewDialog.selectedIcon: 5f98b507ccec0bbc51ca7e8a5b270250 + viewDialog.selectedColor: 192ec1a9973f328c3620cd3fa670c745 + ai.permission.safe.short: c6eea0560cd6f377e78dff2c85cc9122 + ai.permission.safe.control: 386cc66605d50e5bf1fb726f97e55b55 + ai.permission.safe.tooltip: 3bd635e2b2223da302994d77bfd7edfd + ai.permission.powerUser.short: d4a47db271fc1ef3cd17f7396326b057 + ai.permission.powerUser.control: d4a47db271fc1ef3cd17f7396326b057 + ai.permission.powerUser.tooltip: 2585ecdb79ca02a3ee779bb61552de03 + ai.permission.modeAria: 3ff6346566a4c44fd5c7395907125d3e + ai.permission.changed: b5d06ad47e6053fe5a73f4571a40e00c + ai.panel.title: 4412225fe5b326643b8c2e1d9b7b10ae + ai.panel.status.checking: 118740af1c4521b917e29791d661db82 + ai.panel.status.missing: fe07f8eac233c229d81cbe9aa25668a0 + ai.panel.status.ready: 296a0ac791eab2df130789f55fdc109b + ai.panel.mode.chat: 55dcdf017b51fc96f7b5f9d63013b95d + ai.panel.mode.chatDescription: d7744ae39e7920a8eac7b21b58fce348 + ai.panel.copyMcpConfig: 6a8c9fe401557c870e3a90bbfab01b25 + ai.panel.mcpConfig: 53bac93d0314941c8d2c1163026f3ad9 + ai.panel.newChat: 1b0a886d6e77f628ec96c2d71cdb0a9b + ai.panel.close: d00ba22b83762f5a6e66db0be9e916d7 + ai.panel.linkedCount: 5292fda189a357c9597b1cb0e1b0900b + ai.panel.empty.checkingTitle: 0dde2076cb53fe497d05d231296d50bb + ai.panel.empty.checkingDescription: 31e41510d851c366860377ba0c956f0b + ai.panel.empty.missingTitle: a2b764da52cb43b191bb4ecfd8ea4d26 + ai.panel.empty.missingDescription: 91825c7d86b1d588d44a33e1d94af950 + ai.panel.empty.withContextTitle: cea2547385aeeb68fa494bb3b09b98f5 + ai.panel.empty.noContextTitle: e919aeb0a49b58e494e43e5f6f6597c9 + ai.panel.empty.withContextDescription: dd8faefcc8160a2db800a41d6628facb + ai.panel.empty.noContextDescription: 6a6e610835808016d1f2790c3f991a54 + ai.panel.placeholder.checking: 08fec77719c37d3d6279b2641b37b4d3 + ai.panel.placeholder.missing: 547c492026d115608067988b7cfbb9a9 + ai.panel.placeholder.ready: 31f5265c2e0432a7ca10d08e5c6f3114 + ai.panel.send: 747c6a3564774149c989884e0c581d53 + ai.panel.stop: 4c7fadd610e7adf3e0ab80fa4459ad96 + ai.panel.stoppedResponse: d223fbbd5ade7a06a14ea75105df4f9d + ai.error.claude.tooManyRedirects: 590099fedac46db892be91909814b0c7 + ai.error.pi.emptyOutput: f7e27de4f237828178113efbf60e8e52 + ai.error.pi.emptyOutputWithDiagnostic: 41a47dab0883245a1ac42013368e2da4 + ai.message.regenerate: e464a5330788a9f79d06c96989f840cb + ai.message.copy: 53fa249ce3d02c8a686b657b31d02ee7 + ai.message.fork: f5aace5e5b5e7b0954651a2da44b6755 + ai.message.reasoning: 87a222577e9c7b7a0739d92337f4fe46 + ai.message.toolUse: d6093cfe0c95f805875e5724eecd20aa + ai.workspace.title: 375099f0c804e924c1dc275370710d92 + ai.workspace.newChat: a917baaf1484ec5dc85f19ab8fc4f4e9 + ai.workspace.collapseSidebar: 9ecc763686b7c61547281f3e05ffd06f + ai.workspace.expandSidebar: 281b619f3da2ba27e604302c3c00db56 + ai.workspace.close: 184649ab030e9a609488608221c28d8c + ai.workspace.expandPanel: edbe17b531457367c1412e4b6349c194 + ai.workspace.restorePanel: 64eb6176c678c6b395cfc93bac02a16f + ai.workspace.restoreGuidance: 2bd339d85ee3b33e513359ce781b60cc + ai.workspace.closeChat: 6c5be35688a466297db40c3a65ac9805 + ai.workspace.popOut: 85ec25ec056b3082651e6e3fcfa57416 + ai.workspace.dock: 72e81ed23007ec6fb9820b9abec353c4 + ai.workspace.settings: 2c178b1d7a60a3c2b37ca4221d322899 + ai.workspace.archive: a02bc089273a8c460ea3d2cd2c257b59 + ai.workspace.restore: 98ee8b364870e27dd48aca3240801abf + ai.workspace.showArchived: ae2bcb4cb837cc09a5767153efe4bf39 + ai.workspace.hideArchived: e7c72757d5bc7330d876eeb1b6ce9c53 + ai.workspace.noActiveChats: f0cdbf09f23eb9a2efbb5977bcc39c1a + ai.workspace.noArchivedChats: 68410efd12fc0308ce1ed2225000d4d5 + ai.workspace.chatTitle: 189013cfac88018c68ec02e8edc2379b + ai.workspace.renameChat: 0feba25e166f55afc51c4ebb33d8d757 + ai.workspace.status.archived: 7d69b3cb4cada18ae61811304f8fbcb5 + ai.workspace.status.idle: e599161956d626eda4cb0a5ffb85271c + ai.workspace.status.running: 5bda814c4aedb126839228f1a3d92f09 + ai.workspace.status.done: f92965e2c8a7afb3c1b9a5c09a263636 + ai.workspace.status.error: 902b0d55fddef6f8d651fe1035b7d4bd + ai.workspace.permissionMode: 5a7983f759c9e15ce53d7326f0abad71 + ai.workspace.guidanceWarning: 00d95da9bc82394785728dec587b026e + ai.workspace.targetLabel: df5dd38c32585fe0c71c955b3a31f83c + ai.workspace.targetLocalAgents: 5a6a65e960093bfc37b4cc81c4a9f773 + ai.workspace.targetLocalModels: c7c20443d8aaf04271df9d713f5c02ce + ai.workspace.targetApiModels: 66537b9e5200ebbbfbd091144d8afbcc + ai.workspace.noTargets: 7c3ed1f10281b41070e5a5cbe77453d7 + ai.workspace.target.localAgents: 5a6a65e960093bfc37b4cc81c4a9f773 + ai.workspace.target.localModels: c7c20443d8aaf04271df9d713f5c02ce + ai.workspace.target.apiModels: 66537b9e5200ebbbfbd091144d8afbcc + ai.workspace.target.none: 7c3ed1f10281b41070e5a5cbe77453d7 + ai.workspace.target.unavailable: 453e6aa38d87b28ccae545967c53004f + ai.workspace.target.installed: 98dd43dfae05b11befe1f140e0ec787a + ai.workspace.target.installedVersion: 8677756d761e4cce240a637805482946 + ai.workspace.target.localModel: d34f4b997ce78da5aa57d657a5d6fcb5 + ai.workspace.target.apiModel: 589a257cbd265eaa7de93de8b4084dff + window.minimize: d27532d90ecd513e97ab811c0f34dbfd + window.maximize: 9369ba148ee259473fc1fb4939b6c2e8 + window.restore: 2bd339d85ee3b33e513359ce781b60cc + window.close: d3d2e617335f08df83599665eef8a418 + mcp.setup.manageTitle: d786c7b9ab9b2406258648931bca0e01 + mcp.setup.setupTitle: b4b373f690c8a0008885b31e78e93a5c + mcp.setup.connectedDescription: a2a3be70c0ac9fa7ef112df5252249fa + mcp.setup.setupDescription: 8bf199ffbcf303a99abac078c0c5e094 + mcp.setup.reconnect: 813ba447b5e64c17ff9b68c693358ca3 + mcp.setup.connect: fb95777dbc9f7b22014dc360a2957652 + mcp.setup.disconnect: 42ae25231906c83927831e0ef7c317ac + mcp.setup.connecting: 182e7eda4e721ad00737460b88701dee + mcp.setup.disconnecting: 11de134aefe51cb4a5c545b5a057a871 + mcp.setup.runtimeRequirement: 1d7a8c99b2bc875b3601fa57860432fb + mcp.setup.writeEntryDescription: 370aa4403d2ffc37f574503bc3cd00e6 + mcp.setup.clientPathsDescription: 541cd3d86aa9af81a6ab3a9d6f921a09 + mcp.setup.geminiGuidanceDescription: c6d8625d56151afce0bb455504490d84 + mcp.setup.manual.title: b240f1ff497fcaa83e6cd489739f11b6 + mcp.setup.manual.standardTitle: d2bc27dce31d27aec7612b638d443b95 + mcp.setup.manual.opencodeTitle: 5c38a2f5fe774f4cb886fcfa11a1acc4 + mcp.setup.manual.copyStandard: 89932d9339adf088de66947e8b7d4768 + mcp.setup.manual.copyOpenCode: dede4ad6a0b29559ea3bed6f796cd0fa + mcp.setup.manual.loading: 11ee201c121c8e2015c065952474a3ea + mcp.setup.manual.unavailable: 8b4330b839c41bad378c86c91ee0b5cd + mcp.toast.connected: 7716c0f9dad41a7fdbef592074459239 + mcp.toast.refreshed: 227d8721ebec3077792b1223c1f30e54 + mcp.toast.disconnected: 9b5254079561087d679e35aa10c80638 + mcp.toast.alreadyDisconnected: 645bec2e33bd9de2b334b704d9fa84cf + mcp.toast.configCopied: 458b50444b9a1cb069b00475c0951b8a + mcp.toast.configCopyFailed: b451cfb95f87458d142d026a8aca74cf + mcp.toast.setupFailed: 51f290962c08d8ba76bfc6c19552eaed + mcp.toast.disconnectFailed: ddc3387ab6e996ce15bc4950a3548e1e + mcp.error.clipboardUnavailable: 3b9ed3fe7dba627edb74e1cdaa02b2ee + save.toast.saved: 248336101b461380a4b2391a7625493d + save.toast.nothingToSave: 86ae8cef7371e639f3c007449a2f7d1f + save.toast.missingActiveVault: 49cf5064a7dce757f895950d8beeed68 + save.error.failed: b55b9fefe93fabae3c277e4e7956a534 + save.error.autoFailed: 09b2b131f07baddb5ba8f5a3ee6234c5 + save.error.invalidPath: c96fb669715e86508f4c5e73da9f6995 + savedViews.reordered: 531081a566b826eed2e714ce70a0de08 + locale.en: 78463a384a5aa4fad5fa73e2f506ecfc + sidebar.nav.inbox: 3882d32c66e7e768145ecd8f104b0c08 + sidebar.nav.allNotes: cd76cf851b08a9d2feafaf255bc1f4d5 + sidebar.nav.archive: e727b00944f81e1d0a95c12886ac4641 + sidebar.group.favorites: 953bf4d5e8a1807c15dec4e255384b1d + sidebar.group.views: 8f56a72923a6ca8ff53462533d89e567 + sidebar.group.types: 6b20155008db90cf7589c07baa7334e9 + sidebar.group.folders: d86e2756f698bea5aac3e2276639f042 + sidebar.action.createView: ba019414ea8c7fcdb4a83a70ec1bb639 + sidebar.action.editView: acdf34bd08b0692b906d446e590b1eb9 + sidebar.action.renameView: 429b903fddf39bb21887282d188c4b62 + sidebar.action.deleteView: c8f4f9cd8ff820f955474e5c9f70ff39 + sidebar.action.reorderView: ca2f42548fd57160d22fd0809bf8ed6e + sidebar.action.moveViewUp: e601b7f0901f12efb71f7821c0fdb79e + sidebar.action.moveViewDown: 6f86951085367864e3e4eb13f5c8688e + sidebar.action.customizeSections: 050a134cad1e9c6f04abe5d635592703 + sidebar.action.renameSection: 8936914051df04e7550d0eeb4a31e0e0 + sidebar.action.renameType: fda33d75aadfece7de1e1cc61186d99f + sidebar.action.customizeIconColor: 9a2f685c74d261ab483ef00dee5314fe + sidebar.action.deleteType: c52e15c44f6372c6ddb2758254a041ef + sidebar.typeDelete.blockedInUseOne: be7a2deea4ea29e8d5ec02f1fe2433c0 + sidebar.typeDelete.blockedInUseMany: 2b5c044e5c4cce91649de1f1942d0463 + sidebar.typeDelete.missingDefinition: fa8320bdc0d2b4e7e6a2dc4a8a97a311 + sidebar.action.createType: 6cfb8b8aa3fdce38e675819691b48f5c + sidebar.action.collapse: 00873bdddd457791609bff1243c40a6b + sidebar.action.expand: 8f98e9696f6eed958573012f52710faa + sidebar.action.createFolder: 5dc1e97c14fbe72d8b566ce29c39f010 + sidebar.action.renameFolder: db76034f8218cda07dadb746a5643019 + sidebar.action.deleteFolder: 2a2f15300f6f7c81d69860ac1796b517 + sidebar.action.createNodeInFolderMenu: 857a479229a4ab0bc62bbfb621d3180a + sidebar.action.revealFolderMenu: 76f4ed52da9937ae432dcf5a108fabb4 + sidebar.action.copyFolderPathMenu: 1779ec6018a385912c1a5499a1bbf2b2 + sidebar.action.renameFolderMenu: deb1d8fa030292e7eda2c244b313aca2 + sidebar.action.deleteFolderMenu: c78c889ac7396dc148fc859c9221e545 + sidebar.view.name: 67b904f75d1872abba088a3442caff40 + sidebar.folder.name: 710417c4e57a6a01500fe4c0c448ce3d + sidebar.folder.newName: e0ad5b2db20359a85de80c1231323bea + sidebar.folder.expand: b7545cb143816942ff096d890090fb6f + sidebar.folder.collapse: e9f8c6da708219313d72d99cc3998388 + sidebar.section.name: c3642037e309db278bbbfe0590114c02 + sidebar.section.showInSidebar: e38d073926d6fb3dfc4d4347708e3cf5 + sidebar.section.toggle: 2992ffbc76e607a2dc0e99781f101637 + sidebar.disabled.comingSoon: 81151a1669ebd695e837f304a0d8ec79 + noteList.title.archive: e727b00944f81e1d0a95c12886ac4641 + noteList.title.changes: c112bb3542e98308d12d5ecb10a67abc + noteList.title.inbox: 3882d32c66e7e768145ecd8f104b0c08 + noteList.title.history: 16d2b386b2034b9488996466aaae0b57 + noteList.title.view: 4351cfebe4b61d8aa5efa1d020710005 + noteList.title.notes: f4c6f851b00d5518bf888815de279aba + noteList.searchPlaceholder: 4857cd2689a0a40eb4a77cdc745c518e + noteList.searchAction: 5012120fc480c67686dd22ee2f9493cd + noteList.clearSearch: 9983381c21069a3d6e4432e0aaea0079 + noteList.quickOpenCreate: a4e964a3bc5b25e7ae87163624731e05 + noteList.createNote: f1484bc40bd3fe3430c13fb89e2ce1af + noteList.context.renameNote: c31c2b468229232ad6287e734fe67d96 + noteList.rename.title: c31c2b468229232ad6287e734fe67d96 + noteList.rename.description: dad38b42b70ad1518d65a3063bba6708 + noteList.rename.nameLabel: 1351017ac6423911223bc19a8cb7c653 + noteList.rename.cancel: ea4788705e6873b424c65e91c2846b19 + noteList.rename.confirm: 904a8304056d77e4547744781b7ceb50 + noteList.empty.changesError: 4fca500c4b70f61f7d9413c57c34bdc4 + noteList.empty.noChanges: ad82ac153532f5625760c29c176c5d5f + noteList.empty.noArchived: 75fa9e2aaa902f65d433be856a2e8331 + noteList.empty.noMatching: 22838e3b8d3d174d33d7986ab6bdd693 + noteList.empty.allOrganized: ea8fbb54d21c03fae1469635e7043b9b + noteList.empty.noNotes: 910bd677fdd52f3e6edb4abc1d03ade8 + noteList.empty.noMatchingItems: 25cd76c4bbd00b682a45687705b41a14 + noteList.empty.noRelatedItems: 67eecd103b56ef0e56fd892d379b5a5e + noteList.sort.modified: 35e0c8c0b180c95d4e122e55ed62cc64 + noteList.sort.created: 0eceeb45861f9585dd7a97a3e36f85c6 + noteList.sort.title: b78a3223503896721cca1303f776159b + noteList.sort.status: ec53a8c4f07baed5d8825072c89799be + noteList.sort.by: 5cd12b67b005056a94f06368a7645b8e + noteList.sort.menu: 62bfadcf958eb6ad238e6c71e312a0ce + noteList.sort.ascending: cf3fb1ff52ea1eed3347ac5401ee7f0c + noteList.sort.descending: e3cf5ac19407b1a62c6fccaff675a53b + noteList.filter.open: c3bf447eabe632720a3aa1a7ce401274 + noteList.filter.archived: 7d69b3cb4cada18ae61811304f8fbcb5 + noteList.filter.week: d2ce009594dcc60befa6a4e6cbeb71fc + noteList.filter.month: 7cbb885aa1164b390a0bc050a64e1812 + noteList.filter.all: b1c94ca2fbc3e78fc30069c8d0f01680 + noteList.properties.customizeColumns: be39bb193d872e11bb21388add6eca23 + noteList.properties.customizeAllColumns: 0d382ece9a9d08bb9ddf10ed2c88d726 + noteList.properties.customizeInboxColumns: e7cf53b914ce1079801eeee603875660 + noteList.properties.customizeViewColumns: a5612e5d2aa2e86941d536a85b27c8b4 + noteList.properties.showInNoteList: f22a57c5fc14ff477000cc66d837c344 + noteList.properties.searchPlaceholder: 9c7569eb531ff041b2b7cf4de1e5a619 + noteList.properties.searchLabel: de1bc9695d5e79a75296abf74dc25056 + noteList.properties.noMatches: 1627ecc53b9e97c94b2b6b1b93a58721 + noteList.properties.reorder: c0955c7efaa1ce90d449a23cbc52b553 + noteList.changes.restoreNote: d6e99303c0e0b7b2abce06fe9214788b + noteList.changes.discardChanges: 98313f623bb6f464b9a154eca0b99bf3 + noteList.changes.restoreDescription: 45e13380b0538e6cbff330f99c260c0a + noteList.changes.discardDescription: cd953f51a81e10f8c90135e0106a45d6 + noteList.changes.thisFile: 976b976e66879a470635bf0f660e81fc + noteList.changes.restore: 2bd339d85ee3b33e513359ce781b60cc + noteList.changes.discard: d94b42030b9785fd754d5c1754961269 + noteList.changes.cancel: ea4788705e6873b424c65e91c2846b19 + editor.empty.selectNote: a71ec7c80aefe49c59f6aba033bf453e + editor.empty.shortcuts: 1133fdf3ecef6a09dd9e2a185e1bd4ec + editor.raw.label: 7a9754c15264b0604aa31dfea32321ea + editor.sheet.loading: 59df3a3f0bb3834a820e8fbf6a515114 + editor.sheet.unavailable: f0920deeec857fa2251bb073b956364e + editor.sheet.context.auto: 06b9281e396db002010bde1de57262eb + editor.sheet.context.number: b2ee912b91d69b435159c7c3f6df7f5f + editor.sheet.context.percentage: 37be07209f53a5d636d5c904ca9ae64c + editor.sheet.context.currencyUsd: 3cb6cef8f5a71608c2a6903d2dfb6656 + editor.sheet.context.currencyEur: 24c64850c930dcaa85bbf03615fe2e78 + editor.sheet.context.insertRowAbove: 9e4b77d73f7e68e13150a59175bcb6d9 + editor.sheet.context.insertRowBelow: 410af72a850d577d370f3b19db951f29 + editor.sheet.context.insertColumnLeft: 951489ec15082daab6174f1f79e99035 + editor.sheet.context.insertColumnRight: d76ad5e27c1b1268ddd839b7b3f95108 + editor.sheet.context.deleteRow: d501f89526d0fe57c1dfb94aeb3fc234 + editor.sheet.context.deleteColumn: 864c8b59fdcfbf1d3872bc484c510c3b + editor.sheet.context.freezeRows: 5fa71e4def6c2b644894ee2af7285686 + editor.sheet.context.freezeColumns: e23bf93c6a51438420d26004fd51d7bd + editor.sheet.context.unfreezeRows: 7069b8a8fb5d9428c3c8814a1d9bc5e9 + editor.sheet.context.unfreezeColumns: b8bc2e9647448b820cdc20fd31cbdc52 + editor.sheet.context.decreaseDecimals: 6b515416941b465cbbcddaa74f6559c1 + editor.sheet.context.increaseDecimals: 07b8bdec0845dd1acbaebdebb0c15671 + editor.sheet.context.bold: 114c3050111d8b8ddd830b99ccebd246 + editor.sheet.context.italic: 1d874710ccdcd46b95397049d2e7500c + editor.sheet.context.wrapText: bcd38c54cc299e7f9d5d37f9b999c027 + editor.sheet.context.unwrapText: bde49101f3872b8d2bf6b08cccc6c667 + editor.sheet.context.clearFormatting: 4494691b92e1f195feb25bd63ee5468b + editor.sheet.formula.category.logical: e7a731a6342f126055b89b80d800a180 + editor.sheet.formula.category.math: b6980ca593af7e22633cafaa2afbd14a + editor.sheet.formula.category.lookup: 222642b2096833929903c20111fbd29d + editor.sheet.formula.category.text: 9dffbf69ffba8bc38bc4e01abf4b1675 + editor.sheet.formula.category.information: a82be0f551b8708bc08eb33cd9ded0cf + editor.sheet.formula.category.statistical: ad2ee48af836d71513e02e3b82b34e48 + editor.sheet.formula.category.dateTime: 2b0b7c9cbd6d6e9fa6b99aa06e828673 + editor.sheet.formula.category.financial: 35f156073cb9314e5ddcabd2d16f443c + editor.sheet.formula.category.engineering: ab0268fb8036a892dc341945cb7ae3be + editor.sheet.formula.description.generic: 0c79cdd955d17cfe5e586a4e14907be6 + editor.sheet.formula.description.abs: e49ccb55e16059eda228ec3a10f03ac9 + editor.sheet.formula.description.and: 86314c9bc9282ef7234090eb3428271e + editor.sheet.formula.description.average: 725636fff01291d97c3ff6c221a2859b + editor.sheet.formula.description.concat: 0706d31060e4b6d013351d606d783cc7 + editor.sheet.formula.description.concatenate: 0706d31060e4b6d013351d606d783cc7 + editor.sheet.formula.description.count: 3a44e7f9d53ba4b9043767837b703a42 + editor.sheet.formula.description.counta: cff7cefde504a7bfda26f6b025250647 + editor.sheet.formula.description.countif: a7f8ec539622fb56ad1a69a8f0749b99 + editor.sheet.formula.description.date: b0a194b65fecd9326ea5682824d78372 + editor.sheet.formula.description.day: 939afa1fa3226d5de31605a68bf5b504 + editor.sheet.formula.description.if: 0ed6716d89deab3daf691b52eac43da2 + editor.sheet.formula.description.iferror: 78985d37abb3c595234f2f0eebd49647 + editor.sheet.formula.description.ifs: 3260bbc522a06128a1b12e61512490e6 + editor.sheet.formula.description.index: 8074638b36f662023374715a46212475 + editor.sheet.formula.description.left: ba11420ae9f98af2d32e478623432ba8 + editor.sheet.formula.description.len: b2e40842023453512efd37aac98d0509 + editor.sheet.formula.description.lookup: 6bbaf0ac03847d9b9370fea658fdf996 + editor.sheet.formula.description.lower: c2a995fa02ce7f13a234e1d941dd4dbc + editor.sheet.formula.description.match: fd0a986f7a5d74695c8926c060da6bbd + editor.sheet.formula.description.max: 8a6b16d3edf2adf0b14b48ea681f7fca + editor.sheet.formula.description.min: e9b1a7cc7aab7c814fc3199a50486fbd + editor.sheet.formula.description.month: 0e1de9df4811e0f39e6dd1df237ad97b + editor.sheet.formula.description.not: af45222aa223d8464769ab8e92aa068d + editor.sheet.formula.description.now: b079b385cd81adf67130fe02cd0b70c3 + editor.sheet.formula.description.or: fd1436942b6c9d5d03331e98072b98b0 + editor.sheet.formula.description.right: 86696dc97461745b60f015851e900a6e + editor.sheet.formula.description.round: c55ffe7fccf229e07738384dd1941391 + editor.sheet.formula.description.rounddown: b5f24d2cb58ec0829680e4567b4d8a7d + editor.sheet.formula.description.roundup: e9770aba9a743b397694e26b3fa1b8e7 + editor.sheet.formula.description.sum: f3e468f4b6279f941b37a40d1799a4af + editor.sheet.formula.description.sumif: bf24baec6c2916df7328f0f244758225 + editor.sheet.formula.description.sumifs: e590b9b3b695a9e70192f60baeae5a76 + editor.sheet.formula.description.text: b35a184f1978cdd14355a31c8bc17a2b + editor.sheet.formula.description.today: 34b315129b960fc5616e33ab582d403d + editor.sheet.formula.description.trim: ed6e316cd71dc0e96a437e47834691d4 + editor.sheet.formula.description.upper: 68dc8fd2dd906b5ca5462fe1c2bd1479 + editor.sheet.formula.description.value: b1168b1edcb926027d0d939c2f61e91b + editor.sheet.formula.description.vlookup: d7b75f9a4829a2310d654e8e2ad9753d + editor.sheet.formula.description.xlookup: 6bbaf0ac03847d9b9370fea658fdf996 + editor.sheet.formula.description.year: 1bafda4ca42f9bc0002c2c63129bbe90 + editor.find.findLabel: 4cfa6c981549e990fe2344e4c805405e + editor.find.findPlaceholder: 4cfa6c981549e990fe2344e4c805405e + editor.find.replaceLabel: 0ebe6df8a3ac338e0512acc741823fdb + editor.find.replacePlaceholder: 0ebe6df8a3ac338e0512acc741823fdb + editor.find.matchCount: 29d3cd243c6850f5fbac283e79d6d6a7 + editor.find.noMatches: 3b470c1f6a10f58c8a8cb6b904577786 + editor.find.invalidRegex: 9704b2ccc6158864810a313702019d68 + editor.find.regexMustMatchText: 267728c2e7f2889b39847ee9b9b38891 + editor.find.previousMatch: afb1b7e6bbdfaa97114e1b519a817f13 + editor.find.nextMatch: 7f4d218b4f566ea7ff69ab8d912ba7b9 + editor.find.showReplace: 82c8fdb73dfe8baa1759a36c6c0fb282 + editor.find.hideReplace: a85baf9735f9acf26aa15b70f2ee55fb + editor.find.regex: 7486f19f279f95357c84706dd09ce9da + editor.find.matchCase: ba945280b8802ee1a7a50140e2fe94e2 + editor.find.close: b040fbda901d2135cad53e54e0127071 + editor.find.replace: 0ebe6df8a3ac338e0512acc741823fdb + editor.find.replaceAll: b1c94ca2fbc3e78fc30069c8d0f01680 + editor.blockType.paragraph: feaf0a320c3d678ad30dd179b7d21584 + editor.blockType.heading1: fd7500ca2aba5ce6ba82e8dee1ffe5f4 + editor.blockType.heading2: ea59935a58010a876f94374fb2f41d51 + editor.blockType.heading3: 652864eb81a28bd7164f1605ab52a26a + editor.blockType.heading4: 553ddd7695ba37dc6f821067d9f5da30 + editor.blockType.heading5: 8c08de90796fbc4c4aae36014c40eec4 + editor.blockType.heading6: a24b3f7226370522c2a0e7ffd542a031 + editor.blockType.quote: c48e929b2b1eabba2ba036884433345e + editor.blockType.bulletList: 0fc5eb4762a5088027c181896d394181 + editor.blockType.numberedList: bf087c065d8d3f2e77bc07a82c07df52 + editor.blockType.checklist: a5d727df2ce2d168bae39c78df4b9054 + editor.blockType.codeBlock: febf6b24daaaeced55c4cc4db7dc931b + editor.toolbar.rawReturn: 5ddcf5e0dc8b933b344bb6ca3d8e131d + editor.toolbar.rawOpen: 89ad60735a649b60dacd2301cda0c4ec + editor.toolbar.noteWidthNormal: 9bb55413ec536c4a8fa41c4c4464ab44 + editor.toolbar.noteWidthWide: 1a744f406c9150f76739bd167c3e47ec + editor.toolbar.centerLayout: 3fabf7e9b285eeec90704d271f1049b7 + editor.toolbar.leftLayout: acab6c8612816e012ff9f4220a42e485 + editor.toolbar.removeFavorite: 7188286e36fc6c1892dd53aaa54237fb + editor.toolbar.addFavorite: 910885435dbc44a22b68cb45c79e4a7e + editor.toolbar.markUnorganized: 83fb1b23a3609fab493737fe7af0a198 + editor.toolbar.markOrganized: 9d170c916afae3d7a82456838728ffa5 + editor.toolbar.openNeighborhood: 80f659c99954ab7681266b5a1e5da6df + editor.toolbar.noDiff: b2c4189f90648aaeb5cd2e81f9bd8d94 + editor.toolbar.loadingDiff: 430386d6aae3570fd399a133e41c7372 + editor.toolbar.gitDiff: 51b46b6eac27ba484479246b875f76f5 + editor.toolbar.showDiff: 08d579de8d3abdcdfce0953a797362c6 + editor.toolbar.openAi: d2e93006570a66709c8ce0dc27082ef1 + editor.toolbar.closeAi: cd46973ef73076d612dff9903ce54498 + editor.toolbar.openTableOfContents: b733f053ea3fde4a9acd589ec7f58c10 + editor.toolbar.closeTableOfContents: d3292972a10edd1a06a627f3552f760a + editor.toolbar.restoreArchived: 1bff5cf4943af64c05b2e9aeadccbc84 + editor.toolbar.archive: 63dc964c32e715217b49f8739d49636b + editor.toolbar.delete: f48134a07b016a1de05d4ba6d2fbfb41 + editor.toolbar.revealFile: 76f4ed52da9937ae432dcf5a108fabb4 + editor.toolbar.copyFilePath: 64c6cec5ca57efbb8c9c93ae5fbccd27 + editor.toolbar.copyNoteDeepLink: 0fff274a7c47e7c12457c0b7412e86ce + editor.toolbar.copyNoteGitUrl: 04a732b98f7585e3c3b5474688e0baac + editor.toolbar.exportPdf: 7a39acf8742a59d1a4f7ae064b6219a0 + editor.toolbar.moreActions: 89e19353176b94068dec4c768c25e70b + editor.toolbar.openProperties: 948b90b60b8ce827f179e8c5b85b112e + editor.sideMenu.expandSection: 2d8eda764f87bd48192245a238fe8b65 + editor.sideMenu.collapseSection: 3d3b02981ab3390bf66247c92a2d8dd9 + editor.sideMenu.expandItem: 16291fbe2d88fc603b23f97ed2bc22a3 + editor.sideMenu.collapseItem: 91750c64ed8141fdb63ffb1e878a11ad + editor.sideMenu.turnInto: c2d2b328447c4974e839564b054c72f8 + editor.formatting.highlight: 0b90582f4589d84be89f5b847d4d1ed1 + editor.formatting.highlightTooltip: c27173fea9ec1718030c5fa8440b2c78 + editor.whiteboard.enterFullscreen: 606bf766125ecff4b33ca98122222c72 + editor.whiteboard.exitFullscreen: afd0a0414d79d047f00319921e4d4944 + editor.whiteboard.permissionDeniedTitle: f40def4c1928438aa4ac86421fed9fd9 + editor.whiteboard.permissionDeniedBody: eb1399413cae00b1614695d3c5c7ac9f + editor.exportPdf.unavailable: 2fce9834984172b64b66767485d815e9 + editor.exportPdf.failed: bf3fa78ff5725f5cbde8979b67ec2e2a + editor.imageImport.unsupportedHeic: 75481136eb7ca33ef4f82806765e406f + editor.imageImport.unsupported: 9008202219a82edb8e70700bf351ce60 + filePreview.copyDeepLink: b75833669968adbef634495636e3fe50 + deepLinks.copied: 6ada57681192daf3b4afa07ce7a30575 + deepLinks.error.ambiguousVault: 4365c6d5cf8c2c1165259b32d24a6274 + deepLinks.error.copyFailed: 57b74ecce2d00b4c327d15830f79410d + deepLinks.error.invalidScheme: 58eaa7ac701a0508cbae90c5d3837ce4 + deepLinks.error.malformedUrl: 4f85323322ef0a1d627df621edb749c7 + deepLinks.error.missingFile: 79a0c73176508c58b4d785fabaf7d8cc + deepLinks.error.missingPath: a6bb1c92f1a38476ff603a2d3ff81223 + deepLinks.error.missingVault: e2936e1f599c7ed229efb5d4a122fc4b + deepLinks.error.openFailed: 97901feafe25688fcc3fc4dcfaf82619 + deepLinks.error.outsideVault: fda2fd5675b596df7ba0e0b1a11ec47d + deepLinks.error.unavailableVault: 911dfaf8e855bd8cadefe7df9a98a7e8 + deepLinks.error.unknownVault: b4940569e884833f50466177dab1f62f + deepLinks.error.unsafePath: e09caab43be8c2372bfc94e947ea34e6 + noteGitUrls.copied: 6832bf85abaa03a6304cd1cce073d747 + noteGitUrls.error.copyFailed: 1952ee267f473efe8e28ca90659f75f6 + noteGitUrls.error.unavailable: ed08c134bba26350fd28f780c6f1b55b + editor.slash.math: a49950aa047c2292e989e368a97a3aae + tableOfContents.title: f61d6c3e3733db355168b7e1aee6780b + tableOfContents.close: d3292972a10edd1a06a627f3552f760a + tableOfContents.empty: e4f02ad69cbbce1ec65d14215e3d5871 + tableOfContents.emptyNoNote: 046d95682b747a30ed8a7b0b1d581629 + tableOfContents.navLabel: 598b8ae10dfce818e73d3218daddbdae + tableOfContents.untitledHeading: 93c2dde207965b383b7b22af005ebe4f + tableOfContents.expandHeading: 6353ab44fe75f5dd935789bdab8551a0 + tableOfContents.collapseHeading: 040b4c102283028831ea78713235e478 + editor.codeBlock.copy: 8e477020fb3f7ab844b91ff1d7de8347 + editor.imageLightbox.title: 2c5a15c875bcdc2478c4a5cad044e10c + editor.filename.rename: c31c2b468229232ad6287e734fe67d96 + editor.filename.renameToTitle: bff34a6a2dd1eb87c59403946679237f + editor.filename.trigger: 4e9739c2f937c828bbf5d1f935fe7fb9 + editor.banner.archived: 7d69b3cb4cada18ae61811304f8fbcb5 + editor.banner.unarchive: 9fd02d2eb5ce348a74372eea202a0aec + editor.banner.conflict: 384fc5a4b31c662b1e3426a7ef56441e + editor.banner.keepMine: 544a2f2e8e09f93919f10e1920d1b69e + editor.banner.keepMineTooltip: 9988cb86b051dd728e2d2f852f0283e4 + editor.banner.keepTheirs: 46131020af44cc6ec4bfa0a8ff39e044 + editor.banner.keepTheirsTooltip: 9e5b845c4459747d6ab0df6438ccfac4 + inspector.title.properties: 9fc2d28c05ed9eb1d75ba4465abf15a9 + inspector.title.propertiesShortcut: 427d1cbdc813cf54c5fac2508d38d407 + inspector.title.closePropertiesShortcut: 97b953e45042d4143dd19624dc345d29 + inspector.title.collidingProperties: bf71358d0af34696e88bea278eac046a + inspector.title.collidingPropertiesAria: ce5c3569f304cd69b0f33a6fc185f49b + inspector.empty.noNoteSelected: 046d95682b747a30ed8a7b0b1d581629 + inspector.empty.noProperties: 6864166e0f65d81b1eb474e41fde8822 + inspector.empty.initializeProperties: edf249d214b68f5afe8634c577b709c4 + inspector.empty.invalidProperties: 2ee2429c1ffbabcda9f57f91fb6c0b9d + inspector.empty.fixInEditor: b86d4934630d68bc5333c0feb25e5f3f + inspector.properties.addProperty: 9820ade036bf1bdf80c0b7da24a4ce0a + inspector.properties.deleteProperty: f1d0ab48c8596108e949dfc90e13050b + inspector.properties.none: 6adf97f83acf6453d4a6a4b1070f3754 + inspector.properties.displayAs: 6e0b81cc9a0919bc2182df7a5762fd26 + inspector.properties.formatText: 9dffbf69ffba8bc38bc4e01abf4b1675 + inspector.properties.formatSheet: 83a293c58d996a90ac357b10ba864c83 + inspector.properties.workspace: 5b70a213f150f01f0776fa9481ef2ddf + inspector.properties.missingType: 9179080e7bcc72408f3de7cb944ff400 + inspector.properties.missingTypeAria: 582c2c46117cff0534d13e7c8a45a3ba + inspector.properties.searchWorkspaces: ebd1ca0ae74f6d6f5b32f18db699be11 + inspector.properties.noMatchingWorkspaces: a4339dbce88712e787c0615a961636f6 + inspector.properties.searchTypes: 84c8d94846183a79444bf36667e8d7ea + inspector.properties.noMatchingTypes: 9b64c31214171ba3b2d591743990b840 + inspector.properties.yes: 93cba07454f06a4a960172bbd6e2a435 + inspector.properties.no: bafd7322c6e97d25b6299b5d6fe8920b + inspector.properties.pickDate: e8e91fbba934dc8adfd7e433f5193911 + inspector.properties.valuePlaceholder: 689202409e48743b914713f96d93947c + inspector.properties.propertyName: 7e9982311d69d66d38809e5d85377a08 + inspector.relationship.add: ec211f7c20af43e742bf2570c3cb84f9 + inspector.relationship.addRelationship: d2b0cb45dcdbb00ee70db397c36f73ad + inspector.relationship.name: 7ed3fccc071d6939eff211b1a6fd9696 + inspector.relationship.noteTitle: f72ea00f174785710f0369b23c53963c + inspector.relationship.createAndOpen: 55bb53fea743c584c4024e7439c55bfe + inspector.info.title: 4059b0251f66a18cb56f544728796875 + inspector.info.modified: 35e0c8c0b180c95d4e122e55ed62cc64 + inspector.info.created: 0eceeb45861f9585dd7a97a3e36f85c6 + inspector.info.words: 6f15b8d4b7287d60a8ea3d1c5cbadc84 + inspector.info.size: 6f6cb72d544962fa333e2e34ce64f719 + update.available: 992477975168b876be8e77ce2975e390 + update.checking: a07813cc05571f23ce8dd7039583d7da + update.releaseNotes: 5dd03e8d039863e563e049be198c3fd3 + update.updateNow: 99b1054c0f320be9f109c877a5efd0b2 + update.dismiss: c8a59e7135a20b362f9c768b09454fdb + update.downloading: 2c7fdcafb08f831420f661810f826549 + update.readyRestart: 4d7487b02d955a44d599189ec383e469 + update.restartNow: 2d9c2140c53daf05855033aaceeaa8fd + status.update.check: eb1b4bb3667dd670210c7822badc2f1d + status.zoom.reset: dbf36ab275e5fbd256590e65aa1ca9a3 + status.feedback.contribute: 96ab5025e38aef43fdb73c9830795264 + status.feedback.label: 9887a4451812854f0f1b6f669a874307 + status.docs.open: 2066997d2262d4a9b79de68f255dc4a9 + status.docs.label: a3907cd461d8739aa3266047bc4b8c0c + status.theme.light: 4abf27a209985375949aaa426c7c7f3d + status.theme.dark: 541be2a55a52b518b8e510c476c7cc95 + status.settings.open: bae08226d065231df6f01b08cd681c9b + status.build.unknown: d250349dd489bdfeb0cb0750b8b3ff89 + status.vault.switch: 8a2ad262643c556ff0cfa99497aaa529 + status.vault.default: 5b70a213f150f01f0776fa9481ef2ddf + status.vault.createEmpty: f37c03f236fbf1516222360a936249e8 + status.vault.openLocal: a367344e0429a12a5cc9a6cecbbfcde5 + status.vault.cloneGit: a7d0aef7c6237a36e54fd5a26e9c23fe + status.vault.cloneGettingStarted: 9953a11a477b441976a626a9acf5d7df + status.vault.availableHeader: 2be36e986c7859a439c30bc5696c15a9 + status.vault.manageWorkspaces: ecce6ef1f37e975762b1413d8bc7d21d + status.vault.includeWorkspace: fe8ee397206a29f2d91eea7b95e9c112 + status.vault.notFound: 3119b7fa94c96209bca571b7c7ed0b7e + status.vault.reloading: 893da50ef3a9e01d8a99ff5d3ceb55e9 + status.vault.reloadingTooltip: 4a884bd7d113797ad16f905f70742da8 + status.vault.remove: 7f3b4f76df23626170940dbc55c70728 + status.vault.removeConfirmTitle: 713713c6d8c4eb8b75cf12ebaca44f96 + status.vault.removeConfirmMessage: 854744291c28ea9e18bed1110b0737d9 + status.vault.removeConfirmAction: 8b064b07e2f4f1e63d1d5087aead2648 + workspace.manager.title: ecce6ef1f37e975762b1413d8bc7d21d + workspace.manager.description: d0de6e81a4268a52f756f6073f938fc3 + workspace.manager.default: 7a1920d61156abc05a60135aefe8bc67 + workspace.manager.makeDefault: 581e972562013d920e9b4d364265829e + workspace.manager.label: ce0aa8f9a9fad0faf99b6185fbc09a1f + workspace.manager.alias: e89ff14f985995ab3af3bd8af66d019c + workspace.manager.mounted: 38ca22b706f01504d11883bc1984d45b + status.remote.noneConfigured: 18a4edd4e8d862ccb343937f45585fb1 + status.remote.inSync: a56f571b4df55d88fb06e61e343b3c91 + status.remote.aheadTitle: 1516f4b92671b83c17441b4cddf25a7a + status.remote.behindTitle: 55ee20469bc46b7c61e7515fbfdc0b5d + status.remote.ahead: 681557ace3a84e15060ffca1623b75e6 + status.remote.behind: 1825bb12c857a71861280e490e29debd + status.remote.add: f292f8ef85219f0acde6b66eb9c9c0f4 + status.remote.none: ed3f2ebe0d847584fd62fc9e0db2df33 + status.remote.noneDescription: f47636010c21c88a6f81cc41a962b26c + status.remote.noUpstream: 41c05263e8d71d526a70f01481ded660 + status.git.currentBranch: db28ec1bca968fc1dc7a2d32006afc8e + status.git.branchLine: cfda131b88d16e9f47db17a9a8c40102 + status.git.unknownBranch: 658240910e4c727d045070efe206beb6 + status.sync.syncing: b21888f3f83c224b3451901da243d00a + status.sync.conflict: f1d4ac54357cc0932f385d56814ba7e4 + status.sync.failed: 85b08b086888f8ee2680ddd3398f3574 + status.sync.pullRequired: 1ea27a2d3048b078a18665b24e8dcd91 + status.sync.notSynced: d18d6927f7ffe96f72e18b4da12e306b + status.sync.justNow: 7b9275ee2786ef410555041b92b36bb3 + status.sync.minutesAgo: b3ac76d1e2595531940035a8e2e2d13c + status.sync.resolveConflicts: b9f44cc5ee01c964f004f16e3a2833ca + status.sync.inProgress: 278d2bf6768809fbf6e97e7bb319a7c0 + status.sync.pullAndPush: e07c084a9e734a54bd079d5327a924e7 + status.sync.retry: ca43e18d4f930e8a3e6d403f31d30a39 + status.sync.now: c03fafa3a653240e6104348e1293bb88 + status.sync.synced: 5befab0dde764b6dd8b24a34dc30afa7 + status.sync.conflicts: d35cef9595a935771f5e8158f387227b + status.sync.error: 902b0d55fddef6f8d651fe1035b7d4bd + status.sync.status: 76590d5f9708692e3735891bdca40903 + status.sync.pull: 718f59718640c6506b3721fbc8bf3a4d + status.conflict.count: 46293d20ed2071fb30762b71fdaa5894 + status.offline.title: 3bf45a7003646cc4f963810b0b7ac0f5 + status.offline.label: 8d9da4bc0e49a50e09ac9f7e56789d39 + status.changes.view: 6d60d24d8f475ddaef94bbbc58514b5a + status.changes.label: c112bb3542e98308d12d5ecb10a67abc + status.commit.local: 6474fe7e2ed525df3da3eb447943bfcd + status.commit.push: a4aaf10ef20cf17a982bb5b6dad198e0 + status.commit.label: 59d5b10c3a447f036d85cb5ce524c96c + status.commit.openOnGitHub: 445ad18680a573c2aab062c3273ae16a + status.git.disabledTooltip: 6981fe5b4993e4b3299c774b185ecd70 + status.git.disabled: 7f822b0ee91b8922b0c41723da867bd1 + status.history.onlyGit: 7da5397c33ada1b73a81c5a449cbab16 + status.history.open: 802ec1fd97592612c19ebef7bebbb5c2 + status.history.label: 16d2b386b2034b9488996466aaae0b57 + status.mcp.notConnected: 7e8850f4a564088ced4f592996a39309 + status.mcp.unknown: 051ff29ff900592f75974d0ef554c2eb + status.claude.missing: 89021856e6152467f54ed33faed55e2f + status.claude.label: 38c66b824d5769e42c3866005296525b + status.claude.install: 1f4688bc2a2acc268811c9edcec810a1 + status.ai.noAgents: 0876a176a6297e79366d0a2935851f81 + status.ai.noAgentsTooltip: 1bdc02ec2dd7f9701b4371ffd5770318 + status.ai.selectedMissing: a37f3dbc73725219e90d709fe0d3ad97 + status.ai.defaultAgent: 79650d57d1678e56c75dbbcba6ea87f7 + status.ai.defaultTarget: 65ea5bdb609833c4baffd2cbd0e86542 + status.ai.restoreDetails: 47a913b58ce40add6521bc93e807476f + status.ai.withGuidance: 1eafd34810db6bba54dac7aa549b5182 + status.ai.active: 059e3a3167c2ab00f4ca872e82a48d98 + status.ai.unavailable: 0d3e6e3afef545710aa24ed2ea4990af + status.ai.install: 349838fb1d851d3e2014b9fe39203275 + status.ai.installAgent: 78ac3395d977f8b86ca9a02f781f4af8 + status.ai.vaultGuidance: 7bb4644efe6ae7cec9993c8bd85f1519 + status.ai.restoreGuidance: 23331fecc692d11d85cf24838ed273f2 + status.ai.openOptions: d387b1ab67586c7a2d83db8900a4dd8e + status.ai.openWorkspace: 178247429f30251ce21265570b612e9f + status.ai.modelTargets: 8f3ceb96e088f8bdaecc236778401ac9 + status.ai.localChat: 215a24e3d38fb8b68deea6dad0142326 + status.ai.apiChat: 6dffd5b9997885efe701631cea5c470d + pulse.title: 16d2b386b2034b9488996466aaae0b57 + pulse.today: 1dd1c5fb7f25cd41b291d43a89e3aefd + pulse.yesterday: ebfe9ce86e6e9fb953aa7a25b59c1956 + pulse.commitCount: 17dc5f11868c946f55ff8164d318856d + pulse.commitSingular: fffca4d67ea0a788813031b8bbc3b329 + pulse.commitPlural: a90814b38bbdfe717205f6d24183f6a7 + pulse.openOnGitHub: ddab0146d46f585f2ab36072c92a0048 + pulse.expandFiles: 53c567ce9f999dd937fc954c39853e81 + pulse.collapseFiles: 0ae74096d72faf840cd7d35635aa0cf9 + pulse.noActivity: cfbd8245c849f5f00b495d61ea14dfdf + pulse.emptyDescription: b5ee4968b62908444166182fe8e593c1 + pulse.retry: 6327b4e59f58137083214a1fec358855 + pulse.loadingActivity: 26bf6a6bc2fd8148cd8753542a0ddf86 + pulse.loading: 5f02f05c744a0f875aebb8625ae1486f + pulse.loadError: 63c17996fe219e6bd33b27d2b7ce0c96 + command.switchLanguage: 60f1a6760c7d1da0b1f7f6d0529608ee + locale.itIT: 4be8e06d27bca7e1828f2fa9a49ca985 + locale.frFR: ad225f707802ba118c22987186dd38e8 + locale.deDE: 86bc3115eb4e9873ac96904a4a68e19e + locale.ruRU: deba6920e70615401385fe1fb5a379ec + locale.esES: cdbe021be1976335ab583a845edf7ed6 + locale.ptBR: 26d5352ead4a731f3d11eb2e85d0e297 + locale.ptPT: 71bfc0d79772120b52c1c0782450ff84 + locale.es419: edbf64ac382b82a54795fb409beb54be + locale.zhCN: 1e81fc32fea0e9f3a978661e4b5e484d + locale.zhTW: d0bb8c23f6e4f8c8037b6774ba912036 + locale.jaJP: f32ced6a9ba164c4b3c047fd1d7c882e + locale.koKR: d0bdb3cde477d82e766da05ebda50ccb + locale.vi: 7b80fae85640c16cdb0261bef0c27636 + locale.plPL: c730389bc8d99e59c867766babdd48b5 + locale.beBY: 0a6de4460095f398e8a45ed512a7538f + locale.beLatn: 9c454dccccd6aaac0121a45b923dc5ad + locale.idID: cc1c9b57607b4439578b68074a5e2d80 + locale.ukUA: e78a6fc14ad64f7a78386b20568ce95b + locale.svSE: 41171a0fcd362ce98b5f0f11398713b6 + locale.skSK: 9accc467b159e7ce158e53209c0cde2a diff --git a/lara.yaml b/lara.yaml new file mode 100644 index 0000000..1bde63f --- /dev/null +++ b/lara.yaml @@ -0,0 +1,38 @@ +version: "1.0.0" + +project: + instruction: > + Tolaria is a desktop knowledge-management app. Keep product names, CLI names, + markdown wikilinks, frontmatter keys, file paths, and placeholders like + {agent}, {zoom}, {language}, {name}, {label}, {file}, and {count} unchanged. + +locales: + source: en + target: + - it-IT + - fr-FR + - de-DE + - ru-RU + - es-ES + - pt-BR + - pt-PT + - es-419 + - zh-CN + - zh-TW + - ja-JP + - ko-KR + - vi + - pl-PL + - be-BY + - id-ID + - uk-UA + - sv-SE + - sk-SK + +files: + json: + include: + - "src/lib/locales/[locale].json" + exclude: [] + lockedKeys: [] + ignoredKeys: [] diff --git a/mcp-server/agent-instructions.js b/mcp-server/agent-instructions.js new file mode 100644 index 0000000..512e474 --- /dev/null +++ b/mcp-server/agent-instructions.js @@ -0,0 +1,23 @@ +import { readFile } from 'node:fs/promises' +import path from 'node:path' +import { vaultContext } from './vault.js' + +export async function readAgentInstructions(vaultPath) { + const instructionsPath = path.join(vaultPath, 'AGENTS.md') + try { + return { + path: instructionsPath, + content: await readFile(instructionsPath, 'utf8'), + } + } catch (error) { + if (error?.code === 'ENOENT') return null + throw error + } +} + +export async function vaultContextWithInstructions(vaultPath) { + return { + ...(await vaultContext(vaultPath)), + agentInstructions: await readAgentInstructions(vaultPath), + } +} diff --git a/mcp-server/app-config-policy.json b/mcp-server/app-config-policy.json new file mode 100644 index 0000000..cd074b5 --- /dev/null +++ b/mcp-server/app-config-policy.json @@ -0,0 +1,20 @@ +{ + "current_namespace": "com.tolaria.app", + "legacy_namespace": "com.laputa.app", + "namespace_read_order": ["current", "legacy"], + "files": { + "settings": "settings.json", + "vaults": "vaults.json", + "last_vault": "last-vault.txt", + "ai_workspace_sessions": "ai-workspace-sessions.json", + "window_state": "window-state.json", + "ai_provider_secrets": "ai-provider-secrets.json" + }, + "read_order": [ + "preferred config root/current namespace", + "preferred config root/legacy namespace", + "platform config root/current namespace when different", + "platform config root/legacy namespace when different" + ], + "write_target": "preferred config root/current namespace" +} diff --git a/mcp-server/index.js b/mcp-server/index.js new file mode 100644 index 0000000..a2e8b1a --- /dev/null +++ b/mcp-server/index.js @@ -0,0 +1,350 @@ +#!/usr/bin/env node +/** + * Tolaria MCP Server — lightweight vault tools for AI agents. + * + * These MCP tools provide Tolaria-specific capabilities alongside each + * app-managed agent's own Safe / Power User permission profile: + * + * - search_notes: full-text search across vault notes + * - get_vault_context: vault structure overview (types, note count, folders) + * - get_note: parsed frontmatter + content (convenience over raw cat) + * - create_note: create a new markdown note without overwriting existing files + * - open_note: signal Tolaria UI to open a note as a tab + * - highlight_editor: visually highlight a UI element (editor, tab, etc.) + * - refresh_vault: trigger vault rescan so new/modified files appear + */ +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from '@modelcontextprotocol/sdk/types.js' +import WebSocket from 'ws' +import { createMcpToolService } from './tool-service.js' + +const WS_UI_PORT = parseInt(process.env.WS_UI_PORT || '9711', 10) +const WS_UI_URL = `ws://localhost:${WS_UI_PORT}` +const LOCAL_READ_ONLY_TOOL_ANNOTATIONS = Object.freeze({ + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, +}) +const LOCAL_CREATE_TOOL_ANNOTATIONS = Object.freeze({ + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, +}) + +// Connect as a WebSocket CLIENT to the UI bridge (run by ws-bridge.js). +// The bridge relays messages to all other clients (the React frontend). +let uiSocket = null +let reconnectTimer = null +let shutdownStarted = false +const RECONNECT_INTERVAL_MS = 3000 + +function connectUiBridge() { + if (shutdownStarted) return + + try { + const ws = new WebSocket(WS_UI_URL) + uiSocket = ws + ws.on('open', () => { + if (shutdownStarted) { + closeUiSocket() + return + } + console.error(`[mcp] Connected to UI bridge at ${WS_UI_URL}`) + }) + ws.on('close', () => { + if (uiSocket === ws) uiSocket = null + scheduleUiReconnect() + }) + ws.on('error', () => { + // Silent — bridge may not be running yet, will retry + }) + } catch { + scheduleUiReconnect() + } +} + +function scheduleUiReconnect() { + if (shutdownStarted) return + + clearUiReconnectTimer() + reconnectTimer = setTimeout(connectUiBridge, RECONNECT_INTERVAL_MS) + reconnectTimer.unref?.() +} + +function clearUiReconnectTimer() { + if (!reconnectTimer) return + + clearTimeout(reconnectTimer) + reconnectTimer = null +} + +function closeUiSocket() { + const socket = uiSocket + uiSocket = null + if (!socket) return + + socket.removeAllListeners() + socket.on('error', () => {}) + if (socket.readyState === WebSocket.CONNECTING) { + socket.terminate?.() + return + } + + try { + socket.close() + } catch { + // Ignore close races during process teardown. + } + socket.terminate?.() +} + +function broadcastUiAction(action, payload) { + if (!uiSocket || uiSocket.readyState !== WebSocket.OPEN) return + uiSocket.send(JSON.stringify({ type: 'ui_action', action, ...payload })) +} + +const toolService = createMcpToolService({ emitUiAction: broadcastUiAction }) + +const TOOLS = [ + { + name: 'search_notes', + description: 'Full-text search across vault notes by title or content. Returns matching paths, titles, and snippets.', + annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS, + inputSchema: { + type: 'object', + properties: { + query: { type: 'string', description: 'Search query string' }, + limit: { type: 'number', description: 'Maximum number of results (default: 10)' }, + }, + required: ['query'], + }, + }, + { + name: 'get_vault_context', + description: 'Get vault orientation for the active Tolaria vaults: entity types, AGENTS.md instructions, note count, folders, and recent notes.', + annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS, + inputSchema: { + type: 'object', + properties: { + vaultPath: { type: 'string', description: 'Optional target vault root. Omit to inspect all active vaults.' }, + }, + }, + }, + { + name: 'list_vaults', + description: 'List the current active Tolaria vaults available to MCP tools, including whether each vault has AGENTS.md instructions.', + annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS, + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'get_note', + description: 'Read a note with parsed YAML frontmatter and markdown content. Returns {path, frontmatter, content}.', + annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS, + inputSchema: { + type: 'object', + properties: { + path: { type: 'string', description: 'Relative path to the note (e.g. "project/my-project.md")' }, + vaultPath: { type: 'string', description: 'Optional target vault root when multiple vaults are active.' }, + }, + required: ['path'], + }, + }, + { + name: 'create_note', + description: 'Create a new markdown note inside an active Tolaria vault. Does not overwrite existing files. Use content for the full markdown including YAML frontmatter and H1.', + annotations: LOCAL_CREATE_TOOL_ANNOTATIONS, + inputSchema: { + type: 'object', + properties: { + path: { type: 'string', description: 'Relative path inside the vault, or an absolute path inside an active vault. Must end in .md.' }, + content: { type: 'string', description: 'Full markdown note content, including YAML frontmatter when needed.' }, + title: { type: 'string', description: 'Optional title used only when content is omitted.' }, + type: { type: 'string', description: 'Optional note type used only when content is omitted.' }, + is_a: { type: 'string', description: 'Legacy alias for type, used only when content is omitted.' }, + vaultPath: { type: 'string', description: 'Optional target vault root when multiple vaults are active.' }, + }, + required: ['path'], + }, + }, + { + name: 'open_note', + description: 'Open a note in the Tolaria UI as a new tab. Use after creating or editing a note so the user can see it.', + annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS, + inputSchema: { + type: 'object', + properties: { + path: { type: 'string', description: 'Relative path to the note' }, + vaultPath: { type: 'string', description: 'Optional target vault root when opening a note outside the default vault.' }, + }, + required: ['path'], + }, + }, + { + name: 'highlight_editor', + description: 'Visually highlight a UI element in Tolaria (editor, tab, properties panel, or note list). The highlight auto-clears after a short delay.', + annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS, + inputSchema: { + type: 'object', + properties: { + element: { type: 'string', enum: ['editor', 'tab', 'properties', 'notelist'], description: 'Which UI element to highlight' }, + path: { type: 'string', description: 'Optional note path to associate with the highlight' }, + }, + required: ['element'], + }, + }, + { + name: 'refresh_vault', + description: 'Trigger a vault rescan so new or modified files appear immediately in the Tolaria note list.', + annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS, + inputSchema: { + type: 'object', + properties: { + path: { type: 'string', description: 'Optional specific note path that changed' }, + vaultPath: { type: 'string', description: 'Optional target vault root when refreshing a note outside the default vault.' }, + }, + }, + }, +] + +async function handleSearchNotes(args) { + const results = await toolService.searchNotes(args) + const text = results.length === 0 + ? 'No matching notes found.' + : results.map(r => `**${r.title}** (${r.vaultLabel} / ${r.path})\n${r.snippet}`).join('\n\n') + return { content: [{ type: 'text', text }] } +} + +async function handleVaultContext(args = {}) { + const ctx = await toolService.vaultContext(args) + return { content: [{ type: 'text', text: JSON.stringify(ctx, null, 2) }] } +} + +async function handleListVaults() { + return { content: [{ type: 'text', text: JSON.stringify(await toolService.listVaults(), null, 2) }] } +} + +async function handleGetNote(args) { + const note = await toolService.readNote(args) + return { content: [{ type: 'text', text: JSON.stringify(note, null, 2) }] } +} + +async function handleCreateNote(args = {}) { + const note = await toolService.createNote(args) + return { + content: [{ + type: 'text', + text: JSON.stringify(note, null, 2), + }], + } +} + +function handleOpenNote(args) { + // Refresh vault first so the new/modified note appears in the note list, + // then signal the UI to open it in a tab. + const { targetPath } = toolService.openNoteAsTab(args) + return { content: [{ type: 'text', text: `Opening ${targetPath} in Tolaria` }] } +} + +function handleHighlightEditor(args) { + toolService.highlightEditor(args) + return { content: [{ type: 'text', text: `Highlighting ${args.element}` }] } +} + +function handleRefreshVault(args) { + toolService.refreshVault(args) + return { content: [{ type: 'text', text: 'Vault refresh triggered' }] } +} + +const TOOL_HANDLERS = new Map([ + ['search_notes', handleSearchNotes], + ['get_vault_context', handleVaultContext], + ['list_vaults', handleListVaults], + ['get_note', handleGetNote], + ['create_note', handleCreateNote], + ['open_note', handleOpenNote], + ['highlight_editor', handleHighlightEditor], + ['refresh_vault', handleRefreshVault], +]) + +function callToolHandler(name, args) { + const handler = TOOL_HANDLERS.get(name) + if (!handler) throw new Error(`Unknown tool: ${name}`) + return handler(args) +} + +// --- Server setup --- + +const server = new Server( + { name: 'tolaria-mcp-server', version: '0.3.0' }, + { capabilities: { tools: {} } }, +) + +server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: TOOLS, +})) + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params + try { + return await callToolHandler(name, args) + } catch (error) { + return { + content: [{ type: 'text', text: `Error: ${error.message}` }], + isError: true, + } + } +}) + +async function shutdown(exitCode = 0) { + if (shutdownStarted) return + + shutdownStarted = true + clearUiReconnectTimer() + closeUiSocket() + + try { + await server.close() + } catch (error) { + console.error(`[mcp] Error while closing server: ${error.message}`) + } + + process.exitCode = exitCode + setImmediate(() => process.exit(exitCode)) +} + +async function main() { + const transport = new StdioServerTransport() + server.onclose = () => { + void shutdown(0) + } + process.stdin.once('end', () => { + void shutdown(0) + }) + process.stdin.once('close', () => { + void shutdown(0) + }) + process.once('SIGINT', () => { + void shutdown(0) + }) + process.once('SIGTERM', () => { + void shutdown(0) + }) + + connectUiBridge() + await server.connect(transport) + console.error('Tolaria MCP server running (vaults resolved per call)') +} + +main().catch((error) => { + console.error(error) + void shutdown(1) +}) diff --git a/mcp-server/package-lock.json b/mcp-server/package-lock.json new file mode 100644 index 0000000..d209f21 --- /dev/null +++ b/mcp-server/package-lock.json @@ -0,0 +1,1269 @@ +{ + "name": "tolaria-mcp-server", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tolaria-mcp-server", + "version": "0.1.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "gray-matter": "^4.0.3", + "ws": "^8.20.1" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.13", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz", + "integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "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/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.2.tgz", + "integrity": "sha512-Ybv7bqtOgA914MLwaHWVFXMpMYeR1MQu/D+z2MaLYteqBsTIp9sY3AU7mGNLMJv8eLg8uQMpE20I+L2Lv49nSg==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.25", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", + "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.1.tgz", + "integrity": "sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", + "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/mcp-server/package.json b/mcp-server/package.json new file mode 100644 index 0000000..06b1513 --- /dev/null +++ b/mcp-server/package.json @@ -0,0 +1,25 @@ +{ + "name": "tolaria-mcp-server", + "version": "0.1.0", + "description": "MCP server for Tolaria vault operations", + "type": "module", + "main": "index.js", + "scripts": { + "start": "node index.js", + "test": "node --test test.js tool-service.test.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "gray-matter": "^4.0.3", + "ws": "^8.20.1" + }, + "overrides": { + "@hono/node-server": "1.19.13", + "express-rate-limit": "8.2.2", + "fast-uri": "3.1.2", + "hono": "4.12.25", + "qs": "6.15.2", + "ip-address": "10.1.1", + "path-to-regexp": "8.4.0" + } +} diff --git a/mcp-server/test.js b/mcp-server/test.js new file mode 100644 index 0000000..65e633c --- /dev/null +++ b/mcp-server/test.js @@ -0,0 +1,608 @@ +import { describe, it, before, after } from 'node:test' +import assert from 'node:assert/strict' +import { spawn } from 'node:child_process' +import { + access, mkdtemp, mkdir, open, readFile, rm, writeFile, +} from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import process from 'node:process' +import { clearTimeout, setTimeout } from 'node:timers' +import { fileURLToPath } from 'node:url' +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' +import { + createNote, findMarkdownFiles, getNote, searchNotes, vaultContext, +} from './vault.js' +import { + appConfigBaseDirs, + appConfigFilePath, + requireVaultPath, + requireVaultPaths, +} from './vault-path.js' +import { vaultContextWithInstructions } from './agent-instructions.js' +import { evaluateBridgeRequest } from './ws-bridge.js' + +let tmpDir +const ACTIVE_VAULT_ERROR = 'Note path must stay inside the active vault' +const MCP_SERVER_DIR = path.dirname(fileURLToPath(import.meta.url)) + +before(async () => { + tmpDir = await mkdtemp(path.join(os.tmpdir(), 'laputa-mcp-test-')) + + await mkdir(path.join(tmpDir, 'project'), { recursive: true }) + await mkdir(path.join(tmpDir, 'note'), { recursive: true }) + + await writeTextFile(path.join(tmpDir, 'project', 'test-project.md'), `--- +title: Test Project +is_a: Project +status: Active +--- + +# Test Project + +This is a test project for the MCP server. +`) + + await writeTextFile(path.join(tmpDir, 'note', 'daily-log.md'), `--- +title: Daily Log +is_a: Note +--- + +# Daily Log + +Today I worked on the MCP server implementation. +`) + + await writeTextFile(path.join(tmpDir, 'note', 'hashtag-tags.md'), `--- +title: Hashtag Tags +type: Note +tags: [#abc, def, ghi] +--- + +# Hashtag Tags + +This note has AI-generated hashtag-style YAML tags. +`) + + await writeTextFile(path.join(tmpDir, 'project', 'second-project.md'), `--- +title: Second Project +type: Project +status: Draft +belongs_to: + - "[[project/test-project]]" +--- + +# Second Project + +Another project for testing list and context. +`) +}) + +after(async () => { + await rm(tmpDir, { recursive: true, force: true }) +}) + +describe('findMarkdownFiles', () => { + it('should find all .md files recursively', async () => { + const files = await findMarkdownFiles(tmpDir) + assert.equal(files.length, 4) + assert.ok(files.some(f => f.endsWith('test-project.md'))) + assert.ok(files.some(f => f.endsWith('daily-log.md'))) + assert.ok(files.some(f => f.endsWith('second-project.md'))) + assert.ok(files.some(f => f.endsWith('hashtag-tags.md'))) + }) +}) + +describe('getNote', () => { + it('should read a note with parsed frontmatter', async () => { + const note = await getNote(tmpDir, 'project/test-project.md') + assert.equal(note.path, 'project/test-project.md') + assert.equal(note.frontmatter.title, 'Test Project') + assert.equal(note.frontmatter.is_a, 'Project') + assert.ok(note.content.includes('test project for the MCP server')) + }) + + it('should tolerate hashtag-style tags in malformed YAML frontmatter', async () => { + const note = await getNote(tmpDir, 'note/hashtag-tags.md') + assert.equal(note.path, 'note/hashtag-tags.md') + assert.equal(note.frontmatter.title, 'Hashtag Tags') + assert.equal(note.frontmatter.type, 'Note') + assert.deepEqual(note.frontmatter.tags, ['#abc', 'def', 'ghi']) + assert.ok(note.content.includes('has AI-generated hashtag-style YAML tags')) + }) + + it('should throw for missing notes', async () => { + await assert.rejects( + () => getNote(tmpDir, 'nonexistent.md'), + { code: 'ENOENT' } + ) + }) + + it('should reject absolute paths outside the vault', async () => { + await assertRejectsOutsideVault('laputa-mcp-outside-', outsideNote => outsideNote) + }) + + it('should reject traversal paths outside the vault', async () => { + await assertRejectsOutsideVault( + 'laputa-mcp-traversal-', + outsideNote => path.relative(tmpDir, outsideNote), + ) + }) +}) + +describe('createNote', () => { + it('creates a new markdown note inside the vault', async () => { + const vaultDir = await mkdtemp(path.join(os.tmpdir(), 'laputa-mcp-create-')) + const content = `--- +type: Note +--- + +# MCP Created +` + + try { + const note = await createNote(vaultDir, 'note/mcp-created.md', content) + assert.equal(note.path, 'note/mcp-created.md') + assert.equal(await readFile(path.join(vaultDir, note.path), 'utf-8'), content) + } finally { + await rm(vaultDir, { recursive: true, force: true }) + } + }) + + it('does not overwrite an existing note', async () => { + const vaultDir = await mkdtemp(path.join(os.tmpdir(), 'laputa-mcp-create-existing-')) + const notePath = path.join(vaultDir, 'existing.md') + await writeFile(notePath, '# Existing\n', 'utf-8') + + try { + await assert.rejects( + () => createNote(vaultDir, 'existing.md', '# Replacement\n'), + { code: 'EEXIST' }, + ) + assert.equal(await readFile(notePath, 'utf-8'), '# Existing\n') + } finally { + await rm(vaultDir, { recursive: true, force: true }) + } + }) + + it('rejects absolute paths outside the vault', async () => { + const vaultDir = await mkdtemp(path.join(os.tmpdir(), 'laputa-mcp-create-vault-')) + const outsideDir = await mkdtemp(path.join(os.tmpdir(), 'laputa-mcp-create-outside-')) + + try { + await assert.rejects( + () => createNote(vaultDir, path.join(outsideDir, 'outside.md'), '# Outside\n'), + { message: ACTIVE_VAULT_ERROR }, + ) + } finally { + await rm(vaultDir, { recursive: true, force: true }) + await rm(outsideDir, { recursive: true, force: true }) + } + }) + + it('rejects outside paths before creating missing parent folders', async () => { + const vaultDir = await mkdtemp(path.join(os.tmpdir(), 'laputa-mcp-create-vault-')) + const outsideDir = await mkdtemp(path.join(os.tmpdir(), 'laputa-mcp-create-outside-')) + const outsideParent = path.join(outsideDir, 'missing-parent') + + try { + await assert.rejects( + () => createNote(vaultDir, path.join(outsideParent, 'outside.md'), '# Outside\n'), + { message: ACTIVE_VAULT_ERROR }, + ) + await assert.rejects(() => access(outsideParent), { code: 'ENOENT' }) + } finally { + await rm(vaultDir, { recursive: true, force: true }) + await rm(outsideDir, { recursive: true, force: true }) + } + }) +}) + +describe('searchNotes', () => { + it('should find notes matching title', async () => { + const results = await searchNotes(tmpDir, 'Test Project') + assert.ok(results.length >= 1) + assert.equal(results[0].title, 'Test Project') + }) + + it('should find notes matching content', async () => { + const results = await searchNotes(tmpDir, 'MCP server') + assert.ok(results.length >= 1) + }) + + it('should return empty for no matches', async () => { + const results = await searchNotes(tmpDir, 'xyzzy-nonexistent-12345') + assert.equal(results.length, 0) + }) + + it('should respect limit', async () => { + const results = await searchNotes(tmpDir, 'project', 1) + assert.ok(results.length <= 1) + }) +}) + +describe('vaultContext', () => { + it('should return types, recent notes, and vault path', async () => { + const ctx = await vaultContext(tmpDir) + assert.ok(Array.isArray(ctx.types)) + assert.ok(Array.isArray(ctx.recentNotes)) + assert.equal(ctx.vaultPath, tmpDir) + }) + + it('should include known entity types', async () => { + const ctx = await vaultContext(tmpDir) + assert.ok(ctx.types.includes('Project')) + assert.ok(ctx.types.includes('Note')) + }) + + it('should include notes with hashtag-style tags in malformed YAML frontmatter', async () => { + const ctx = await vaultContext(tmpDir) + const note = ctx.recentNotes.find(entry => entry.path === 'note/hashtag-tags.md') + assert.ok(note) + assert.equal(note.title, 'Hashtag Tags') + assert.equal(note.type, 'Note') + }) + + it('should cap recent notes at 20', async () => { + const ctx = await vaultContext(tmpDir) + assert.ok(ctx.recentNotes.length <= 20) + }) + + it('should include path and title in recent notes', async () => { + const ctx = await vaultContext(tmpDir) + for (const note of ctx.recentNotes) { + assert.ok(note.path) + assert.ok(note.title) + } + }) + + it('should include folders', async () => { + const ctx = await vaultContext(tmpDir) + assert.ok(ctx.folders.includes('project/')) + assert.ok(ctx.folders.includes('note/')) + }) + + it('should report correct note count', async () => { + const ctx = await vaultContext(tmpDir) + assert.equal(ctx.noteCount, 4) + }) + + it('includes root AGENTS.md instructions when present', async () => { + const agentsPath = path.join(tmpDir, 'AGENTS.md') + await writeFile(agentsPath, '# Vault Rules\n\nUse this vault carefully.\n', 'utf-8') + + try { + const ctx = await vaultContextWithInstructions(tmpDir) + assert.deepEqual(ctx.agentInstructions, { + path: agentsPath, + content: '# Vault Rules\n\nUse this vault carefully.\n', + }) + } finally { + await rm(agentsPath, { force: true }) + } + }) + + it('reports null agent instructions when AGENTS.md is absent', async () => { + const ctx = await vaultContextWithInstructions(tmpDir) + assert.equal(ctx.agentInstructions, null) + }) +}) + +describe('evaluateBridgeRequest', () => { + it('accepts loopback UI requests from trusted origins', () => { + assert.deepEqual( + evaluateBridgeRequest({ + bridgeType: 'ui', + origin: 'http://localhost:5202', + remoteAddress: '127.0.0.1', + }), + { ok: true, reason: null }, + ) + }) + + it('rejects browser origins on the tool bridge', () => { + assert.deepEqual( + evaluateBridgeRequest({ + bridgeType: 'tool', + origin: 'https://evil.example', + remoteAddress: '127.0.0.1', + }), + { ok: false, reason: 'browser origins are not allowed on the tool bridge' }, + ) + }) + + it('rejects non-loopback clients even without an origin', () => { + assert.deepEqual( + evaluateBridgeRequest({ + bridgeType: 'ui', + origin: undefined, + remoteAddress: '192.168.1.10', + }), + { ok: false, reason: 'non-local client' }, + ) + }) +}) + +describe('requireVaultPath', () => { + it('returns the explicit configured vault path', () => { + assert.equal( + requireVaultPath({ VAULT_PATH: '/tmp/Selected Vault' }), + '/tmp/Selected Vault', + ) + }) + + it('rejects missing vault paths instead of falling back to ~/Laputa', async () => { + const configDir = await mkdtemp(path.join(os.tmpdir(), 'tolaria-mcp-empty-config-')) + assert.throws( + () => requireVaultPaths({}, { configDir }), + /VAULT_PATH is required/, + ) + await rm(configDir, { recursive: true, force: true }) + }) + + it('returns all configured active vault paths with the primary vault first', () => { + assert.deepEqual( + requireVaultPaths({ + VAULT_PATH: '/tmp/Default Vault', + VAULT_PATHS: JSON.stringify(['/tmp/Default Vault', '/tmp/Second Vault']), + }), + ['/tmp/Default Vault', '/tmp/Second Vault'], + ) + }) + + it('loads active mounted vault paths from Tolaria config when env is vault-neutral', async () => { + const configDir = await mkdtemp(path.join(os.tmpdir(), 'tolaria-mcp-config-')) + const primaryVault = path.join(configDir, 'Primary Vault') + const secondaryVault = path.join(configDir, 'Secondary Vault') + const hiddenVault = path.join(configDir, 'Hidden Vault') + const configPath = path.join(configDir, 'com.tolaria.app', 'vaults.json') + + await mkdir(path.dirname(configPath), { recursive: true }) + await writeFile(configPath, JSON.stringify({ + active_vault: primaryVault, + vaults: [ + { label: 'Secondary', path: secondaryVault, mounted: true }, + { label: 'Hidden', path: hiddenVault, mounted: false }, + { label: 'Primary', path: primaryVault, mounted: true }, + ], + }), 'utf-8') + + try { + assert.deepEqual( + requireVaultPaths({}, { configDir }), + [primaryVault, secondaryVault], + ) + } finally { + await rm(configDir, { recursive: true, force: true }) + } + }) + + it('uses one app config resolver for settings and vault registry files', async () => { + const configDir = await mkdtemp(path.join(os.tmpdir(), 'tolaria-mcp-shared-config-')) + const files = ['settings.json', 'vaults.json'] + + for (const fileName of files) { + const legacyPath = path.join(configDir, 'com.laputa.app', fileName) + await mkdir(path.dirname(legacyPath), { recursive: true }) + await writeFile(legacyPath, '{}', 'utf-8') + } + + try { + for (const fileName of files) { + assert.equal( + appConfigFilePath(fileName, { configDir }), + path.join(configDir, 'com.laputa.app', fileName), + ) + } + } finally { + await rm(configDir, { recursive: true, force: true }) + } + }) + + it('loads macOS vault registry from the XDG-backed Tolaria config before Application Support', async () => { + const homeDir = await mkdtemp(path.join(os.tmpdir(), 'tolaria-mcp-macos-home-')) + const primaryVault = path.join(homeDir, 'Primary Vault') + const legacyPlatformVault = path.join(homeDir, 'Legacy Platform Vault') + const xdgConfigPath = path.join(homeDir, '.config', 'com.tolaria.app', 'vaults.json') + const platformConfigPath = path.join( + homeDir, + 'Library', + 'Application Support', + 'com.tolaria.app', + 'vaults.json', + ) + + await mkdir(path.dirname(xdgConfigPath), { recursive: true }) + await mkdir(path.dirname(platformConfigPath), { recursive: true }) + await writeFile(xdgConfigPath, JSON.stringify({ active_vault: primaryVault, vaults: [] }), 'utf-8') + await writeFile( + platformConfigPath, + JSON.stringify({ active_vault: legacyPlatformVault, vaults: [] }), + 'utf-8', + ) + + try { + assert.deepEqual( + requireVaultPaths({}, { + configDirs: appConfigBaseDirs({ env: {}, homeDir, platformName: 'darwin' }), + }), + [primaryVault], + ) + } finally { + await rm(homeDir, { recursive: true, force: true }) + } + }) +}) + +describe('stdio process lifecycle', () => { + it('advertises local vault tools as approval-safe for MCP clients', async () => { + const { client, stderr } = await connectMcpClient() + + try { + const { tools } = await client.listTools() + const toolsByName = new Map(tools.map(tool => [tool.name, tool])) + const safeReadTools = [ + 'search_notes', + 'get_vault_context', + 'list_vaults', + 'get_note', + 'open_note', + 'highlight_editor', + 'refresh_vault', + ] + + for (const name of safeReadTools) { + const tool = toolsByName.get(name) + assert.ok(tool, `Missing MCP tool: ${name}`) + assert.equal(tool.annotations?.readOnlyHint, true, `${name} should not require destructive approval`) + assert.equal(tool.annotations?.destructiveHint, false, `${name} should not be treated as destructive`) + assert.equal(tool.annotations?.openWorldHint, false, `${name} should stay scoped to local active vaults`) + } + + const createTool = toolsByName.get('create_note') + assert.ok(createTool, 'Missing MCP tool: create_note') + assert.equal(createTool.annotations?.readOnlyHint, false) + assert.equal(createTool.annotations?.destructiveHint, false) + assert.equal(createTool.annotations?.openWorldHint, false) + } finally { + await closeMcpClient(client, stderr) + } + }) + + it('creates a note through the MCP create_note tool', async () => { + const { client, stderr } = await connectMcpClient() + const relativePath = 'note/mcp-tool-created.md' + const absolutePath = path.join(tmpDir, relativePath) + const content = `--- +type: Note +--- + +# MCP Tool Created +` + + try { + await rm(absolutePath, { force: true }) + const result = await client.callTool({ + name: 'create_note', + arguments: { path: relativePath, content }, + }) + + assert.equal(await readFile(absolutePath, 'utf-8'), content) + assert.match(JSON.stringify(result.content), /mcp-tool-created\.md/) + } finally { + await rm(absolutePath, { force: true }) + await closeMcpClient(client, stderr) + } + }) + + it('exits when the MCP client closes stdin', async () => { + const child = spawn(process.execPath, ['index.js'], { + cwd: MCP_SERVER_DIR, + env: { ...process.env, VAULT_PATH: tmpDir, WS_UI_PORT: '65534' }, + stdio: ['pipe', 'ignore', 'pipe'], + }) + let stderr = '' + child.stderr.setEncoding('utf8') + child.stderr.on('data', chunk => { + stderr += chunk + }) + + await sleep(200) + child.stdin.end() + + const exit = await waitForExit(child, 1_500) + if (!exit) { + child.kill() + await waitForExit(child, 1_000) + assert.fail(`MCP server stayed alive after stdin closed.\n${stderr}`) + } + + assert.equal(exit.signal, null) + assert.equal(exit.code, 0, stderr) + }) +}) + +async function connectMcpClient() { + const transport = new StdioClientTransport({ + command: process.execPath, + args: ['index.js'], + cwd: MCP_SERVER_DIR, + env: { ...process.env, VAULT_PATH: tmpDir, WS_UI_PORT: '65534' }, + stderr: 'pipe', + }) + const stderr = collectTransportStderr(transport) + const client = new Client( + { name: 'tolaria-mcp-test-client', version: '0.0.0' }, + { capabilities: {} }, + ) + + await client.connect(transport) + return { client, stderr } +} + +function collectTransportStderr(transport) { + const chunks = [] + transport.stderr?.setEncoding('utf8') + transport.stderr?.on('data', chunk => { + chunks.push(chunk) + }) + return () => chunks.join('') +} + +async function closeMcpClient(client, stderr) { + try { + await client.close() + } catch (error) { + assert.fail(`Failed to close MCP test client: ${error.message}\n${stderr()}`) + } +} + +async function assertRejectsOutsideVault(prefix, resolveNotePath) { + const outsideDir = await mkdtemp(path.join(os.tmpdir(), prefix)) + const outsideNote = path.join(outsideDir, 'outside.md') + + try { + await writeTextFile(outsideNote, '# Outside\n') + await assert.rejects( + () => getNote(tmpDir, resolveNotePath(outsideNote)), + { message: ACTIVE_VAULT_ERROR }, + ) + } finally { + await rm(outsideDir, { recursive: true, force: true }) + } +} + +async function writeTextFile(filePath, content) { + const handle = await open(filePath, 'w') + try { + await handle.writeFile(content, 'utf-8') + } finally { + await handle.close() + } +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +function waitForExit(child, timeoutMs) { + return new Promise((resolve) => { + const timer = setTimeout(() => { + cleanup() + resolve(null) + }, timeoutMs) + + child.once('exit', onExit) + + function onExit(code, signal) { + cleanup() + resolve({ code, signal }) + } + + function cleanup() { + clearTimeout(timer) + child.off('exit', onExit) + } + }) +} diff --git a/mcp-server/tool-service.js b/mcp-server/tool-service.js new file mode 100644 index 0000000..f533d8b --- /dev/null +++ b/mcp-server/tool-service.js @@ -0,0 +1,213 @@ +import path from 'node:path' +import { + createNote as createVaultNote, + getNote, + searchNotes as searchVaultNotes, +} from './vault.js' +import { requireVaultPaths } from './vault-path.js' +import { readAgentInstructions, vaultContextWithInstructions } from './agent-instructions.js' + +export function createMcpToolService({ + resolveVaultPaths = () => requireVaultPaths(), + emitUiAction = () => {}, +} = {}) { + function activeVaultPaths() { + return resolveVaultPaths() + } + + function requestedVaultPath(args = {}) { + const requested = typeof args.vaultPath === 'string' ? args.vaultPath.trim() : '' + if (!requested) return null + if (!activeVaultPaths().includes(requested)) { + throw new Error(`Vault is not active in Tolaria: ${requested}`) + } + return requested + } + + function resolveUiPath(args = {}) { + const notePath = typeof args.path === 'string' ? args.path : '' + if (path.isAbsolute(notePath)) return notePath + const roots = activeVaultPaths() + const vaultPath = requestedVaultPath(args) ?? (roots.length === 1 ? roots[0] : '') + return vaultPath ? path.join(vaultPath, notePath) : notePath + } + + async function readNote(args = {}) { + return getNoteFromActiveVaults(notePathArg(args), requestedVaultPath(args)) + } + + async function searchNotes(args = {}) { + const requestedLimit = Number.isFinite(args.limit) && args.limit > 0 ? args.limit : 10 + const results = [] + + for (const vaultPath of activeVaultPaths()) { + const vaultResults = await searchVaultNotes(vaultPath, args.query, requestedLimit) + results.push(...vaultResults.map((result) => withVaultMetadata(result, vaultPath))) + if (results.length >= requestedLimit) break + } + + return results.slice(0, requestedLimit) + } + + async function vaultContext(args = {}) { + const targetVaultPath = requestedVaultPath(args) + const roots = activeVaultPaths() + if (targetVaultPath) return vaultContextWithInstructions(targetVaultPath) + if (roots.length === 1) return vaultContextWithInstructions(roots[0]) + + return { + vaults: await Promise.all(roots.map(vaultContextWithInstructions)), + } + } + + async function listVaults() { + const vaults = await Promise.all(activeVaultPaths().map(async (vaultPath) => { + const agentInstructions = await readAgentInstructions(vaultPath) + return { + path: vaultPath, + label: vaultLabel(vaultPath), + agentInstructionsPath: agentInstructions?.path ?? null, + hasAgentInstructions: agentInstructions !== null, + } + })) + + return { vaults } + } + + async function createNote(args = {}) { + const vaultPath = writableVaultPath(args) + const notePath = writableNotePath(args, vaultPath) + const note = await createVaultNote(vaultPath, notePath, createNoteContent(args)) + const targetPath = resolveUiPath({ ...args, path: note.path, vaultPath }) + emitUiAction('vault_changed', { path: targetPath }) + emitUiAction('open_tab', { path: targetPath }) + return { path: note.path, absolutePath: note.absolutePath, vaultPath } + } + + function openNoteAsTab(args = {}) { + const targetPath = resolveUiPath(args) + emitUiAction('vault_changed', { path: targetPath }) + emitUiAction('open_tab', { path: targetPath }) + return { targetPath } + } + + function openNoteInEditor(args = {}) { + const targetPath = resolveUiPath(args) + emitUiAction('vault_changed', { path: targetPath }) + emitUiAction('open_note', { path: targetPath }) + return { targetPath } + } + + function highlightEditor(args = {}) { + emitUiAction('highlight', { element: args.element, path: args.path }) + } + + function setFilter(args = {}) { + emitUiAction('set_filter', { filterType: args.type }) + } + + function refreshVault(args = {}) { + emitUiAction('vault_changed', { path: resolveUiPath(args) }) + } + + async function getNoteFromActiveVaults(notePath, vaultPath = null) { + const candidates = vaultPath ? [vaultPath] : activeVaultPaths() + const matches = [] + const errors = [] + + for (const candidate of candidates) { + try { + matches.push(withVaultMetadata(await getNote(candidate, notePath), candidate)) + } catch (error) { + errors.push(error) + } + } + + if (matches.length === 1) return matches[0] + if (matches.length > 1) { + throw new Error(`Note path is ambiguous across active vaults. Pass vaultPath for ${notePath}.`) + } + throw errors[0] ?? new Error(`Note not found: ${notePath}`) + } + + function writableVaultPath(args = {}) { + const requested = requestedVaultPath(args) + if (requested) return requested + + const roots = activeVaultPaths() + const notePath = notePathArg(args) + if (path.isAbsolute(notePath)) { + const root = roots.find(vaultPath => isInsideVaultRoot(vaultPath, notePath)) + if (root) return root + } + if (roots.length === 1) return roots[0] + throw new Error(`Note path is ambiguous across active vaults. Pass vaultPath for ${notePath}.`) + } + + return { + activeVaultPaths, + createNote, + highlightEditor, + listVaults, + openNoteAsTab, + openNoteInEditor, + readNote, + refreshVault, + requestedVaultPath, + resolveUiPath, + searchNotes, + setFilter, + vaultContext, + } +} + +function writableNotePath(args, vaultPath) { + const notePath = notePathArg(args) + if (!path.isAbsolute(notePath) || !isInsideVaultRoot(vaultPath, notePath)) return notePath + return path.relative(vaultPath, notePath) +} + +function withVaultMetadata(note, vaultPath) { + return { + ...note, + vaultPath, + vaultLabel: vaultLabel(vaultPath), + } +} + +function vaultLabel(vaultPath) { + return path.basename(vaultPath) || vaultPath +} + +function isInsideVaultRoot(vaultPath, notePath) { + const relative = path.relative(vaultPath, notePath) + return Boolean(relative) && !relative.startsWith('..') && !path.isAbsolute(relative) +} + +function notePathArg(args = {}) { + const notePath = typeof args.path === 'string' ? args.path.trim() : '' + if (!notePath) throw new Error('Note path is required') + return notePath +} + +function yamlScalar(value) { + return JSON.stringify(value) +} + +function fallbackCreateNoteContent(args = {}) { + const title = typeof args.title === 'string' && args.title.trim() + ? args.title.trim() + : path.basename(notePathArg(args), '.md') + const type = typeof args.type === 'string' && args.type.trim() + ? args.type.trim() + : typeof args.is_a === 'string' && args.is_a.trim() + ? args.is_a.trim() + : 'Note' + return `---\ntype: ${yamlScalar(type)}\n---\n\n# ${title}\n` +} + +function createNoteContent(args = {}) { + return typeof args.content === 'string' && args.content.trim() + ? args.content + : fallbackCreateNoteContent(args) +} diff --git a/mcp-server/tool-service.test.js b/mcp-server/tool-service.test.js new file mode 100644 index 0000000..ff1c709 --- /dev/null +++ b/mcp-server/tool-service.test.js @@ -0,0 +1,156 @@ +import { describe, it, beforeEach, afterEach } from 'node:test' +import assert from 'node:assert/strict' +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import { createMcpToolService } from './tool-service.js' + +let tmpDir +let firstVault +let secondVault + +beforeEach(async () => { + tmpDir = await mkdtemp(path.join(os.tmpdir(), 'tolaria-mcp-service-')) + firstVault = path.join(tmpDir, 'First Vault') + secondVault = path.join(tmpDir, 'Second Vault') + + await seedVault(firstVault, { + 'note/shared.md': noteFixture('Shared Note', 'Shared content from the first vault.'), + 'note/alpha.md': noteFixture('Alpha Project', 'Project planning in the first vault.'), + }) + await seedVault(secondVault, { + 'AGENTS.md': '# Second Vault Rules\n', + 'note/shared.md': noteFixture('Shared Note', 'Shared content from the second vault.'), + 'note/beta.md': noteFixture('Beta Project', 'Project planning in the second vault.'), + }) +}) + +afterEach(async () => { + await rm(tmpDir, { recursive: true, force: true }) +}) + +describe('createMcpToolService', () => { + it('requires vaultPath when reading an ambiguous note path', async () => { + const service = makeService() + + await assert.rejects( + () => service.readNote({ path: 'note/shared.md' }), + /Note path is ambiguous across active vaults/, + ) + + const note = await service.readNote({ + path: 'note/shared.md', + vaultPath: secondVault, + }) + + assert.equal(note.vaultPath, secondVault) + assert.equal(note.vaultLabel, 'Second Vault') + assert.match(note.content, /second vault/) + }) + + it('creates notes with fallback markdown and emits refresh and tab actions', async () => { + const emittedActions = [] + const service = makeService({ emittedActions }) + const absolutePath = path.join(secondVault, 'note/created.md') + + const note = await service.createNote({ + path: absolutePath, + title: 'Created From MCP', + type: 'Project', + }) + + assert.equal(note.path, 'note/created.md') + assert.equal(note.vaultPath, secondVault) + assert.equal(path.basename(note.absolutePath), 'created.md') + assert.equal( + await readFile(note.absolutePath, 'utf-8'), + '---\ntype: "Project"\n---\n\n# Created From MCP\n', + ) + assert.deepEqual(emittedActions, [ + { action: 'vault_changed', payload: { path: absolutePath } }, + { action: 'open_tab', payload: { path: absolutePath } }, + ]) + }) + + it('searches active vaults with consistent vault metadata', async () => { + const service = makeService() + + const results = await service.searchNotes({ query: 'Project', limit: 2 }) + + assert.equal(results.length, 2) + assert.deepEqual( + results.map(({ path: notePath, vaultPath, vaultLabel }) => ({ + notePath, + vaultPath, + vaultLabel, + })), + [ + { notePath: 'note/alpha.md', vaultPath: firstVault, vaultLabel: 'First Vault' }, + { notePath: 'note/beta.md', vaultPath: secondVault, vaultLabel: 'Second Vault' }, + ], + ) + }) + + it('lists active vaults with agent-instruction metadata', async () => { + const service = makeService() + + assert.deepEqual(await service.listVaults(), { + vaults: [ + { + path: firstVault, + label: 'First Vault', + agentInstructionsPath: null, + hasAgentInstructions: false, + }, + { + path: secondVault, + label: 'Second Vault', + agentInstructionsPath: path.join(secondVault, 'AGENTS.md'), + hasAgentInstructions: true, + }, + ], + }) + }) + + it('emits transport-neutral UI intents for note opening and filters', () => { + const emittedActions = [] + const service = makeService({ emittedActions }) + + service.openNoteAsTab({ path: 'note/beta.md', vaultPath: secondVault }) + service.openNoteInEditor({ path: 'note/beta.md', vaultPath: secondVault }) + service.highlightEditor({ element: 'editor', path: 'note/beta.md' }) + service.setFilter({ type: 'Project' }) + service.refreshVault({ path: 'note/beta.md', vaultPath: secondVault }) + + assert.deepEqual(emittedActions, [ + { action: 'vault_changed', payload: { path: path.join(secondVault, 'note/beta.md') } }, + { action: 'open_tab', payload: { path: path.join(secondVault, 'note/beta.md') } }, + { action: 'vault_changed', payload: { path: path.join(secondVault, 'note/beta.md') } }, + { action: 'open_note', payload: { path: path.join(secondVault, 'note/beta.md') } }, + { action: 'highlight', payload: { element: 'editor', path: 'note/beta.md' } }, + { action: 'set_filter', payload: { filterType: 'Project' } }, + { action: 'vault_changed', payload: { path: path.join(secondVault, 'note/beta.md') } }, + ]) + }) +}) + +function makeService({ emittedActions = [] } = {}) { + return createMcpToolService({ + resolveVaultPaths: () => [firstVault, secondVault], + emitUiAction: (action, payload) => { + emittedActions.push({ action, payload }) + }, + }) +} + +async function seedVault(vaultPath, files) { + for (const [relativePath, content] of Object.entries(files)) { + const filePath = path.join(vaultPath, relativePath) + await mkdir(path.dirname(filePath), { recursive: true }) + await writeFile(filePath, content, 'utf-8') + } +} + +function noteFixture(title, body) { + return `---\ntitle: ${JSON.stringify(title)}\ntype: Note\n---\n\n# ${title}\n\n${body}\n` +} diff --git a/mcp-server/vault-path.js b/mcp-server/vault-path.js new file mode 100644 index 0000000..d3f2279 --- /dev/null +++ b/mcp-server/vault-path.js @@ -0,0 +1,139 @@ +import { existsSync, readFileSync } from 'node:fs' +import { homedir, platform } from 'node:os' +import { isAbsolute, join } from 'node:path' +import appConfigPolicy from './app-config-policy.json' with { type: 'json' } + +const APP_CONFIG_DIR = appConfigPolicy.current_namespace +const APP_CONFIG_FILES = Object.freeze(appConfigPolicy.files) + +function parseVaultPathList(rawValue) { + if (!rawValue?.trim()) return [] + + try { + const parsed = JSON.parse(rawValue) + if (Array.isArray(parsed)) return parsed.filter(value => typeof value === 'string') + } catch { + // Older clients only set VAULT_PATH; keep VAULT_PATHS strict JSON so paths + // with platform separators are never split incorrectly. + } + + return [] +} + +function uniqueVaultPaths(paths) { + const seen = new Set() + const unique = [] + for (const path of paths) { + const trimmed = path.trim() + if (!trimmed || seen.has(trimmed)) continue + seen.add(trimmed) + unique.push(trimmed) + } + return unique +} + +function absolutePath(path) { + return typeof path === 'string' && isAbsolute(path) ? path : null +} + +function defaultXdgConfigHome(platformName, homeDir) { + if (platformName === 'win32') return null + return absolutePath(homeDir) ? join(homeDir, '.config') : null +} + +function platformConfigDir(env, platformName, homeDir) { + if (platformName === 'darwin') return join(homeDir, 'Library', 'Application Support') + if (platformName === 'win32') return absolutePath(env.APPDATA) || join(homeDir, 'AppData', 'Roaming') + return absolutePath(env.XDG_CONFIG_HOME) || defaultXdgConfigHome(platformName, homeDir) +} + +export function appConfigBaseDirs({ + env = process.env, + homeDir = homedir(), + platformName = platform(), + platformDir = platformConfigDir(env, platformName, homeDir), +} = {}) { + const primary = absolutePath(env.XDG_CONFIG_HOME) + || defaultXdgConfigHome(platformName, homeDir) + || platformDir + const dirs = primary ? [primary] : [] + if (platformDir && platformDir !== primary) dirs.push(platformDir) + return dirs +} + +function namespaceDir(namespace) { + if (namespace === 'current') return APP_CONFIG_DIR + if (namespace === 'legacy') return appConfigPolicy.legacy_namespace + throw new Error(`Unknown app config namespace: ${namespace}`) +} + +function preferredAppConfigPath(configDir, fileName) { + return join(configDir, APP_CONFIG_DIR, fileName) +} + +function existingOrPreferredAppConfigPath(configDirs, fileName) { + for (const configDir of configDirs) { + for (const namespace of appConfigPolicy.namespace_read_order) { + const candidate = join(configDir, namespaceDir(namespace), fileName) + if (existsSync(candidate)) return candidate + } + } + + return preferredAppConfigPath(configDirs[0], fileName) +} + +export function appConfigFilePath( + fileName, + { configDir, configDirs = configDir ? [configDir] : appConfigBaseDirs() } = {}, +) { + return existingOrPreferredAppConfigPath(configDirs, fileName) +} + +export function vaultsJsonPath({ + configDir, + configDirs = configDir ? [configDir] : appConfigBaseDirs(), +} = {}) { + return existingOrPreferredAppConfigPath(configDirs, APP_CONFIG_FILES.vaults) +} + +function pushUniquePath(paths, value) { + const path = typeof value === 'string' ? value.trim() : '' + if (!path || paths.includes(path)) return + paths.push(path) +} + +function activeVaultPathsFromList(list) { + const paths = [] + pushUniquePath(paths, list?.active_vault) + + for (const vault of list?.vaults ?? []) { + if (vault?.mounted === false) continue + pushUniquePath(paths, vault?.path) + } + + return paths +} + +export function configuredVaultPaths(options = {}) { + const filePath = vaultsJsonPath(options) + if (!existsSync(filePath)) return [] + + return activeVaultPathsFromList(JSON.parse(readFileSync(filePath, 'utf-8'))) +} + +export function requireVaultPaths(env = process.env, options = {}) { + const vaultPaths = uniqueVaultPaths([ + env.VAULT_PATH?.trim() ?? '', + ...parseVaultPathList(env.VAULT_PATHS), + ]) + if (vaultPaths.length === 0) { + const configuredPaths = configuredVaultPaths(options) + if (configuredPaths.length > 0) return configuredPaths + throw new Error('VAULT_PATH is required. Open a vault in Tolaria before starting MCP tools.') + } + return vaultPaths +} + +export function requireVaultPath(env = process.env, options = {}) { + return requireVaultPaths(env, options)[0] +} diff --git a/mcp-server/vault.js b/mcp-server/vault.js new file mode 100644 index 0000000..0c9ac3f --- /dev/null +++ b/mcp-server/vault.js @@ -0,0 +1,460 @@ +/** + * Vault operations — read-only helpers for Tolaria markdown vault. + * Most write operations are handled by the app-managed agent's active + * permission profile and native file-edit tools; createNote is intentionally + * narrow so read-only agents can create a new Markdown file without overwrite. + */ +import { mkdir, open, opendir, realpath } from 'node:fs/promises' +import path from 'node:path' +import matter from 'gray-matter' + +const ACTIVE_VAULT_ERROR = 'Note path must stay inside the active vault' + +/** + * Recursively find all .md files under a directory. + * @param {string} dir + * @returns {Promise} + */ +export async function findMarkdownFiles(dir) { + const results = [] + const items = await opendir(dir) + for await (const item of items) { + await collectMarkdownFile(results, dir, item) + } + return results +} + +async function resolveVaultNotePath(vaultPath, notePath) { + const vaultRoot = await realpath(vaultPath) + const requestedPath = resolveRequestedNotePath(vaultRoot, notePath) + const noteRealPath = await realpath(requestedPath) + const relativePath = path.relative(vaultRoot, noteRealPath) + + if (!isVaultRelativePath(relativePath)) { + throw new Error(ACTIVE_VAULT_ERROR) + } + + return { + vaultRoot, + noteRealPath, + relativePath, + } +} + +/** + * Read a note with parsed frontmatter and content. + * @param {string} vaultPath + * @param {string} notePath + * @returns {Promise<{path: string, frontmatter: Record, content: string}>} + */ +export async function getNote(vaultPath, notePath) { + const { + noteRealPath, + relativePath, + } = await resolveVaultNotePath(vaultPath, notePath) + const raw = await readUtf8File(noteRealPath) + const parsed = parseMarkdownNote(raw) + return { + path: relativePath, + frontmatter: parsed.data, + content: parsed.content.trim(), + } +} + +/** + * Create a new markdown note inside the vault without overwriting an existing file. + * @param {string} vaultPath + * @param {string} notePath + * @param {string} content + * @returns {Promise<{path: string, absolutePath: string}>} + */ +export async function createNote(vaultPath, notePath, content) { + const { requestedPath, relativePath } = await resolveNewVaultNotePath(vaultPath, notePath) + await writeNewUtf8File(requestedPath, content) + return { + path: relativePath, + absolutePath: requestedPath, + } +} + +/** + * Search notes by title or content substring. + * @param {string} vaultPath + * @param {string} query + * @param {number} [limit=10] + * @returns {Promise>} + */ +export async function searchNotes(vaultPath, query, limit = 10) { + const files = await findMarkdownFiles(vaultPath) + const q = query.toLowerCase() + const results = [] + + for (const filePath of files) { + if (results.length >= limit) break + const content = await readUtf8File(filePath) + const filename = path.basename(filePath, '.md') + const titleMatch = extractTitle(content, filename) + if (!matchesSearchQuery(titleMatch, content, q)) continue + + const snippet = extractSnippet(content, q) + results.push({ + path: path.relative(vaultPath, filePath), + title: titleMatch, + snippet, + }) + } + + return results +} + +/** + * Get vault context: unique types, note count, top-level folders, and 20 most recent notes. + * @param {string} vaultPath + * @returns {Promise<{types: string[], noteCount: number, folders: string[], recentNotes: Array<{path: string, title: string, type: string|null}>, vaultPath: string}>} + */ +export async function vaultContext(vaultPath) { + const files = await findMarkdownFiles(vaultPath) + const typesSet = new Set() + const foldersSet = new Set() + const notesWithMtime = [] + + for (const filePath of files) { + const { topFolder, note, type } = await readVaultContextNote(vaultPath, filePath) + if (type) typesSet.add(type) + if (topFolder) foldersSet.add(topFolder) + notesWithMtime.push(note) + } + + notesWithMtime.sort((a, b) => b.mtime - a.mtime) + const recentNotes = notesWithMtime.slice(0, 20).map(contextNoteWithoutMtime) + + return { + types: [...typesSet].sort(), + noteCount: files.length, + folders: [...foldersSet].sort(), + recentNotes, + configFiles: await readConfigFiles(vaultPath), + vaultPath, + } +} + +// --- Helpers --- + +async function collectMarkdownFile(results, dir, item) { + if (item.name.startsWith('.')) return + + const full = resolveInside(dir, item.name) + if (!full) return + if (item.isDirectory()) { + results.push(...await findMarkdownFiles(full)) + return + } + + if (item.name.endsWith('.md')) { + results.push(full) + } +} + +function resolveRequestedNotePath(vaultRoot, notePath) { + if (path.isAbsolute(notePath)) return notePath + const resolved = resolveInside(vaultRoot, notePath) + if (!resolved) throw new Error(ACTIVE_VAULT_ERROR) + return resolved +} + +async function resolveNewVaultNotePath(vaultPath, notePath) { + const requestedNotePath = validateNewNotePath(notePath) + const vaultRoot = await realpath(vaultPath) + const requestedPath = resolveRequestedNotePath(vaultRoot, requestedNotePath) + const relativePath = relativeNotePathInsideVault(vaultRoot, requestedPath) + await ensureWritableParentInsideVault(vaultRoot, requestedPath) + return { requestedPath, relativePath } +} + +function validateNewNotePath(notePath) { + const trimmedPath = typeof notePath === 'string' ? notePath.trim() : '' + if (!trimmedPath) { + throw new Error('Note path is required') + } + if (!trimmedPath.endsWith('.md')) { + throw new Error('New notes must be markdown files ending in .md') + } + return trimmedPath +} + +async function ensureWritableParentInsideVault(vaultRoot, requestedPath) { + const parentPath = path.dirname(requestedPath) + const existingAncestor = await nearestExistingAncestor(parentPath) + assertInsideVault(vaultRoot, existingAncestor) + await mkdir(parentPath, { recursive: true }) + assertInsideVault(vaultRoot, await realpath(parentPath)) +} + +async function nearestExistingAncestor(targetPath) { + let currentPath = targetPath + while (currentPath && currentPath !== path.dirname(currentPath)) { + try { + return await realpath(currentPath) + } catch (error) { + if (error?.code !== 'ENOENT') throw error + currentPath = path.dirname(currentPath) + } + } + return realpath(currentPath) +} + +function assertInsideVault(vaultRoot, targetPath) { + if (!isVaultRelativePath(path.relative(vaultRoot, targetPath))) { + throw new Error(ACTIVE_VAULT_ERROR) + } +} + +function relativeNotePathInsideVault(vaultRoot, requestedPath) { + const relativePath = path.relative(vaultRoot, requestedPath) + if (!isVaultRelativePath(relativePath) || !relativePath) { + throw new Error(ACTIVE_VAULT_ERROR) + } + return relativePath +} + +function resolveInside(root, target) { + const resolved = path.resolve(root, target) + const relative = path.relative(root, resolved) + if (isVaultRelativePath(relative)) return resolved + return null +} + +function isVaultRelativePath(relativePath) { + return !relativePath.startsWith('..') && !path.isAbsolute(relativePath) +} + +function matchesSearchQuery(title, content, query) { + return title.toLowerCase().includes(query) || content.toLowerCase().includes(query) +} + +function contextNoteWithoutMtime(note) { + return { + path: note.path, + title: note.title, + type: note.type, + } +} + +async function readVaultContextNote(vaultPath, filePath) { + const raw = await readUtf8File(filePath) + const parsed = parseMarkdownNote(raw) + const rel = path.relative(vaultPath, filePath) + const topFolder = extractTopFolder(rel) + const stat = await statFile(filePath) + const type = parsed.data.type || parsed.data.is_a || null + + return { + topFolder, + type, + note: { + path: rel, + title: parsed.data.title || extractTitle(raw, path.basename(filePath, '.md')), + type, + mtime: stat.mtimeMs, + }, + } +} + +function parseMarkdownNote(raw) { + try { + const parsed = matter(raw) + const fallback = parseFrontmatterFallback(raw) + return shouldUseFallbackFrontmatter(parsed, fallback) ? fallback : parsed + } catch { + return parseFrontmatterFallback(raw) + } +} + +function shouldUseFallbackFrontmatter(parsed, fallback) { + return Object.keys(parsed.data).length === 0 && Object.keys(fallback.data).length > 0 +} + +function parseFrontmatterFallback(raw) { + const split = splitFrontmatter(raw) + if (!split) return { data: {}, content: raw } + + return { + data: parseFrontmatterBlock(split.frontmatter), + content: split.content, + } +} + +function splitFrontmatter(raw) { + const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)([\s\S]*)$/) + if (!match) return null + return { frontmatter: match[1], content: match[2] } +} + +function parseFrontmatterBlock(frontmatter) { + const data = {} + let listKey = null + + for (const line of frontmatter.split(/\r?\n/)) { + const item = parseYamlListItem(line) + if (listKey && item !== null) { + data[listKey].push(parseYamlScalar(item)) + continue + } + + listKey = null + const field = parseTopLevelYamlField(line) + if (!field) continue + + data[field.key] = field.value ? parseYamlValue(field.value) : [] + listKey = field.value ? null : field.key + } + + return data +} + +function parseTopLevelYamlField(line) { + if (!line || line.trimStart() !== line || line.trimStart().startsWith('#')) return null + + const separatorIndex = line.indexOf(':') + if (separatorIndex <= 0) return null + + return { + key: stripMatchingQuotes(line.slice(0, separatorIndex).trim()), + value: line.slice(separatorIndex + 1).trim(), + } +} + +function parseYamlValue(value) { + if (value.startsWith('[') && value.endsWith(']')) { + return splitInlineYamlArray(value).map(parseYamlScalar) + } + return parseYamlScalar(value) +} + +function splitInlineYamlArray(value) { + const inner = value.slice(1, -1) + const items = [] + let current = '' + let quote = null + + for (const char of inner) { + if (quote) { + current += char + if (char === quote) quote = null + continue + } + if (char === '"' || char === "'") { + quote = char + current += char + continue + } + if (char === ',') { + items.push(current.trim()) + current = '' + continue + } + current += char + } + + if (current.trim()) items.push(current.trim()) + return items +} + +function parseYamlListItem(line) { + const match = line.match(/^\s+-\s*(.*)$/) + return match ? match[1].trim() : null +} + +function parseYamlScalar(value) { + const unquoted = stripMatchingQuotes(value.trim()) + if (unquoted !== value.trim()) return unquoted + + if (/^(true|yes)$/i.test(unquoted)) return true + if (/^(false|no)$/i.test(unquoted)) return false + if (/^(null|~)$/i.test(unquoted)) return null + if (/^-?\d+(\.\d+)?$/.test(unquoted)) return Number(unquoted) + + return unquoted +} + +function stripMatchingQuotes(value) { + const first = value[0] + const last = value[value.length - 1] + return (first === '"' || first === "'") && first === last ? value.slice(1, -1) : value +} + +function extractTopFolder(relativePath) { + const topFolder = relativePath.split(path.sep)[0] + return topFolder === relativePath ? null : `${topFolder}/` +} + +async function readConfigFiles(vaultPath) { + const configFiles = {} + + try { + const agentsPath = resolveInside(vaultPath, 'config/agents.md') + if (agentsPath) configFiles.agents = await readUtf8File(agentsPath) + } catch { + // config/agents.md may not exist yet + } + + return configFiles +} + +async function readUtf8File(filePath) { + const handle = await open(filePath, 'r') + try { + return await handle.readFile('utf-8') + } finally { + await handle.close() + } +} + +async function writeNewUtf8File(filePath, content) { + const handle = await open(filePath, 'wx') + try { + await handle.writeFile(content, 'utf-8') + } finally { + await handle.close() + } +} + +async function statFile(filePath) { + const handle = await open(filePath, 'r') + try { + return await handle.stat() + } finally { + await handle.close() + } +} + +/** + * Extract title from markdown content (first H1 or frontmatter title). + * @param {string} content + * @param {string} fallback + * @returns {string} + */ +function extractTitle(content, fallback) { + const h1Match = content.match(/^#\s+(.+)$/m) + if (h1Match) return h1Match[1].trim() + + const titleMatch = content.match(/^title:\s*(.+)$/m) + if (titleMatch) return titleMatch[1].trim() + + return fallback +} + +/** + * Extract a snippet around the query match. + * @param {string} content + * @param {string} query + * @returns {string} + */ +function extractSnippet(content, query) { + const body = content.replace(/^---[\s\S]*?---\n?/, '').trim() + const idx = body.toLowerCase().indexOf(query) + if (idx === -1) return body.slice(0, 120) + const start = Math.max(0, idx - 40) + const end = Math.min(body.length, idx + query.length + 80) + return (start > 0 ? '...' : '') + body.slice(start, end) + (end < body.length ? '...' : '') +} diff --git a/mcp-server/ws-bridge.js b/mcp-server/ws-bridge.js new file mode 100644 index 0000000..50b2a44 --- /dev/null +++ b/mcp-server/ws-bridge.js @@ -0,0 +1,241 @@ +#!/usr/bin/env node +/** + * WebSocket bridge for Tolaria MCP tools. + * + * Exposes vault operations over WebSocket so the Tolaria app frontend + * can invoke MCP tools in real-time without going through stdio. + * + * Port 9710: Tool bridge — Claude/AI clients call vault tools here. + * Port 9711: UI bridge — Frontend listens for UI action broadcasts. + * + * Usage: + * VAULT_PATH=/path/to/vault WS_PORT=9710 WS_UI_PORT=9711 node ws-bridge.js + * + * Protocol (tool bridge): + * Client sends: { "id": "req-1", "tool": "search_notes", "args": { "query": "test" } } + * Server sends: { "id": "req-1", "result": { ... } } + * On error: { "id": "req-1", "error": "message" } + * + * Protocol (UI bridge): + * Server broadcasts: { "type": "ui_action", "action": "open_note", "path": "..." } + */ +import { createServer } from 'node:http' +import { WebSocketServer } from 'ws' +import { createMcpToolService } from './tool-service.js' + +const WS_PORT = parseInt(process.env.WS_PORT || '9710', 10) +const WS_UI_PORT = parseInt(process.env.WS_UI_PORT || '9711', 10) +const LOOPBACK_HOST = 'localhost' +const TRUSTED_UI_ORIGINS = new Set([ + 'tauri://localhost', + 'http://tauri.localhost', + 'https://tauri.localhost', +]) + +/** @type {WebSocketServer | null} */ +let uiBridge = null +const UNKNOWN_TOOL = Symbol('unknown tool') + +function broadcastUiAction(action, payload) { + if (!uiBridge) return + const msg = JSON.stringify({ type: 'ui_action', action, ...payload }) + for (const client of uiBridge.clients) { + if (client.readyState === 1) client.send(msg) + } +} + +const toolService = createMcpToolService({ emitUiAction: broadcastUiAction }) + +async function readNoteTool(args) { + const note = await toolService.readNote(args) + return { content: note.content, frontmatter: note.frontmatter } +} + +function uiOpenNoteTool(args) { + toolService.openNoteInEditor(args) + return { ok: true } +} + +function uiOpenTabTool(args) { + toolService.openNoteAsTab(args) + return { ok: true } +} + +async function createNoteTool(args = {}) { + return { ok: true, ...(await toolService.createNote(args)) } +} + +function highlightTool(args) { + toolService.highlightEditor(args) + return { ok: true } +} + +function uiSetFilterTool(args) { + toolService.setFilter(args) + return { ok: true } +} + +function refreshVaultTool(args) { + toolService.refreshVault(args) + return { ok: true } +} + +const TOOL_EXECUTORS = [ + ['open_note', readNoteTool], + ['read_note', readNoteTool], + ['create_note', createNoteTool], + ['search_notes', (args) => toolService.searchNotes(args)], + ['vault_context', (args) => toolService.vaultContext(args)], + ['list_vaults', () => toolService.listVaults()], + ['ui_open_note', uiOpenNoteTool], + ['ui_open_tab', uiOpenTabTool], + ['ui_highlight', highlightTool], + ['highlight_editor', highlightTool], + ['ui_set_filter', uiSetFilterTool], + ['refresh_vault', refreshVaultTool], +] + +function callToolHandler(tool, args) { + const executor = TOOL_EXECUTORS.find(([name]) => name === tool)?.[1] + return executor ? executor(args) : UNKNOWN_TOOL +} + +async function handleMessage(data) { + const msg = JSON.parse(data) + const { id, tool, args } = msg + + try { + const result = await callToolHandler(tool, args || {}) + if (result === UNKNOWN_TOOL) { + return { id, error: `Unknown tool: ${tool}` } + } + return { id, result } + } catch (err) { + return { id, error: err.message } + } +} + +export function isLoopbackAddress(remoteAddress) { + return remoteAddress === '127.0.0.1' + || remoteAddress === '::1' + || remoteAddress === '::ffff:127.0.0.1' +} + +export function isTrustedUiOrigin(origin) { + if (!origin) return true + if (TRUSTED_UI_ORIGINS.has(origin)) return true + return /^http:\/\/(?:localhost|127\.0\.0\.1):\d+$/u.test(origin) +} + +export function evaluateBridgeRequest({ bridgeType, origin, remoteAddress }) { + if (!isLoopbackAddress(remoteAddress)) { + return { ok: false, reason: 'non-local client' } + } + + if (bridgeType === 'tool' && origin) { + return { ok: false, reason: 'browser origins are not allowed on the tool bridge' } + } + + if (bridgeType === 'ui' && !isTrustedUiOrigin(origin)) { + return { ok: false, reason: 'untrusted UI origin' } + } + + return { ok: true, reason: null } +} + +function verifyBridgeRequest(bridgeType) { + return (info, done) => { + const verdict = evaluateBridgeRequest({ + bridgeType, + origin: info.origin, + remoteAddress: info.req.socket.remoteAddress, + }) + + if (!verdict.ok) { + console.error(`[ws-bridge] Rejected ${bridgeType} bridge client: ${verdict.reason}`) + done(false, 403, 'Forbidden') + return + } + + done(true) + } +} + +/** + * Attempt to start the UI bridge WebSocket server. + * Returns a Promise that resolves to the WebSocketServer or null if the port + * is unavailable (e.g. another Tolaria instance owns it). + */ +export function startUiBridge(port = WS_UI_PORT) { + return new Promise((resolve) => { + const httpServer = createServer() + + httpServer.on('error', (err) => { + if (err.code === 'EADDRINUSE') { + console.error(`[ws-bridge] UI bridge port ${port} already in use, disabling bridge`) + } else { + console.error(`[ws-bridge] UI bridge error: ${err.message}`) + } + resolve(null) + }) + + httpServer.listen(port, LOOPBACK_HOST, () => { + const wss = new WebSocketServer({ + server: httpServer, + verifyClient: verifyBridgeRequest('ui'), + }) + wss.on('connection', (ws) => { + console.error(`[ws-bridge] UI client connected on port ${port}`) + // Relay: when a client sends a message, broadcast to all OTHER clients. + // This allows the MCP stdio server (connected as a client) to reach the frontend. + ws.on('message', (raw) => { + for (const client of wss.clients) { + if (client !== ws && client.readyState === 1) client.send(raw.toString()) + } + }) + }) + uiBridge = wss + console.error(`[ws-bridge] UI bridge listening on ws://localhost:${port}`) + resolve(wss) + }) + }) +} + +export function startBridge(port = WS_PORT) { + const currentVaultPaths = toolService.activeVaultPaths() + const wss = new WebSocketServer({ + port, + host: LOOPBACK_HOST, + verifyClient: verifyBridgeRequest('tool'), + }) + + wss.on('connection', (ws) => { + console.error(`[ws-bridge] Client connected (vaults: ${currentVaultPaths.join(', ')})`) + + ws.on('message', async (raw) => { + try { + const response = await handleMessage(raw.toString()) + ws.send(JSON.stringify(response)) + } catch (err) { + ws.send(JSON.stringify({ error: `Parse error: ${err.message}` })) + } + }) + + ws.on('close', () => console.error('[ws-bridge] Client disconnected')) + }) + + console.error(`[ws-bridge] Listening on ws://${LOOPBACK_HOST}:${port}`) + return wss +} + +// Run directly if invoked as main module +const isMain = process.argv[1]?.endsWith('ws-bridge.js') +if (isMain) { + try { + toolService.activeVaultPaths() + startUiBridge().then(() => startBridge()) + } catch (err) { + console.error(`[ws-bridge] ${err.message}`) + process.exit(1) + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7475a23 --- /dev/null +++ b/package.json @@ -0,0 +1,156 @@ +{ + "name": "tolaria", + "private": true, + "license": "AGPL-3.0-or-later", + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "agent-docs": "node scripts/build-agent-docs.mjs", + "bundle-mcp": "node scripts/bundle-mcp-server.mjs", + "docs:dev": "vitepress dev site --host 127.0.0.1", + "docs:build": "pnpm agent-docs && vitepress build site", + "docs:preview": "vitepress preview site --host 127.0.0.1", + "lint": "eslint . --max-warnings=0", + "l10n:translate": "lara-cli translate", + "l10n:translate:force": "lara-cli translate --force", + "l10n:validate": "node scripts/validate-locales.mjs", + "perf:editor": "node scripts/editor-performance-benchmark.mjs", + "perf:editor:update": "node scripts/editor-performance-benchmark.mjs --update", + "preview": "vite preview", + "tauri": "tauri", + "test": "vitest run", + "test:watch": "vitest", + "test:e2e": "playwright test", + "playwright:smoke": "playwright test --config playwright.smoke.config.ts tests/smoke/autosave-low-end-typing.spec.ts tests/smoke/create-note-backing-file.spec.ts tests/smoke/delete-note-nonblocking.spec.ts tests/smoke/example.spec.ts tests/smoke/fix-crash-create-note.spec.ts tests/smoke/quick-open-create-note.spec.ts tests/smoke/save-before-note-switch.spec.ts tests/smoke/h1-untitled-auto-rename.spec.ts tests/smoke/keyboard-command-routing.spec.ts tests/smoke/missing-string-metadata-open-note.spec.ts tests/smoke/multibyte-search-snippet.spec.ts tests/smoke/pull-refresh-open-note.spec.ts tests/smoke/wikilink-path-fix.spec.ts", + "playwright:regression": "playwright test tests/smoke/", + "playwright:integration": "playwright test --config playwright.integration.config.ts", + "test:coverage": "node scripts/run-vitest-coverage.mjs", + "prepare": "husky" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.78.0", + "@blocknote/code-block": "^0.46.2", + "@blocknote/core": "^0.46.2", + "@blocknote/mantine": "^0.46.2", + "@blocknote/react": "^0.46.2", + "@codemirror/commands": "^6.10.2", + "@codemirror/lang-javascript": "^6.2.5", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/lang-python": "^6.2.1", + "@codemirror/lang-sql": "^6.10.0", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/language": "^6.12.2", + "@codemirror/state": "^6.5.4", + "@codemirror/view": "^6.39.16", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@ironcalc/wasm": "0.5.4", + "@ironcalc/workbook": "0.5.7", + "@lezer/highlight": "^1.2.3", + "@mantine/core": "^8.3.14", + "@phosphor-icons/react": "^2.1.10", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", + "@sentry/react": "^10.47.0", + "@shikijs/langs": "3.23.0", + "@tailwindcss/vite": "^4.1.18", + "@tauri-apps/api": "^2.10.1", + "@tauri-apps/plugin-deep-link": "2.4.9", + "@tauri-apps/plugin-dialog": "^2.6.0", + "@tauri-apps/plugin-opener": "^2.5.3", + "@tauri-apps/plugin-process": "^2.3.1", + "@tauri-apps/plugin-updater": "^2.10.0", + "@tiptap/pm": "3.22.5", + "@tldraw/assets": "4.5.10", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "dompurify": "3.4.2", + "katex": "^0.16.28", + "mermaid": "^11.14.0", + "posthog-js": "^1.363.5", + "radix-ui": "^1.4.3", + "react": "^19.2.0", + "react-day-picker": "^9.13.2", + "react-dom": "^19.2.0", + "react-markdown": "^10.1.0", + "react-virtuoso": "^4.18.1", + "rehype-highlight": "^7.0.2", + "remark-gfm": "^4.0.1", + "safe-regex2": "5.1.1", + "tailwind-merge": "^3.4.1", + "tailwindcss": "^4.1.18", + "tldraw": "^4.5.10", + "tw-animate-css": "^1.4.0", + "unicode-emoji-json": "^0.8.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@playwright/test": "^1.58.2", + "@tauri-apps/cli": "^2.10.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@translated/lara-cli": "^1.3.2", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@types/ws": "^8.18.1", + "@vitejs/plugin-react": "^5.1.1", + "@vitest/coverage-v8": "^4.0.18", + "esbuild": "^0.27.3", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "gray-matter": "^4.0.3", + "husky": "^9.1.7", + "jsdom": "^28.0.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.5", + "vitepress": "^1.6.4", + "vitest": "^4.0.18", + "ws": "^8.19.0" + }, + "pnpm": { + "overrides": { + "@hono/node-server": "1.19.13", + "express-rate-limit": "8.2.2", + "hono": "4.12.25", + "ip-address": "10.1.1", + "mermaid>uuid": "11.1.1", + "path-to-regexp": "8.4.0", + "fast-uri": "3.1.2", + "fast-xml-builder": "1.1.7", + "flatted": "3.4.2", + "minimatch@3.1.2": "3.1.5", + "minimatch@3.1.3": "3.1.5", + "minimatch@9.0.5": "9.0.9", + "minimatch@9.0.6": "9.0.9", + "picomatch": "4.0.4", + "postcss": "8.5.10", + "protobufjs": "7.6.3", + "linkify-it": "5.0.1", + "qs": "6.15.2", + "rollup": "4.59.0", + "undici": "7.25.0", + "@blocknote/core>uuid": "11.1.1" + }, + "patchedDependencies": { + "@blocknote/core@0.46.2": "patches/@blocknote__core@0.46.2.patch", + "@blocknote/react@0.46.2": "patches/@blocknote__react@0.46.2.patch", + "@tiptap/extension-link@3.19.0": "patches/@tiptap__extension-link@3.19.0.patch", + "prosemirror-tables@1.8.5": "patches/prosemirror-tables@1.8.5.patch", + "@blocknote/code-block@0.46.2": "patches/@blocknote__code-block@0.46.2.patch" + } + } +} diff --git a/patches/@blocknote__code-block@0.46.2.patch b/patches/@blocknote__code-block@0.46.2.patch new file mode 100644 index 0000000..e37b45c --- /dev/null +++ b/patches/@blocknote__code-block@0.46.2.patch @@ -0,0 +1,86 @@ +diff --git a/dist/blocknote-code-block.cjs b/dist/blocknote-code-block.cjs +index 1df95b16a98b56ff314d5069b20c864f831640e4..27b9e8fa03566327175e4d3dddb1aebee2950c71 100644 +--- a/dist/blocknote-code-block.cjs ++++ b/dist/blocknote-code-block.cjs +@@ -1,2 +1,2 @@ +-"use strict";var r=Object.create;var l=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var o=Object.getOwnPropertyNames;var n=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty;var h=(a,e,i,m)=>{if(e&&typeof e=="object"||typeof e=="function")for(let t of o(e))!c.call(a,t)&&t!==i&&l(a,t,{get:()=>e[t],enumerable:!(m=p(e,t))||m.enumerable});return a};var s=(a,e,i)=>(i=a!=null?r(n(a)):{},h(e||!a||!a.__esModule?l(i,"default",{value:a,enumerable:!0}):i,a));Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const g=require("@shikijs/core"),u=require("@shikijs/engine-javascript"),j={c:()=>import("@shikijs/langs-precompiled/c"),cpp:()=>import("@shikijs/langs-precompiled/cpp"),"c++":()=>import("@shikijs/langs-precompiled/cpp"),css:()=>import("@shikijs/langs-precompiled/css"),glsl:()=>import("@shikijs/langs-precompiled/glsl"),graphql:()=>import("@shikijs/langs-precompiled/graphql"),gql:()=>import("@shikijs/langs-precompiled/graphql"),haml:()=>import("@shikijs/langs-precompiled/haml"),html:()=>import("@shikijs/langs-precompiled/html"),java:()=>import("@shikijs/langs-precompiled/java"),javascript:()=>import("@shikijs/langs-precompiled/javascript"),js:()=>import("@shikijs/langs-precompiled/javascript"),json:()=>import("@shikijs/langs-precompiled/json"),jsonc:()=>import("@shikijs/langs-precompiled/jsonc"),jsonl:()=>import("@shikijs/langs-precompiled/jsonl"),jsx:()=>import("@shikijs/langs-precompiled/jsx"),julia:()=>import("@shikijs/langs-precompiled/julia"),jl:()=>import("@shikijs/langs-precompiled/julia"),less:()=>import("@shikijs/langs-precompiled/less"),markdown:()=>import("@shikijs/langs-precompiled/markdown"),md:()=>import("@shikijs/langs-precompiled/markdown"),mdx:()=>import("@shikijs/langs-precompiled/mdx"),php:()=>import("@shikijs/langs-precompiled/php"),postcss:()=>import("@shikijs/langs-precompiled/postcss"),pug:()=>import("@shikijs/langs-precompiled/pug"),jade:()=>import("@shikijs/langs-precompiled/pug"),python:()=>import("@shikijs/langs-precompiled/python"),py:()=>import("@shikijs/langs-precompiled/python"),r:()=>import("@shikijs/langs-precompiled/r"),regexp:()=>import("@shikijs/langs-precompiled/regexp"),regex:()=>import("@shikijs/langs-precompiled/regexp"),sass:()=>import("@shikijs/langs-precompiled/sass"),scss:()=>import("@shikijs/langs-precompiled/scss"),shellscript:()=>import("@shikijs/langs-precompiled/shellscript"),bash:()=>import("@shikijs/langs-precompiled/shellscript"),sh:()=>import("@shikijs/langs-precompiled/shellscript"),shell:()=>import("@shikijs/langs-precompiled/shellscript"),zsh:()=>import("@shikijs/langs-precompiled/shellscript"),sql:()=>import("@shikijs/langs-precompiled/sql"),svelte:()=>import("@shikijs/langs-precompiled/svelte"),typescript:()=>import("@shikijs/langs-precompiled/typescript"),ts:()=>import("@shikijs/langs-precompiled/typescript"),vue:()=>import("@shikijs/langs-precompiled/vue"),"vue-html":()=>import("@shikijs/langs-precompiled/vue-html"),wasm:()=>import("@shikijs/langs-precompiled/wasm"),wgsl:()=>import("@shikijs/langs-precompiled/wgsl"),xml:()=>import("@shikijs/langs-precompiled/xml"),yaml:()=>import("@shikijs/langs-precompiled/yaml"),yml:()=>import("@shikijs/langs-precompiled/yaml"),tsx:()=>import("@shikijs/langs-precompiled/tsx"),typescriptreact:()=>import("@shikijs/langs-precompiled/tsx"),haskell:()=>import("@shikijs/langs-precompiled/haskell"),hs:()=>import("@shikijs/langs-precompiled/haskell"),"c#":()=>import("@shikijs/langs-precompiled/csharp"),csharp:()=>import("@shikijs/langs-precompiled/csharp"),cs:()=>import("@shikijs/langs-precompiled/csharp"),latex:()=>import("@shikijs/langs-precompiled/latex"),lua:()=>import("@shikijs/langs-precompiled/lua"),mermaid:()=>import("@shikijs/langs-precompiled/mermaid"),mmd:()=>import("@shikijs/langs-precompiled/mermaid"),ruby:()=>import("@shikijs/langs-precompiled/ruby"),rb:()=>import("@shikijs/langs-precompiled/ruby"),rust:()=>import("@shikijs/langs-precompiled/rust"),rs:()=>import("@shikijs/langs-precompiled/rust"),scala:()=>import("@shikijs/langs-precompiled/scala"),swift:()=>import("@shikijs/langs/swift"),kotlin:()=>import("@shikijs/langs-precompiled/kotlin"),kt:()=>import("@shikijs/langs-precompiled/kotlin"),kts:()=>import("@shikijs/langs-precompiled/kotlin"),"objective-c":()=>import("@shikijs/langs-precompiled/objective-c"),objc:()=>import("@shikijs/langs-precompiled/objective-c")},d={"github-dark":()=>import("@shikijs/themes/github-dark"),"github-light":()=>import("@shikijs/themes/github-light")},v=g.createdBundledHighlighter({langs:j,themes:d,engine:()=>u.createJavaScriptRegexEngine()}),x={defaultLanguage:"javascript",supportedLanguages:{text:{name:"Plain Text",aliases:["text","txt","plain"]},c:{name:"C",aliases:["c"]},cpp:{name:"C++",aliases:["cpp","c++"]},css:{name:"CSS",aliases:["css"]},glsl:{name:"GLSL",aliases:["glsl"]},graphql:{name:"GraphQL",aliases:["graphql","gql"]},haml:{name:"Ruby Haml",aliases:["haml"]},html:{name:"HTML",aliases:["html"]},java:{name:"Java",aliases:["java"]},javascript:{name:"JavaScript",aliases:["javascript","js"]},json:{name:"JSON",aliases:["json"]},jsonc:{name:"JSON with Comments",aliases:["jsonc"]},jsonl:{name:"JSON Lines",aliases:["jsonl"]},jsx:{name:"JSX",aliases:["jsx"]},julia:{name:"Julia",aliases:["julia","jl"]},less:{name:"Less",aliases:["less"]},markdown:{name:"Markdown",aliases:["markdown","md"]},mdx:{name:"MDX",aliases:["mdx"]},php:{name:"PHP",aliases:["php"]},postcss:{name:"PostCSS",aliases:["postcss"]},pug:{name:"Pug",aliases:["pug","jade"]},python:{name:"Python",aliases:["python","py"]},r:{name:"R",aliases:["r"]},regexp:{name:"RegExp",aliases:["regexp","regex"]},sass:{name:"Sass",aliases:["sass"]},scss:{name:"SCSS",aliases:["scss"]},shellscript:{name:"Shell",aliases:["shellscript","bash","sh","shell","zsh"]},sql:{name:"SQL",aliases:["sql"]},svelte:{name:"Svelte",aliases:["svelte"]},typescript:{name:"TypeScript",aliases:["typescript","ts"]},vue:{name:"Vue",aliases:["vue"]},"vue-html":{name:"Vue HTML",aliases:["vue-html"]},wasm:{name:"WebAssembly",aliases:["wasm"]},wgsl:{name:"WGSL",aliases:["wgsl"]},xml:{name:"XML",aliases:["xml"]},yaml:{name:"YAML",aliases:["yaml","yml"]},tsx:{name:"TSX",aliases:["tsx","typescriptreact"]},haskell:{name:"Haskell",aliases:["haskell","hs"]},csharp:{name:"C#",aliases:["c#","csharp","cs"]},latex:{name:"LaTeX",aliases:["latex"]},lua:{name:"Lua",aliases:["lua"]},mermaid:{name:"Mermaid",aliases:["mermaid","mmd"]},ruby:{name:"Ruby",aliases:["ruby","rb"]},rust:{name:"Rust",aliases:["rust","rs"]},scala:{name:"Scala",aliases:["scala"]},swift:{name:"Swift",aliases:["swift"]},kotlin:{name:"Kotlin",aliases:["kotlin","kt","kts"]},"objective-c":{name:"Objective C",aliases:["objective-c","objc"]}},createHighlighter:()=>v({themes:["github-dark","github-light"],langs:[]})};exports.codeBlockOptions=x; ++"use strict";var r=Object.create;var l=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var o=Object.getOwnPropertyNames;var n=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty;var h=(a,e,i,m)=>{if(e&&typeof e=="object"||typeof e=="function")for(let t of o(e))!c.call(a,t)&&t!==i&&l(a,t,{get:()=>e[t],enumerable:!(m=p(e,t))||m.enumerable});return a};var s=(a,e,i)=>(i=a!=null?r(n(a)):{},h(e||!a||!a.__esModule?l(i,"default",{value:a,enumerable:!0}):i,a));Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const g=require("@shikijs/core"),u=require("@shikijs/engine-javascript"),j={c:()=>import("@shikijs/langs-precompiled/c"),cpp:()=>import("@shikijs/langs-precompiled/cpp"),"c++":()=>import("@shikijs/langs-precompiled/cpp"),css:()=>import("@shikijs/langs-precompiled/css"),glsl:()=>import("@shikijs/langs-precompiled/glsl"),graphql:()=>import("@shikijs/langs-precompiled/graphql"),gql:()=>import("@shikijs/langs-precompiled/graphql"),haml:()=>import("@shikijs/langs-precompiled/haml"),html:()=>import("@shikijs/langs-precompiled/html"),java:()=>import("@shikijs/langs-precompiled/java"),go:()=>import("@shikijs/langs-precompiled/go"),golang:()=>import("@shikijs/langs-precompiled/go"),javascript:()=>import("@shikijs/langs-precompiled/javascript"),js:()=>import("@shikijs/langs-precompiled/javascript"),json:()=>import("@shikijs/langs-precompiled/json"),jsonc:()=>import("@shikijs/langs-precompiled/jsonc"),jsonl:()=>import("@shikijs/langs-precompiled/jsonl"),jsx:()=>import("@shikijs/langs-precompiled/jsx"),julia:()=>import("@shikijs/langs-precompiled/julia"),jl:()=>import("@shikijs/langs-precompiled/julia"),less:()=>import("@shikijs/langs-precompiled/less"),markdown:()=>import("@shikijs/langs-precompiled/markdown"),md:()=>import("@shikijs/langs-precompiled/markdown"),mdx:()=>import("@shikijs/langs-precompiled/mdx"),php:()=>import("@shikijs/langs-precompiled/php"),postcss:()=>import("@shikijs/langs-precompiled/postcss"),pug:()=>import("@shikijs/langs-precompiled/pug"),jade:()=>import("@shikijs/langs-precompiled/pug"),python:()=>import("@shikijs/langs-precompiled/python"),py:()=>import("@shikijs/langs-precompiled/python"),r:()=>import("@shikijs/langs-precompiled/r"),regexp:()=>import("@shikijs/langs-precompiled/regexp"),regex:()=>import("@shikijs/langs-precompiled/regexp"),sass:()=>import("@shikijs/langs-precompiled/sass"),scss:()=>import("@shikijs/langs-precompiled/scss"),shellscript:()=>import("@shikijs/langs-precompiled/shellscript"),bash:()=>import("@shikijs/langs-precompiled/shellscript"),sh:()=>import("@shikijs/langs-precompiled/shellscript"),shell:()=>import("@shikijs/langs-precompiled/shellscript"),zsh:()=>import("@shikijs/langs-precompiled/shellscript"),sql:()=>import("@shikijs/langs-precompiled/sql"),svelte:()=>import("@shikijs/langs-precompiled/svelte"),typescript:()=>import("@shikijs/langs-precompiled/typescript"),ts:()=>import("@shikijs/langs-precompiled/typescript"),vue:()=>import("@shikijs/langs-precompiled/vue"),"vue-html":()=>import("@shikijs/langs-precompiled/vue-html"),wasm:()=>import("@shikijs/langs-precompiled/wasm"),wgsl:()=>import("@shikijs/langs-precompiled/wgsl"),xml:()=>import("@shikijs/langs-precompiled/xml"),yaml:()=>import("@shikijs/langs-precompiled/yaml"),yml:()=>import("@shikijs/langs-precompiled/yaml"),tsx:()=>import("@shikijs/langs-precompiled/tsx"),typescriptreact:()=>import("@shikijs/langs-precompiled/tsx"),haskell:()=>import("@shikijs/langs-precompiled/haskell"),hs:()=>import("@shikijs/langs-precompiled/haskell"),"c#":()=>import("@shikijs/langs-precompiled/csharp"),csharp:()=>import("@shikijs/langs-precompiled/csharp"),cs:()=>import("@shikijs/langs-precompiled/csharp"),latex:()=>import("@shikijs/langs-precompiled/latex"),lua:()=>import("@shikijs/langs-precompiled/lua"),mermaid:()=>import("@shikijs/langs-precompiled/mermaid"),mmd:()=>import("@shikijs/langs-precompiled/mermaid"),ruby:()=>import("@shikijs/langs-precompiled/ruby"),rb:()=>import("@shikijs/langs-precompiled/ruby"),rust:()=>import("@shikijs/langs-precompiled/rust"),rs:()=>import("@shikijs/langs-precompiled/rust"),scala:()=>import("@shikijs/langs-precompiled/scala"),swift:()=>import("@shikijs/langs/swift"),kotlin:()=>import("@shikijs/langs-precompiled/kotlin"),kt:()=>import("@shikijs/langs-precompiled/kotlin"),kts:()=>import("@shikijs/langs-precompiled/kotlin"),"objective-c":()=>import("@shikijs/langs-precompiled/objective-c"),objc:()=>import("@shikijs/langs-precompiled/objective-c")},d={"github-dark":()=>import("@shikijs/themes/github-dark"),"github-light":()=>import("@shikijs/themes/github-light")},v=g.createdBundledHighlighter({langs:j,themes:d,engine:()=>u.createJavaScriptRegexEngine()}),x={defaultLanguage:"javascript",supportedLanguages:{text:{name:"Plain Text",aliases:["text","txt","plain"]},c:{name:"C",aliases:["c"]},cpp:{name:"C++",aliases:["cpp","c++"]},css:{name:"CSS",aliases:["css"]},glsl:{name:"GLSL",aliases:["glsl"]},graphql:{name:"GraphQL",aliases:["graphql","gql"]},haml:{name:"Ruby Haml",aliases:["haml"]},html:{name:"HTML",aliases:["html"]},java:{name:"Java",aliases:["java"]},go:{name:"Go",aliases:["go","golang"]},javascript:{name:"JavaScript",aliases:["javascript","js"]},json:{name:"JSON",aliases:["json"]},jsonc:{name:"JSON with Comments",aliases:["jsonc"]},jsonl:{name:"JSON Lines",aliases:["jsonl"]},jsx:{name:"JSX",aliases:["jsx"]},julia:{name:"Julia",aliases:["julia","jl"]},less:{name:"Less",aliases:["less"]},markdown:{name:"Markdown",aliases:["markdown","md"]},mdx:{name:"MDX",aliases:["mdx"]},php:{name:"PHP",aliases:["php"]},postcss:{name:"PostCSS",aliases:["postcss"]},pug:{name:"Pug",aliases:["pug","jade"]},python:{name:"Python",aliases:["python","py"]},r:{name:"R",aliases:["r"]},regexp:{name:"RegExp",aliases:["regexp","regex"]},sass:{name:"Sass",aliases:["sass"]},scss:{name:"SCSS",aliases:["scss"]},shellscript:{name:"Shell",aliases:["shellscript","bash","sh","shell","zsh"]},sql:{name:"SQL",aliases:["sql"]},svelte:{name:"Svelte",aliases:["svelte"]},typescript:{name:"TypeScript",aliases:["typescript","ts"]},vue:{name:"Vue",aliases:["vue"]},"vue-html":{name:"Vue HTML",aliases:["vue-html"]},wasm:{name:"WebAssembly",aliases:["wasm"]},wgsl:{name:"WGSL",aliases:["wgsl"]},xml:{name:"XML",aliases:["xml"]},yaml:{name:"YAML",aliases:["yaml","yml"]},tsx:{name:"TSX",aliases:["tsx","typescriptreact"]},haskell:{name:"Haskell",aliases:["haskell","hs"]},csharp:{name:"C#",aliases:["c#","csharp","cs"]},latex:{name:"LaTeX",aliases:["latex"]},lua:{name:"Lua",aliases:["lua"]},mermaid:{name:"Mermaid",aliases:["mermaid","mmd"]},ruby:{name:"Ruby",aliases:["ruby","rb"]},rust:{name:"Rust",aliases:["rust","rs"]},scala:{name:"Scala",aliases:["scala"]},swift:{name:"Swift",aliases:["swift"]},kotlin:{name:"Kotlin",aliases:["kotlin","kt","kts"]},"objective-c":{name:"Objective C",aliases:["objective-c","objc"]}},createHighlighter:()=>v({themes:["github-dark","github-light"],langs:[]})};exports.codeBlockOptions=x; + //# sourceMappingURL=blocknote-code-block.cjs.map +diff --git a/dist/blocknote-code-block.js b/dist/blocknote-code-block.js +index 076d13a36736bb100e60c0113d231e716f1d9024..87b7919ef41fd6c8d0d39aa0df8f04bffa16284e 100644 +--- a/dist/blocknote-code-block.js ++++ b/dist/blocknote-code-block.js +@@ -11,6 +11,8 @@ const e = { + haml: () => import("@shikijs/langs-precompiled/haml"), + html: () => import("@shikijs/langs-precompiled/html"), + java: () => import("@shikijs/langs-precompiled/java"), ++ go: () => import("@shikijs/langs-precompiled/go"), ++ golang: () => import("@shikijs/langs-precompiled/go"), + javascript: () => import("@shikijs/langs-precompiled/javascript"), + js: () => import("@shikijs/langs-precompiled/javascript"), + json: () => import("@shikijs/langs-precompiled/json"), +@@ -119,6 +121,10 @@ const e = { + name: "Java", + aliases: ["java"] + }, ++ go: { ++ name: "Go", ++ aliases: ["go", "golang"] ++ }, + javascript: { + name: "JavaScript", + aliases: ["javascript", "js"] +diff --git a/src/index.ts b/src/index.ts +index 2cb588092df03b2b25ea7e89ccc9249c61d45de7..f7b834aaf35f406ea2318b16cf86ddd9808389bd 100644 +--- a/src/index.ts ++++ b/src/index.ts +@@ -40,6 +40,10 @@ export const codeBlockOptions = { + name: "Java", + aliases: ["java"], + }, ++ go: { ++ name: "Go", ++ aliases: ["go", "golang"], ++ }, + javascript: { + name: "JavaScript", + aliases: ["javascript", "js"], +diff --git a/src/shiki.bundle.ts b/src/shiki.bundle.ts +index 75962c807f46bc44f327790ac96fb4b034821ec3..c1c2cda0563e0215b592d4f178524f7d20a9ee3f 100644 +--- a/src/shiki.bundle.ts ++++ b/src/shiki.bundle.ts +@@ -22,6 +22,8 @@ const bundledLanguages = { + haml: () => import("@shikijs/langs-precompiled/haml"), + html: () => import("@shikijs/langs-precompiled/html"), + java: () => import("@shikijs/langs-precompiled/java"), ++ go: () => import("@shikijs/langs-precompiled/go"), ++ golang: () => import("@shikijs/langs-precompiled/go"), + javascript: () => import("@shikijs/langs-precompiled/javascript"), + js: () => import("@shikijs/langs-precompiled/javascript"), + json: () => import("@shikijs/langs-precompiled/json"), +diff --git a/types/src/index.d.ts b/types/src/index.d.ts +index c1c64ef8ae6be44336af47fc2a1af3dfdac8ecc5..5d5797013aadec08337b1a6cdc30f8941da38d3a 100644 +--- a/types/src/index.d.ts ++++ b/types/src/index.d.ts +@@ -37,6 +37,10 @@ export declare const codeBlockOptions: { + name: string; + aliases: string[]; + }; ++ go: { ++ name: string; ++ aliases: string[]; ++ }; + javascript: { + name: string; + aliases: string[]; +diff --git a/types/src/shiki.bundle.d.ts b/types/src/shiki.bundle.d.ts +index 6a65953a2c51cebca6fbf0da74f968f69f27ab44..dd7ca33f1a9636137704432635a7abf91e483419 100644 +--- a/types/src/shiki.bundle.d.ts ++++ b/types/src/shiki.bundle.d.ts +@@ -1,5 +1,5 @@ + import type { HighlighterGeneric } from "@shikijs/types"; +-type BundledLanguage = "typescript" | "ts" | "javascript" | "js" | "vue"; ++type BundledLanguage = "typescript" | "ts" | "javascript" | "js" | "vue" | "go" | "golang"; + type BundledTheme = "github-light" | "github-dark"; + type Highlighter = HighlighterGeneric; + declare const createHighlighter: import("@shikijs/types").CreateHighlighterFactory; diff --git a/patches/@blocknote__core@0.46.2.patch b/patches/@blocknote__core@0.46.2.patch new file mode 100644 index 0000000..0276657 --- /dev/null +++ b/patches/@blocknote__core@0.46.2.patch @@ -0,0 +1,1209 @@ +diff --git a/dist/TrailingNode-CxM966vN.js b/dist/TrailingNode-CxM966vN.js +index 3ac6fe16aa9ae605a59916f8343b8440befda89e..e9aa5d6b0a2b4608271de17e6c6332746b46b7d8 100644 +--- a/dist/TrailingNode-CxM966vN.js ++++ b/dist/TrailingNode-CxM966vN.js +@@ -80,7 +80,7 @@ function be(n, e, t, o) { + let l = document.createTextNode( + c.textContent + ); +- for (const u of c.marks.toReversed()) ++ for (const u of Array.from(c.marks).reverse()) + if (u.type.name in n.schema.styleSpecs) { + const h = (n.schema.styleSpecs[u.type.name].implementation.toExternalHTML ?? n.schema.styleSpecs[u.type.name].implementation.render)(u.attrs.stringValue, n); + h.contentDOM.appendChild(l), l = h.dom; +@@ -1076,7 +1076,15 @@ class B extends N { + return { type: "multiple-node", anchor: this.anchor, head: this.head }; + } + } +-N.jsonID("multiple-node", B); ++try { ++ N.jsonID("multiple-node", B); ++} catch (n) { ++ if (!(n instanceof RangeError) || n.message !== "Duplicate use of selection JSON ID multiple-node") ++ throw n; ++ Object.defineProperty(B.prototype, "jsonID", { ++ value: "multiple-node" ++ }); ++} + let E; + function Lt(n, e) { + let t, o; +@@ -1183,6 +1191,10 @@ class Ft { + b(this, "updateState", (e) => { + this.state = e, this.emitUpdate(this.state); + }); ++ b(this, "hideMenu", () => { ++ var e; ++ (e = this.state) != null && e.show && (this.state.show = !1, this.updateState(this.state)); ++ }); + b(this, "updateStateFromMousePos", () => { + var o, r, s, i, a; + if (this.menuFrozen || !this.mousePos) +@@ -1192,33 +1204,39 @@ class Ft { + clientY: this.mousePos.y + }); + if ((e == null ? void 0 : e.element) !== this.pmView.dom || e.distance > se) { +- (o = this.state) != null && o.show && (this.state.show = !1, this.updateState(this.state)); ++ this.hideMenu(); + return; + } + const t = Ht(this.mousePos, this.pmView); + if (!t || !this.editor.isEditable) { +- (r = this.state) != null && r.show && (this.state.show = !1, this.updateState(this.state)); ++ this.hideMenu(); + return; + } +- if (!((s = this.state) != null && s.show && ((i = this.hoveredBlock) != null && i.hasAttribute("data-id")) && ((a = this.hoveredBlock) == null ? void 0 : a.getAttribute("data-id")) === t.id) && (this.hoveredBlock = t.node, this.editor.isEditable)) { +- const c = t.node.getBoundingClientRect(), l = t.node.closest("[data-node-type=column]"); ++ if ((s = this.state) != null && s.show && ((i = this.hoveredBlock) != null && i.hasAttribute("data-id")) && ((a = this.hoveredBlock) == null ? void 0 : a.getAttribute("data-id")) === t.id) ++ return; ++ this.hoveredBlock = t.node; ++ const c = this.hoveredBlock.getAttribute("data-id"), l = c ? this.editor.getBlock(c) : void 0; ++ if (!l) { ++ this.hideMenu(); ++ return; ++ } ++ if (this.editor.isEditable) { ++ const u = t.node.getBoundingClientRect(), h = t.node.closest("[data-node-type=column]"); + this.state = { + show: !0, + referencePos: new DOMRect( +- l ? ( ++ h ? ( + // We take the first child as column elements have some default + // padding. This is a little weird since this child element will + // be the first block, but since it's always non-nested and we + // only take the x coordinate, it's ok. +- l.firstElementChild.getBoundingClientRect().x ++ h.firstElementChild.getBoundingClientRect().x + ) : this.pmView.dom.firstChild.getBoundingClientRect().x, +- c.y, +- c.width, +- c.height ++ u.y, ++ u.width, ++ u.height + ), +- block: this.editor.getBlock( +- this.hoveredBlock.getAttribute("data-id") +- ) ++ block: l + }, this.updateState(this.state); + } + }); +@@ -1522,6 +1540,30 @@ function Ut(n) { + function M(n) { + return Array.prototype.indexOf.call(n.parentElement.childNodes, n); + } ++function isValidTablePosition(n) { ++ return Number.isInteger(n) && n >= 0; ++} ++function isValidCellIndex(n) { ++ return Number.isInteger(n) && n >= 0; ++} ++function isValidRelativeCell(n) { ++ return isValidCellIndex(n.row) && isValidCellIndex(n.col); ++} ++function resolveCellPosition(n, e, t) { ++ if (!isValidTablePosition(e) || !isValidRelativeCell(t)) ++ return; ++ try { ++ const o = n.doc.resolve(e + 1); ++ if (t.row >= o.node().childCount) ++ return; ++ const r = n.doc.resolve( ++ o.posAtIndex(t.row) + 1 ++ ); ++ return t.col >= r.node().childCount ? void 0 : n.doc.resolve(r.posAtIndex(t.col)); ++ } catch { ++ return; ++ } ++} + function _t(n) { + let e = n; + for (; e && e.nodeName !== "TD" && e.nodeName !== "TH" && !e.classList.contains("tableWrapper"); ) { +@@ -1557,6 +1599,12 @@ class Kt { + b(this, "menuFrozen", !1); + b(this, "mouseState", "up"); + b(this, "prevWasEditable", null); ++ b(this, "hideHandles", (e = {}) => { ++ const { resetCell: t = !1, resetDragging: o = !1 } = e; ++ if (!this.state || !this.state.show && !t && !o) ++ return; ++ this.state.show = !1, this.state.showAddOrRemoveRowsButton = !1, this.state.showAddOrRemoveColumnsButton = !1, o && (this.state.draggingState = void 0), t && (this.state.rowIndex = void 0, this.state.colIndex = void 0, this.state.referencePosCell = void 0), this.emitUpdate(); ++ }); + b(this, "viewMousedownHandler", () => { + this.mouseState = "down"; + }); +@@ -1569,11 +1617,11 @@ class Kt { + return; + const t = _t(e.target); + if ((t == null ? void 0 : t.type) === "cell" && this.mouseState === "down" && !((l = this.state) != null && l.draggingState)) { +- this.mouseState = "selecting", (u = this.state) != null && u.show && (this.state.show = !1, this.state.showAddOrRemoveRowsButton = !1, this.state.showAddOrRemoveColumnsButton = !1, this.emitUpdate()); ++ this.mouseState = "selecting", this.hideHandles(); + return; + } + if (!t || !this.editor.isEditable) { +- (h = this.state) != null && h.show && (this.state.show = !1, this.state.showAddOrRemoveRowsButton = !1, this.state.showAddOrRemoveColumnsButton = !1, this.emitUpdate()); ++ this.hideHandles(); + return; + } + if (!t.tbodyNode) +@@ -1582,12 +1630,19 @@ class Kt { + if (!r) + return; + this.tableElement = r.node; +- let s; +- const i = this.editor.transact( +- (w) => me(r.id, w.doc) +- ); +- if (!i) +- throw new Error(`Block with ID ${r.id} not found`); ++ let s, i; ++ try { ++ i = this.editor.transact( ++ (w) => me(r.id, w.doc) ++ ); ++ } catch { ++ this.hideHandles({ resetCell: true, resetDragging: true }); ++ return; ++ } ++ if (!i) { ++ this.hideHandles({ resetCell: true, resetDragging: true }); ++ return; ++ } + const a = L( + i.node, + this.editor.pmSchema, +@@ -1603,7 +1658,7 @@ class Kt { + const w = e.clientY >= o.bottom - 1 && // -1 to account for fractions of pixels in "bottom" + e.clientY < o.bottom + 20, y = e.clientX >= o.right - 1 && e.clientX < o.right + 20, C = ( + // always hide handles when the actively hovered table changed +- ((m = this.state) == null ? void 0 : m.block.id) !== s.id || // make sure we don't hide existing handles (keep col / row index) when ++ ((p = (m = this.state) == null ? void 0 : m.block) == null ? void 0 : p.id) !== s.id || // make sure we don't hide existing handles (keep col / row index) when + // we're hovering just above or to the right of a table + e.clientX > o.right || e.clientY > o.bottom + ); +@@ -1671,9 +1726,7 @@ class Kt { + if (this.mouseState = "up", this.state === void 0 || this.state.draggingState === void 0) + return !1; + if (this.state.rowIndex === void 0 || this.state.colIndex === void 0) +- throw new Error( +- "Attempted to drop table row or column, but no table block was hovered prior." +- ); ++ return e.preventDefault(), this.hideHandles({ resetCell: !0, resetDragging: !0 }), !1; + e.preventDefault(); + const { draggingState: t, colIndex: o, rowIndex: r } = this.state, s = this.state.block.content.columnWidths; + if (t.draggedCellOrientation === "row") { +@@ -1735,26 +1788,31 @@ class Kt { + var r; + if (!this.state || !this.state.show) + return; +- if (this.state.block = this.editor.getBlock(this.state.block.id), !this.state.block || this.state.block.type !== "table" || // when collaborating, the table element might be replaced and out of date ++ const e = this.state.block; ++ if (!e) { ++ this.hideHandles({ resetCell: !0, resetDragging: !0 }); ++ return; ++ } ++ if (this.state.block = this.editor.getBlock(e.id), !this.state.block || this.state.block.type !== "table" || // when collaborating, the table element might be replaced and out of date + // because yjs replaces the element when for example you change the color via the side menu + !((r = this.tableElement) != null && r.isConnected)) { +- this.state.show = !1, this.state.showAddOrRemoveRowsButton = !1, this.state.showAddOrRemoveColumnsButton = !1, this.emitUpdate(); ++ this.hideHandles(); + return; + } +- const { height: e, width: t } = Ve( ++ const { height: t, width: o } = Ve( + this.state.block + ); +- this.state.rowIndex !== void 0 && this.state.colIndex !== void 0 && (this.state.rowIndex >= e && (this.state.rowIndex = e - 1), this.state.colIndex >= t && (this.state.colIndex = t - 1)); +- const o = this.tableElement.querySelector("tbody"); +- if (!o) +- throw new Error( +- "Table block does not contain a 'tbody' HTML element. This should never happen." +- ); ++ this.state.rowIndex !== void 0 && this.state.colIndex !== void 0 && (this.state.rowIndex >= t && (this.state.rowIndex = t - 1), this.state.colIndex >= o && (this.state.colIndex = o - 1)); ++ const i = this.tableElement.querySelector("tbody"); ++ if (!i) { ++ this.hideHandles({ resetCell: !0 }); ++ return; ++ } + if (this.state.rowIndex !== void 0 && this.state.colIndex !== void 0) { +- const i = o.children[this.state.rowIndex].children[this.state.colIndex]; +- i ? this.state.referencePosCell = i.getBoundingClientRect() : (this.state.rowIndex = void 0, this.state.colIndex = void 0); ++ const a = i.children[this.state.rowIndex].children[this.state.colIndex]; ++ a ? this.state.referencePosCell = a.getBoundingClientRect() : (this.state.rowIndex = void 0, this.state.colIndex = void 0); + } +- this.state.referencePosTable = o.getBoundingClientRect(), this.emitUpdate(); ++ this.state.referencePosTable = i.getBoundingClientRect(), this.emitUpdate(); + } + destroy() { + this.pmView.dom.removeEventListener("mousemove", this.mouseMoveHandler), window.removeEventListener("mouseup", this.mouseUpHandler), this.pmView.dom.removeEventListener("mousedown", this.viewMousedownHandler), this.pmView.root.removeEventListener( +@@ -1838,10 +1896,10 @@ const D = new P("TableHandlesPlugin"), Po = k(({ editor: n }) => { + * is used as the column drag handle. + */ + colDragStart(o) { +- if (e === void 0 || e.state === void 0 || e.state.colIndex === void 0) +- throw new Error( +- "Attempted to drag table column, but no table block was hovered prior." +- ); ++ if (e === void 0 || e.state === void 0 || e.state.colIndex === void 0 || e.tablePos === void 0) { ++ e == null || e.hideHandles({ resetCell: !0, resetDragging: !0 }); ++ return; ++ } + e.state.draggingState = { + draggedCellOrientation: "col", + originalIndex: e.state.colIndex, +@@ -1860,10 +1918,10 @@ const D = new P("TableHandlesPlugin"), Po = k(({ editor: n }) => { + * is used as the row drag handle. + */ + rowDragStart(o) { +- if (e.state === void 0 || e.state.rowIndex === void 0) +- throw new Error( +- "Attempted to drag table row, but no table block was hovered prior." +- ); ++ if (e === void 0 || e.state === void 0 || e.state.rowIndex === void 0 || e.tablePos === void 0) { ++ e == null || e.hideHandles({ resetCell: !0, resetDragging: !0 }); ++ return; ++ } + e.state.draggingState = { + draggedCellOrientation: "row", + originalIndex: e.state.rowIndex, +@@ -1882,10 +1940,10 @@ const D = new P("TableHandlesPlugin"), Po = k(({ editor: n }) => { + * used as the row drag handle, and the one used as the column drag handle. + */ + dragEnd() { +- if (e.state === void 0) +- throw new Error( +- "Attempted to drag table row, but no table block was hovered prior." +- ); ++ if (e === void 0 || e.state === void 0) { ++ n.headless || Ut(n.prosemirrorView.root); ++ return; ++ } + e.state.draggingState = void 0, e.emitUpdate(), n.transact((o) => o.setMeta(D, null)), !n.headless && Ut(n.prosemirrorView.root); + }, + /** +@@ -1918,30 +1976,37 @@ const D = new P("TableHandlesPlugin"), Po = k(({ editor: n }) => { + setCellSelection(o, r, s = r) { + if (!e) + throw new Error("Table handles view not initialized"); +- const i = o.doc.resolve(e.tablePos + 1), a = o.doc.resolve( +- i.posAtIndex(r.row) + 1 +- ), c = o.doc.resolve( +- // No need for +1, since CellSelection expects the position before the cell +- a.posAtIndex(r.col) +- ), l = o.doc.resolve( +- i.posAtIndex(s.row) + 1 +- ), u = o.doc.resolve( +- // No need for +1, since CellSelection expects the position before the cell +- l.posAtIndex(s.col) +- ), h = o.tr; +- return h.setSelection( +- new yt(c, u) +- ), o.apply(h); ++ const i = resolveCellPosition( ++ o, ++ e.tablePos, ++ r ++ ), a = resolveCellPosition( ++ o, ++ e.tablePos, ++ s ++ ); ++ if (!i || !a) ++ return; ++ const c = o.tr; ++ return c.setSelection( ++ new yt(i, a) ++ ), o.apply(c); + }, + /** + * Adds a row or column to the table using prosemirror-table commands + */ + addRowOrColumn(o, r) { ++ if (!e || !isValidTablePosition(e.tablePos) || !isValidCellIndex(o)) { ++ e == null || e.hideHandles({ resetCell: !0 }); ++ return; ++ } + n.exec((s, i) => { + const a = this.setCellSelection( + s, + r.orientation === "row" ? { row: o, col: 0 } : { row: 0, col: o } + ); ++ if (!a) ++ return e == null || e.hideHandles({ resetCell: !0 }), !1; + return r.orientation === "row" ? r.side === "above" ? pt(a, i) : ft(a, i) : r.side === "left" ? gt(a, i) : wt(a, i); + }); + }, +@@ -1949,17 +2014,23 @@ const D = new P("TableHandlesPlugin"), Po = k(({ editor: n }) => { + * Removes a row or column from the table using prosemirror-table commands + */ + removeRowOrColumn(o, r) { ++ if (!isValidCellIndex(o)) ++ return !1; + return r === "row" ? n.exec((s, i) => { + const a = this.setCellSelection(s, { + row: o, + col: 0 + }); ++ if (!a) ++ return !1; + return ht(a, i); + }) : n.exec((s, i) => { + const a = this.setCellSelection(s, { + row: 0, + col: o + }); ++ if (!a) ++ return !1; + return mt(a, i); + }); + }, +@@ -1973,6 +2044,8 @@ const D = new P("TableHandlesPlugin"), Po = k(({ editor: n }) => { + o.relativeStartCell, + o.relativeEndCell + ) : r; ++ if (!i) ++ return !1; + return ut(i, s); + }); + }, +@@ -1983,6 +2056,8 @@ const D = new P("TableHandlesPlugin"), Po = k(({ editor: n }) => { + splitCell(o) { + return n.exec((r, s) => { + const i = o ? this.setCellSelection(r, o) : r; ++ if (!i) ++ return !1; + return dt(i, s); + }); + }, +diff --git a/dist/TrailingNode-D-CZ76FS.cjs b/dist/TrailingNode-D-CZ76FS.cjs +index 8406fda31f41172a823c26412698616a56800aae..b53947892a39f3fabac0fe4cb37186dd84f9a369 100644 +--- a/dist/TrailingNode-D-CZ76FS.cjs ++++ b/dist/TrailingNode-D-CZ76FS.cjs +@@ -1,2 +1,2 @@ +-"use strict";var we=Object.defineProperty;var be=(n,e,t)=>e in n?we(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var b=(n,e,t)=>be(n,typeof e!="symbol"?e+"":e,t);const C=require("prosemirror-state"),R=require("@tiptap/core"),ke=require("fast-deep-equal"),k=require("./blockToNode-CumVjgem.cjs"),O=require("./defaultBlocks-DosClM5E.cjs"),v=require("./BlockNoteExtension-BWw0r8Gy.cjs"),M=require("y-prosemirror"),Ce=require("yjs"),V=require("@tiptap/pm/state"),ve=require("prosemirror-dropcursor"),q=require("@tiptap/pm/history"),D=require("prosemirror-view"),Se=require("uuid"),Z=require("@tiptap/pm/model"),H=require("prosemirror-model"),xe=require("rehype-parse"),Ee=require("rehype-remark"),Ie=require("remark-gfm"),Pe=require("remark-stringify"),Te=require("unified"),Be=require("hast-util-from-dom"),De=require("unist-util-visit"),T=require("prosemirror-tables"),F=n=>n&&typeof n=="object"&&"default"in n?n:{default:n};function Oe(n){if(n&&typeof n=="object"&&"default"in n)return n;const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(n){for(const t in n)if(t!=="default"){const o=Object.getOwnPropertyDescriptor(n,t);Object.defineProperty(e,t,o.get?o:{enumerable:!0,get:()=>n[t]})}}return e.default=n,Object.freeze(e)}const Me=F(ke),B=Oe(Ce),Ae=F(xe),Ne=F(Ee),Le=F(Ie),Re=F(Pe);function ae(n){const e=Array.from(n.classList).filter(t=>!t.startsWith("bn-"))||[];e.length>0?n.className=e.join(" "):n.removeAttribute("class")}function le(n,e,t,o){var a;let r;if(e)if(typeof e=="string")r=k.inlineContentToNodes([e],n.pmSchema);else if(Array.isArray(e))r=k.inlineContentToNodes(e,n.pmSchema);else if(e.type==="tableContent")r=k.tableContentToNodes(e,n.pmSchema);else throw new k.UnreachableCaseError(e.type);else throw new Error("blockContent is required");const i=((o==null?void 0:o.document)??document).createDocumentFragment();for(const c of r)if(c.type.name!=="text"&&n.schema.inlineContentSchema[c.type.name]){const l=n.schema.inlineContentSpecs[c.type.name].implementation;if(l){const u=k.nodeToCustomInlineContent(c,n.schema.inlineContentSchema,n.schema.styleSchema),h=l.toExternalHTML?l.toExternalHTML(u,n):l.render.call({renderType:"dom",props:void 0},u,()=>{},n);if(h){if(i.appendChild(h.dom),h.contentDOM){const g=t.serializeFragment(c.content,o);h.contentDOM.dataset.editable="",h.contentDOM.appendChild(g)}continue}}}else if(c.type.name==="text"){let l=document.createTextNode(c.textContent);for(const u of c.marks.toReversed())if(u.type.name in n.schema.styleSpecs){const h=(n.schema.styleSpecs[u.type.name].implementation.toExternalHTML??n.schema.styleSpecs[u.type.name].implementation.render)(u.attrs.stringValue,n);h.contentDOM.appendChild(l),l=h.dom}else{const h=u.type.spec.toDOM(u,!0),g=H.DOMSerializer.renderSpec(document,h);g.contentDOM.appendChild(l),l=g.dom}i.appendChild(l)}else{const l=t.serializeFragment(H.Fragment.from([c]),o);i.appendChild(l)}return i.childNodes.length===1&&((a=i.firstChild)==null?void 0:a.nodeType)===1&&ae(i.firstChild),i}function Ve(n,e,t,o,r,s,i,a){var y,w,x,L,X,W,G,J,Q;const c=(a==null?void 0:a.document)??document,l=e.pmSchema.nodes.blockContainer,u=t.props||{};for(const[S,I]of Object.entries(e.schema.blockSchema[t.type].propSchema))!(S in u)&&I.default!==void 0&&(u[S]=I.default);const h=(w=(y=l.spec)==null?void 0:y.toDOM)==null?void 0:w.call(y,l.create({id:t.id,...u})),g=Array.from(h.dom.attributes),m=e.blockImplementations[t.type].implementation,p=((x=m.toExternalHTML)==null?void 0:x.call({},{...t,props:u},e,{nestingLevel:i}))||m.render.call({},{...t,props:u},e),d=c.createDocumentFragment();if(p.dom.classList.contains("bn-block-content")){const S=[...g,...Array.from(p.dom.attributes)].filter(I=>I.name.startsWith("data")&&I.name!=="data-content-type"&&I.name!=="data-file-block"&&I.name!=="data-node-view-wrapper"&&I.name!=="data-node-type"&&I.name!=="data-id"&&I.name!=="data-editable");for(const I of S)p.dom.firstChild.setAttribute(I.name,I.value);ae(p.dom.firstChild),i>0&&p.dom.firstChild.setAttribute("data-nesting-level",i.toString()),d.append(...Array.from(p.dom.childNodes))}else d.append(p.dom),i>0&&p.dom.setAttribute("data-nesting-level",i.toString());if(p.contentDOM&&t.content){const S=le(e,t.content,o,a);p.contentDOM.appendChild(S)}let f;if(r.has(t.type)?f="OL":s.has(t.type)&&(f="UL"),f){if(((L=n.lastChild)==null?void 0:L.nodeName)!==f){const S=c.createElement(f);f==="OL"&&"start"in u&&u.start&&(u==null?void 0:u.start)!==1&&S.setAttribute("start",u.start+""),n.append(S)}n.lastChild.appendChild(d)}else n.append(d);if(t.children&&t.children.length>0){const S=c.createDocumentFragment();if(ce(S,e,t.children,o,r,s,i+1,a),((X=n.lastChild)==null?void 0:X.nodeName)==="UL"||((W=n.lastChild)==null?void 0:W.nodeName)==="OL")for(;((G=S.firstChild)==null?void 0:G.nodeName)==="UL"||((J=S.firstChild)==null?void 0:J.nodeName)==="OL";)n.lastChild.lastChild.appendChild(S.firstChild);e.pmSchema.nodes[t.type].isInGroup("blockContent")?n.append(S):(Q=p.contentDOM)==null||Q.append(S)}}const ce=(n,e,t,o,r,s,i=0,a)=>{for(const c of t)Ve(n,e,c,o,r,s,i,a)},He=(n,e,t,o,r,s)=>{const a=((s==null?void 0:s.document)??document).createDocumentFragment();return ce(a,n,e,t,o,r,0,s),a},Y=(n,e)=>{const t=H.DOMSerializer.fromSchema(n);return{exportBlocks:(o,r)=>{const s=He(e,o,t,new Set(["numberedListItem"]),new Set(["bulletListItem","checkListItem","toggleListItem"]),r),i=document.createElement("div");return i.append(s),i.innerHTML},exportInlineContent:(o,r)=>{const s=le(e,o,t,r),i=document.createElement("div");return i.append(s.cloneNode(!0)),i.innerHTML}}};function Fe(n,e){if(e===0)return;const t=n.resolve(e);for(let o=t.depth;o>0;o--){const r=t.node(o);if(O.isNodeBlock(r))return r.attrs.id}}function _e(n){return n.getMeta("paste")?{type:"paste"}:n.getMeta("uiEvent")==="drop"?{type:"drop"}:n.getMeta("history$")?{type:n.getMeta("history$").redo?"redo":"undo"}:n.getMeta("y-sync$")?n.getMeta("y-sync$").isUndoRedoOperation?{type:"undo-redo"}:{type:"yjs-remote"}:{type:"local"}}function ee(n){const e="__root__",t={},o={},r=k.getPmSchema(n);return n.descendants((s,i)=>{if(!O.isNodeBlock(s))return!0;const a=Fe(n,i),c=a??e;o[c]||(o[c]=[]);const l=k.nodeToBlock(s,r);return t[s.attrs.id]={block:l,parentId:a},o[c].push(s.attrs.id),!0}),{byId:t,childrenByParent:o}}function Ue(n,e){const t=new Set;if(!n||!e)return t;const o=new Set(n),r=e.filter(d=>o.has(d)),s=n.filter(d=>r.includes(d));if(s.length<=1||r.length<=1)return t;const i={};for(let d=0;di[d]),c=a.length,l=[],u=[],h=new Array(c).fill(-1),g=(d,f)=>{let y=0,w=d.length;for(;y>>1;d[x]0&&(h[d]=u[y-1]),y===l.length?(l.push(f),u.push(d)):(l[y]=f,u[y]=d)}const m=new Set;let p=u[u.length-1]??-1;for(;p!==-1;)m.add(p),p=h[p];for(let d=0;d!(m in r.byId)).forEach(m=>{i.push({type:"insert",block:s.byId[m].block,source:t,prevBlock:void 0}),a.add(m)}),Object.keys(r.byId).filter(m=>!(m in s.byId)).forEach(m=>{i.push({type:"delete",block:r.byId[m].block,source:t,prevBlock:void 0}),a.add(m)}),Object.keys(s.byId).filter(m=>m in r.byId).forEach(m=>{var y,w;const p=r.byId[m],d=s.byId[m];p.parentId!==d.parentId?(i.push({type:"move",block:d.block,prevBlock:p.block,source:t,prevParent:p.parentId?(y=r.byId[p.parentId])==null?void 0:y.block:void 0,currentParent:d.parentId?(w=s.byId[d.parentId])==null?void 0:w.block:void 0}),a.add(m)):Me.default({...p.block,children:void 0},{...d.block,children:void 0})||(i.push({type:"update",block:d.block,prevBlock:p.block,source:t}),a.add(m))});const c=r.childrenByParent,l=s.childrenByParent,u="__root__",h=new Set([...Object.keys(c),...Object.keys(l)]),g=new Set;return h.forEach(m=>{const p=Ue(c[m],l[m]);p.size!==0&&p.forEach(d=>{var x,L;const f=r.byId[d],y=s.byId[d];!f||!y||f.parentId!==y.parentId||a.has(d)||(f.parentId??u)!==m||g.has(d)||(g.add(d),i.push({type:"move",block:y.block,prevBlock:f.block,source:t,prevParent:f.parentId?(x=r.byId[f.parentId])==null?void 0:x.block:void 0,currentParent:y.parentId?(L=s.byId[y.parentId])==null?void 0:L.block:void 0}),a.add(d))})}),i}const $e=v.createExtension(()=>{const n=[];return{key:"blockChange",prosemirrorPlugins:[new C.Plugin({key:new C.PluginKey("blockChange"),filterTransaction:e=>{let t;return n.reduce((o,r)=>o===!1?o:r({getChanges(){return t||(t=de(e),t)},tr:e})!==!1,!0)}})],subscribe(e){return n.push(e),()=>{n.splice(n.indexOf(e),1)}}}});function te(n){const e=n.charAt(0)==="#"?n.substring(1,7):n,t=parseInt(e.substring(0,2),16),o=parseInt(e.substring(2,4),16),r=parseInt(e.substring(4,6),16),i=[t/255,o/255,r/255].map(c=>c<=.03928?c/12.92:Math.pow((c+.055)/1.055,2.4));return .2126*i[0]+.7152*i[1]+.0722*i[2]<=.179}function qe(n){const e=document.createElement("span");e.classList.add("bn-collaboration-cursor__base");const t=document.createElement("span");t.setAttribute("contentedEditable","false"),t.classList.add("bn-collaboration-cursor__caret"),t.setAttribute("style",`background-color: ${n.color}; color: ${te(n.color)?"white":"black"}`);const o=document.createElement("span");return o.classList.add("bn-collaboration-cursor__label"),o.setAttribute("style",`background-color: ${n.color}; color: ${te(n.color)?"white":"black"}`),o.insertBefore(document.createTextNode(n.name),null),t.insertBefore(o,null),e.insertBefore(document.createTextNode("⁠"),null),e.insertBefore(t,null),e.insertBefore(document.createTextNode("⁠"),null),e}const z=v.createExtension(({options:n})=>{const e=new Map,t=n.provider&&"awareness"in n.provider&&typeof n.provider.awareness=="object"?n.provider.awareness:void 0;return t&&("setLocalStateField"in t&&typeof t.setLocalStateField=="function"&&t.setLocalStateField("user",n.user),"on"in t&&typeof t.on=="function"&&n.showCursorLabels!=="always"&&t.on("change",({updated:o})=>{for(const r of o){const s=e.get(r);s&&(s.element.setAttribute("data-active",""),s.hideTimeout&&clearTimeout(s.hideTimeout),e.set(r,{element:s.element,hideTimeout:setTimeout(()=>{s.element.removeAttribute("data-active")},2e3)}))}})),{key:"yCursor",prosemirrorPlugins:[t?M.yCursorPlugin(t,{selectionBuilder:M.defaultSelectionBuilder,cursorBuilder(o,r){let s=e.get(r);if(!s){const i=(n.renderCursor??qe)(o);n.showCursorLabels!=="always"&&(i.addEventListener("mouseenter",()=>{const a=e.get(r);a.element.setAttribute("data-active",""),a.hideTimeout&&(clearTimeout(a.hideTimeout),e.set(r,{element:a.element,hideTimeout:void 0}))}),i.addEventListener("mouseleave",()=>{const a=e.get(r);e.set(r,{element:a.element,hideTimeout:setTimeout(()=>{a.element.removeAttribute("data-active")},2e3)})})),s={element:i,hideTimeout:void 0},e.set(r,s)}return s.element}}):void 0].filter(Boolean),dependsOn:["ySync"],updateUser(o){t==null||t.setLocalStateField("user",o)}}}),U=v.createExtension(({options:n})=>({key:"ySync",prosemirrorPlugins:[M.ySyncPlugin(n.fragment)],runsBefore:["default"]})),$=v.createExtension(()=>({key:"yUndo",prosemirrorPlugins:[M.yUndoPlugin()],dependsOn:["yCursor","ySync"],undoCommand:M.undoCommand,redoCommand:M.redoCommand}));function ze(n,e){const t=n.doc;if(n._item===null){const o=Array.from(t.share.keys()).find(r=>t.share.get(r)===n);if(o==null)throw new Error("type does not exist in other ydoc");return e.get(o,n.constructor)}else{const o=n._item,r=e.store.clients.get(o.id.client)??[],s=B.findIndexSS(r,o.id.clock);return r[s].content.type}}const Ke=v.createExtension(({editor:n,options:e})=>{let t;const o=v.createStore({isForked:!1});return{key:"yForkDoc",store:o,fork(){if(t)return;const r=e.fragment;if(!r)throw new Error("No fragment to fork from");const s=new B.Doc;B.applyUpdate(s,B.encodeStateAsUpdate(r.doc));const i=ze(r,s);t={undoStack:M.yUndoPluginKey.getState(n.prosemirrorState).undoManager.undoStack,originalFragment:r,forkedFragment:i},n.unregisterExtension([$,z,U]);const a={...e,fragment:i};n.registerExtension([U(a),$()]),o.setState({isForked:!0})},merge({keepChanges:r}){if(!t)return;n.unregisterExtension(["ySync","yCursor","yUndo"]);const{originalFragment:s,forkedFragment:i,undoStack:a}=t;if(n.registerExtension([U(e),z(e),$()]),M.yUndoPluginKey.getState(n.prosemirrorState).undoManager.undoStack=a,r){const c=B.encodeStateAsUpdate(i.doc,B.encodeStateVector(s.doc));B.applyUpdate(s.doc,c,n)}t=void 0,o.setState({isForked:!1})}}}),ue=(n,e)=>{e(n),n.forEach(t=>{t instanceof B.XmlElement&&ue(t,e)})},Ye=(n,e)=>{const t=new Map;return n.forEach(o=>{o instanceof B.XmlElement&&ue(o,r=>{if(r.nodeName==="blockContainer"&&r.hasAttribute("id")){const s=r.getAttribute("textColor"),i=r.getAttribute("backgroundColor"),a={textColor:s===O.defaultProps.textColor.default?void 0:s,backgroundColor:i===O.defaultProps.backgroundColor.default?void 0:i};(a.textColor||a.backgroundColor)&&t.set(r.getAttribute("id"),a)}})}),t.size===0?!1:(e.doc.descendants((o,r)=>{if(o.type.name==="blockContainer"&&t.has(o.attrs.id)){const s=e.doc.nodeAt(r+1);if(!s)throw new Error("No element found");e.setNodeMarkup(r+1,void 0,{...s.attrs,...t.get(o.attrs.id)})}}),!0)},je=[Ye],Xe=v.createExtension(({options:n})=>{let e=!1;const t=new V.PluginKey("schemaMigration");return{key:"schemaMigration",prosemirrorPlugins:[new V.Plugin({key:t,appendTransaction:(o,r,s)=>{if(e||!o.some(a=>a.getMeta("y-sync$"))||o.every(a=>!a.docChanged)||!n.fragment.firstChild)return;const i=s.tr;for(const a of je)a(n.fragment,i);if(e=!0,!!i.docChanged)return i}})]}}),We=v.createExtension(({editor:n,options:e})=>({key:"dropCursor",prosemirrorPlugins:[(e.dropCursor??ve.dropCursor)({width:5,color:"#ddeeff",editor:n})]})),Ge=v.createExtension(({editor:n})=>{const e=v.createStore(!1),t=()=>n.transact(o=>{var s;if(o.selection.empty||o.selection instanceof C.NodeSelection&&(o.selection.node.type.spec.content==="inline*"||((s=o.selection.node.firstChild)==null?void 0:s.type.spec.content)==="inline*")||o.selection instanceof C.TextSelection&&o.doc.textBetween(o.selection.from,o.selection.to).length===0)return!1;let r=!1;return o.selection.content().content.descendants(i=>(i.type.spec.code&&(r=!0),!r)),!r});return{key:"formattingToolbar",store:e,mount({dom:o,signal:r}){let s=!1;const i=n.onChange(()=>{s||e.setState(t())}),a=n.onSelectionChange(()=>{s||e.setState(t())});o.addEventListener("pointerdown",()=>{s=!0,e.setState(!1)},{signal:r}),n.prosemirrorView.root.addEventListener("pointerup",()=>{s=!1,n.isFocused()&&e.setState(t())},{signal:r,capture:!0}),o.addEventListener("pointercancel",()=>{s=!1},{signal:r,capture:!0}),r.addEventListener("abort",()=>{i(),a()})}}}),Je=v.createExtension(()=>({key:"history",prosemirrorPlugins:[q.history()],undoCommand:q.undo,redoCommand:q.redo})),Qe=v.createExtension(({editor:n})=>{function e(r){let s=n.prosemirrorView.nodeDOM(r);for(;s&&s.parentElement;){if(s.nodeName==="A")return s;s=s.parentElement}return null}function t(r,s){return n.transact(i=>{const a=i.doc.resolve(r),c=a.marks().find(u=>u.type.name===s);if(!c)return;const l=R.getMarkRange(a,c.type);if(l)return{range:l,mark:c,get text(){return i.doc.textBetween(l.from,l.to)},get position(){return R.posToDOMRect(n.prosemirrorView,l.from,l.to).toJSON()}}})}function o(){return n.transact(r=>{const s=r.selection;if(s.empty)return t(s.anchor,"link")})}return{key:"linkToolbar",getLinkAtSelection:o,getLinkElementAtPos:e,getMarkAtPos:t,getLinkAtElement(r){return n.transact(()=>{const s=n.prosemirrorView.posAtDOM(r,0)+1;return t(s,"link")})},editLink(r,s,i=n.transact(a=>a.selection.anchor)){n.transact(a=>{const c=k.getPmSchema(a),{range:l}=t(i+1,"link")||{range:{from:a.selection.from,to:a.selection.to}};l&&(a.insertText(s,l.from,l.to),a.addMark(l.from,l.from+s.length,c.mark("link",{href:r})))}),n.prosemirrorView.focus()},deleteLink(r=n.transact(s=>s.selection.anchor)){n.transact(s=>{const i=k.getPmSchema(s),{range:a}=t(r+1,"link")||{range:{from:s.selection.from,to:s.selection.to}};a&&s.removeMark(a.from,a.to,i.marks.link).setMeta("preventAutolink",!0)}),n.prosemirrorView.focus()}}}),Ze=["http","https","ftp","ftps","mailto","tel","callto","sms","cid","xmpp"],et="https",tt=new C.PluginKey("node-selection-keyboard"),ot=v.createExtension(()=>({key:"nodeSelectionKeyboard",prosemirrorPlugins:[new C.Plugin({key:tt,props:{handleKeyDown:(n,e)=>{if("node"in n.state.selection){if(e.ctrlKey||e.metaKey)return!1;if(e.key.length===1)return e.preventDefault(),!0;if(e.key==="Enter"&&!e.isComposing&&!e.shiftKey&&!e.altKey&&!e.ctrlKey&&!e.metaKey){const t=n.state.tr;return n.dispatch(t.insert(n.state.tr.selection.$to.after(),n.state.schema.nodes.paragraph.createChecked()).setSelection(new C.TextSelection(t.doc.resolve(n.state.tr.selection.$to.after()+1)))),!0}}return!1}}})]})),nt=new C.PluginKey("blocknote-placeholder"),rt=v.createExtension(({editor:n,options:e})=>{const t=e.placeholders;return{key:"placeholder",prosemirrorPlugins:[new C.Plugin({key:nt,view:o=>{const r=`placeholder-selector-${Se.v4()}`;o.dom.classList.add(r);const s=document.createElement("style"),i=n._tiptapEditor.options.injectNonce;i&&s.setAttribute("nonce",i),o.root instanceof window.ShadowRoot?o.root.append(s):o.root.head.appendChild(s);const a=s.sheet,c=(l="")=>`.${r} .bn-block-content${l} .bn-inline-content:has(> .ProseMirror-trailingBreak:only-child):before`;try{const{default:l,emptyDocument:u,...h}=t||{};for(const[p,d]of Object.entries(h)){const f=`[data-content-type="${p}"]`;a.insertRule(`${c(f)} { content: ${JSON.stringify(d)}; }`)}const g="[data-is-only-empty-block]",m="[data-is-empty-and-focused]";a.insertRule(`${c(g)} { content: ${JSON.stringify(u)}; }`),a.insertRule(`${c(m)} { content: ${JSON.stringify(l)}; }`)}catch(l){console.warn("Failed to insert placeholder CSS rule - this is likely due to the browser not supporting certain CSS pseudo-element selectors (:has, :only-child:, or :before)",l)}return{destroy:()=>{o.root instanceof window.ShadowRoot?o.root.removeChild(s):o.root.head.removeChild(s)}}},props:{decorations:o=>{const{doc:r,selection:s}=o;if(!n.isEditable||!s.empty||s.$from.parent.type.spec.code)return;const i=[];o.doc.content.size===6&&i.push(D.Decoration.node(2,4,{"data-is-only-empty-block":"true"}));const a=s.$anchor,c=a.parent;if(c.content.size===0){const l=a.before();i.push(D.Decoration.node(l,l+c.nodeSize,{"data-is-empty-and-focused":"true"}))}return D.DecorationSet.create(r,i)}}})]}}),oe=new C.PluginKey("previous-blocks"),st={index:"index",level:"level",type:"type",depth:"depth","depth-change":"depth-change"},it=v.createExtension(()=>{let n;return{key:"previousBlockType",prosemirrorPlugins:[new C.Plugin({key:oe,view(e){return{update:async(t,o)=>{var r;((r=this.key)==null?void 0:r.getState(t.state).updatedBlocks.size)>0&&(n=setTimeout(()=>{t.dispatch(t.state.tr.setMeta(oe,{clearUpdate:!0}))},0))},destroy:()=>{n&&clearTimeout(n)}}},state:{init(){return{prevTransactionOldBlockAttrs:{},currentTransactionOldBlockAttrs:{},updatedBlocks:new Set}},apply(e,t,o,r){if(t.currentTransactionOldBlockAttrs={},t.updatedBlocks.clear(),!e.docChanged||o.doc.eq(r.doc))return t;const s={},i=R.findChildren(o.doc,l=>l.attrs.id),a=new Map(i.map(l=>[l.node.attrs.id,l])),c=R.findChildren(r.doc,l=>l.attrs.id);for(const l of c){const u=a.get(l.node.attrs.id),h=u==null?void 0:u.node.firstChild,g=l.node.firstChild;if(u&&h&&g){const m={index:g.attrs.index,level:g.attrs.level,type:g.type.name,depth:r.doc.resolve(l.pos).depth},p={index:h.attrs.index,level:h.attrs.level,type:h.type.name,depth:o.doc.resolve(u.pos).depth};s[l.node.attrs.id]=p,t.currentTransactionOldBlockAttrs[l.node.attrs.id]=p,JSON.stringify(p)!==JSON.stringify(m)&&(p["depth-change"]=p.depth-m.depth,t.updatedBlocks.add(l.node.attrs.id))}}return t.prevTransactionOldBlockAttrs=s,t}},props:{decorations(e){const t=this.getState(e);if(t.updatedBlocks.size===0)return;const o=[];return e.doc.descendants((r,s)=>{if(!r.attrs.id||!t.updatedBlocks.has(r.attrs.id))return;const i=t.currentTransactionOldBlockAttrs[r.attrs.id],a={};for(const[l,u]of Object.entries(i))a["data-prev-"+st[l]]=u||"none";const c=D.Decoration.node(s,s+r.nodeSize,{...a});o.push(c)}),D.DecorationSet.create(e.doc,o)}}})]}});function he(n,e){var t,o;for(;n&&n.parentElement&&n.parentElement!==e.dom&&((t=n.getAttribute)==null?void 0:t.call(n,"data-node-type"))!=="blockContainer";)n=n.parentElement;if(((o=n.getAttribute)==null?void 0:o.call(n,"data-node-type"))==="blockContainer")return{node:n,id:n.getAttribute("data-id")}}function at(){const n=e=>{let t=e.children.length;for(let o=0;o0){e.children.splice(o,1,...r.children);const s=r.children.length-1;t+=s,o+=s}else e.children.splice(o,1),t--,o--}};return n}function lt(){const n=e=>{var t;if(e.children&&"length"in e.children&&e.children.length)for(let o=e.children.length-1;o>=0;o--){const r=e.children[o],s=o+1{De.visit(n,"element",(e,t,o)=>{var r,s,i,a;if(o&&e.tagName==="video"){const c=((r=e.properties)==null?void 0:r.src)||((s=e.properties)==null?void 0:s["data-url"])||"",l=((i=e.properties)==null?void 0:i.title)||((a=e.properties)==null?void 0:a["data-name"])||"";o.children[t]={type:"text",value:`![${l}](${c})`}}})}}function j(n){return Te.unified().use(Ae.default,{fragment:!0}).use(ct).use(at).use(lt).use(Ne.default).use(Le.default).use(Re.default,{handlers:{text:t=>t.value}}).processSync(n).value}function dt(n,e,t,o){const s=Y(e,t).exportBlocks(n,o);return j(s)}function me(n){const e=[];return n.descendants(t=>{var r,s;const o=k.getPmSchema(t);return t.type.name==="blockContainer"&&((r=t.firstChild)==null?void 0:r.type.name)==="blockGroup"?!0:t.type.name==="columnList"&&t.childCount===1?((s=t.firstChild)==null||s.forEach(i=>{e.push(k.nodeToBlock(i,o))}),!1):t.type.isInGroup("bnBlock")?(e.push(k.nodeToBlock(t,o)),!1):!0}),e}class A extends C.Selection{constructor(t,o){super(t,o);b(this,"nodes");const r=t.node();this.nodes=[],t.doc.nodesBetween(t.pos,o.pos,(s,i,a)=>{if(a!==null&&a.eq(r))return this.nodes.push(s),!1})}static create(t,o,r=o){return new A(t.resolve(o),t.resolve(r))}content(){return new H.Slice(H.Fragment.from(this.nodes),0,0)}eq(t){if(!(t instanceof A)||this.nodes.length!==t.nodes.length||this.from!==t.from||this.to!==t.to)return!1;for(let o=0;oArray.prototype.indexOf.call(h.children,g),i=s(r,n.domAtPos(e+1).node.parentElement),a=s(r,n.domAtPos(t-1).node.parentElement);for(let h=r.childElementCount-1;h>=0;h--)(h>a||hh!=="ProseMirror"&&h!=="bn-root"&&h!=="bn-editor").join(" ");P.className=P.className+" bn-drag-preview "+u,n.root instanceof ShadowRoot?n.root.appendChild(P):n.root.body.appendChild(P)}function pe(n){P!==void 0&&(n instanceof ShadowRoot?n.removeChild(P):n.body.removeChild(P),P=void 0)}function ht(n,e,t){if(!n.dataTransfer||t.headless)return;const o=t.prosemirrorView,r=O.getNodeById(e.id,o.state.doc);if(!r)throw new Error(`Block with ID ${e.id} not found`);const s=r.posBeforeNode;if(s!=null){const i=o.state.selection,a=o.state.doc,{from:c,to:l}=ut(i,a),u=c<=s&&s{this.state=e,this.emitUpdate(this.state)});b(this,"updateStateFromMousePos",()=>{var o,r,s,i,a;if(this.menuFrozen||!this.mousePos)return;const e=this.findClosestEditorElement({clientX:this.mousePos.x,clientY:this.mousePos.y});if((e==null?void 0:e.element)!==this.pmView.dom||e.distance>re){(o=this.state)!=null&&o.show&&(this.state.show=!1,this.updateState(this.state));return}const t=mt(this.mousePos,this.pmView);if(!t||!this.editor.isEditable){(r=this.state)!=null&&r.show&&(this.state.show=!1,this.updateState(this.state));return}if(!((s=this.state)!=null&&s.show&&((i=this.hoveredBlock)!=null&&i.hasAttribute("data-id"))&&((a=this.hoveredBlock)==null?void 0:a.getAttribute("data-id"))===t.id)&&(this.hoveredBlock=t.node,this.editor.isEditable)){const c=t.node.getBoundingClientRect(),l=t.node.closest("[data-node-type=column]");this.state={show:!0,referencePos:new DOMRect(l?l.firstElementChild.getBoundingClientRect().x:this.pmView.dom.firstChild.getBoundingClientRect().x,c.y,c.width,c.height),block:this.editor.getBlock(this.hoveredBlock.getAttribute("data-id"))},this.updateState(this.state)}});b(this,"onDragStart",e=>{var i;const t=(i=e.dataTransfer)==null?void 0:i.getData("blocknote/html");if(!t||this.pmView.dragging)return;const o=document.createElement("div");o.innerHTML=t;const s=Z.DOMParser.fromSchema(this.pmView.state.schema).parse(o,{topNode:this.pmView.state.schema.nodes.blockGroup.create()});this.pmView.dragging={slice:new Z.Slice(s.content,0,0),move:!0}});b(this,"findClosestEditorElement",e=>{const t=Array.from(this.pmView.root.querySelectorAll(".bn-editor"));if(t.length===0)return null;let o=t[0],r=Number.MAX_VALUE;return t.forEach(s=>{const i=s.querySelector(".bn-block-group").getBoundingClientRect(),a=e.clientXi.right?e.clientX-i.right:0,c=e.clientYi.bottom?e.clientY-i.bottom:0,l=Math.sqrt(Math.pow(a,2)+Math.pow(c,2));l{if(e.synthetic)return;const t=this.getDragEventContext(e);if(!t||!t.isDropPoint){this.closeDropCursor();return}t.isDropPoint&&!t.isDropWithinEditorBounds&&this.dispatchSyntheticEvent(e)});b(this,"closeDropCursor",()=>{const e=new Event("dragleave",{bubbles:!1});e.synthetic=!0,this.pmView.dom.dispatchEvent(e)});b(this,"getDragEventContext",e=>{var c;const t=!((c=e.dataTransfer)!=null&&c.types.includes("blocknote/html"))&&!!this.pmView.dragging,o=!!this.isDragOrigin,r=t||o,s=this.findClosestEditorElement(e);if(!s||s.distance>re)return;const i=s.element===this.pmView.dom,a=i&&s.distance===0;if(!(!i&&!r))return{isDropPoint:i,isDropWithinEditorBounds:a,isDragOrigin:r}});b(this,"onDrop",e=>{if(e.synthetic)return;const t=this.getDragEventContext(e);if(!t){this.closeDropCursor();return}const{isDropPoint:o,isDropWithinEditorBounds:r,isDragOrigin:s}=t;if(!r&&o&&this.dispatchSyntheticEvent(e),o){if(this.pmView.dragging)return;this.pmView.dispatch(this.pmView.state.tr.setSelection(V.TextSelection.create(this.pmView.state.tr.doc,this.pmView.state.tr.selection.anchor)));return}else if(s){setTimeout(()=>this.pmView.dispatch(this.pmView.state.tr.deleteSelection()),0);return}});b(this,"onDragEnd",e=>{e.synthetic||(this.pmView.dragging=null)});b(this,"onKeyDown",e=>{var t;(t=this.state)!=null&&t.show&&this.editor.isFocused()&&(this.state.show=!1,this.emitUpdate(this.state))});b(this,"onMouseMove",e=>{var s;if(this.menuFrozen)return;this.mousePos={x:e.clientX,y:e.clientY};const t=this.pmView.dom.getBoundingClientRect(),o=this.mousePos.x>t.left&&this.mousePos.xt.top&&this.mousePos.y{if(!this.state)throw new Error("Attempting to update uninitialized side menu");o(this.state)},this.pmView.root.addEventListener("dragstart",this.onDragStart),this.pmView.root.addEventListener("dragover",this.onDragOver),this.pmView.root.addEventListener("drop",this.onDrop,!0),this.pmView.root.addEventListener("dragend",this.onDragEnd,!0),this.pmView.root.addEventListener("mousemove",this.onMouseMove,!0),this.pmView.root.addEventListener("keydown",this.onKeyDown,!0)}dispatchSyntheticEvent(e){const t=new Event(e.type,e),o=this.pmView.dom.firstChild.getBoundingClientRect();t.clientX=e.clientX,t.clientY=e.clientY,t.clientX=Math.min(Math.max(e.clientX,o.left),o.left+o.width),t.clientY=Math.min(Math.max(e.clientY,o.top),o.top+o.height),t.dataTransfer=e.dataTransfer,t.preventDefault=()=>e.preventDefault(),t.synthetic=!0,this.pmView.dom.dispatchEvent(t)}update(e,t){var r;!t.doc.eq(this.pmView.state.doc)&&((r=this.state)!=null&&r.show)&&this.updateStateFromMousePos()}destroy(){var e;(e=this.state)!=null&&e.show&&(this.state.show=!1,this.emitUpdate(this.state)),this.pmView.root.removeEventListener("mousemove",this.onMouseMove,!0),this.pmView.root.removeEventListener("dragstart",this.onDragStart),this.pmView.root.removeEventListener("dragover",this.onDragOver),this.pmView.root.removeEventListener("drop",this.onDrop,!0),this.pmView.root.removeEventListener("dragend",this.onDragEnd,!0),this.pmView.root.removeEventListener("keydown",this.onKeyDown,!0)}}const ge=new V.PluginKey("SideMenuPlugin"),pt=v.createExtension(({editor:n})=>{let e;const t=v.createStore(void 0);return{key:"sideMenu",store:t,prosemirrorPlugins:[new V.Plugin({key:ge,view:o=>(e=new fe(n,o,r=>{t.setState({...r})}),e)})],blockDragStart(o,r){e&&(e.isDragOrigin=!0),ht(o,r,n)},blockDragEnd(){pe(n.prosemirrorView.root),e&&(e.isDragOrigin=!1),n.blur()},freezeMenu(){e.menuFrozen=!0,e.state.show=!0,e.emitUpdate(e.state)},unfreezeMenu(){e.menuFrozen=!1,e.state.show=!1,e.emitUpdate(e.state)}}});let E;function se(n){E||(E=document.createElement("div"),E.innerHTML="_",E.style.opacity="0",E.style.height="1px",E.style.width="1px",n instanceof Document?n.body.appendChild(E):n.appendChild(E))}function ft(n){E&&(n instanceof Document?n.body.removeChild(E):n.removeChild(E),E=void 0)}function _(n){return Array.prototype.indexOf.call(n.parentElement.childNodes,n)}function gt(n){let e=n;for(;e&&e.nodeName!=="TD"&&e.nodeName!=="TH"&&!e.classList.contains("tableWrapper");){if(e.classList.contains("ProseMirror"))return;const t=e.parentNode;if(!t||!(t instanceof Element))return;e=t}return e.nodeName==="TD"||e.nodeName==="TH"?{type:"cell",domNode:e,tbodyNode:e.closest("tbody")}:{type:"wrapper",domNode:e,tbodyNode:e.querySelector("tbody")}}function yt(n,e){const t=e.querySelectorAll(n);for(let o=0;o{this.mouseState="down"});b(this,"mouseUpHandler",e=>{this.mouseState="up",this.mouseMoveHandler(e)});b(this,"mouseMoveHandler",e=>{var l,u,h,g,m,p,d,f;if(this.menuFrozen||this.mouseState==="selecting"||!(e.target instanceof Element)||!this.pmView.dom.contains(e.target))return;const t=gt(e.target);if((t==null?void 0:t.type)==="cell"&&this.mouseState==="down"&&!((l=this.state)!=null&&l.draggingState)){this.mouseState="selecting",(u=this.state)!=null&&u.show&&(this.state.show=!1,this.state.showAddOrRemoveRowsButton=!1,this.state.showAddOrRemoveColumnsButton=!1,this.emitUpdate());return}if(!t||!this.editor.isEditable){(h=this.state)!=null&&h.show&&(this.state.show=!1,this.state.showAddOrRemoveRowsButton=!1,this.state.showAddOrRemoveColumnsButton=!1,this.emitUpdate());return}if(!t.tbodyNode)return;const o=t.tbodyNode.getBoundingClientRect(),r=he(t.domNode,this.pmView);if(!r)return;this.tableElement=r.node;let s;const i=this.editor.transact(y=>O.getNodeById(r.id,y.doc));if(!i)throw new Error(`Block with ID ${r.id} not found`);const a=k.nodeToBlock(i.node,this.editor.pmSchema,this.editor.schema.blockSchema,this.editor.schema.inlineContentSchema,this.editor.schema.styleSchema);if(O.editorHasBlockWithType(this.editor,"table")&&(this.tablePos=i.posBeforeNode+1,s=a),!s)return;this.tableId=r.id;const c=(g=t.domNode.closest(".tableWrapper"))==null?void 0:g.querySelector(".table-widgets-container");if((t==null?void 0:t.type)==="wrapper"){const y=e.clientY>=o.bottom-1&&e.clientY=o.right-1&&e.clientXo.right||e.clientY>o.bottom;this.state={...this.state,show:!0,showAddOrRemoveRowsButton:y,showAddOrRemoveColumnsButton:w,referencePosTable:o,block:s,widgetContainer:c,colIndex:x||(p=this.state)==null?void 0:p.colIndex,rowIndex:x||(d=this.state)==null?void 0:d.rowIndex,referencePosCell:x||(f=this.state)==null?void 0:f.referencePosCell}}else{const y=_(t.domNode),w=_(t.domNode.parentElement),x=t.domNode.getBoundingClientRect();if(this.state!==void 0&&this.state.show&&this.tableId===r.id&&this.state.rowIndex===w&&this.state.colIndex===y)return;this.state={show:!0,showAddOrRemoveColumnsButton:y===s.content.rows[0].cells.length-1,showAddOrRemoveRowsButton:w===s.content.rows.length-1,referencePosTable:o,block:s,draggingState:void 0,referencePosCell:x,colIndex:y,rowIndex:w,widgetContainer:c}}return this.emitUpdate(),!1});b(this,"dragOverHandler",e=>{var g;if(((g=this.state)==null?void 0:g.draggingState)===void 0)return;e.preventDefault(),e.dataTransfer.dropEffect="move",yt(".prosemirror-dropcursor-block, .prosemirror-dropcursor-inline",this.pmView.root);const t={left:Math.min(Math.max(e.clientX,this.state.referencePosTable.left+1),this.state.referencePosTable.right-1),top:Math.min(Math.max(e.clientY,this.state.referencePosTable.top+1),this.state.referencePosTable.bottom-1)},o=this.pmView.root.elementsFromPoint(t.left,t.top).filter(m=>m.tagName==="TD"||m.tagName==="TH");if(o.length===0)return;const r=o[0];let s=!1;const i=_(r.parentElement),a=_(r),c=this.state.draggingState.draggedCellOrientation==="row"?this.state.rowIndex:this.state.colIndex,u=(this.state.draggingState.draggedCellOrientation==="row"?i:a)!==c;(this.state.rowIndex!==i||this.state.colIndex!==a)&&(this.state.rowIndex=i,this.state.colIndex=a,this.state.referencePosCell=r.getBoundingClientRect(),s=!0);const h=this.state.draggingState.draggedCellOrientation==="row"?t.top:t.left;this.state.draggingState.mousePos!==h&&(this.state.draggingState.mousePos=h,s=!0),s&&this.emitUpdate(),u&&this.editor.transact(m=>m.setMeta(N,!0))});b(this,"dropHandler",e=>{if(this.mouseState="up",this.state===void 0||this.state.draggingState===void 0)return!1;if(this.state.rowIndex===void 0||this.state.colIndex===void 0)throw new Error("Attempted to drop table row or column, but no table block was hovered prior.");e.preventDefault();const{draggingState:t,colIndex:o,rowIndex:r}=this.state,s=this.state.block.content.columnWidths;if(t.draggedCellOrientation==="row"){if(!k.canRowBeDraggedInto(this.state.block,t.originalIndex,r))return!1;const i=k.moveRow(this.state.block,t.originalIndex,r);this.editor.updateBlock(this.state.block,{type:"table",content:{...this.state.block.content,rows:i}})}else{if(!k.canColumnBeDraggedInto(this.state.block,t.originalIndex,o))return!1;const i=k.moveColumn(this.state.block,t.originalIndex,o),[a]=s.splice(t.originalIndex,1);s.splice(o,0,a),this.editor.updateBlock(this.state.block,{type:"table",content:{...this.state.block.content,columnWidths:s,rows:i}})}return this.editor.setTextCursorPosition(this.state.block.id),!0});this.editor=e,this.pmView=t,this.emitUpdate=()=>{if(!this.state)throw new Error("Attempting to update uninitialized image toolbar");o(this.state)},t.dom.addEventListener("mousemove",this.mouseMoveHandler),t.dom.addEventListener("mousedown",this.viewMousedownHandler),window.addEventListener("mouseup",this.mouseUpHandler),t.root.addEventListener("dragover",this.dragOverHandler),t.root.addEventListener("drop",this.dropHandler)}update(){var r;if(!this.state||!this.state.show)return;if(this.state.block=this.editor.getBlock(this.state.block.id),!this.state.block||this.state.block.type!=="table"||!((r=this.tableElement)!=null&&r.isConnected)){this.state.show=!1,this.state.showAddOrRemoveRowsButton=!1,this.state.showAddOrRemoveColumnsButton=!1,this.emitUpdate();return}const{height:e,width:t}=k.getDimensionsOfTable(this.state.block);this.state.rowIndex!==void 0&&this.state.colIndex!==void 0&&(this.state.rowIndex>=e&&(this.state.rowIndex=e-1),this.state.colIndex>=t&&(this.state.colIndex=t-1));const o=this.tableElement.querySelector("tbody");if(!o)throw new Error("Table block does not contain a 'tbody' HTML element. This should never happen.");if(this.state.rowIndex!==void 0&&this.state.colIndex!==void 0){const i=o.children[this.state.rowIndex].children[this.state.colIndex];i?this.state.referencePosCell=i.getBoundingClientRect():(this.state.rowIndex=void 0,this.state.colIndex=void 0)}this.state.referencePosTable=o.getBoundingClientRect(),this.emitUpdate()}destroy(){this.pmView.dom.removeEventListener("mousemove",this.mouseMoveHandler),window.removeEventListener("mouseup",this.mouseUpHandler),this.pmView.dom.removeEventListener("mousedown",this.viewMousedownHandler),this.pmView.root.removeEventListener("dragover",this.dragOverHandler),this.pmView.root.removeEventListener("drop",this.dropHandler)}}const N=new C.PluginKey("TableHandlesPlugin"),wt=v.createExtension(({editor:n})=>{let e;const t=v.createStore(void 0);return{key:"tableHandles",store:t,prosemirrorPlugins:[new C.Plugin({key:N,view:o=>(e=new ye(n,o,r=>{t.setState(r.block?{...r,draggingState:r.draggingState?{...r.draggingState}:void 0}:void 0)}),e),props:{decorations:o=>{if(e===void 0||e.state===void 0||e.state.draggingState===void 0||e.tablePos===void 0)return;const r=e.state.draggingState.draggedCellOrientation==="row"?e.state.rowIndex:e.state.colIndex;if(r===void 0)return;const s=[],{block:i,draggingState:a}=e.state,{originalIndex:c,draggedCellOrientation:l}=a;if(r===c||!i||l==="row"&&!k.canRowBeDraggedInto(i,c,r)||l==="col"&&!k.canColumnBeDraggedInto(i,c,r))return D.DecorationSet.create(o.doc,s);const u=o.doc.resolve(e.tablePos+1);return e.state.draggingState.draggedCellOrientation==="row"?k.getCellsAtRowHandle(e.state.block,r).forEach(({row:g,col:m})=>{const p=o.doc.resolve(u.posAtIndex(g)+1),d=o.doc.resolve(p.posAtIndex(m)+1),f=d.node(),y=d.pos+(r>c?f.nodeSize-2:0);s.push(D.Decoration.widget(y,()=>{const w=document.createElement("div");return w.className="bn-table-drop-cursor",w.style.left="0",w.style.right="0",r>c?w.style.bottom="-2px":w.style.top="-3px",w.style.height="4px",w}))}):k.getCellsAtColumnHandle(e.state.block,r).forEach(({row:g,col:m})=>{const p=o.doc.resolve(u.posAtIndex(g)+1),d=o.doc.resolve(p.posAtIndex(m)+1),f=d.node(),y=d.pos+(r>c?f.nodeSize-2:0);s.push(D.Decoration.widget(y,()=>{const w=document.createElement("div");return w.className="bn-table-drop-cursor",w.style.top="0",w.style.bottom="0",r>c?w.style.right="-2px":w.style.left="-3px",w.style.width="4px",w}))}),D.DecorationSet.create(o.doc,s)}}})],colDragStart(o){if(e===void 0||e.state===void 0||e.state.colIndex===void 0)throw new Error("Attempted to drag table column, but no table block was hovered prior.");e.state.draggingState={draggedCellOrientation:"col",originalIndex:e.state.colIndex,mousePos:o.clientX},e.emitUpdate(),n.transact(r=>r.setMeta(N,{draggedCellOrientation:e.state.draggingState.draggedCellOrientation,originalIndex:e.state.colIndex,newIndex:e.state.colIndex,tablePos:e.tablePos})),!n.headless&&(se(n.prosemirrorView.root),o.dataTransfer.setDragImage(E,0,0),o.dataTransfer.effectAllowed="move")},rowDragStart(o){if(e.state===void 0||e.state.rowIndex===void 0)throw new Error("Attempted to drag table row, but no table block was hovered prior.");e.state.draggingState={draggedCellOrientation:"row",originalIndex:e.state.rowIndex,mousePos:o.clientY},e.emitUpdate(),n.transact(r=>r.setMeta(N,{draggedCellOrientation:e.state.draggingState.draggedCellOrientation,originalIndex:e.state.rowIndex,newIndex:e.state.rowIndex,tablePos:e.tablePos})),!n.headless&&(se(n.prosemirrorView.root),o.dataTransfer.setDragImage(E,0,0),o.dataTransfer.effectAllowed="copyMove")},dragEnd(){if(e.state===void 0)throw new Error("Attempted to drag table row, but no table block was hovered prior.");e.state.draggingState=void 0,e.emitUpdate(),n.transact(o=>o.setMeta(N,null)),!n.headless&&ft(n.prosemirrorView.root)},freezeHandles(){e.menuFrozen=!0},unfreezeHandles(){e.menuFrozen=!1},getCellsAtRowHandle(o,r){return k.getCellsAtRowHandle(o,r)},getCellsAtColumnHandle(o,r){return k.getCellsAtColumnHandle(o,r)},setCellSelection(o,r,s=r){if(!e)throw new Error("Table handles view not initialized");const i=o.doc.resolve(e.tablePos+1),a=o.doc.resolve(i.posAtIndex(r.row)+1),c=o.doc.resolve(a.posAtIndex(r.col)),l=o.doc.resolve(i.posAtIndex(s.row)+1),u=o.doc.resolve(l.posAtIndex(s.col)),h=o.tr;return h.setSelection(new T.CellSelection(c,u)),o.apply(h)},addRowOrColumn(o,r){n.exec((s,i)=>{const a=this.setCellSelection(s,r.orientation==="row"?{row:o,col:0}:{row:0,col:o});return r.orientation==="row"?r.side==="above"?T.addRowBefore(a,i):T.addRowAfter(a,i):r.side==="left"?T.addColumnBefore(a,i):T.addColumnAfter(a,i)})},removeRowOrColumn(o,r){return r==="row"?n.exec((s,i)=>{const a=this.setCellSelection(s,{row:o,col:0});return T.deleteRow(a,i)}):n.exec((s,i)=>{const a=this.setCellSelection(s,{row:0,col:o});return T.deleteColumn(a,i)})},mergeCells(o){return n.exec((r,s)=>{const i=o?this.setCellSelection(r,o.relativeStartCell,o.relativeEndCell):r;return T.mergeCells(i,s)})},splitCell(o){return n.exec((r,s)=>{const i=o?this.setCellSelection(r,o):r;return T.splitCell(i,s)})},getCellSelection(){return n.transact(o=>{const r=o.selection;let s=r.$from,i=r.$to;if(O.isTableCellSelection(r)){const{ranges:d}=r;d.forEach(f=>{s=f.$from.min(s??f.$from),i=f.$to.max(i??f.$to)})}else if(s=o.doc.resolve(r.$from.pos-r.$from.parentOffset-1),i=o.doc.resolve(r.$to.pos-r.$to.parentOffset-1),s.pos===0||i.pos===0)return;const a=o.doc.resolve(s.pos-s.parentOffset-1),c=o.doc.resolve(i.pos-i.parentOffset-1),l=o.doc.resolve(a.pos-a.parentOffset-1),u=s.index(a.depth),h=a.index(l.depth),g=i.index(c.depth),m=c.index(l.depth),p=[];for(let d=h;d<=m;d++)for(let f=u;f<=g;f++)p.push({row:d,col:f});return{from:{row:h,col:u},to:{row:m,col:g},cells:p}})},getMergeDirection(o){return n.transact(r=>{const s=O.isTableCellSelection(r.selection)?r.selection:void 0;if(!s||!o||s.ranges.length<=1)return;const i=this.getCellSelection();if(i)return k.areInSameColumn(i.from,i.to,o)?"vertical":"horizontal"})},cropEmptyRowsOrColumns(o,r){return k.cropEmptyRowsOrColumns(o,r)},addRowsOrColumns(o,r,s){return k.addRowsOrColumns(o,r,s)}}}),ie=new C.PluginKey("trailingNode"),bt=v.createExtension(()=>({key:"trailingNode",prosemirrorPlugins:[new C.Plugin({key:ie,appendTransaction:(n,e,t)=>{const{doc:o,tr:r,schema:s}=t,i=ie.getState(t),a=o.content.size-2,c=s.nodes.blockContainer,l=s.nodes.paragraph;if(i)return r.insert(a,c.create(void 0,l.create()))},state:{init:(n,e)=>{},apply:(n,e)=>{if(!n.docChanged)return e;let t=n.doc.lastChild;if(!t||t.type.name!=="blockGroup")throw new Error("Expected blockGroup");if(t=t.lastChild,!t||t.type.name!=="blockContainer")return!0;const o=t.firstChild;if(!o)throw new Error("Expected blockContent");return t.nodeSize>4||o.type.spec.content!=="inline*"}}})]}));exports.BlockChangeExtension=$e;exports.DEFAULT_LINK_PROTOCOL=et;exports.DropCursorExtension=We;exports.ForkYDocExtension=Ke;exports.FormattingToolbarExtension=Ge;exports.HistoryExtension=Je;exports.LinkToolbarExtension=Qe;exports.NodeSelectionKeyboardExtension=ot;exports.PlaceholderExtension=rt;exports.PreviousBlockTypeExtension=it;exports.SchemaMigration=Xe;exports.SideMenuExtension=pt;exports.SideMenuView=fe;exports.TableHandlesExtension=wt;exports.TableHandlesView=ye;exports.TrailingNodeExtension=bt;exports.VALID_LINK_PROTOCOLS=Ze;exports.YCursorExtension=z;exports.YSyncExtension=U;exports.YUndoExtension=$;exports.blocksToMarkdown=dt;exports.cleanHTMLToMarkdown=j;exports.createExternalHTMLExporter=Y;exports.fragmentToBlocks=me;exports.getBlocksChangedByTransaction=de;exports.sideMenuPluginKey=ge;exports.tableHandlesPluginKey=N; ++"use strict";var we=Object.defineProperty;var be=(n,e,t)=>e in n?we(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var b=(n,e,t)=>be(n,typeof e!="symbol"?e+"":e,t);const C=require("prosemirror-state"),R=require("@tiptap/core"),ke=require("fast-deep-equal"),k=require("./blockToNode-CumVjgem.cjs"),O=require("./defaultBlocks-DosClM5E.cjs"),v=require("./BlockNoteExtension-BWw0r8Gy.cjs"),M=require("y-prosemirror"),Ce=require("yjs"),V=require("@tiptap/pm/state"),ve=require("prosemirror-dropcursor"),q=require("@tiptap/pm/history"),D=require("prosemirror-view"),Se=require("uuid"),Z=require("@tiptap/pm/model"),H=require("prosemirror-model"),xe=require("rehype-parse"),Ee=require("rehype-remark"),Ie=require("remark-gfm"),Pe=require("remark-stringify"),Te=require("unified"),Be=require("hast-util-from-dom"),De=require("unist-util-visit"),T=require("prosemirror-tables"),F=n=>n&&typeof n=="object"&&"default"in n?n:{default:n};function Oe(n){if(n&&typeof n=="object"&&"default"in n)return n;const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(n){for(const t in n)if(t!=="default"){const o=Object.getOwnPropertyDescriptor(n,t);Object.defineProperty(e,t,o.get?o:{enumerable:!0,get:()=>n[t]})}}return e.default=n,Object.freeze(e)}const Me=F(ke),B=Oe(Ce),Ae=F(xe),Ne=F(Ee),Le=F(Ie),Re=F(Pe);function ae(n){const e=Array.from(n.classList).filter(t=>!t.startsWith("bn-"))||[];e.length>0?n.className=e.join(" "):n.removeAttribute("class")}function le(n,e,t,o){var a;let r;if(e)if(typeof e=="string")r=k.inlineContentToNodes([e],n.pmSchema);else if(Array.isArray(e))r=k.inlineContentToNodes(e,n.pmSchema);else if(e.type==="tableContent")r=k.tableContentToNodes(e,n.pmSchema);else throw new k.UnreachableCaseError(e.type);else throw new Error("blockContent is required");const i=((o==null?void 0:o.document)??document).createDocumentFragment();for(const c of r)if(c.type.name!=="text"&&n.schema.inlineContentSchema[c.type.name]){const l=n.schema.inlineContentSpecs[c.type.name].implementation;if(l){const u=k.nodeToCustomInlineContent(c,n.schema.inlineContentSchema,n.schema.styleSchema),h=l.toExternalHTML?l.toExternalHTML(u,n):l.render.call({renderType:"dom",props:void 0},u,()=>{},n);if(h){if(i.appendChild(h.dom),h.contentDOM){const g=t.serializeFragment(c.content,o);h.contentDOM.dataset.editable="",h.contentDOM.appendChild(g)}continue}}}else if(c.type.name==="text"){let l=document.createTextNode(c.textContent);for(const u of Array.from(c.marks).reverse())if(u.type.name in n.schema.styleSpecs){const h=(n.schema.styleSpecs[u.type.name].implementation.toExternalHTML??n.schema.styleSpecs[u.type.name].implementation.render)(u.attrs.stringValue,n);h.contentDOM.appendChild(l),l=h.dom}else{const h=u.type.spec.toDOM(u,!0),g=H.DOMSerializer.renderSpec(document,h);g.contentDOM.appendChild(l),l=g.dom}i.appendChild(l)}else{const l=t.serializeFragment(H.Fragment.from([c]),o);i.appendChild(l)}return i.childNodes.length===1&&((a=i.firstChild)==null?void 0:a.nodeType)===1&&ae(i.firstChild),i}function Ve(n,e,t,o,r,s,i,a){var y,w,x,L,X,W,G,J,Q;const c=(a==null?void 0:a.document)??document,l=e.pmSchema.nodes.blockContainer,u=t.props||{};for(const[S,I]of Object.entries(e.schema.blockSchema[t.type].propSchema))!(S in u)&&I.default!==void 0&&(u[S]=I.default);const h=(w=(y=l.spec)==null?void 0:y.toDOM)==null?void 0:w.call(y,l.create({id:t.id,...u})),g=Array.from(h.dom.attributes),m=e.blockImplementations[t.type].implementation,p=((x=m.toExternalHTML)==null?void 0:x.call({},{...t,props:u},e,{nestingLevel:i}))||m.render.call({},{...t,props:u},e),d=c.createDocumentFragment();if(p.dom.classList.contains("bn-block-content")){const S=[...g,...Array.from(p.dom.attributes)].filter(I=>I.name.startsWith("data")&&I.name!=="data-content-type"&&I.name!=="data-file-block"&&I.name!=="data-node-view-wrapper"&&I.name!=="data-node-type"&&I.name!=="data-id"&&I.name!=="data-editable");for(const I of S)p.dom.firstChild.setAttribute(I.name,I.value);ae(p.dom.firstChild),i>0&&p.dom.firstChild.setAttribute("data-nesting-level",i.toString()),d.append(...Array.from(p.dom.childNodes))}else d.append(p.dom),i>0&&p.dom.setAttribute("data-nesting-level",i.toString());if(p.contentDOM&&t.content){const S=le(e,t.content,o,a);p.contentDOM.appendChild(S)}let f;if(r.has(t.type)?f="OL":s.has(t.type)&&(f="UL"),f){if(((L=n.lastChild)==null?void 0:L.nodeName)!==f){const S=c.createElement(f);f==="OL"&&"start"in u&&u.start&&(u==null?void 0:u.start)!==1&&S.setAttribute("start",u.start+""),n.append(S)}n.lastChild.appendChild(d)}else n.append(d);if(t.children&&t.children.length>0){const S=c.createDocumentFragment();if(ce(S,e,t.children,o,r,s,i+1,a),((X=n.lastChild)==null?void 0:X.nodeName)==="UL"||((W=n.lastChild)==null?void 0:W.nodeName)==="OL")for(;((G=S.firstChild)==null?void 0:G.nodeName)==="UL"||((J=S.firstChild)==null?void 0:J.nodeName)==="OL";)n.lastChild.lastChild.appendChild(S.firstChild);e.pmSchema.nodes[t.type].isInGroup("blockContent")?n.append(S):(Q=p.contentDOM)==null||Q.append(S)}}const ce=(n,e,t,o,r,s,i=0,a)=>{for(const c of t)Ve(n,e,c,o,r,s,i,a)},He=(n,e,t,o,r,s)=>{const a=((s==null?void 0:s.document)??document).createDocumentFragment();return ce(a,n,e,t,o,r,0,s),a},Y=(n,e)=>{const t=H.DOMSerializer.fromSchema(n);return{exportBlocks:(o,r)=>{const s=He(e,o,t,new Set(["numberedListItem"]),new Set(["bulletListItem","checkListItem","toggleListItem"]),r),i=document.createElement("div");return i.append(s),i.innerHTML},exportInlineContent:(o,r)=>{const s=le(e,o,t,r),i=document.createElement("div");return i.append(s.cloneNode(!0)),i.innerHTML}}};function Fe(n,e){if(e===0)return;const t=n.resolve(e);for(let o=t.depth;o>0;o--){const r=t.node(o);if(O.isNodeBlock(r))return r.attrs.id}}function _e(n){return n.getMeta("paste")?{type:"paste"}:n.getMeta("uiEvent")==="drop"?{type:"drop"}:n.getMeta("history$")?{type:n.getMeta("history$").redo?"redo":"undo"}:n.getMeta("y-sync$")?n.getMeta("y-sync$").isUndoRedoOperation?{type:"undo-redo"}:{type:"yjs-remote"}:{type:"local"}}function ee(n){const e="__root__",t={},o={},r=k.getPmSchema(n);return n.descendants((s,i)=>{if(!O.isNodeBlock(s))return!0;const a=Fe(n,i),c=a??e;o[c]||(o[c]=[]);const l=k.nodeToBlock(s,r);return t[s.attrs.id]={block:l,parentId:a},o[c].push(s.attrs.id),!0}),{byId:t,childrenByParent:o}}function Ue(n,e){const t=new Set;if(!n||!e)return t;const o=new Set(n),r=e.filter(d=>o.has(d)),s=n.filter(d=>r.includes(d));if(s.length<=1||r.length<=1)return t;const i={};for(let d=0;di[d]),c=a.length,l=[],u=[],h=new Array(c).fill(-1),g=(d,f)=>{let y=0,w=d.length;for(;y>>1;d[x]0&&(h[d]=u[y-1]),y===l.length?(l.push(f),u.push(d)):(l[y]=f,u[y]=d)}const m=new Set;let p=u[u.length-1]??-1;for(;p!==-1;)m.add(p),p=h[p];for(let d=0;d!(m in r.byId)).forEach(m=>{i.push({type:"insert",block:s.byId[m].block,source:t,prevBlock:void 0}),a.add(m)}),Object.keys(r.byId).filter(m=>!(m in s.byId)).forEach(m=>{i.push({type:"delete",block:r.byId[m].block,source:t,prevBlock:void 0}),a.add(m)}),Object.keys(s.byId).filter(m=>m in r.byId).forEach(m=>{var y,w;const p=r.byId[m],d=s.byId[m];p.parentId!==d.parentId?(i.push({type:"move",block:d.block,prevBlock:p.block,source:t,prevParent:p.parentId?(y=r.byId[p.parentId])==null?void 0:y.block:void 0,currentParent:d.parentId?(w=s.byId[d.parentId])==null?void 0:w.block:void 0}),a.add(m)):Me.default({...p.block,children:void 0},{...d.block,children:void 0})||(i.push({type:"update",block:d.block,prevBlock:p.block,source:t}),a.add(m))});const c=r.childrenByParent,l=s.childrenByParent,u="__root__",h=new Set([...Object.keys(c),...Object.keys(l)]),g=new Set;return h.forEach(m=>{const p=Ue(c[m],l[m]);p.size!==0&&p.forEach(d=>{var x,L;const f=r.byId[d],y=s.byId[d];!f||!y||f.parentId!==y.parentId||a.has(d)||(f.parentId??u)!==m||g.has(d)||(g.add(d),i.push({type:"move",block:y.block,prevBlock:f.block,source:t,prevParent:f.parentId?(x=r.byId[f.parentId])==null?void 0:x.block:void 0,currentParent:y.parentId?(L=s.byId[y.parentId])==null?void 0:L.block:void 0}),a.add(d))})}),i}const $e=v.createExtension(()=>{const n=[];return{key:"blockChange",prosemirrorPlugins:[new C.Plugin({key:new C.PluginKey("blockChange"),filterTransaction:e=>{let t;return n.reduce((o,r)=>o===!1?o:r({getChanges(){return t||(t=de(e),t)},tr:e})!==!1,!0)}})],subscribe(e){return n.push(e),()=>{n.splice(n.indexOf(e),1)}}}});function te(n){const e=n.charAt(0)==="#"?n.substring(1,7):n,t=parseInt(e.substring(0,2),16),o=parseInt(e.substring(2,4),16),r=parseInt(e.substring(4,6),16),i=[t/255,o/255,r/255].map(c=>c<=.03928?c/12.92:Math.pow((c+.055)/1.055,2.4));return .2126*i[0]+.7152*i[1]+.0722*i[2]<=.179}function qe(n){const e=document.createElement("span");e.classList.add("bn-collaboration-cursor__base");const t=document.createElement("span");t.setAttribute("contentedEditable","false"),t.classList.add("bn-collaboration-cursor__caret"),t.setAttribute("style",`background-color: ${n.color}; color: ${te(n.color)?"white":"black"}`);const o=document.createElement("span");return o.classList.add("bn-collaboration-cursor__label"),o.setAttribute("style",`background-color: ${n.color}; color: ${te(n.color)?"white":"black"}`),o.insertBefore(document.createTextNode(n.name),null),t.insertBefore(o,null),e.insertBefore(document.createTextNode("⁠"),null),e.insertBefore(t,null),e.insertBefore(document.createTextNode("⁠"),null),e}const z=v.createExtension(({options:n})=>{const e=new Map,t=n.provider&&"awareness"in n.provider&&typeof n.provider.awareness=="object"?n.provider.awareness:void 0;return t&&("setLocalStateField"in t&&typeof t.setLocalStateField=="function"&&t.setLocalStateField("user",n.user),"on"in t&&typeof t.on=="function"&&n.showCursorLabels!=="always"&&t.on("change",({updated:o})=>{for(const r of o){const s=e.get(r);s&&(s.element.setAttribute("data-active",""),s.hideTimeout&&clearTimeout(s.hideTimeout),e.set(r,{element:s.element,hideTimeout:setTimeout(()=>{s.element.removeAttribute("data-active")},2e3)}))}})),{key:"yCursor",prosemirrorPlugins:[t?M.yCursorPlugin(t,{selectionBuilder:M.defaultSelectionBuilder,cursorBuilder(o,r){let s=e.get(r);if(!s){const i=(n.renderCursor??qe)(o);n.showCursorLabels!=="always"&&(i.addEventListener("mouseenter",()=>{const a=e.get(r);a.element.setAttribute("data-active",""),a.hideTimeout&&(clearTimeout(a.hideTimeout),e.set(r,{element:a.element,hideTimeout:void 0}))}),i.addEventListener("mouseleave",()=>{const a=e.get(r);e.set(r,{element:a.element,hideTimeout:setTimeout(()=>{a.element.removeAttribute("data-active")},2e3)})})),s={element:i,hideTimeout:void 0},e.set(r,s)}return s.element}}):void 0].filter(Boolean),dependsOn:["ySync"],updateUser(o){t==null||t.setLocalStateField("user",o)}}}),U=v.createExtension(({options:n})=>({key:"ySync",prosemirrorPlugins:[M.ySyncPlugin(n.fragment)],runsBefore:["default"]})),$=v.createExtension(()=>({key:"yUndo",prosemirrorPlugins:[M.yUndoPlugin()],dependsOn:["yCursor","ySync"],undoCommand:M.undoCommand,redoCommand:M.redoCommand}));function ze(n,e){const t=n.doc;if(n._item===null){const o=Array.from(t.share.keys()).find(r=>t.share.get(r)===n);if(o==null)throw new Error("type does not exist in other ydoc");return e.get(o,n.constructor)}else{const o=n._item,r=e.store.clients.get(o.id.client)??[],s=B.findIndexSS(r,o.id.clock);return r[s].content.type}}const Ke=v.createExtension(({editor:n,options:e})=>{let t;const o=v.createStore({isForked:!1});return{key:"yForkDoc",store:o,fork(){if(t)return;const r=e.fragment;if(!r)throw new Error("No fragment to fork from");const s=new B.Doc;B.applyUpdate(s,B.encodeStateAsUpdate(r.doc));const i=ze(r,s);t={undoStack:M.yUndoPluginKey.getState(n.prosemirrorState).undoManager.undoStack,originalFragment:r,forkedFragment:i},n.unregisterExtension([$,z,U]);const a={...e,fragment:i};n.registerExtension([U(a),$()]),o.setState({isForked:!0})},merge({keepChanges:r}){if(!t)return;n.unregisterExtension(["ySync","yCursor","yUndo"]);const{originalFragment:s,forkedFragment:i,undoStack:a}=t;if(n.registerExtension([U(e),z(e),$()]),M.yUndoPluginKey.getState(n.prosemirrorState).undoManager.undoStack=a,r){const c=B.encodeStateAsUpdate(i.doc,B.encodeStateVector(s.doc));B.applyUpdate(s.doc,c,n)}t=void 0,o.setState({isForked:!1})}}}),ue=(n,e)=>{e(n),n.forEach(t=>{t instanceof B.XmlElement&&ue(t,e)})},Ye=(n,e)=>{const t=new Map;return n.forEach(o=>{o instanceof B.XmlElement&&ue(o,r=>{if(r.nodeName==="blockContainer"&&r.hasAttribute("id")){const s=r.getAttribute("textColor"),i=r.getAttribute("backgroundColor"),a={textColor:s===O.defaultProps.textColor.default?void 0:s,backgroundColor:i===O.defaultProps.backgroundColor.default?void 0:i};(a.textColor||a.backgroundColor)&&t.set(r.getAttribute("id"),a)}})}),t.size===0?!1:(e.doc.descendants((o,r)=>{if(o.type.name==="blockContainer"&&t.has(o.attrs.id)){const s=e.doc.nodeAt(r+1);if(!s)throw new Error("No element found");e.setNodeMarkup(r+1,void 0,{...s.attrs,...t.get(o.attrs.id)})}}),!0)},je=[Ye],Xe=v.createExtension(({options:n})=>{let e=!1;const t=new V.PluginKey("schemaMigration");return{key:"schemaMigration",prosemirrorPlugins:[new V.Plugin({key:t,appendTransaction:(o,r,s)=>{if(e||!o.some(a=>a.getMeta("y-sync$"))||o.every(a=>!a.docChanged)||!n.fragment.firstChild)return;const i=s.tr;for(const a of je)a(n.fragment,i);if(e=!0,!!i.docChanged)return i}})]}}),We=v.createExtension(({editor:n,options:e})=>({key:"dropCursor",prosemirrorPlugins:[(e.dropCursor??ve.dropCursor)({width:5,color:"#ddeeff",editor:n})]})),Ge=v.createExtension(({editor:n})=>{const e=v.createStore(!1),t=()=>n.transact(o=>{var s;if(o.selection.empty||o.selection instanceof C.NodeSelection&&(o.selection.node.type.spec.content==="inline*"||((s=o.selection.node.firstChild)==null?void 0:s.type.spec.content)==="inline*")||o.selection instanceof C.TextSelection&&o.doc.textBetween(o.selection.from,o.selection.to).length===0)return!1;let r=!1;return o.selection.content().content.descendants(i=>(i.type.spec.code&&(r=!0),!r)),!r});return{key:"formattingToolbar",store:e,mount({dom:o,signal:r}){let s=!1;const i=n.onChange(()=>{s||e.setState(t())}),a=n.onSelectionChange(()=>{s||e.setState(t())});o.addEventListener("pointerdown",()=>{s=!0,e.setState(!1)},{signal:r}),n.prosemirrorView.root.addEventListener("pointerup",()=>{s=!1,n.isFocused()&&e.setState(t())},{signal:r,capture:!0}),o.addEventListener("pointercancel",()=>{s=!1},{signal:r,capture:!0}),r.addEventListener("abort",()=>{i(),a()})}}}),Je=v.createExtension(()=>({key:"history",prosemirrorPlugins:[q.history()],undoCommand:q.undo,redoCommand:q.redo})),Qe=v.createExtension(({editor:n})=>{function e(r){let s=n.prosemirrorView.nodeDOM(r);for(;s&&s.parentElement;){if(s.nodeName==="A")return s;s=s.parentElement}return null}function t(r,s){return n.transact(i=>{const a=i.doc.resolve(r),c=a.marks().find(u=>u.type.name===s);if(!c)return;const l=R.getMarkRange(a,c.type);if(l)return{range:l,mark:c,get text(){return i.doc.textBetween(l.from,l.to)},get position(){return R.posToDOMRect(n.prosemirrorView,l.from,l.to).toJSON()}}})}function o(){return n.transact(r=>{const s=r.selection;if(s.empty)return t(s.anchor,"link")})}return{key:"linkToolbar",getLinkAtSelection:o,getLinkElementAtPos:e,getMarkAtPos:t,getLinkAtElement(r){return n.transact(()=>{const s=n.prosemirrorView.posAtDOM(r,0)+1;return t(s,"link")})},editLink(r,s,i=n.transact(a=>a.selection.anchor)){n.transact(a=>{const c=k.getPmSchema(a),{range:l}=t(i+1,"link")||{range:{from:a.selection.from,to:a.selection.to}};l&&(a.insertText(s,l.from,l.to),a.addMark(l.from,l.from+s.length,c.mark("link",{href:r})))}),n.prosemirrorView.focus()},deleteLink(r=n.transact(s=>s.selection.anchor)){n.transact(s=>{const i=k.getPmSchema(s),{range:a}=t(r+1,"link")||{range:{from:s.selection.from,to:s.selection.to}};a&&s.removeMark(a.from,a.to,i.marks.link).setMeta("preventAutolink",!0)}),n.prosemirrorView.focus()}}}),Ze=["http","https","ftp","ftps","mailto","tel","callto","sms","cid","xmpp"],et="https",tt=new C.PluginKey("node-selection-keyboard"),ot=v.createExtension(()=>({key:"nodeSelectionKeyboard",prosemirrorPlugins:[new C.Plugin({key:tt,props:{handleKeyDown:(n,e)=>{if("node"in n.state.selection){if(e.ctrlKey||e.metaKey)return!1;if(e.key.length===1)return e.preventDefault(),!0;if(e.key==="Enter"&&!e.isComposing&&!e.shiftKey&&!e.altKey&&!e.ctrlKey&&!e.metaKey){const t=n.state.tr;return n.dispatch(t.insert(n.state.tr.selection.$to.after(),n.state.schema.nodes.paragraph.createChecked()).setSelection(new C.TextSelection(t.doc.resolve(n.state.tr.selection.$to.after()+1)))),!0}}return!1}}})]})),nt=new C.PluginKey("blocknote-placeholder"),rt=v.createExtension(({editor:n,options:e})=>{const t=e.placeholders;return{key:"placeholder",prosemirrorPlugins:[new C.Plugin({key:nt,view:o=>{const r=`placeholder-selector-${Se.v4()}`;o.dom.classList.add(r);const s=document.createElement("style"),i=n._tiptapEditor.options.injectNonce;i&&s.setAttribute("nonce",i),o.root instanceof window.ShadowRoot?o.root.append(s):o.root.head.appendChild(s);const a=s.sheet,c=(l="")=>`.${r} .bn-block-content${l} .bn-inline-content:has(> .ProseMirror-trailingBreak:only-child):before`;try{const{default:l,emptyDocument:u,...h}=t||{};for(const[p,d]of Object.entries(h)){const f=`[data-content-type="${p}"]`;a.insertRule(`${c(f)} { content: ${JSON.stringify(d)}; }`)}const g="[data-is-only-empty-block]",m="[data-is-empty-and-focused]";a.insertRule(`${c(g)} { content: ${JSON.stringify(u)}; }`),a.insertRule(`${c(m)} { content: ${JSON.stringify(l)}; }`)}catch(l){console.warn("Failed to insert placeholder CSS rule - this is likely due to the browser not supporting certain CSS pseudo-element selectors (:has, :only-child:, or :before)",l)}return{destroy:()=>{o.root instanceof window.ShadowRoot?o.root.removeChild(s):o.root.head.removeChild(s)}}},props:{decorations:o=>{const{doc:r,selection:s}=o;if(!n.isEditable||!s.empty||s.$from.parent.type.spec.code)return;const i=[];o.doc.content.size===6&&i.push(D.Decoration.node(2,4,{"data-is-only-empty-block":"true"}));const a=s.$anchor,c=a.parent;if(c.content.size===0){const l=a.before();i.push(D.Decoration.node(l,l+c.nodeSize,{"data-is-empty-and-focused":"true"}))}return D.DecorationSet.create(r,i)}}})]}}),oe=new C.PluginKey("previous-blocks"),st={index:"index",level:"level",type:"type",depth:"depth","depth-change":"depth-change"},it=v.createExtension(()=>{let n;return{key:"previousBlockType",prosemirrorPlugins:[new C.Plugin({key:oe,view(e){return{update:async(t,o)=>{var r;((r=this.key)==null?void 0:r.getState(t.state).updatedBlocks.size)>0&&(n=setTimeout(()=>{t.dispatch(t.state.tr.setMeta(oe,{clearUpdate:!0}))},0))},destroy:()=>{n&&clearTimeout(n)}}},state:{init(){return{prevTransactionOldBlockAttrs:{},currentTransactionOldBlockAttrs:{},updatedBlocks:new Set}},apply(e,t,o,r){if(t.currentTransactionOldBlockAttrs={},t.updatedBlocks.clear(),!e.docChanged||o.doc.eq(r.doc))return t;const s={},i=R.findChildren(o.doc,l=>l.attrs.id),a=new Map(i.map(l=>[l.node.attrs.id,l])),c=R.findChildren(r.doc,l=>l.attrs.id);for(const l of c){const u=a.get(l.node.attrs.id),h=u==null?void 0:u.node.firstChild,g=l.node.firstChild;if(u&&h&&g){const m={index:g.attrs.index,level:g.attrs.level,type:g.type.name,depth:r.doc.resolve(l.pos).depth},p={index:h.attrs.index,level:h.attrs.level,type:h.type.name,depth:o.doc.resolve(u.pos).depth};s[l.node.attrs.id]=p,t.currentTransactionOldBlockAttrs[l.node.attrs.id]=p,JSON.stringify(p)!==JSON.stringify(m)&&(p["depth-change"]=p.depth-m.depth,t.updatedBlocks.add(l.node.attrs.id))}}return t.prevTransactionOldBlockAttrs=s,t}},props:{decorations(e){const t=this.getState(e);if(t.updatedBlocks.size===0)return;const o=[];return e.doc.descendants((r,s)=>{if(!r.attrs.id||!t.updatedBlocks.has(r.attrs.id))return;const i=t.currentTransactionOldBlockAttrs[r.attrs.id],a={};for(const[l,u]of Object.entries(i))a["data-prev-"+st[l]]=u||"none";const c=D.Decoration.node(s,s+r.nodeSize,{...a});o.push(c)}),D.DecorationSet.create(e.doc,o)}}})]}});function he(n,e){var t,o;for(;n&&n.parentElement&&n.parentElement!==e.dom&&((t=n.getAttribute)==null?void 0:t.call(n,"data-node-type"))!=="blockContainer";)n=n.parentElement;if(((o=n.getAttribute)==null?void 0:o.call(n,"data-node-type"))==="blockContainer")return{node:n,id:n.getAttribute("data-id")}}function at(){const n=e=>{let t=e.children.length;for(let o=0;o0){e.children.splice(o,1,...r.children);const s=r.children.length-1;t+=s,o+=s}else e.children.splice(o,1),t--,o--}};return n}function lt(){const n=e=>{var t;if(e.children&&"length"in e.children&&e.children.length)for(let o=e.children.length-1;o>=0;o--){const r=e.children[o],s=o+1{De.visit(n,"element",(e,t,o)=>{var r,s,i,a;if(o&&e.tagName==="video"){const c=((r=e.properties)==null?void 0:r.src)||((s=e.properties)==null?void 0:s["data-url"])||"",l=((i=e.properties)==null?void 0:i.title)||((a=e.properties)==null?void 0:a["data-name"])||"";o.children[t]={type:"text",value:`![${l}](${c})`}}})}}function j(n){return Te.unified().use(Ae.default,{fragment:!0}).use(ct).use(at).use(lt).use(Ne.default).use(Le.default).use(Re.default,{handlers:{text:t=>t.value}}).processSync(n).value}function dt(n,e,t,o){const s=Y(e,t).exportBlocks(n,o);return j(s)}function me(n){const e=[];return n.descendants(t=>{var r,s;const o=k.getPmSchema(t);return t.type.name==="blockContainer"&&((r=t.firstChild)==null?void 0:r.type.name)==="blockGroup"?!0:t.type.name==="columnList"&&t.childCount===1?((s=t.firstChild)==null||s.forEach(i=>{e.push(k.nodeToBlock(i,o))}),!1):t.type.isInGroup("bnBlock")?(e.push(k.nodeToBlock(t,o)),!1):!0}),e}class A extends C.Selection{constructor(t,o){super(t,o);b(this,"nodes");const r=t.node();this.nodes=[],t.doc.nodesBetween(t.pos,o.pos,(s,i,a)=>{if(a!==null&&a.eq(r))return this.nodes.push(s),!1})}static create(t,o,r=o){return new A(t.resolve(o),t.resolve(r))}content(){return new H.Slice(H.Fragment.from(this.nodes),0,0)}eq(t){if(!(t instanceof A)||this.nodes.length!==t.nodes.length||this.from!==t.from||this.to!==t.to)return!1;for(let o=0;oArray.prototype.indexOf.call(h.children,g),i=s(r,n.domAtPos(e+1).node.parentElement),a=s(r,n.domAtPos(t-1).node.parentElement);for(let h=r.childElementCount-1;h>=0;h--)(h>a||hh!=="ProseMirror"&&h!=="bn-root"&&h!=="bn-editor").join(" ");P.className=P.className+" bn-drag-preview "+u,n.root instanceof ShadowRoot?n.root.appendChild(P):n.root.body.appendChild(P)}function pe(n){P!==void 0&&(n instanceof ShadowRoot?n.removeChild(P):n.body.removeChild(P),P=void 0)}function ht(n,e,t){if(!n.dataTransfer||t.headless)return;const o=t.prosemirrorView,r=O.getNodeById(e.id,o.state.doc);if(!r)throw new Error(`Block with ID ${e.id} not found`);const s=r.posBeforeNode;if(s!=null){const i=o.state.selection,a=o.state.doc,{from:c,to:l}=ut(i,a),u=c<=s&&s{this.state=e,this.emitUpdate(this.state)});b(this,"updateStateFromMousePos",()=>{var o,r,s,i,a;if(this.menuFrozen||!this.mousePos)return;const e=this.findClosestEditorElement({clientX:this.mousePos.x,clientY:this.mousePos.y});if((e==null?void 0:e.element)!==this.pmView.dom||e.distance>re){(o=this.state)!=null&&o.show&&(this.state.show=!1,this.updateState(this.state));return}const t=mt(this.mousePos,this.pmView);if(!t||!this.editor.isEditable){(r=this.state)!=null&&r.show&&(this.state.show=!1,this.updateState(this.state));return}if(!((s=this.state)!=null&&s.show&&((i=this.hoveredBlock)!=null&&i.hasAttribute("data-id"))&&((a=this.hoveredBlock)==null?void 0:a.getAttribute("data-id"))===t.id)&&(this.hoveredBlock=t.node,this.editor.isEditable)){const c=t.node.getBoundingClientRect(),l=t.node.closest("[data-node-type=column]");this.state={show:!0,referencePos:new DOMRect(l?l.firstElementChild.getBoundingClientRect().x:this.pmView.dom.firstChild.getBoundingClientRect().x,c.y,c.width,c.height),block:this.editor.getBlock(this.hoveredBlock.getAttribute("data-id"))},this.updateState(this.state)}});b(this,"onDragStart",e=>{var i;const t=(i=e.dataTransfer)==null?void 0:i.getData("blocknote/html");if(!t||this.pmView.dragging)return;const o=document.createElement("div");o.innerHTML=t;const s=Z.DOMParser.fromSchema(this.pmView.state.schema).parse(o,{topNode:this.pmView.state.schema.nodes.blockGroup.create()});this.pmView.dragging={slice:new Z.Slice(s.content,0,0),move:!0}});b(this,"findClosestEditorElement",e=>{const t=Array.from(this.pmView.root.querySelectorAll(".bn-editor"));if(t.length===0)return null;let o=t[0],r=Number.MAX_VALUE;return t.forEach(s=>{const i=s.querySelector(".bn-block-group").getBoundingClientRect(),a=e.clientXi.right?e.clientX-i.right:0,c=e.clientYi.bottom?e.clientY-i.bottom:0,l=Math.sqrt(Math.pow(a,2)+Math.pow(c,2));l{if(e.synthetic)return;const t=this.getDragEventContext(e);if(!t||!t.isDropPoint){this.closeDropCursor();return}t.isDropPoint&&!t.isDropWithinEditorBounds&&this.dispatchSyntheticEvent(e)});b(this,"closeDropCursor",()=>{const e=new Event("dragleave",{bubbles:!1});e.synthetic=!0,this.pmView.dom.dispatchEvent(e)});b(this,"getDragEventContext",e=>{var c;const t=!((c=e.dataTransfer)!=null&&c.types.includes("blocknote/html"))&&!!this.pmView.dragging,o=!!this.isDragOrigin,r=t||o,s=this.findClosestEditorElement(e);if(!s||s.distance>re)return;const i=s.element===this.pmView.dom,a=i&&s.distance===0;if(!(!i&&!r))return{isDropPoint:i,isDropWithinEditorBounds:a,isDragOrigin:r}});b(this,"onDrop",e=>{if(e.synthetic)return;const t=this.getDragEventContext(e);if(!t){this.closeDropCursor();return}const{isDropPoint:o,isDropWithinEditorBounds:r,isDragOrigin:s}=t;if(!r&&o&&this.dispatchSyntheticEvent(e),o){if(this.pmView.dragging)return;this.pmView.dispatch(this.pmView.state.tr.setSelection(V.TextSelection.create(this.pmView.state.tr.doc,this.pmView.state.tr.selection.anchor)));return}else if(s){setTimeout(()=>this.pmView.dispatch(this.pmView.state.tr.deleteSelection()),0);return}});b(this,"onDragEnd",e=>{e.synthetic||(this.pmView.dragging=null)});b(this,"onKeyDown",e=>{var t;(t=this.state)!=null&&t.show&&this.editor.isFocused()&&(this.state.show=!1,this.emitUpdate(this.state))});b(this,"onMouseMove",e=>{var s;if(this.menuFrozen)return;this.mousePos={x:e.clientX,y:e.clientY};const t=this.pmView.dom.getBoundingClientRect(),o=this.mousePos.x>t.left&&this.mousePos.xt.top&&this.mousePos.y{if(!this.state)throw new Error("Attempting to update uninitialized side menu");o(this.state)},this.pmView.root.addEventListener("dragstart",this.onDragStart),this.pmView.root.addEventListener("dragover",this.onDragOver),this.pmView.root.addEventListener("drop",this.onDrop,!0),this.pmView.root.addEventListener("dragend",this.onDragEnd,!0),this.pmView.root.addEventListener("mousemove",this.onMouseMove,!0),this.pmView.root.addEventListener("keydown",this.onKeyDown,!0)}dispatchSyntheticEvent(e){const t=new Event(e.type,e),o=this.pmView.dom.firstChild.getBoundingClientRect();t.clientX=e.clientX,t.clientY=e.clientY,t.clientX=Math.min(Math.max(e.clientX,o.left),o.left+o.width),t.clientY=Math.min(Math.max(e.clientY,o.top),o.top+o.height),t.dataTransfer=e.dataTransfer,t.preventDefault=()=>e.preventDefault(),t.synthetic=!0,this.pmView.dom.dispatchEvent(t)}update(e,t){var r;!t.doc.eq(this.pmView.state.doc)&&((r=this.state)!=null&&r.show)&&this.updateStateFromMousePos()}destroy(){var e;(e=this.state)!=null&&e.show&&(this.state.show=!1,this.emitUpdate(this.state)),this.pmView.root.removeEventListener("mousemove",this.onMouseMove,!0),this.pmView.root.removeEventListener("dragstart",this.onDragStart),this.pmView.root.removeEventListener("dragover",this.onDragOver),this.pmView.root.removeEventListener("drop",this.onDrop,!0),this.pmView.root.removeEventListener("dragend",this.onDragEnd,!0),this.pmView.root.removeEventListener("keydown",this.onKeyDown,!0)}}const ge=new V.PluginKey("SideMenuPlugin"),pt=v.createExtension(({editor:n})=>{let e;const t=v.createStore(void 0);return{key:"sideMenu",store:t,prosemirrorPlugins:[new V.Plugin({key:ge,view:o=>(e=new fe(n,o,r=>{t.setState({...r})}),e)})],blockDragStart(o,r){e&&(e.isDragOrigin=!0),ht(o,r,n)},blockDragEnd(){pe(n.prosemirrorView.root),e&&(e.isDragOrigin=!1),n.blur()},freezeMenu(){e.menuFrozen=!0,e.state.show=!0,e.emitUpdate(e.state)},unfreezeMenu(){e.menuFrozen=!1,e.state.show=!1,e.emitUpdate(e.state)}}});let E;function se(n){E||(E=document.createElement("div"),E.innerHTML="_",E.style.opacity="0",E.style.height="1px",E.style.width="1px",n instanceof Document?n.body.appendChild(E):n.appendChild(E))}function ft(n){E&&(n instanceof Document?n.body.removeChild(E):n.removeChild(E),E=void 0)}function _(n){return Array.prototype.indexOf.call(n.parentElement.childNodes,n)}function gt(n){let e=n;for(;e&&e.nodeName!=="TD"&&e.nodeName!=="TH"&&!e.classList.contains("tableWrapper");){if(e.classList.contains("ProseMirror"))return;const t=e.parentNode;if(!t||!(t instanceof Element))return;e=t}return e.nodeName==="TD"||e.nodeName==="TH"?{type:"cell",domNode:e,tbodyNode:e.closest("tbody")}:{type:"wrapper",domNode:e,tbodyNode:e.querySelector("tbody")}}function yt(n,e){const t=e.querySelectorAll(n);for(let o=0;o=0}function bnIsValidCellIndex(n){return Number.isInteger(n)&&n>=0}function bnIsValidRelativeCell(n){return bnIsValidCellIndex(n.row)&&bnIsValidCellIndex(n.col)}function bnResolveCellPosition(n,e,t){if(!bnIsValidTablePosition(e)||!bnIsValidRelativeCell(t))return;try{const o=n.doc.resolve(e+1);if(t.row>=o.node().childCount)return;const r=n.doc.resolve(o.posAtIndex(t.row)+1);return t.col>=r.node().childCount?void 0:n.doc.resolve(r.posAtIndex(t.col))}catch{return}}class ye{constructor(e,t,o){b(this,"state");b(this,"emitUpdate");b(this,"tableId");b(this,"tablePos");b(this,"tableElement");b(this,"menuFrozen",!1);b(this,"mouseState","up");b(this,"prevWasEditable",null);b(this,"hideHandles",(e={})=>{const{resetCell:t=!1,resetDragging:o=!1}=e;if(!this.state||!this.state.show&&!t&&!o)return;this.state.show=!1,this.state.showAddOrRemoveRowsButton=!1,this.state.showAddOrRemoveColumnsButton=!1,o&&(this.state.draggingState=void 0),t&&(this.state.rowIndex=void 0,this.state.colIndex=void 0,this.state.referencePosCell=void 0),this.emitUpdate()});b(this,"viewMousedownHandler",()=>{this.mouseState="down"});b(this,"mouseUpHandler",e=>{this.mouseState="up",this.mouseMoveHandler(e)});b(this,"mouseMoveHandler",e=>{var l,u,h,g,m,p,d,f;if(this.menuFrozen||this.mouseState==="selecting"||!(e.target instanceof Element)||!this.pmView.dom.contains(e.target))return;const t=gt(e.target);if((t==null?void 0:t.type)==="cell"&&this.mouseState==="down"&&!((l=this.state)!=null&&l.draggingState)){this.mouseState="selecting",(u=this.state)!=null&&u.show&&(this.state.show=!1,this.state.showAddOrRemoveRowsButton=!1,this.state.showAddOrRemoveColumnsButton=!1,this.emitUpdate());return}if(!t||!this.editor.isEditable){(h=this.state)!=null&&h.show&&(this.state.show=!1,this.state.showAddOrRemoveRowsButton=!1,this.state.showAddOrRemoveColumnsButton=!1,this.emitUpdate());return}if(!t.tbodyNode)return;const o=t.tbodyNode.getBoundingClientRect(),r=he(t.domNode,this.pmView);if(!r)return;this.tableElement=r.node;let s,i;try{i=this.editor.transact(y=>O.getNodeById(r.id,y.doc))}catch{this.hideHandles({resetCell:!0,resetDragging:!0});return}if(!i){this.hideHandles({resetCell:!0,resetDragging:!0});return}const a=k.nodeToBlock(i.node,this.editor.pmSchema,this.editor.schema.blockSchema,this.editor.schema.inlineContentSchema,this.editor.schema.styleSchema);if(O.editorHasBlockWithType(this.editor,"table")&&(this.tablePos=i.posBeforeNode+1,s=a),!s)return;this.tableId=r.id;const c=(g=t.domNode.closest(".tableWrapper"))==null?void 0:g.querySelector(".table-widgets-container");if((t==null?void 0:t.type)==="wrapper"){const y=e.clientY>=o.bottom-1&&e.clientY=o.right-1&&e.clientXo.right||e.clientY>o.bottom;this.state={...this.state,show:!0,showAddOrRemoveRowsButton:y,showAddOrRemoveColumnsButton:w,referencePosTable:o,block:s,widgetContainer:c,colIndex:x||(p=this.state)==null?void 0:p.colIndex,rowIndex:x||(d=this.state)==null?void 0:d.rowIndex,referencePosCell:x||(f=this.state)==null?void 0:f.referencePosCell}}else{const y=_(t.domNode),w=_(t.domNode.parentElement),x=t.domNode.getBoundingClientRect();if(this.state!==void 0&&this.state.show&&this.tableId===r.id&&this.state.rowIndex===w&&this.state.colIndex===y)return;this.state={show:!0,showAddOrRemoveColumnsButton:y===s.content.rows[0].cells.length-1,showAddOrRemoveRowsButton:w===s.content.rows.length-1,referencePosTable:o,block:s,draggingState:void 0,referencePosCell:x,colIndex:y,rowIndex:w,widgetContainer:c}}return this.emitUpdate(),!1});b(this,"dragOverHandler",e=>{var g;if(((g=this.state)==null?void 0:g.draggingState)===void 0)return;e.preventDefault(),e.dataTransfer.dropEffect="move",yt(".prosemirror-dropcursor-block, .prosemirror-dropcursor-inline",this.pmView.root);const t={left:Math.min(Math.max(e.clientX,this.state.referencePosTable.left+1),this.state.referencePosTable.right-1),top:Math.min(Math.max(e.clientY,this.state.referencePosTable.top+1),this.state.referencePosTable.bottom-1)},o=this.pmView.root.elementsFromPoint(t.left,t.top).filter(m=>m.tagName==="TD"||m.tagName==="TH");if(o.length===0)return;const r=o[0];let s=!1;const i=_(r.parentElement),a=_(r),c=this.state.draggingState.draggedCellOrientation==="row"?this.state.rowIndex:this.state.colIndex,u=(this.state.draggingState.draggedCellOrientation==="row"?i:a)!==c;(this.state.rowIndex!==i||this.state.colIndex!==a)&&(this.state.rowIndex=i,this.state.colIndex=a,this.state.referencePosCell=r.getBoundingClientRect(),s=!0);const h=this.state.draggingState.draggedCellOrientation==="row"?t.top:t.left;this.state.draggingState.mousePos!==h&&(this.state.draggingState.mousePos=h,s=!0),s&&this.emitUpdate(),u&&this.editor.transact(m=>m.setMeta(N,!0))});b(this,"dropHandler",e=>{if(this.mouseState="up",this.state===void 0||this.state.draggingState===void 0)return!1;if(this.state.rowIndex===void 0||this.state.colIndex===void 0)return e.preventDefault(),this.hideHandles({resetCell:!0,resetDragging:!0}),!1;e.preventDefault();const{draggingState:t,colIndex:o,rowIndex:r}=this.state,s=this.state.block.content.columnWidths;if(t.draggedCellOrientation==="row"){if(!k.canRowBeDraggedInto(this.state.block,t.originalIndex,r))return!1;const i=k.moveRow(this.state.block,t.originalIndex,r);this.editor.updateBlock(this.state.block,{type:"table",content:{...this.state.block.content,rows:i}})}else{if(!k.canColumnBeDraggedInto(this.state.block,t.originalIndex,o))return!1;const i=k.moveColumn(this.state.block,t.originalIndex,o),[a]=s.splice(t.originalIndex,1);s.splice(o,0,a),this.editor.updateBlock(this.state.block,{type:"table",content:{...this.state.block.content,columnWidths:s,rows:i}})}return this.editor.setTextCursorPosition(this.state.block.id),!0});this.editor=e,this.pmView=t,this.emitUpdate=()=>{if(!this.state)throw new Error("Attempting to update uninitialized image toolbar");o(this.state)},t.dom.addEventListener("mousemove",this.mouseMoveHandler),t.dom.addEventListener("mousedown",this.viewMousedownHandler),window.addEventListener("mouseup",this.mouseUpHandler),t.root.addEventListener("dragover",this.dragOverHandler),t.root.addEventListener("drop",this.dropHandler)}update(){var r;if(!this.state||!this.state.show)return;const e=this.state.block;if(!e){this.hideHandles({resetCell:!0,resetDragging:!0});return}if(this.state.block=this.editor.getBlock(e.id),!this.state.block||this.state.block.type!=="table"||!((r=this.tableElement)!=null&&r.isConnected)){this.hideHandles();return}const{height:t,width:o}=k.getDimensionsOfTable(this.state.block);this.state.rowIndex!==void 0&&this.state.colIndex!==void 0&&(this.state.rowIndex>=t&&(this.state.rowIndex=t-1),this.state.colIndex>=o&&(this.state.colIndex=o-1));const i=this.tableElement.querySelector("tbody");if(!i){this.hideHandles({resetCell:!0});return}if(this.state.rowIndex!==void 0&&this.state.colIndex!==void 0){const a=i.children[this.state.rowIndex].children[this.state.colIndex];a?this.state.referencePosCell=a.getBoundingClientRect():(this.state.rowIndex=void 0,this.state.colIndex=void 0)}this.state.referencePosTable=i.getBoundingClientRect(),this.emitUpdate()}destroy(){this.pmView.dom.removeEventListener("mousemove",this.mouseMoveHandler),window.removeEventListener("mouseup",this.mouseUpHandler),this.pmView.dom.removeEventListener("mousedown",this.viewMousedownHandler),this.pmView.root.removeEventListener("dragover",this.dragOverHandler),this.pmView.root.removeEventListener("drop",this.dropHandler)}}const N=new C.PluginKey("TableHandlesPlugin"),wt=v.createExtension(({editor:n})=>{let e;const t=v.createStore(void 0);return{key:"tableHandles",store:t,prosemirrorPlugins:[new C.Plugin({key:N,view:o=>(e=new ye(n,o,r=>{t.setState(r.block?{...r,draggingState:r.draggingState?{...r.draggingState}:void 0}:void 0)}),e),props:{decorations:o=>{if(e===void 0||e.state===void 0||e.state.draggingState===void 0||e.tablePos===void 0)return;const r=e.state.draggingState.draggedCellOrientation==="row"?e.state.rowIndex:e.state.colIndex;if(r===void 0)return;const s=[],{block:i,draggingState:a}=e.state,{originalIndex:c,draggedCellOrientation:l}=a;if(r===c||!i||l==="row"&&!k.canRowBeDraggedInto(i,c,r)||l==="col"&&!k.canColumnBeDraggedInto(i,c,r))return D.DecorationSet.create(o.doc,s);const u=o.doc.resolve(e.tablePos+1);return e.state.draggingState.draggedCellOrientation==="row"?k.getCellsAtRowHandle(e.state.block,r).forEach(({row:g,col:m})=>{const p=o.doc.resolve(u.posAtIndex(g)+1),d=o.doc.resolve(p.posAtIndex(m)+1),f=d.node(),y=d.pos+(r>c?f.nodeSize-2:0);s.push(D.Decoration.widget(y,()=>{const w=document.createElement("div");return w.className="bn-table-drop-cursor",w.style.left="0",w.style.right="0",r>c?w.style.bottom="-2px":w.style.top="-3px",w.style.height="4px",w}))}):k.getCellsAtColumnHandle(e.state.block,r).forEach(({row:g,col:m})=>{const p=o.doc.resolve(u.posAtIndex(g)+1),d=o.doc.resolve(p.posAtIndex(m)+1),f=d.node(),y=d.pos+(r>c?f.nodeSize-2:0);s.push(D.Decoration.widget(y,()=>{const w=document.createElement("div");return w.className="bn-table-drop-cursor",w.style.top="0",w.style.bottom="0",r>c?w.style.right="-2px":w.style.left="-3px",w.style.width="4px",w}))}),D.DecorationSet.create(o.doc,s)}}})],colDragStart(o){if(e===void 0||e.state===void 0||e.state.colIndex===void 0||e.tablePos===void 0){e==null||e.hideHandles({resetCell:!0,resetDragging:!0});return}e.state.draggingState={draggedCellOrientation:"col",originalIndex:e.state.colIndex,mousePos:o.clientX},e.emitUpdate(),n.transact(r=>r.setMeta(N,{draggedCellOrientation:e.state.draggingState.draggedCellOrientation,originalIndex:e.state.colIndex,newIndex:e.state.colIndex,tablePos:e.tablePos})),!n.headless&&(se(n.prosemirrorView.root),o.dataTransfer.setDragImage(E,0,0),o.dataTransfer.effectAllowed="move")},rowDragStart(o){if(e===void 0||e.state===void 0||e.state.rowIndex===void 0||e.tablePos===void 0){e==null||e.hideHandles({resetCell:!0,resetDragging:!0});return}e.state.draggingState={draggedCellOrientation:"row",originalIndex:e.state.rowIndex,mousePos:o.clientY},e.emitUpdate(),n.transact(r=>r.setMeta(N,{draggedCellOrientation:e.state.draggingState.draggedCellOrientation,originalIndex:e.state.rowIndex,newIndex:e.state.rowIndex,tablePos:e.tablePos})),!n.headless&&(se(n.prosemirrorView.root),o.dataTransfer.setDragImage(E,0,0),o.dataTransfer.effectAllowed="copyMove")},dragEnd(){if(e===void 0||e.state===void 0){n.headless||ft(n.prosemirrorView.root);return}e.state.draggingState=void 0,e.emitUpdate(),n.transact(o=>o.setMeta(N,null)),!n.headless&&ft(n.prosemirrorView.root)},freezeHandles(){e.menuFrozen=!0},unfreezeHandles(){e.menuFrozen=!1},getCellsAtRowHandle(o,r){return k.getCellsAtRowHandle(o,r)},getCellsAtColumnHandle(o,r){return k.getCellsAtColumnHandle(o,r)},setCellSelection(o,r,s=r){if(!e)throw new Error("Table handles view not initialized");const i=bnResolveCellPosition(o,e.tablePos,r),a=bnResolveCellPosition(o,e.tablePos,s);if(!i||!a)return;const c=o.tr;return c.setSelection(new T.CellSelection(i,a)),o.apply(c)},addRowOrColumn(o,r){if(!e||!bnIsValidTablePosition(e.tablePos)||!bnIsValidCellIndex(o)){e==null||e.hideHandles({resetCell:!0});return}n.exec((s,i)=>{const a=this.setCellSelection(s,r.orientation==="row"?{row:o,col:0}:{row:0,col:o});if(!a)return e==null||e.hideHandles({resetCell:!0}),!1;return r.orientation==="row"?r.side==="above"?T.addRowBefore(a,i):T.addRowAfter(a,i):r.side==="left"?T.addColumnBefore(a,i):T.addColumnAfter(a,i)})},removeRowOrColumn(o,r){return bnIsValidCellIndex(o)?r==="row"?n.exec((s,i)=>{const a=this.setCellSelection(s,{row:o,col:0});return a?T.deleteRow(a,i):!1}):n.exec((s,i)=>{const a=this.setCellSelection(s,{row:0,col:o});return a?T.deleteColumn(a,i):!1}):!1},mergeCells(o){return n.exec((r,s)=>{const i=o?this.setCellSelection(r,o.relativeStartCell,o.relativeEndCell):r;return i?T.mergeCells(i,s):!1})},splitCell(o){return n.exec((r,s)=>{const i=o?this.setCellSelection(r,o):r;return i?T.splitCell(i,s):!1})},getCellSelection(){return n.transact(o=>{const r=o.selection;let s=r.$from,i=r.$to;if(O.isTableCellSelection(r)){const{ranges:d}=r;d.forEach(f=>{s=f.$from.min(s??f.$from),i=f.$to.max(i??f.$to)})}else if(s=o.doc.resolve(r.$from.pos-r.$from.parentOffset-1),i=o.doc.resolve(r.$to.pos-r.$to.parentOffset-1),s.pos===0||i.pos===0)return;const a=o.doc.resolve(s.pos-s.parentOffset-1),c=o.doc.resolve(i.pos-i.parentOffset-1),l=o.doc.resolve(a.pos-a.parentOffset-1),u=s.index(a.depth),h=a.index(l.depth),g=i.index(c.depth),m=c.index(l.depth),p=[];for(let d=h;d<=m;d++)for(let f=u;f<=g;f++)p.push({row:d,col:f});return{from:{row:h,col:u},to:{row:m,col:g},cells:p}})},getMergeDirection(o){return n.transact(r=>{const s=O.isTableCellSelection(r.selection)?r.selection:void 0;if(!s||!o||s.ranges.length<=1)return;const i=this.getCellSelection();if(i)return k.areInSameColumn(i.from,i.to,o)?"vertical":"horizontal"})},cropEmptyRowsOrColumns(o,r){return k.cropEmptyRowsOrColumns(o,r)},addRowsOrColumns(o,r,s){return k.addRowsOrColumns(o,r,s)}}}),ie=new C.PluginKey("trailingNode"),bt=v.createExtension(()=>({key:"trailingNode",prosemirrorPlugins:[new C.Plugin({key:ie,appendTransaction:(n,e,t)=>{const{doc:o,tr:r,schema:s}=t,i=ie.getState(t),a=o.content.size-2,c=s.nodes.blockContainer,l=s.nodes.paragraph;if(i)return r.insert(a,c.create(void 0,l.create()))},state:{init:(n,e)=>{},apply:(n,e)=>{if(!n.docChanged)return e;let t=n.doc.lastChild;if(!t||t.type.name!=="blockGroup")throw new Error("Expected blockGroup");if(t=t.lastChild,!t||t.type.name!=="blockContainer")return!0;const o=t.firstChild;if(!o)throw new Error("Expected blockContent");return t.nodeSize>4||o.type.spec.content!=="inline*"}}})]}));exports.BlockChangeExtension=$e;exports.DEFAULT_LINK_PROTOCOL=et;exports.DropCursorExtension=We;exports.ForkYDocExtension=Ke;exports.FormattingToolbarExtension=Ge;exports.HistoryExtension=Je;exports.LinkToolbarExtension=Qe;exports.NodeSelectionKeyboardExtension=ot;exports.PlaceholderExtension=rt;exports.PreviousBlockTypeExtension=it;exports.SchemaMigration=Xe;exports.SideMenuExtension=pt;exports.SideMenuView=fe;exports.TableHandlesExtension=wt;exports.TableHandlesView=ye;exports.TrailingNodeExtension=bt;exports.VALID_LINK_PROTOCOLS=Ze;exports.YCursorExtension=z;exports.YSyncExtension=U;exports.YUndoExtension=$;exports.blocksToMarkdown=dt;exports.cleanHTMLToMarkdown=j;exports.createExternalHTMLExporter=Y;exports.fragmentToBlocks=me;exports.getBlocksChangedByTransaction=de;exports.sideMenuPluginKey=ge;exports.tableHandlesPluginKey=N; + //# sourceMappingURL=TrailingNode-D-CZ76FS.cjs.map +diff --git a/dist/blocknote.cjs b/dist/blocknote.cjs +index 778ed7a4062755747312db827f920e47a5aff127..f9e34b3ceb54a976b734fd70ac7fc183e0070c2b 100644 +--- a/dist/blocknote.cjs ++++ b/dist/blocknote.cjs +@@ -1,5 +1,5 @@ + "use strict";var Ee=Object.defineProperty;var Te=(o,e,t)=>e in o?Ee(o,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):o[e]=t;var k=(o,e,t)=>Te(o,typeof e!="symbol"?e+"":e,t);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const x=require("prosemirror-model"),L=require("prosemirror-transform"),u=require("./blockToNode-CumVjgem.cjs"),c=require("./defaultBlocks-DosClM5E.cjs"),b=require("./TrailingNode-D-CZ76FS.cjs"),M=require("./BlockNoteSchema-DmFDeA0n.cjs"),B=require("@tiptap/core"),R=require("./EventEmitter-CLwfmbqG.cjs"),A=require("@tiptap/pm/model"),xe=require("./en-Cl87Uuyf.cjs"),K=require("@handlewithcare/prosemirror-inputrules"),Me=require("@tiptap/pm/keymap"),F=require("./BlockNoteExtension-BWw0r8Gy.cjs"),Ie=require("@tiptap/extensions/gap-cursor"),Pe=require("@tiptap/extension-link"),we=require("@tiptap/extension-text"),C=require("prosemirror-state"),N=require("prosemirror-tables"),ve=require("./ShowSelection-BxnbRvy4.cjs"),Ae=require("remark-gfm"),Ne=require("remark-parse"),H=require("remark-rehype"),Le=require("rehype-stringify"),_e=require("unified"),De=require("@tiptap/pm/state"),O=o=>o&&typeof o=="object"&&"default"in o?o:{default:o},Fe=O(Ae),Oe=O(Ne),He=O(H),$e=O(Le);function oe(o,e){const t=[{tag:`[data-inline-content-type="${o.type}"]`,contentElement:n=>{const r=n;return r.matches("[data-editable]")?r:r.querySelector("[data-editable]")||r}}];return e&&t.push({tag:"*",getAttrs(n){if(typeof n=="string")return!1;const r=e==null?void 0:e(n);return r===void 0?!1:r}}),t}function Ue(o,e){var n;const t=B.Node.create({name:o.type,inline:!0,group:"inline",draggable:(n=e.meta)==null?void 0:n.draggable,selectable:o.content==="styled",atom:o.content==="none",content:o.content==="styled"?"inline*":"",addAttributes(){return c.propsToAttributes(o.propSchema)},addKeyboardShortcuts(){return c.addInlineContentKeyboardShortcuts(o)},parseHTML(){return oe(o,e.parse)},renderHTML({node:r}){const s=this.options.editor,i=e.render.call({renderType:"dom",props:void 0},u.nodeToCustomInlineContent(r,s.schema.inlineContentSchema,s.schema.styleSchema),()=>{},s);return c.addInlineContentAttributes(i,o.type,r.attrs,o.propSchema)},addNodeView(){return r=>{const{node:s,getPos:i}=r,l=this.options.editor,a=e.render.call({renderType:"nodeView",props:r},u.nodeToCustomInlineContent(s,l.schema.inlineContentSchema,l.schema.styleSchema),d=>{const p=u.inlineContentToNodes([d],l.pmSchema),f=i();f&&l.transact(h=>h.replaceWith(f,f+s.nodeSize,p))},l);return c.addInlineContentAttributes(a,o.type,s.attrs,o.propSchema)}}});return c.createInlineContentSpecFromTipTapNode(t,o.propSchema,{...e,toExternalHTML:e.toExternalHTML,render(r,s,i){const l=e.render(r,s,i);return c.addInlineContentAttributes(l,o.type,r.props,o.propSchema)}})}function ne(o,e,t,n="before"){const r=typeof t=="string"?t:t.id,s=u.getPmSchema(o),i=e.map(p=>u.blockToNode(p,s)),l=c.getNodeById(r,o.doc);if(!l)throw new Error(`Block with ID ${r} not found`);let a=l.posBeforeNode;return n==="after"&&(a+=l.node.nodeSize),o.step(new L.ReplaceStep(a,a,new x.Slice(x.Fragment.from(i),0,0))),i.map(p=>u.nodeToBlock(p,s))}function _(o){if(!o||o.type.name!=="column")throw new Error("Invalid columnPos: does not point to column node.");const e=o.firstChild;if(!e)throw new Error("Invalid column: does not have child node.");const t=e.firstChild;if(!t)throw new Error("Invalid blockContainer: does not have child node.");return o.childCount===1&&e.childCount===1&&t.type.name==="paragraph"&&t.content.content.length===0}function re(o,e){const t=o.doc.resolve(e),n=t.nodeAfter;if(!n||n.type.name!=="columnList")throw new Error("Invalid columnListPos: does not point to columnList node.");for(let r=n.childCount-1;r>=0;r--){const s=o.doc.resolve(t.pos+1).posAtIndex(r),l=o.doc.resolve(s).nodeAfter;if(!l||l.type.name!=="column")throw new Error("Invalid columnPos: does not point to column node.");_(l)&&o.delete(s,s+l.nodeSize)}}function D(o,e){re(o,e);const n=o.doc.resolve(e).nodeAfter;if(!n||n.type.name!=="columnList")throw new Error("Invalid columnListPos: does not point to columnList node.");if(n.childCount>2)return;if(n.childCount<2)throw new Error("Invalid columnList: contains fewer than two children.");const r=e+1,i=o.doc.resolve(r).nodeAfter,l=e+n.nodeSize-1,d=o.doc.resolve(l).nodeBefore;if(!i||!d)throw new Error("Invalid columnList: does not contain children.");const p=_(i),f=_(d);if(p&&f){o.delete(e,e+n.nodeSize);return}if(p){o.step(new L.ReplaceAroundStep(e,e+n.nodeSize,l-d.nodeSize+1,l-1,x.Slice.empty,0,!1));return}if(f){o.step(new L.ReplaceAroundStep(e,e+n.nodeSize,r+1,r+i.nodeSize-1,x.Slice.empty,0,!1));return}}function $(o,e,t){const n=u.getPmSchema(o),r=t.map(f=>u.blockToNode(f,n)),s=new Set(e.map(f=>typeof f=="string"?f:f.id)),i=[],l=new Set,a=typeof e[0]=="string"?e[0]:e[0].id;let d=0;if(o.doc.descendants((f,h)=>{if(s.size===0)return!1;if(!f.type.isInGroup("bnBlock")||!s.has(f.attrs.id))return!0;if(i.push(u.nodeToBlock(f,n)),s.delete(f.attrs.id),t.length>0&&f.attrs.id===a){const y=o.doc.nodeSize;o.insert(h,r);const E=o.doc.nodeSize;d+=y-E}const g=o.doc.nodeSize,m=o.doc.resolve(h-d);m.node().type.name==="column"?l.add(m.before(-1)):m.node().type.name==="columnList"&&l.add(m.before()),m.node().type.name==="blockGroup"&&m.node(m.depth-1).type.name!=="doc"&&m.node().childCount===1?o.delete(m.before(),m.after()):o.delete(h-d,h-d+f.nodeSize);const S=o.doc.nodeSize;return d+=g-S,!1}),s.size>0){const f=[...s].join(` +-`);throw Error("Blocks with the following IDs could not be found in the editor: "+f)}return l.forEach(f=>D(o,f)),{insertedBlocks:r.map(f=>u.nodeToBlock(f,n)),removedBlocks:i}}function Ve(o,e,t,n,r){let s;if(e)if(typeof e=="string")s=u.inlineContentToNodes([e],o.pmSchema,n);else if(Array.isArray(e))s=u.inlineContentToNodes(e,o.pmSchema,n);else if(e.type==="tableContent")s=u.tableContentToNodes(e,o.pmSchema);else throw new u.UnreachableCaseError(e.type);else throw new Error("blockContent is required");const l=((r==null?void 0:r.document)??document).createDocumentFragment();for(const a of s)if(a.type.name!=="text"&&o.schema.inlineContentSchema[a.type.name]){const d=o.schema.inlineContentSpecs[a.type.name].implementation;if(d){const p=u.nodeToCustomInlineContent(a,o.schema.inlineContentSchema,o.schema.styleSchema),f=d.render.call({renderType:"dom",props:void 0},p,()=>{},o);if(f){if(l.appendChild(f.dom),f.contentDOM){const h=t.serializeFragment(a.content,r);f.contentDOM.dataset.editable="",f.contentDOM.appendChild(h)}continue}}}else if(a.type.name==="text"){let d=document.createTextNode(a.textContent);for(const p of a.marks.toReversed())if(p.type.name in o.schema.styleSpecs){const f=o.schema.styleSpecs[p.type.name].implementation.render(p.attrs.stringValue,o);f.contentDOM.appendChild(d),d=f.dom}else{const f=p.type.spec.toDOM(p,!0),h=x.DOMSerializer.renderSpec(document,f);h.contentDOM.appendChild(d),d=h.dom}l.appendChild(d)}else{const d=t.serializeFragment(x.Fragment.from([a]),r);l.appendChild(d)}return l}function Re(o,e,t,n){var f,h,g,m,S;const r=o.pmSchema.nodes.blockContainer,s=e.props||{};for(const[y,E]of Object.entries(o.schema.blockSchema[e.type].propSchema))!(y in s)&&E.default!==void 0&&(s[y]=E.default);const i=e.children||[],a=o.blockImplementations[e.type].implementation.render.call({renderType:"dom",props:void 0},{...e,props:s,children:i},o);if(a.contentDOM&&e.content){const y=Ve(o,e.content,t,e.type,n);a.contentDOM.appendChild(y)}if(o.pmSchema.nodes[e.type].isInGroup("bnBlock")){if(e.children&&e.children.length>0){const y=se(o,e.children,t,n);(f=a.contentDOM)==null||f.append(y)}return a.dom}const p=(g=(h=r.spec)==null?void 0:h.toDOM)==null?void 0:g.call(h,r.create({id:e.id,...s}));return(m=p.contentDOM)==null||m.appendChild(a.dom),e.children&&e.children.length>0&&((S=p.contentDOM)==null||S.appendChild(ie(o,e.children,t,n))),p.dom}function se(o,e,t,n){const s=((n==null?void 0:n.document)??document).createDocumentFragment();for(const i of e){const l=Re(o,i,t,n);s.appendChild(l)}return s}const ie=(o,e,t,n)=>{var l;const r=o.pmSchema.nodes.blockGroup,s=r.spec.toDOM(r.create({})),i=se(o,e,t,n);return(l=s.contentDOM)==null||l.appendChild(i),s.dom},ze=o=>(o.querySelectorAll('[data-content-type="numberedListItem"]').forEach(t=>{var r,s;const n=(s=(r=t.closest(".bn-block-outer"))==null?void 0:r.previousElementSibling)==null?void 0:s.querySelector('[data-content-type="numberedListItem"]');if(!n)t.setAttribute("data-index",t.getAttribute("data-start")||"1");else{const i=n.getAttribute("data-index");t.setAttribute("data-index",(parseInt(i||"0")+1).toString())}}),o),Ge=o=>(o.querySelectorAll('[data-content-type="checkListItem"] input').forEach(t=>{t.disabled=!0}),o),qe=o=>(o.querySelectorAll('.bn-toggle-wrapper[data-show-children="false"]').forEach(t=>{t.setAttribute("data-show-children","true")}),o),We=o=>(o.querySelectorAll('[data-content-type="table"] table').forEach(t=>{t.setAttribute("style",`--default-cell-min-width: ${c.EMPTY_CELL_WIDTH}px;`),t.setAttribute("data-show-children","true")}),o),je=o=>(o.querySelectorAll('[data-content-type="table"] table').forEach(t=>{var s;const n=document.createElement("div");n.className="tableWrapper";const r=document.createElement("div");r.className="tableWrapper-inner",n.appendChild(r),(s=t.parentElement)==null||s.appendChild(n),n.appendChild(t)}),o),Ke=o=>(o.querySelectorAll(".bn-inline-content:empty").forEach(t=>{const n=document.createElement("span");n.className="ProseMirror-trailingBreak",n.setAttribute("style","display: inline-block;"),t.appendChild(n)}),o),ce=(o,e)=>{const t=x.DOMSerializer.fromSchema(o),n=[ze,Ge,qe,We,je,Ke];return{serializeBlocks:(r,s)=>{let i=ie(e,r,t,s);for(const l of n)i=l(i);return i.outerHTML}}};function Ye(o){return o.transact(e=>{const t=u.getNearestBlockPos(e.doc,e.selection.anchor);if(e.selection instanceof N.CellSelection)return{type:"cell",anchorBlockId:t.node.attrs.id,anchorCellOffset:e.selection.$anchorCell.pos-t.posBeforeNode,headCellOffset:e.selection.$headCell.pos-t.posBeforeNode};if(e.selection instanceof C.NodeSelection)return{type:"node",anchorBlockId:t.node.attrs.id};{const n=u.getNearestBlockPos(e.doc,e.selection.head);return{type:"text",anchorBlockId:t.node.attrs.id,headBlockId:n.node.attrs.id,anchorOffset:e.selection.anchor-t.posBeforeNode,headOffset:e.selection.head-n.posBeforeNode}}})}function Je(o,e){var r,s;const t=(r=c.getNodeById(e.anchorBlockId,o.doc))==null?void 0:r.posBeforeNode;if(t===void 0)throw new Error(`Could not find block with ID ${e.anchorBlockId} to update selection`);let n;if(e.type==="cell")n=N.CellSelection.create(o.doc,t+e.anchorCellOffset,t+e.headCellOffset);else if(e.type==="node")n=C.NodeSelection.create(o.doc,t+1);else{const i=(s=c.getNodeById(e.headBlockId,o.doc))==null?void 0:s.posBeforeNode;if(i===void 0)throw new Error(`Could not find block with ID ${e.headBlockId} to update selection`);n=C.TextSelection.create(o.doc,t+e.anchorOffset,i+e.headOffset)}o.setSelection(n)}function U(o){return o.map(e=>e.type==="columnList"?e.children.map(t=>U(t.children)).flat():{...e,children:U(e.children)}).flat()}function ae(o,e,t){o.transact(n=>{var i;const r=((i=o.getSelection())==null?void 0:i.blocks)||[o.getTextCursorPosition().block],s=Ye(o);o.removeBlocks(r),o.insertBlocks(U(r),e,t),Je(n,s)})}function le(o){return!o||o.type!=="columnList"}function de(o,e,t){let n,r;if(e?e.children.length>0?(n=e.children[e.children.length-1],r="after"):(n=e,r="before"):t&&(n=t,r="before"),!n||!r)return;const s=o.getParentBlock(n);return le(s)?{referenceBlock:n,placement:r}:de(o,r==="after"?n:o.getPrevBlock(n),s)}function ue(o,e,t){let n,r;if(e?e.children.length>0?(n=e.children[0],r="before"):(n=e,r="after"):t&&(n=t,r="after"),!n||!r)return;const s=o.getParentBlock(n);return le(s)?{referenceBlock:n,placement:r}:ue(o,r==="before"?n:o.getNextBlock(n),s)}function Qe(o){o.transact(()=>{const e=o.getSelection(),t=(e==null?void 0:e.blocks[0])||o.getTextCursorPosition().block,n=de(o,o.getPrevBlock(t),o.getParentBlock(t));n&&ae(o,n.referenceBlock,n.placement)})}function Xe(o){o.transact(()=>{const e=o.getSelection(),t=(e==null?void 0:e.blocks[(e==null?void 0:e.blocks.length)-1])||o.getTextCursorPosition().block,n=ue(o,o.getNextBlock(t),o.getParentBlock(t));n&&ae(o,n.referenceBlock,n.placement)})}function Ze(o,e,t){const{$from:n,$to:r}=o.selection,s=n.blockRange(r,m=>m.childCount>0&&(m.type.name==="blockGroup"||m.type.name==="column"));if(!s)return!1;const i=s.startIndex;if(i===0)return!1;const a=s.parent.child(i-1);if(a.type!==e)return!1;const d=a.lastChild&&a.lastChild.type===t,p=x.Fragment.from(d?e.create():null),f=new x.Slice(x.Fragment.from(e.create(null,x.Fragment.from(t.create(null,p)))),d?3:1,0),h=s.start,g=s.end;return o.step(new L.ReplaceAroundStep(h-(d?3:1),g,h,g,f,1,!0)).scrollIntoView(),!0}function pe(o){return o.transact(e=>Ze(e,o.pmSchema.nodes.blockContainer,o.pmSchema.nodes.blockGroup))}function et(o){o._tiptapEditor.commands.liftListItem("blockContainer")}function tt(o){return o.transact(e=>{const{bnBlock:t}=u.getBlockInfoFromTransaction(e);return e.doc.resolve(t.beforePos).nodeBefore!==null})}function ot(o){return o.transact(e=>{const{bnBlock:t}=u.getBlockInfoFromTransaction(e);return e.doc.resolve(t.beforePos).depth>1})}function fe(o,e){const t=typeof e=="string"?e:e.id,n=u.getPmSchema(o),r=c.getNodeById(t,o);if(r)return u.nodeToBlock(r.node,n)}function he(o,e){const t=typeof e=="string"?e:e.id,n=c.getNodeById(t,o),r=u.getPmSchema(o);if(!n)return;const i=o.resolve(n.posBeforeNode).nodeBefore;if(i)return u.nodeToBlock(i,r)}function me(o,e){const t=typeof e=="string"?e:e.id,n=c.getNodeById(t,o),r=u.getPmSchema(o);if(!n)return;const i=o.resolve(n.posBeforeNode+n.node.nodeSize).nodeAfter;if(i)return u.nodeToBlock(i,r)}function ke(o,e){const t=typeof e=="string"?e:e.id,n=u.getPmSchema(o),r=c.getNodeById(t,o);if(!r)return;const s=o.resolve(r.posBeforeNode),i=s.node(),l=s.node(-1),a=l.type.name!=="doc"?i.type.name==="blockGroup"?l:i:void 0;if(a)return u.nodeToBlock(a,n)}class nt{constructor(e){this.editor=e}get document(){return this.editor.transact(e=>u.docToBlocks(e.doc,this.editor.pmSchema))}getBlock(e){return this.editor.transact(t=>fe(t.doc,e))}getPrevBlock(e){return this.editor.transact(t=>he(t.doc,e))}getNextBlock(e){return this.editor.transact(t=>me(t.doc,e))}getParentBlock(e){return this.editor.transact(t=>ke(t.doc,e))}forEachBlock(e,t=!1){const n=this.document.slice();t&&n.reverse();function r(s){for(const i of s){if(e(i)===!1)return!1;const l=t?i.children.slice().reverse():i.children;if(!r(l))return!1}return!0}r(n)}insertBlocks(e,t,n="before"){return this.editor.transact(r=>ne(r,e,t,n))}updateBlock(e,t){return this.editor.transact(n=>c.updateBlock(n,e,t))}removeBlocks(e){return this.editor.transact(t=>$(t,e,[]).removedBlocks)}replaceBlocks(e,t){return this.editor.transact(n=>$(n,e,t))}canNestBlock(){return tt(this.editor)}nestBlock(){pe(this.editor)}canUnnestBlock(){return ot(this.editor)}unnestBlock(){et(this.editor)}moveBlocksUp(){return Qe(this.editor)}moveBlocksDown(){return Xe(this.editor)}}class rt extends R.EventEmitter{constructor(e){super(),this.editor=e,e.on("create",()=>{e._tiptapEditor.on("update",({transaction:t,appendedTransactions:n})=>{this.emit("onChange",{editor:e,transaction:t,appendedTransactions:n})}),e._tiptapEditor.on("selectionUpdate",({transaction:t})=>{this.emit("onSelectionChange",{editor:e,transaction:t})}),e._tiptapEditor.on("mount",()=>{this.emit("onMount",{editor:e})}),e._tiptapEditor.on("unmount",()=>{this.emit("onUnmount",{editor:e})})})}onChange(e,t=!0){const n=({transaction:r,appendedTransactions:s})=>{!t&&Y(r)||e(this.editor,{getChanges(){return b.getBlocksChangedByTransaction(r,s)}})};return this.on("onChange",n),()=>{this.off("onChange",n)}}onSelectionChange(e,t=!1){const n=r=>{!t&&Y(r.transaction)||e(this.editor)};return this.on("onSelectionChange",n),()=>{this.off("onSelectionChange",n)}}onMount(e){return this.on("onMount",e),()=>{this.off("onMount",e)}}onUnmount(e){return this.on("onUnmount",e),()=>{this.off("onUnmount",e)}}}function Y(o){return!!o.getMeta("y-sync$")}function st(o){return Array.prototype.indexOf.call(o.parentElement.childNodes,o)}function it(o){return o.nodeType===3&&!/\S/.test(o.nodeValue||"")}function ct(o){o.querySelectorAll("li > ul, li > ol").forEach(e=>{const t=st(e),n=e.parentElement,r=Array.from(n.childNodes).slice(t+1);e.remove(),r.forEach(s=>{s.remove()}),n.insertAdjacentElement("afterend",e),r.reverse().forEach(s=>{if(it(s))return;const i=document.createElement("li");i.append(s),e.insertAdjacentElement("afterend",i)}),n.childNodes.length===0&&n.remove()})}function at(o){o.querySelectorAll("li + ul, li + ol").forEach(e=>{var s,i;const t=e.previousElementSibling,n=document.createElement("div");t.insertAdjacentElement("afterend",n),n.append(t);const r=document.createElement("div");for(r.setAttribute("data-node-type","blockGroup"),n.append(r);((s=n.nextElementSibling)==null?void 0:s.nodeName)==="UL"||((i=n.nextElementSibling)==null?void 0:i.nodeName)==="OL";)r.append(n.nextElementSibling)})}let J=null;function lt(){return J||(J=document.implementation.createHTMLDocument("title"))}function dt(o){if(typeof o=="string"){const e=lt().createElement("div");e.innerHTML=o,o=e}return ct(o),at(o),o}function z(o,e){const t=dt(o),r=x.DOMParser.fromSchema(e).parse(t,{topNode:e.nodes.blockGroup.create()}),s=[];for(let i=0;i{const r=String((n==null?void 0:n.url)||"");return c.isVideoUrl(r)?pt(t,n):H.defaultHandlers.image(t,n)},code:ut,blockquote:(t,n)=>{const r={type:"element",tagName:"blockquote",properties:{},children:t.wrap(t.all(n),!1)};return t.patch(n,r),t.applyData(n,r)}}}).use($e.default).processSync(o).value}function ge(o,e){const t=G(o);return z(t,e)}class ft{constructor(e){this.editor=e}blocksToHTMLLossy(e=this.editor.document){return b.createExternalHTMLExporter(this.editor.pmSchema,this.editor).exportBlocks(e,{})}blocksToFullHTML(e=this.editor.document){return ce(this.editor.pmSchema,this.editor).serializeBlocks(e,{})}tryParseHTMLToBlocks(e){return z(e,this.editor.pmSchema)}blocksToMarkdownLossy(e=this.editor.document){return b.blocksToMarkdown(e,this.editor.pmSchema,this.editor,{})}tryParseMarkdownToBlocks(e){return ge(e,this.editor.pmSchema)}pasteHTML(e,t=!1){var r;let n=e;if(!t){const s=this.tryParseHTMLToBlocks(e);n=this.blocksToFullHTML(s)}n&&((r=this.editor.prosemirrorView)==null||r.pasteHTML(n))}pasteText(e){var t;return(t=this.editor.prosemirrorView)==null?void 0:t.pasteText(e)}pasteMarkdown(e){const t=G(e);return this.pasteHTML(t)}}const q=["vscode-editor-data","blocknote/html","text/markdown","text/html","text/plain","Files"];function ht(o,e){if(!o.startsWith(".")||!e.startsWith("."))throw new Error("The strings provided are not valid file extensions.");return o===e}function mt(o,e){const t=o.split("/"),n=e.split("/");if(t.length!==2)throw new Error(`The string ${o} is not a valid MIME type.`);if(n.length!==2)throw new Error(`The string ${e} is not a valid MIME type.`);return t[1]==="*"||n[1]==="*"?t[0]===n[0]:(t[0]==="*"||n[0]==="*"||t[0]===n[0])&&t[1]===n[1]}function Q(o,e,t,n="after"){let r;return Array.isArray(e.content)&&e.content.length===0?r=o.updateBlock(e,t).id:r=o.insertBlocks([t],e,n)[0].id,r}async function be(o,e){var s;if(!e.uploadFile){console.warn("Attempted ot insert file, but uploadFile is not set in the BlockNote editor options");return}const t="dataTransfer"in o?o.dataTransfer:o.clipboardData;if(t===null)return;let n=null;for(const i of q)if(t.types.includes(i)){n=i;break}if(n!=="Files")return;const r=t.items;if(r){o.preventDefault();for(let i=0;i{var T;const y=u.getNearestBlockPos(S.doc,m.pos),E=(T=e.domElement)==null?void 0:T.querySelector(`[data-id="${y.node.attrs.id}"]`),P=E==null?void 0:E.getBoundingClientRect();return Q(e,e.getBlock(y.node.attrs.id),d,P&&(P.top+P.bottom)/2>g.top?"before":"after")})}else return;const f=await e.uploadFile(a,p),h=typeof f=="string"?{props:{url:f}}:{...f};e.updateBlock(p,h)}}}}const kt=o=>B.Extension.create({name:"dropFile",addProseMirrorPlugins(){return[new C.Plugin({props:{handleDOMEvents:{drop(e,t){if(!o.isEditable)return;let n=null;for(const r of q)if(t.dataTransfer.types.includes(r)){n=r;break}return n===null?!0:n==="Files"?(be(t,o),!0):!1}}}})]}}),gt=/(^|\n) {0,3}#{1,6} {1,8}[^\n]{1,64}\r?\n\r?\n\s{0,32}\S/,bt=/(_|__|\*|\*\*|~~|==|\+\+)(?!\s)(?:[^\s](?:.{0,62}[^\s])?|\S)(?=\1)/,St=/\[[^\]]{1,128}\]\(https?:\/\/\S{1,999}\)/,Bt=/(?:\s|^)`(?!\s)(?:[^\s`](?:[^`]{0,46}[^\s`])?|[^\s`])`([^\w]|$)/,yt=/(?:^|\n)\s{0,5}-\s{1}[^\n]+\n\s{0,15}-\s/,Ct=/(?:^|\n)\s{0,5}\d+\.\s{1}[^\n]+\n\s{0,15}\d+\.\s/,Et=/\n{2} {0,3}-{2,48}\n{2}/,Tt=/(?:\n|^)(```|~~~|\$\$)(?!`|~)[^\s]{0,64} {0,64}[^\n]{0,64}\n[\s\S]{0,9999}?\s*\1 {0,64}(?:\n+|$)/,xt=/(?:\n|^)(?!\s)\w[^\n]{0,64}\r?\n(-|=)\1{0,64}\n\n\s{0,64}(\w|$)/,Mt=/(?:^|(\r?\n\r?\n))( {0,3}>[^\n]{1,333}\n){1,999}($|(\r?\n))/,It=/^\s*\|(.+\|)+\s*$/m,Pt=/^\s*\|(\s*[-:]+[-:]\s*\|)+\s*$/m,wt=/^\s*\|(.+\|)+\s*$/m,vt=o=>gt.test(o)||bt.test(o)||St.test(o)||Bt.test(o)||yt.test(o)||Ct.test(o)||Et.test(o)||Tt.test(o)||xt.test(o)||Mt.test(o)||It.test(o)||Pt.test(o)||wt.test(o);async function At(o,e){const{schema:t}=e.state;if(!o.clipboardData)return!1;const n=o.clipboardData.getData("text/plain");if(!n)return!1;if(!t.nodes.codeBlock)return e.pasteText(n),!0;const r=o.clipboardData.getData("vscode-editor-data"),s=r?JSON.parse(r):void 0,i=s==null?void 0:s.mode;return i?(e.pasteHTML(`
${n.replace(/\r\n?/g,`
++`);throw Error("Blocks with the following IDs could not be found in the editor: "+f)}return l.forEach(f=>D(o,f)),{insertedBlocks:r.map(f=>u.nodeToBlock(f,n)),removedBlocks:i}}function Ve(o,e,t,n,r){let s;if(e)if(typeof e=="string")s=u.inlineContentToNodes([e],o.pmSchema,n);else if(Array.isArray(e))s=u.inlineContentToNodes(e,o.pmSchema,n);else if(e.type==="tableContent")s=u.tableContentToNodes(e,o.pmSchema);else throw new u.UnreachableCaseError(e.type);else throw new Error("blockContent is required");const l=((r==null?void 0:r.document)??document).createDocumentFragment();for(const a of s)if(a.type.name!=="text"&&o.schema.inlineContentSchema[a.type.name]){const d=o.schema.inlineContentSpecs[a.type.name].implementation;if(d){const p=u.nodeToCustomInlineContent(a,o.schema.inlineContentSchema,o.schema.styleSchema),f=d.render.call({renderType:"dom",props:void 0},p,()=>{},o);if(f){if(l.appendChild(f.dom),f.contentDOM){const h=t.serializeFragment(a.content,r);f.contentDOM.dataset.editable="",f.contentDOM.appendChild(h)}continue}}}else if(a.type.name==="text"){let d=document.createTextNode(a.textContent);for(const p of Array.from(a.marks).reverse())if(p.type.name in o.schema.styleSpecs){const f=o.schema.styleSpecs[p.type.name].implementation.render(p.attrs.stringValue,o);f.contentDOM.appendChild(d),d=f.dom}else{const f=p.type.spec.toDOM(p,!0),h=x.DOMSerializer.renderSpec(document,f);h.contentDOM.appendChild(d),d=h.dom}l.appendChild(d)}else{const d=t.serializeFragment(x.Fragment.from([a]),r);l.appendChild(d)}return l}function Re(o,e,t,n){var f,h,g,m,S;const r=o.pmSchema.nodes.blockContainer,s=e.props||{};for(const[y,E]of Object.entries(o.schema.blockSchema[e.type].propSchema))!(y in s)&&E.default!==void 0&&(s[y]=E.default);const i=e.children||[],a=o.blockImplementations[e.type].implementation.render.call({renderType:"dom",props:void 0},{...e,props:s,children:i},o);if(a.contentDOM&&e.content){const y=Ve(o,e.content,t,e.type,n);a.contentDOM.appendChild(y)}if(o.pmSchema.nodes[e.type].isInGroup("bnBlock")){if(e.children&&e.children.length>0){const y=se(o,e.children,t,n);(f=a.contentDOM)==null||f.append(y)}return a.dom}const p=(g=(h=r.spec)==null?void 0:h.toDOM)==null?void 0:g.call(h,r.create({id:e.id,...s}));return(m=p.contentDOM)==null||m.appendChild(a.dom),e.children&&e.children.length>0&&((S=p.contentDOM)==null||S.appendChild(ie(o,e.children,t,n))),p.dom}function se(o,e,t,n){const s=((n==null?void 0:n.document)??document).createDocumentFragment();for(const i of e){const l=Re(o,i,t,n);s.appendChild(l)}return s}const ie=(o,e,t,n)=>{var l;const r=o.pmSchema.nodes.blockGroup,s=r.spec.toDOM(r.create({})),i=se(o,e,t,n);return(l=s.contentDOM)==null||l.appendChild(i),s.dom},ze=o=>(o.querySelectorAll('[data-content-type="numberedListItem"]').forEach(t=>{var r,s;const n=(s=(r=t.closest(".bn-block-outer"))==null?void 0:r.previousElementSibling)==null?void 0:s.querySelector('[data-content-type="numberedListItem"]');if(!n)t.setAttribute("data-index",t.getAttribute("data-start")||"1");else{const i=n.getAttribute("data-index");t.setAttribute("data-index",(parseInt(i||"0")+1).toString())}}),o),Ge=o=>(o.querySelectorAll('[data-content-type="checkListItem"] input').forEach(t=>{t.disabled=!0}),o),qe=o=>(o.querySelectorAll('.bn-toggle-wrapper[data-show-children="false"]').forEach(t=>{t.setAttribute("data-show-children","true")}),o),We=o=>(o.querySelectorAll('[data-content-type="table"] table').forEach(t=>{t.setAttribute("style",`--default-cell-min-width: ${c.EMPTY_CELL_WIDTH}px;`),t.setAttribute("data-show-children","true")}),o),je=o=>(o.querySelectorAll('[data-content-type="table"] table').forEach(t=>{var s;const n=document.createElement("div");n.className="tableWrapper";const r=document.createElement("div");r.className="tableWrapper-inner",n.appendChild(r),(s=t.parentElement)==null||s.appendChild(n),n.appendChild(t)}),o),Ke=o=>(o.querySelectorAll(".bn-inline-content:empty").forEach(t=>{const n=document.createElement("span");n.className="ProseMirror-trailingBreak",n.setAttribute("style","display: inline-block;"),t.appendChild(n)}),o),ce=(o,e)=>{const t=x.DOMSerializer.fromSchema(o),n=[ze,Ge,qe,We,je,Ke];return{serializeBlocks:(r,s)=>{let i=ie(e,r,t,s);for(const l of n)i=l(i);return i.outerHTML}}};function Ye(o){return o.transact(e=>{const t=u.getNearestBlockPos(e.doc,e.selection.anchor);if(e.selection instanceof N.CellSelection)return{type:"cell",anchorBlockId:t.node.attrs.id,anchorCellOffset:e.selection.$anchorCell.pos-t.posBeforeNode,headCellOffset:e.selection.$headCell.pos-t.posBeforeNode};if(e.selection instanceof C.NodeSelection)return{type:"node",anchorBlockId:t.node.attrs.id};{const n=u.getNearestBlockPos(e.doc,e.selection.head);return{type:"text",anchorBlockId:t.node.attrs.id,headBlockId:n.node.attrs.id,anchorOffset:e.selection.anchor-t.posBeforeNode,headOffset:e.selection.head-n.posBeforeNode}}})}function Je(o,e){var r,s;const t=(r=c.getNodeById(e.anchorBlockId,o.doc))==null?void 0:r.posBeforeNode;if(t===void 0)throw new Error(`Could not find block with ID ${e.anchorBlockId} to update selection`);let n;if(e.type==="cell")n=N.CellSelection.create(o.doc,t+e.anchorCellOffset,t+e.headCellOffset);else if(e.type==="node")n=C.NodeSelection.create(o.doc,t+1);else{const i=(s=c.getNodeById(e.headBlockId,o.doc))==null?void 0:s.posBeforeNode;if(i===void 0)throw new Error(`Could not find block with ID ${e.headBlockId} to update selection`);n=C.TextSelection.create(o.doc,t+e.anchorOffset,i+e.headOffset)}o.setSelection(n)}function U(o){return o.map(e=>e.type==="columnList"?e.children.map(t=>U(t.children)).flat():{...e,children:U(e.children)}).flat()}function ae(o,e,t){o.transact(n=>{var i;const r=((i=o.getSelection())==null?void 0:i.blocks)||[o.getTextCursorPosition().block],s=Ye(o);o.removeBlocks(r),o.insertBlocks(U(r),e,t),Je(n,s)})}function le(o){return!o||o.type!=="columnList"}function de(o,e,t){let n,r;if(e?e.children.length>0?(n=e.children[e.children.length-1],r="after"):(n=e,r="before"):t&&(n=t,r="before"),!n||!r)return;const s=o.getParentBlock(n);return le(s)?{referenceBlock:n,placement:r}:de(o,r==="after"?n:o.getPrevBlock(n),s)}function ue(o,e,t){let n,r;if(e?e.children.length>0?(n=e.children[0],r="before"):(n=e,r="after"):t&&(n=t,r="after"),!n||!r)return;const s=o.getParentBlock(n);return le(s)?{referenceBlock:n,placement:r}:ue(o,r==="before"?n:o.getNextBlock(n),s)}function Qe(o){o.transact(()=>{const e=o.getSelection(),t=(e==null?void 0:e.blocks[0])||o.getTextCursorPosition().block,n=de(o,o.getPrevBlock(t),o.getParentBlock(t));n&&ae(o,n.referenceBlock,n.placement)})}function Xe(o){o.transact(()=>{const e=o.getSelection(),t=(e==null?void 0:e.blocks[(e==null?void 0:e.blocks.length)-1])||o.getTextCursorPosition().block,n=ue(o,o.getNextBlock(t),o.getParentBlock(t));n&&ae(o,n.referenceBlock,n.placement)})}function Ze(o,e,t){const{$from:n,$to:r}=o.selection,s=n.blockRange(r,m=>m.childCount>0&&(m.type.name==="blockGroup"||m.type.name==="column"));if(!s)return!1;const i=s.startIndex;if(i===0)return!1;const a=s.parent.child(i-1);if(a.type!==e)return!1;const d=a.lastChild&&a.lastChild.type===t,p=x.Fragment.from(d?e.create():null),f=new x.Slice(x.Fragment.from(e.create(null,x.Fragment.from(t.create(null,p)))),d?3:1,0),h=s.start,g=s.end;return o.step(new L.ReplaceAroundStep(h-(d?3:1),g,h,g,f,1,!0)).scrollIntoView(),!0}function pe(o){return o.transact(e=>Ze(e,o.pmSchema.nodes.blockContainer,o.pmSchema.nodes.blockGroup))}function et(o){o._tiptapEditor.commands.liftListItem("blockContainer")}function tt(o){return o.transact(e=>{const{bnBlock:t}=u.getBlockInfoFromTransaction(e);return e.doc.resolve(t.beforePos).nodeBefore!==null})}function ot(o){return o.transact(e=>{const{bnBlock:t}=u.getBlockInfoFromTransaction(e);return e.doc.resolve(t.beforePos).depth>1})}function fe(o,e){const t=typeof e=="string"?e:e.id,n=u.getPmSchema(o),r=c.getNodeById(t,o);if(r)return u.nodeToBlock(r.node,n)}function he(o,e){const t=typeof e=="string"?e:e.id,n=c.getNodeById(t,o),r=u.getPmSchema(o);if(!n)return;const i=o.resolve(n.posBeforeNode).nodeBefore;if(i)return u.nodeToBlock(i,r)}function me(o,e){const t=typeof e=="string"?e:e.id,n=c.getNodeById(t,o),r=u.getPmSchema(o);if(!n)return;const i=o.resolve(n.posBeforeNode+n.node.nodeSize).nodeAfter;if(i)return u.nodeToBlock(i,r)}function ke(o,e){const t=typeof e=="string"?e:e.id,n=u.getPmSchema(o),r=c.getNodeById(t,o);if(!r)return;const s=o.resolve(r.posBeforeNode),i=s.node(),l=s.node(-1),a=l.type.name!=="doc"?i.type.name==="blockGroup"?l:i:void 0;if(a)return u.nodeToBlock(a,n)}class nt{constructor(e){this.editor=e}get document(){return this.editor.transact(e=>u.docToBlocks(e.doc,this.editor.pmSchema))}getBlock(e){return this.editor.transact(t=>fe(t.doc,e))}getPrevBlock(e){return this.editor.transact(t=>he(t.doc,e))}getNextBlock(e){return this.editor.transact(t=>me(t.doc,e))}getParentBlock(e){return this.editor.transact(t=>ke(t.doc,e))}forEachBlock(e,t=!1){const n=this.document.slice();t&&n.reverse();function r(s){for(const i of s){if(e(i)===!1)return!1;const l=t?i.children.slice().reverse():i.children;if(!r(l))return!1}return!0}r(n)}insertBlocks(e,t,n="before"){return this.editor.transact(r=>ne(r,e,t,n))}updateBlock(e,t){return this.editor.transact(n=>c.updateBlock(n,e,t))}removeBlocks(e){return this.editor.transact(t=>$(t,e,[]).removedBlocks)}replaceBlocks(e,t){return this.editor.transact(n=>$(n,e,t))}canNestBlock(){return tt(this.editor)}nestBlock(){pe(this.editor)}canUnnestBlock(){return ot(this.editor)}unnestBlock(){et(this.editor)}moveBlocksUp(){return Qe(this.editor)}moveBlocksDown(){return Xe(this.editor)}}class rt extends R.EventEmitter{constructor(e){super(),this.editor=e,e.on("create",()=>{e._tiptapEditor.on("update",({transaction:t,appendedTransactions:n})=>{this.emit("onChange",{editor:e,transaction:t,appendedTransactions:n})}),e._tiptapEditor.on("selectionUpdate",({transaction:t})=>{this.emit("onSelectionChange",{editor:e,transaction:t})}),e._tiptapEditor.on("mount",()=>{this.emit("onMount",{editor:e})}),e._tiptapEditor.on("unmount",()=>{this.emit("onUnmount",{editor:e})})})}onChange(e,t=!0){const n=({transaction:r,appendedTransactions:s})=>{!t&&Y(r)||e(this.editor,{getChanges(){return b.getBlocksChangedByTransaction(r,s)}})};return this.on("onChange",n),()=>{this.off("onChange",n)}}onSelectionChange(e,t=!1){const n=r=>{!t&&Y(r.transaction)||e(this.editor)};return this.on("onSelectionChange",n),()=>{this.off("onSelectionChange",n)}}onMount(e){return this.on("onMount",e),()=>{this.off("onMount",e)}}onUnmount(e){return this.on("onUnmount",e),()=>{this.off("onUnmount",e)}}}function Y(o){return!!o.getMeta("y-sync$")}function st(o){return Array.prototype.indexOf.call(o.parentElement.childNodes,o)}function it(o){return o.nodeType===3&&!/\S/.test(o.nodeValue||"")}function ct(o){o.querySelectorAll("li > ul, li > ol").forEach(e=>{const t=st(e),n=e.parentElement,r=Array.from(n.childNodes).slice(t+1);e.remove(),r.forEach(s=>{s.remove()}),n.insertAdjacentElement("afterend",e),r.reverse().forEach(s=>{if(it(s))return;const i=document.createElement("li");i.append(s),e.insertAdjacentElement("afterend",i)}),n.childNodes.length===0&&n.remove()})}function at(o){o.querySelectorAll("li + ul, li + ol").forEach(e=>{var s,i;const t=e.previousElementSibling,n=document.createElement("div");t.insertAdjacentElement("afterend",n),n.append(t);const r=document.createElement("div");for(r.setAttribute("data-node-type","blockGroup"),n.append(r);((s=n.nextElementSibling)==null?void 0:s.nodeName)==="UL"||((i=n.nextElementSibling)==null?void 0:i.nodeName)==="OL";)r.append(n.nextElementSibling)})}let J=null;function lt(){return J||(J=document.implementation.createHTMLDocument("title"))}function dt(o){if(typeof o=="string"){const e=lt().createElement("div");e.innerHTML=o,o=e}return ct(o),at(o),o}function z(o,e){const t=dt(o),r=x.DOMParser.fromSchema(e).parse(t,{topNode:e.nodes.blockGroup.create()}),s=[];for(let i=0;i{const r=String((n==null?void 0:n.url)||"");return c.isVideoUrl(r)?pt(t,n):H.defaultHandlers.image(t,n)},code:ut,blockquote:(t,n)=>{const r={type:"element",tagName:"blockquote",properties:{},children:t.wrap(t.all(n),!1)};return t.patch(n,r),t.applyData(n,r)}}}).use($e.default).processSync(o).value}function ge(o,e){const t=G(o);return z(t,e)}class ft{constructor(e){this.editor=e}blocksToHTMLLossy(e=this.editor.document){return b.createExternalHTMLExporter(this.editor.pmSchema,this.editor).exportBlocks(e,{})}blocksToFullHTML(e=this.editor.document){return ce(this.editor.pmSchema,this.editor).serializeBlocks(e,{})}tryParseHTMLToBlocks(e){return z(e,this.editor.pmSchema)}blocksToMarkdownLossy(e=this.editor.document){return b.blocksToMarkdown(e,this.editor.pmSchema,this.editor,{})}tryParseMarkdownToBlocks(e){return ge(e,this.editor.pmSchema)}pasteHTML(e,t=!1){var r;let n=e;if(!t){const s=this.tryParseHTMLToBlocks(e);n=this.blocksToFullHTML(s)}n&&((r=this.editor.prosemirrorView)==null||r.pasteHTML(n))}pasteText(e){var t;return(t=this.editor.prosemirrorView)==null?void 0:t.pasteText(e)}pasteMarkdown(e){const t=G(e);return this.pasteHTML(t)}}const q=["vscode-editor-data","blocknote/html","text/markdown","text/html","text/plain","Files"];function ht(o,e){if(!o.startsWith(".")||!e.startsWith("."))throw new Error("The strings provided are not valid file extensions.");return o===e}function mt(o,e){const t=o.split("/"),n=e.split("/");if(t.length!==2)throw new Error(`The string ${o} is not a valid MIME type.`);if(n.length!==2)throw new Error(`The string ${e} is not a valid MIME type.`);return t[1]==="*"||n[1]==="*"?t[0]===n[0]:(t[0]==="*"||n[0]==="*"||t[0]===n[0])&&t[1]===n[1]}function Q(o,e,t,n="after"){let r;return Array.isArray(e.content)&&e.content.length===0?r=o.updateBlock(e,t).id:r=o.insertBlocks([t],e,n)[0].id,r}async function be(o,e){var s;if(!e.uploadFile){console.warn("Attempted ot insert file, but uploadFile is not set in the BlockNote editor options");return}const t="dataTransfer"in o?o.dataTransfer:o.clipboardData;if(t===null)return;let n=null;for(const i of q)if(t.types.includes(i)){n=i;break}if(n!=="Files")return;const r=t.items;if(r){o.preventDefault();for(let i=0;i{var T;const y=u.getNearestBlockPos(S.doc,m.pos),E=(T=e.domElement)==null?void 0:T.querySelector(`[data-id="${y.node.attrs.id}"]`),P=E==null?void 0:E.getBoundingClientRect();return Q(e,e.getBlock(y.node.attrs.id),d,P&&(P.top+P.bottom)/2>g.top?"before":"after")})}else return;const f=await e.uploadFile(a,p),h=typeof f=="string"?{props:{url:f}}:{...f};e.updateBlock(p,h)}}}}const kt=o=>B.Extension.create({name:"dropFile",addProseMirrorPlugins(){return[new C.Plugin({props:{handleDOMEvents:{drop(e,t){if(!o.isEditable)return;let n=null;for(const r of q)if(t.dataTransfer.types.includes(r)){n=r;break}return n===null?!0:n==="Files"?(be(t,o),!0):!1}}}})]}}),gt=/(^|\n) {0,3}#{1,6} {1,8}[^\n]{1,64}\r?\n\r?\n\s{0,32}\S/,bt=/(_|__|\*|\*\*|~~|==|\+\+)(?!\s)(?:[^\s](?:.{0,62}[^\s])?|\S)(?=\1)/,St=/\[[^\]]{1,128}\]\(https?:\/\/\S{1,999}\)/,Bt=/(?:\s|^)`(?!\s)(?:[^\s`](?:[^`]{0,46}[^\s`])?|[^\s`])`([^\w]|$)/,yt=/(?:^|\n)\s{0,5}-\s{1}[^\n]+\n\s{0,15}-\s/,Ct=/(?:^|\n)\s{0,5}\d+\.\s{1}[^\n]+\n\s{0,15}\d+\.\s/,Et=/\n{2} {0,3}-{2,48}\n{2}/,Tt=/(?:\n|^)(```|~~~|\$\$)(?!`|~)[^\s]{0,64} {0,64}[^\n]{0,64}\n[\s\S]{0,9999}?\s*\1 {0,64}(?:\n+|$)/,xt=/(?:\n|^)(?!\s)\w[^\n]{0,64}\r?\n(-|=)\1{0,64}\n\n\s{0,64}(\w|$)/,Mt=/(?:^|(\r?\n\r?\n))( {0,3}>[^\n]{1,333}\n){1,999}($|(\r?\n))/,It=/^\s*\|(.+\|)+\s*$/m,Pt=/^\s*\|(\s*[-:]+[-:]\s*\|)+\s*$/m,wt=/^\s*\|(.+\|)+\s*$/m,vt=o=>gt.test(o)||bt.test(o)||St.test(o)||Bt.test(o)||yt.test(o)||Ct.test(o)||Et.test(o)||Tt.test(o)||xt.test(o)||Mt.test(o)||It.test(o)||Pt.test(o)||wt.test(o);async function At(o,e){const{schema:t}=e.state;if(!o.clipboardData)return!1;const n=o.clipboardData.getData("text/plain");if(!n)return!1;if(!t.nodes.codeBlock)return e.pasteText(n),!0;const r=o.clipboardData.getData("vscode-editor-data"),s=r?JSON.parse(r):void 0,i=s==null?void 0:s.mode;return i?(e.pasteHTML(`
${n.replace(/\r\n?/g,`
+ `)}
`),!0):!1}function Nt({event:o,editor:e,prioritizeMarkdownOverHTML:t,plainTextAsMarkdown:n}){var l;if(e.transact(a=>a.selection.$from.parent.type.spec.code&&a.selection.$to.parent.type.spec.code)){const a=(l=o.clipboardData)==null?void 0:l.getData("text/plain");if(a)return e.pasteText(a),!0}let s;for(const a of q)if(o.clipboardData.types.includes(a)){s=a;break}if(!s)return!0;if(s==="vscode-editor-data")return At(o,e.prosemirrorView),!0;if(s==="Files")return be(o,e),!0;const i=o.clipboardData.getData(s);if(s==="blocknote/html")return e.pasteHTML(i,!0),!0;if(s==="text/markdown")return e.pasteMarkdown(i),!0;if(t){const a=o.clipboardData.getData("text/plain");if(vt(a))return e.pasteMarkdown(a),!0}return s==="text/html"?(e.pasteHTML(i),!0):n?(e.pasteMarkdown(i),!0):(e.pasteText(i),!0)}const Lt=(o,e)=>B.Extension.create({name:"pasteFromClipboard",addProseMirrorPlugins(){return[new C.Plugin({props:{handleDOMEvents:{paste(t,n){if(n.preventDefault(),!!o.isEditable)return e({event:n,editor:o,defaultPasteHandler:({prioritizeMarkdownOverHTML:r=!0,plainTextAsMarkdown:s=!0}={})=>Nt({event:n,editor:o,prioritizeMarkdownOverHTML:r,plainTextAsMarkdown:s})})}}}})]}});function _t(o,e,t){var l;let n=!1;const r=o.state.selection instanceof N.CellSelection;if(!r){const a=o.state.doc.slice(o.state.selection.from,o.state.selection.to,!1).content,d=[];for(let p=0;pp.type.isInGroup("bnBlock")||p.type.name==="blockGroup"||p.type.spec.group==="blockContent")===void 0,n&&(e=a)}let s;const i=b.createExternalHTMLExporter(o.state.schema,t);if(r){((l=e.firstChild)==null?void 0:l.type.name)==="table"&&(e=e.firstChild.content);const a=u.contentNodeToTableContent(e,t.schema.inlineContentSchema,t.schema.styleSchema);s=`${i.exportInlineContent(a,{})}
`}else if(n){const a=u.contentNodeToInlineContent(e,t.schema.inlineContentSchema,t.schema.styleSchema);s=i.exportInlineContent(a,{})}else{const a=b.fragmentToBlocks(e);s=i.exportBlocks(a,{})}return s}function W(o,e){"node"in o.state.selection&&o.state.selection.node.type.spec.group==="blockContent"&&e.transact(i=>i.setSelection(new C.NodeSelection(i.doc.resolve(o.state.selection.from-1))));const t=o.serializeForClipboard(o.state.selection.content()).dom.innerHTML,n=o.state.selection.content().content,r=_t(o,n,e),s=b.cleanHTMLToMarkdown(r);return{clipboardHTML:t,externalHTML:r,markdown:s}}const X=()=>{const o=window.getSelection();if(!o||o.isCollapsed)return!0;let e=o.focusNode;for(;e;){if(e instanceof HTMLElement&&e.getAttribute("contenteditable")==="false")return!0;e=e.parentElement}return!1},Z=(o,e,t)=>{t.preventDefault(),t.clipboardData.clearData();const{clipboardHTML:n,externalHTML:r,markdown:s}=W(e,o);t.clipboardData.setData("blocknote/html",n),t.clipboardData.setData("text/html",r),t.clipboardData.setData("text/plain",s)},Dt=o=>B.Extension.create({name:"copyToClipboard",addProseMirrorPlugins(){return[new C.Plugin({props:{handleDOMEvents:{copy(e,t){return X()||Z(o,e,t),!0},cut(e,t){return X()||(Z(o,e,t),e.editable&&e.dispatch(e.state.tr.deleteSelection())),!0},dragstart(e,t){if(!("node"in e.state.selection)||e.state.selection.node.type.spec.group!=="blockContent")return;o.transact(i=>i.setSelection(new C.NodeSelection(i.doc.resolve(e.state.selection.from-1)))),t.preventDefault(),t.dataTransfer.clearData();const{clipboardHTML:n,externalHTML:r,markdown:s}=W(e,o);return t.dataTransfer.setData("blocknote/html",n),t.dataTransfer.setData("text/html",r),t.dataTransfer.setData("text/plain",s),!0}}}})]}}),Ft=B.Extension.create({name:"blockBackgroundColor",addGlobalAttributes(){return[{types:["tableCell","tableHeader"],attributes:{backgroundColor:c.getBackgroundColorAttribute()}}]}}),Ot=B.Node.create({name:"hardBreak",inline:!0,group:"inline",selectable:!1,linebreakReplacement:!0,priority:10,parseHTML(){return[{tag:"br"}]},renderHTML({HTMLAttributes:o}){return["br",B.mergeAttributes(this.options.HTMLAttributes,o)]},renderText(){return` + `}}),V=(o,e)=>{const t=o.resolve(e),n=t.index();if(n===0)return;const r=t.posAtIndex(n-1);return u.getBlockInfoFromResolvedPos(o.resolve(r))},Se=(o,e)=>{for(;e.childContainer;){const t=e.childContainer.node,n=o.resolve(e.childContainer.beforePos+1).posAtIndex(t.childCount-1);e=u.getBlockInfoFromResolvedPos(o.resolve(n))}return e},Ht=(o,e)=>o.isBlockContainer&&o.blockContent.node.type.spec.content==="inline*"&&o.blockContent.node.childCount>0&&e.isBlockContainer&&e.blockContent.node.type.spec.content==="inline*",$t=(o,e,t,n)=>{if(!n.isBlockContainer)throw new Error(`Attempted to merge block at position ${n.bnBlock.beforePos} into previous block at position ${t.bnBlock.beforePos}, but next block is not a block container`);if(n.childContainer){const r=o.doc.resolve(n.childContainer.beforePos+1),s=o.doc.resolve(n.childContainer.afterPos-1),i=r.blockRange(s);if(e){const l=o.doc.resolve(n.bnBlock.beforePos);o.tr.lift(i,l.depth)}}if(e){if(!t.isBlockContainer)throw new Error(`Attempted to merge block at position ${n.bnBlock.beforePos} into previous block at position ${t.bnBlock.beforePos}, but previous block is not a block container`);e(o.tr.delete(t.blockContent.afterPos-1,n.blockContent.beforePos+1))}return!0},ee=o=>({state:e,dispatch:t})=>{const n=e.doc.resolve(o),r=u.getBlockInfoFromResolvedPos(n),s=V(e.doc,r.bnBlock.beforePos);if(!s)return!1;const i=Se(e.doc,s);return Ht(i,r)?$t(e,t,i,r):!1},Ut=B.Extension.create({priority:50,addKeyboardShortcuts(){const o=()=>this.editor.commands.first(({chain:n,commands:r})=>[()=>r.deleteSelection(),()=>r.undoInputRule(),()=>r.command(({state:s})=>{const i=u.getBlockInfoFromSelection(s);if(!i.isBlockContainer)return!1;const l=s.selection.from===i.blockContent.beforePos+1,a=i.blockContent.node.type.name==="paragraph";return l&&!a?r.command(c.updateBlockCommand(i.bnBlock.beforePos,{type:"paragraph",props:{}})):!1}),()=>r.command(({state:s})=>{const i=u.getBlockInfoFromSelection(s);if(!i.isBlockContainer)return!1;const{blockContent:l}=i;return s.selection.from===l.beforePos+1?r.liftListItem("blockContainer"):!1}),()=>r.command(({state:s})=>{const i=u.getBlockInfoFromSelection(s);if(!i.isBlockContainer)return!1;const{bnBlock:l,blockContent:a}=i,d=s.selection.from===a.beforePos+1,p=s.selection.empty,f=l.beforePos;return d&&p?n().command(ee(f)).scrollIntoView().run():!1}),()=>r.command(({state:s,tr:i,dispatch:l})=>{const a=u.getBlockInfoFromSelection(s);if(!a.isBlockContainer||!(i.selection.from===a.blockContent.beforePos+1))return!1;const p=i.doc.resolve(a.bnBlock.beforePos);if(p.nodeBefore||p.node().type.name!=="column")return!1;const g=i.doc.resolve(a.bnBlock.beforePos),m=i.doc.resolve(g.before()),S=m.before();if(l){const y=i.doc.slice(a.bnBlock.beforePos,a.bnBlock.afterPos).content;i.delete(a.bnBlock.beforePos,a.bnBlock.afterPos),m.index()===0?(D(i,S),i.insert(S,y),i.setSelection(C.TextSelection.near(i.doc.resolve(S)))):(i.insert(m.pos-1,y),i.setSelection(C.TextSelection.near(i.doc.resolve(m.pos-1))),D(i,S))}return!0}),()=>r.command(({state:s})=>{const i=u.getBlockInfoFromSelection(s);if(!i.isBlockContainer)return!1;if(i.blockContent.node.childCount===0&&i.blockContent.node.type.spec.content==="inline*"){const a=V(s.doc,i.bnBlock.beforePos);if(!a||!a.isBlockContainer)return!1;let d=n();if(a.blockContent.node.type.spec.content==="tableRow+"){const m=i.bnBlock.beforePos-1-1-1-1-1;d=d.setTextSelection(m)}else if(a.blockContent.node.type.spec.content===""){const p=a.blockContent.afterPos-a.blockContent.node.nodeSize;d=d.setNodeSelection(p)}else{const p=a.blockContent.afterPos-a.blockContent.node.nodeSize;d=d.setTextSelection(p)}return d.deleteRange({from:i.bnBlock.beforePos,to:i.bnBlock.afterPos}).scrollIntoView().run()}return!1}),()=>r.command(({state:s})=>{const i=u.getBlockInfoFromSelection(s);if(!i.isBlockContainer)throw new Error("todo");const l=s.selection.from===i.blockContent.beforePos+1,a=s.selection.empty,d=V(s.doc,i.bnBlock.beforePos);if(d&&l&&a){const p=Se(s.doc,d);if(!p.isBlockContainer)throw new Error("todo");if(p.blockContent.node.type.spec.content===""||p.blockContent.node.type.spec.content==="inline*"&&p.blockContent.node.childCount===0)return n().cut({from:i.bnBlock.beforePos,to:i.bnBlock.afterPos},p.bnBlock.afterPos).deleteRange({from:p.bnBlock.beforePos,to:p.bnBlock.afterPos}).run()}return!1})]),e=()=>this.editor.commands.first(({commands:n})=>[()=>n.deleteSelection(),()=>n.command(({state:r})=>{const s=u.getBlockInfoFromSelection(r);if(!s.isBlockContainer)return!1;const{bnBlock:i,blockContent:l,childContainer:a}=s,{depth:d}=r.doc.resolve(i.beforePos),p=i.afterPos===r.doc.nodeSize-3,f=r.selection.from===l.afterPos-1,h=r.selection.empty;if(!p&&f&&h&&!(a!==void 0)){let m=d,S=i.afterPos+1,y=r.doc.resolve(S).depth;for(;ythis.editor.commands.first(({commands:r,tr:s})=>[()=>r.command(({state:i})=>{const l=u.getBlockInfoFromSelection(i);if(!l.isBlockContainer)return!1;const{bnBlock:a,blockContent:d}=l,{depth:p}=i.doc.resolve(a.beforePos),f=i.selection.$anchor.parentOffset===0,h=i.selection.anchor===i.selection.head,g=d.node.childCount===0,m=p>1;return f&&h&&g&&m?r.liftListItem("blockContainer"):!1}),()=>r.command(({state:i})=>{var d;const l=u.getBlockInfoFromSelection(i),a=((d=this.options.editor.schema.blockSchema[l.blockNoteType].meta)==null?void 0:d.hardBreakShortcut)??"shift+enter";if(a==="none")return!1;if(a==="shift+enter"&&n||a==="enter"){const p=s.storedMarks||s.selection.$head.marks().filter(f=>this.editor.extensionManager.splittableMarks.includes(f.type.name));return s.insert(s.selection.head,s.doc.type.schema.nodes.hardBreak.create()).ensureMarks(p),!0}return!1}),()=>r.command(({state:i,dispatch:l})=>{const a=u.getBlockInfoFromSelection(i);if(!a.isBlockContainer)return!1;const{bnBlock:d,blockContent:p}=a,f=i.selection.$anchor.parentOffset===0,h=i.selection.anchor===i.selection.head,g=p.node.childCount===0;if(f&&h&&g){const m=d.afterPos,S=m+2;if(l){const y=i.schema.nodes.blockContainer.createAndFill();i.tr.insert(m,y).scrollIntoView(),i.tr.setSelection(new C.TextSelection(i.doc.resolve(S)))}return!0}return!1}),()=>r.command(({state:i,chain:l})=>{const a=u.getBlockInfoFromSelection(i);if(!a.isBlockContainer)return!1;const{blockContent:d}=a,p=i.selection.$anchor.parentOffset===0;return d.node.childCount===0?!1:(l().deleteSelection().command(c.splitBlockCommand(i.selection.from,p,p)).run(),!0)})]);return{Backspace:o,Delete:e,Enter:()=>t(),"Shift-Enter":()=>t(!0),Tab:()=>{var n,r;return this.options.tabBehavior!=="prefer-indent"&&((n=this.options.editor.getExtension(b.FormattingToolbarExtension))!=null&&n.store.state||((r=this.options.editor.getExtension(c.FilePanelExtension))==null?void 0:r.store.state)!==void 0)?!1:pe(this.options.editor)},"Shift-Tab":()=>{var n,r;return this.options.tabBehavior!=="prefer-indent"&&((n=this.options.editor.getExtension(b.FormattingToolbarExtension))!=null&&n.store.state||((r=this.options.editor.getExtension(c.FilePanelExtension))==null?void 0:r.store.state)!==void 0)?!1:this.editor.commands.liftListItem("blockContainer")},"Shift-Mod-ArrowUp":()=>(this.options.editor.moveBlocksUp(),!0),"Shift-Mod-ArrowDown":()=>(this.options.editor.moveBlocksDown(),!0),"Mod-z":()=>this.options.editor.undo(),"Mod-y":()=>this.options.editor.redo(),"Shift-Mod-z":()=>this.options.editor.redo()}}}),Vt=B.Mark.create({name:"insertion",inclusive:!1,excludes:"deletion modification insertion",addAttributes(){return{id:{default:null,validate:"number"}}},extendMarkSchema(o){return o.name!=="insertion"?{}:{blocknoteIgnore:!0,inclusive:!1,toDOM(e,t){return["ins",{"data-id":String(e.attrs.id),"data-inline":String(t),...!t&&{style:"display: contents"}},0]},parseDOM:[{tag:"ins",getAttrs(e){return e.dataset.id?{id:parseInt(e.dataset.id,10)}:!1}}]}}}),Rt=B.Mark.create({name:"deletion",inclusive:!1,excludes:"insertion modification deletion",addAttributes(){return{id:{default:null,validate:"number"}}},extendMarkSchema(o){return o.name!=="deletion"?{}:{blocknoteIgnore:!0,inclusive:!1,toDOM(e,t){return["del",{"data-id":String(e.attrs.id),"data-inline":String(t),...!t&&{style:"display: contents"}},0]},parseDOM:[{tag:"del",getAttrs(e){return e.dataset.id?{id:parseInt(e.dataset.id,10)}:!1}}]}}}),zt=B.Mark.create({name:"modification",inclusive:!1,excludes:"deletion insertion",addAttributes(){return{id:{default:null,validate:"number"},type:{validate:"string"},attrName:{default:null,validate:"string|null"},previousValue:{default:null},newValue:{default:null}}},extendMarkSchema(o){return o.name!=="modification"?{}:{blocknoteIgnore:!0,inclusive:!1,toDOM(e,t){return[t?"span":"div",{"data-type":"modification","data-id":String(e.attrs.id),"data-mod-type":e.attrs.type,"data-mod-prev-val":JSON.stringify(e.attrs.previousValue),"data-mod-new-val":JSON.stringify(e.attrs.newValue)},0]},parseDOM:[{tag:"span[data-type='modification']",getAttrs(e){return e.dataset.id?{id:parseInt(e.dataset.id,10),type:e.dataset.modType,previousValue:e.dataset.modPrevVal,newValue:e.dataset.modNewVal}:!1}},{tag:"div[data-type='modification']",getAttrs(e){return e.dataset.id?{id:parseInt(e.dataset.id,10),type:e.dataset.modType,previousValue:e.dataset.modPrevVal}:!1}}]}}}),Gt=B.Extension.create({name:"textAlignment",addGlobalAttributes(){return[{types:["tableCell","tableHeader"],attributes:{textAlignment:{default:"left",parseHTML:o=>o.getAttribute("data-text-alignment"),renderHTML:o=>o.textAlignment==="left"?{}:{"data-text-alignment":o.textAlignment}}}}]}}),qt=B.Extension.create({name:"blockTextColor",addGlobalAttributes(){return[{types:["table","tableCell","tableHeader"],attributes:{textColor:c.getTextColorAttribute()}}]}}),Wt={blockColor:"data-block-color",blockStyle:"data-block-style",id:"data-id",depth:"data-depth",depthChange:"data-depth-change"},jt=B.Node.create({name:"blockContainer",group:"blockGroupChild bnBlock",content:"blockContent blockGroup?",priority:50,defining:!0,marks:"insertion modification deletion",parseHTML(){return[{tag:"div[data-node-type="+this.name+"]",getAttrs:o=>{if(typeof o=="string")return!1;const e={};for(const[t,n]of Object.entries(Wt))o.getAttribute(n)&&(e[t]=o.getAttribute(n));return e}},{tag:'div[data-node-type="blockOuter"]',skip:!0}]},renderHTML({HTMLAttributes:o}){var r;const e=document.createElement("div");e.className="bn-block-outer",e.setAttribute("data-node-type","blockOuter");for(const[s,i]of Object.entries(o))s!=="class"&&e.setAttribute(s,i);const t={...((r=this.options.domAttributes)==null?void 0:r.block)||{},...o},n=document.createElement("div");n.className=c.mergeCSSClasses("bn-block",t.class),n.setAttribute("data-node-type",this.name);for(const[s,i]of Object.entries(t))s!=="class"&&n.setAttribute(s,i);return e.appendChild(n),{dom:e,contentDOM:n}}}),Kt=B.Node.create({name:"blockGroup",group:"childContainer",content:"blockGroupChild+",marks:"deletion insertion modification",parseHTML(){return[{tag:"div",getAttrs:o=>typeof o=="string"?!1:o.getAttribute("data-node-type")==="blockGroup"?null:!1}]},renderHTML({HTMLAttributes:o}){var n;const e={...((n=this.options.domAttributes)==null?void 0:n.blockGroup)||{},...o},t=document.createElement("div");t.className=c.mergeCSSClasses("bn-block-group",e.class),t.setAttribute("data-node-type","blockGroup");for(const[r,s]of Object.entries(e))r!=="class"&&t.setAttribute(r,s);return{dom:t,contentDOM:t}}}),Yt=B.Node.create({name:"doc",topNode:!0,content:"blockGroup",marks:"insertion modification deletion"}),Jt=F.createExtension(({options:o})=>({key:"collaboration",blockNoteExtensions:[b.ForkYDocExtension(o),b.YCursorExtension(o),b.YSyncExtension(o),b.YUndoExtension(),b.SchemaMigration(o)]}));let te=!1;function Qt(o,e){const t=[B.extensions.ClipboardTextSerializer,B.extensions.Commands,B.extensions.Editable,B.extensions.FocusEvents,B.extensions.Tabindex,Ie.Gapcursor,u.UniqueID.configure({types:["blockContainer","columnList","column"],setIdAttribute:e.setIdAttribute}),Ot,we.Text,Vt,Rt,zt,Pe.Link.extend({inclusive:!1}).configure({defaultProtocol:b.DEFAULT_LINK_PROTOCOL,protocols:te?[]:b.VALID_LINK_PROTOCOLS}),...Object.values(o.schema.styleSpecs).map(n=>n.implementation.mark.configure({editor:o})),qt,Ft,Gt,B.Extension.create({name:"OverrideEscape",addKeyboardShortcuts:()=>({Escape:()=>{var n;return(n=o.getExtension(c.SuggestionMenu))!=null&&n.shown()?!1:(o.blur(),!0)}})}),Yt,jt.configure({editor:o,domAttributes:e.domAttributes}),Ut.configure({editor:o,tabBehavior:e.tabBehavior}),Kt.configure({domAttributes:e.domAttributes}),...Object.values(o.schema.inlineContentSpecs).filter(n=>n.config!=="link"&&n.config!=="text").map(n=>n.implementation.node.configure({editor:o})),...Object.values(o.schema.blockSpecs).flatMap(n=>[..."node"in n.implementation?[n.implementation.node.configure({editor:o,domAttributes:e.domAttributes})]:[]]),Dt(o),Lt(o,e.pasteHandler||(n=>n.defaultPasteHandler())),kt(o)];return te=!0,t}function Xt(o,e){const t=[b.BlockChangeExtension(),b.DropCursorExtension(e),c.FilePanelExtension(e),b.FormattingToolbarExtension(e),b.LinkToolbarExtension(e),b.NodeSelectionKeyboardExtension(),b.PlaceholderExtension(e),ve.ShowSelectionExtension(e),b.SideMenuExtension(e),c.SuggestionMenu(e),...e.trailingBlock!==!1?[b.TrailingNodeExtension()]:[]];return e.collaboration?t.push(Jt(e.collaboration)):t.push(b.HistoryExtension()),"table"in o.schema.blockSpecs&&t.push(b.TableHandlesExtension(e)),e.animations!==!1&&t.push(b.PreviousBlockTypeExtension()),t}class Zt{constructor(e,t){k(this,"disabledExtensions",new Set);k(this,"extensions",[]);k(this,"abortMap",new Map);k(this,"extensionFactories",new Map);k(this,"extensionPlugins",new Map);this.editor=e,this.options=t,e.onMount(()=>{for(const n of this.extensions)if(n.mount){const r=new window.AbortController,s=n.mount({dom:e.prosemirrorView.dom,root:e.prosemirrorView.root,signal:r.signal});s&&r.signal.addEventListener("abort",()=>{s()}),this.abortMap.set(n,r)}}),e.onUnmount(()=>{for(const[n,r]of this.abortMap.entries())this.abortMap.delete(n),r.abort()}),this.disabledExtensions=new Set(t.disableExtensions||[]);for(const n of Xt(this.editor,this.options))this.addExtension(n);for(const n of this.options.extensions??[])this.addExtension(n);for(const n of Object.values(this.editor.schema.blockSpecs))for(const r of n.extensions??[])this.addExtension(r)}registerExtension(e){var s;const t=[].concat(e).filter(Boolean);if(!t.length){console.warn("No extensions found to register",e);return}const n=t.map(i=>this.addExtension(i)).filter(Boolean),r=new Set;for(const i of n)i!=null&&i.tiptapExtensions&&console.warn(`Extension ${i.key} has tiptap extensions, but these cannot be changed after initializing the editor. Please separate the extension into multiple extensions if you want to add them, or re-initialize the editor.`,i),(s=i==null?void 0:i.inputRules)!=null&&s.length&&console.warn(`Extension ${i.key} has input rules, but these cannot be changed after initializing the editor. Please separate the extension into multiple extensions if you want to add them, or re-initialize the editor.`,i),this.getProsemirrorPluginsFromExtension(i).plugins.forEach(l=>{r.add(l)});this.updatePlugins(i=>[...i,...r])}addExtension(e){let t;if(typeof e=="function"?t=e({editor:this.editor}):t=e,!(!t||this.disabledExtensions.has(t.key))){if(typeof e=="function"){const n=t[F.originalFactorySymbol];typeof n=="function"&&this.extensionFactories.set(n,t)}if(this.extensions.push(t),t.blockNoteExtensions)for(const n of t.blockNoteExtensions)this.addExtension(n);return t}}resolveExtensions(e){const t=[];if(typeof e=="function"){const n=this.extensionFactories.get(e);n&&t.push(n)}else if(Array.isArray(e))for(const n of e)t.push(...this.resolveExtensions(n));else if(typeof e=="object"&&"key"in e)t.push(e);else if(typeof e=="string"){const n=this.extensions.find(r=>r.key===e);n&&t.push(n)}return t}unregisterExtension(e){var s;const t=this.resolveExtensions(e);if(!t.length){console.warn("No extensions found to unregister",e);return}let n=!1;const r=new Set;for(const i of t){this.extensions=this.extensions.filter(a=>a!==i),this.extensionFactories.forEach((a,d)=>{a===i&&this.extensionFactories.delete(d)}),(s=this.abortMap.get(i))==null||s.abort(),this.abortMap.delete(i);const l=this.extensionPlugins.get(i);l==null||l.forEach(a=>{r.add(a)}),this.extensionPlugins.delete(i),i.tiptapExtensions&&!n&&(n=!0,console.warn(`Extension ${i.key} has tiptap extensions, but they will not be removed. Please separate the extension into multiple extensions if you want to remove them, or re-initialize the editor.`,e))}this.updatePlugins(i=>i.filter(l=>!r.has(l)))}updatePlugins(e){const t=this.editor.prosemirrorState,n=t.reconfigure({plugins:e(t.plugins.slice())});this.editor.prosemirrorView.updateState(n)}getTiptapExtensions(){var r;const e=Qt(this.editor,this.options).filter(s=>!this.disabledExtensions.has(s.name)),t=M.sortByDependencies(this.extensions),n=new Map;for(const s of this.extensions){s.tiptapExtensions&&e.push(...s.tiptapExtensions);const i=t(s.key),{plugins:l,inputRules:a}=this.getProsemirrorPluginsFromExtension(s);l.length&&e.push(B.Extension.create({name:s.key,priority:i,addProseMirrorPlugins:()=>l})),a.length&&(n.has(i)||n.set(i,[]),n.get(i).push(...a))}e.push(B.Extension.create({name:"blocknote-input-rules",addProseMirrorPlugins(){const s=[];return Array.from(n.keys()).sort().reverse().forEach(i=>{s.push(...n.get(i))}),[K.inputRules({rules:s})]}}));for(const s of((r=this.options._tiptapOptions)==null?void 0:r.extensions)??[])e.push(s);return e}getProsemirrorPluginsFromExtension(e){var r,s,i;const t=[...e.prosemirrorPlugins??[]],n=[];return!((r=e.prosemirrorPlugins)!=null&&r.length)&&!Object.keys(e.keyboardShortcuts||{}).length&&!((s=e.inputRules)!=null&&s.length)?{plugins:t,inputRules:n}:(this.extensionPlugins.set(e,t),(i=e.inputRules)!=null&&i.length&&n.push(...e.inputRules.map(l=>new K.InputRule(l.find,(a,d,p,f)=>{const h=l.replace({match:d,range:{from:p,to:f},editor:this.editor});if(h){const g=this.editor.getTextCursorPosition();if(this.editor.schema.blockSchema[g.block.type].content!=="inline")return null;const m=u.getBlockInfoFromTransaction(a.tr),S=a.tr.deleteRange(p,f);return c.updateBlockTr(S,m.bnBlock.beforePos,h),S}return null}))),Object.keys(e.keyboardShortcuts||{}).length&&t.push(Me.keymap(Object.fromEntries(Object.entries(e.keyboardShortcuts).map(([l,a])=>[l,()=>a({editor:this.editor})])))),{plugins:t,inputRules:n})}getExtensions(){return new Map(this.extensions.map(e=>[e.key,e]))}getExtension(e){if(typeof e=="string"){const t=this.extensions.find(n=>n.key===e);return t||void 0}else if(typeof e=="function"){const t=this.extensionFactories.get(e);return t||void 0}throw new Error(`Invalid extension type: ${typeof e}`)}hasExtension(e){return typeof e=="string"?this.extensions.some(t=>t.key===e):typeof e=="object"&&"key"in e?this.extensions.some(t=>t.key===e.key):typeof e=="function"?this.extensionFactories.has(e):!1}}function Be(o,e){let{$from:t,$to:n}=e;if(t.pos>t.start()&&t.pos0){const r=o.textBetween(n.pos-1,n.pos);if(/^[\w\p{P}]$/u.test(r)){const i=o.textBetween(n.pos,n.end()).match(/^[\w\p{P}]+/u);i&&(n=o.resolve(n.pos+i[0].length))}}return{$from:t,$to:n,from:t.pos,to:n.pos}}function eo(o){const e=u.getPmSchema(o);if(o.selection.empty||"node"in o.selection)return;const t=o.doc.resolve(u.getNearestBlockPos(o.doc,o.selection.from).posBeforeNode),n=o.doc.resolve(u.getNearestBlockPos(o.doc,o.selection.to).posBeforeNode),r=(d,p)=>{const f=t.posAtIndex(d,p),h=o.doc.resolve(f).nodeAfter;if(!h)throw new Error(`Error getting selection - node not found at position ${f}`);return u.nodeToBlock(h,e)},s=[],i=t.sharedDepth(n.pos),l=t.index(i),a=n.index(i);if(t.depth>i){s.push(u.nodeToBlock(t.nodeAfter,e));for(let d=t.depth;d>i;d--)if(t.node(d).type.isInGroup("childContainer")){const f=t.index(d)+1,h=t.node(d).childCount;for(let g=f;g=s.parent.nodeSize-2&&s.depth>0;)s=o.doc.resolve(s.pos+1);for(;s.parentOffset===0&&s.depth>0;)s=o.doc.resolve(s.pos-1);for(;r.parentOffset===0&&r.depth>0;)r=o.doc.resolve(r.pos-1);for(;r.parentOffset>=r.parent.nodeSize-2&&r.depth>0;)r=o.doc.resolve(r.pos+1);const i=u.prosemirrorSliceToSlicedBlocks(o.doc.slice(r.pos,s.pos,!0),t);return{_meta:{startPos:r.pos,endPos:s.pos},...i}}function no(o){const{bnBlock:e}=u.getBlockInfoFromTransaction(o),t=u.getPmSchema(o.doc),n=o.doc.resolve(e.beforePos),r=n.nodeBefore,s=o.doc.resolve(e.afterPos).nodeAfter;let i;return n.depth>1&&(i=n.node(),i.type.isInGroup("bnBlock")||(i=n.node(n.depth-1))),{block:u.nodeToBlock(e.node,t),prevBlock:r===null?void 0:u.nodeToBlock(r,t),nextBlock:s===null?void 0:u.nodeToBlock(s,t),parentBlock:i===void 0?void 0:u.nodeToBlock(i,t)}}function ye(o,e,t="start"){const n=typeof e=="string"?e:e.id,r=u.getPmSchema(o.doc),s=u.getBlockNoteSchema(r),i=c.getNodeById(n,o.doc);if(!i)throw new Error(`Block with ID ${n} not found`);const l=u.getBlockInfo(i),a=s.blockSchema[l.blockNoteType].content;if(l.isBlockContainer){const d=l.blockContent;if(a==="none"){o.setSelection(C.NodeSelection.create(o.doc,d.beforePos));return}if(a==="inline")t==="start"?o.setSelection(C.TextSelection.create(o.doc,d.beforePos+1)):o.setSelection(C.TextSelection.create(o.doc,d.afterPos-1));else if(a==="table")t==="start"?o.setSelection(C.TextSelection.create(o.doc,d.beforePos+4)):o.setSelection(C.TextSelection.create(o.doc,d.afterPos-4));else throw new u.UnreachableCaseError(a)}else{const d=t==="start"?l.childContainer.node.firstChild:l.childContainer.node.lastChild;ye(o,d.attrs.id,t)}}class ro{constructor(e){this.editor=e}getSelection(){return this.editor.transact(e=>eo(e))}getSelectionCutBlocks(e=!1){return this.editor.transact(t=>oo(t,e))}setSelection(e,t){return this.editor.transact(n=>to(n,e,t))}getTextCursorPosition(){return this.editor.transact(e=>no(e))}setTextCursorPosition(e,t="start"){return this.editor.transact(n=>ye(n,e,t))}getSelectionBoundingBox(){if(!this.editor.prosemirrorView)return;const{selection:e}=this.editor.prosemirrorState,{ranges:t}=e,n=Math.min(...t.map(s=>s.$from.pos)),r=Math.max(...t.map(s=>s.$to.pos));if(B.isNodeSelection(e)){const s=this.editor.prosemirrorView.nodeDOM(n);if(s)return s.getBoundingClientRect()}return B.posToDOMRect(this.editor.prosemirrorView,n,r).toJSON()}}class so{constructor(e){k(this,"activeTransaction",null);k(this,"isInCan",!1);this.editor=e}can(e){try{return this.isInCan=!0,e()}finally{this.isInCan=!1}}exec(e){if(this.activeTransaction)throw new Error("`exec` should not be called within a `transact` call, move the `exec` call outside of the `transact` call");if(this.isInCan)return this.canExec(e);const t=this.prosemirrorState,n=this.prosemirrorView;return e(t,s=>this.prosemirrorView.dispatch(s),n)}canExec(e){if(this.activeTransaction)throw new Error("`canExec` should not be called within a `transact` call, move the `canExec` call outside of the `transact` call");const t=this.prosemirrorState,n=this.prosemirrorView;return e(t,void 0,n)}transact(e){if(this.activeTransaction)return e(this.activeTransaction);try{this.activeTransaction=this.editor._tiptapEditor.state.tr;const t=e(this.activeTransaction),n=this.activeTransaction;return this.activeTransaction=null,n&&(n.docChanged||n.selectionSet||n.scrolledIntoView||n.storedMarksSet||!n.isGeneric)&&this.prosemirrorView.dispatch(n),t}finally{this.activeTransaction=null}}get prosemirrorState(){if(this.activeTransaction)throw new Error("`prosemirrorState` should not be called within a `transact` call, move the `prosemirrorState` call outside of the `transact` call or use `editor.transact` to read the current editor state");return this.editor._tiptapEditor.state}get prosemirrorView(){return this.editor._tiptapEditor.view}isFocused(){var e;return((e=this.prosemirrorView)==null?void 0:e.hasFocus())||!1}focus(){var e;(e=this.prosemirrorView)==null||e.focus()}get isEditable(){if(!this.editor._tiptapEditor){if(!this.editor.headless)throw new Error("no editor, but also not headless?");return!1}return this.editor._tiptapEditor.isEditable===void 0?!0:this.editor._tiptapEditor.isEditable}set isEditable(e){if(!this.editor._tiptapEditor){if(!this.editor.headless)throw new Error("no editor, but also not headless?");return}this.editor._tiptapEditor.options.editable!==e&&this.editor._tiptapEditor.setEditable(e)}undo(){const e=this.editor.getExtension("yUndo");if(e)return this.exec(e.undoCommand);const t=this.editor.getExtension("history");if(t)return this.exec(t.undoCommand);throw new Error("No undo plugin found")}redo(){const e=this.editor.getExtension("yUndo");if(e)return this.exec(e.redoCommand);const t=this.editor.getExtension("history");if(t)return this.exec(t.redoCommand);throw new Error("No redo plugin found")}}function io(o,e,t,n={updateSelection:!0}){let{from:r,to:s}=typeof e=="number"?{from:e,to:e}:{from:e.from,to:e.to},i=!0,l=!0,a="";if(t.forEach(d=>{d.check(),i&&d.isText&&d.marks.length===0?a+=d.text:i=!1,l=l?d.isBlock:!1}),r===s&&l){const{parent:d}=o.doc.resolve(r);d.isTextblock&&!d.type.spec.code&&!d.childCount&&(r-=1,s+=1)}return i?o.insertText(a,r,s):o.replaceWith(r,s,t),n.updateSelection&&B.selectionToInsertionEnd(o,o.steps.length-1,-1),!0}class co{constructor(e){this.editor=e}insertInlineContent(e,{updateSelection:t=!1}={}){const n=u.inlineContentToNodes(e,this.editor.pmSchema);this.editor.transact(r=>{io(r,{from:r.selection.from,to:r.selection.to},n,{updateSelection:t})})}getActiveStyles(){return this.editor.transact(e=>{const t={},n=e.selection.$to.marks();for(const r of n){const s=this.editor.schema.styleSchema[r.type.name];if(!s){r.type.name!=="link"&&!r.type.spec.blocknoteIgnore&&console.warn("mark not found in styleschema",r.type.name);continue}s.propSchema==="boolean"?t[s.type]=!0:t[s.type]=r.attrs.stringValue}return t})}addStyles(e){for(const[t,n]of Object.entries(e)){const r=this.editor.schema.styleSchema[t];if(!r)throw new Error(`style ${t} not found in styleSchema`);if(r.propSchema==="boolean")this.editor._tiptapEditor.commands.setMark(t);else if(r.propSchema==="string")this.editor._tiptapEditor.commands.setMark(t,{stringValue:n});else throw new u.UnreachableCaseError(r.propSchema)}}removeStyles(e){for(const t of Object.keys(e))this.editor._tiptapEditor.commands.unsetMark(t)}toggleStyles(e){for(const[t,n]of Object.entries(e)){const r=this.editor.schema.styleSchema[t];if(!r)throw new Error(`style ${t} not found in styleSchema`);if(r.propSchema==="boolean")this.editor._tiptapEditor.commands.toggleMark(t);else if(r.propSchema==="string")this.editor._tiptapEditor.commands.toggleMark(t,{stringValue:n});else throw new u.UnreachableCaseError(r.propSchema)}}getSelectedText(){return this.editor.transact(e=>e.doc.textBetween(e.selection.from,e.selection.to))}getSelectedLinkUrl(){return this.editor._tiptapEditor.getAttributes("link").href}createLink(e,t){if(e==="")return;const n=this.editor.pmSchema.mark("link",{href:e});this.editor.transact(r=>{const{from:s,to:i}=r.selection;t?r.insertText(t,s,i).addMark(s,s+t.length,n):r.setSelection(De.TextSelection.create(r.doc,i)).addMark(s,i,n)})}}function ao(o,e){const t=[];return o.forEach((n,r,s)=>{s!==e&&t.push(n)}),A.Fragment.from(t)}function lo(o,e){const t=[];for(let n=0;n0&&t[t.length-1].type.name==="table"){const r=t[t.length-1],s=r.copy(r.content.addToEnd(o.child(n)));t[t.length-1]=s}else{const r=e.nodes.table.createChecked(void 0,o.child(n));t.push(r)}else t.push(o.child(n));return o=A.Fragment.from(t),o}function uo(o,e){let t=A.Fragment.from(o.content);if(t=lo(t,e.state.schema),!po(t,e))return new A.Slice(t,o.openStart,o.openEnd);for(let n=0;nthis._extensionManager.unregisterExtension(...t));k(this,"registerExtension",(...t)=>this._extensionManager.registerExtension(...t));k(this,"getExtension",(...t)=>this._extensionManager.getExtension(...t));k(this,"mount",t=>{this._tiptapEditor.mount({mount:t})});k(this,"unmount",()=>{this._tiptapEditor.unmount()});this.options=t,this.dictionary=t.dictionary||xe.en,this.settings={tables:{splitCells:((d=t==null?void 0:t.tables)==null?void 0:d.splitCells)??!1,cellBackgroundColor:((p=t==null?void 0:t.tables)==null?void 0:p.cellBackgroundColor)??!1,cellTextColor:((f=t==null?void 0:t.tables)==null?void 0:f.cellTextColor)??!1,headers:((h=t==null?void 0:t.tables)==null?void 0:h.headers)??!1}};const n={defaultStyles:!0,schema:t.schema||M.BlockNoteSchema.create(),...t,placeholders:{...this.dictionary.placeholders,...t.placeholders}};if(this.schema=n.schema,this.blockImplementations=n.schema.blockSpecs,this.inlineContentImplementations=n.schema.inlineContentSpecs,this.styleImplementations=n.schema.styleSpecs,n.uploadFile){const T=n.uploadFile;this.uploadFile=async(w,I)=>{this.onUploadStartCallbacks.forEach(v=>v.apply(this,[I]));try{return await T(w,I)}finally{this.onUploadEndCallbacks.forEach(v=>v.apply(this,[I]))}}}this.resolveFileUrl=n.resolveFileUrl,this._eventManager=new rt(this),this._extensionManager=new Zt(this,n);const r=this._extensionManager.getTiptapExtensions(),s=this._extensionManager.hasExtension("ySync")||this._extensionManager.hasExtension("liveblocksExtension");s&&n.initialContent&&console.warn("When using Collaboration, initialContent might cause conflicts, because changes should come from the collaboration provider");const i={...fo,...n._tiptapOptions,element:null,autofocus:n.autofocus??!1,extensions:r,editorProps:{...(g=n._tiptapOptions)==null?void 0:g.editorProps,attributes:{tabIndex:"0",...(S=(m=n._tiptapOptions)==null?void 0:m.editorProps)==null?void 0:S.attributes,...(y=n.domAttributes)==null?void 0:y.editor,class:c.mergeCSSClasses("bn-editor",n.defaultStyles?"bn-default-styles":"",((P=(E=n.domAttributes)==null?void 0:E.editor)==null?void 0:P.class)||"")},transformPasted:uo}};try{const T=n.initialContent||(s?[{type:"paragraph",id:"initialBlockId"}]:[{type:"paragraph",id:u.UniqueID.options.generateID()}]);if(!Array.isArray(T)||T.length===0)throw new Error("initialContent must be a non-empty array of blocks, received: "+T);const w=B.getSchema(i.extensions),I=T.map(Ce=>u.blockToNode(Ce,w,this.schema.styleSchema).toJSON()),v=B.createDocument({type:"doc",content:[{type:"blockGroup",content:I}]},w,i.parseOptions);this._tiptapEditor=new B.Editor({...i,content:v.toJSON()}),this.pmSchema=this._tiptapEditor.schema}catch(T){throw new Error("Error creating document from blocks passed as `initialContent`",{cause:T})}let l;const a=this.pmSchema.nodes.doc.createAndFill;this.pmSchema.nodes.doc.createAndFill=(...T)=>{if(l)return l;const w=a.apply(this.pmSchema.nodes.doc,T),I=JSON.parse(JSON.stringify(w.toJSON()));return I.content[0].content[0].attrs.id="initialBlockId",l=x.Node.fromJSON(this.pmSchema,I),l},this.pmSchema.cached.blockNoteEditor=this,this._blockManager=new nt(this),this._exportManager=new ft(this),this._selectionManager=new ro(this),this._stateManager=new so(this),this._styleManager=new co(this),this.emit("create")}static create(t){return new j(t??{})}get extensions(){return this._extensionManager.getExtensions()}exec(t){return this._stateManager.exec(t)}canExec(t){return this._stateManager.canExec(t)}transact(t){return this._stateManager.transact(t)}get prosemirrorState(){return this._stateManager.prosemirrorState}get prosemirrorView(){return this._stateManager.prosemirrorView}get domElement(){var t;if(!this.headless)return(t=this.prosemirrorView)==null?void 0:t.dom}isFocused(){var t;return this.headless?!1:((t=this.prosemirrorView)==null?void 0:t.hasFocus())||!1}get headless(){return!this._tiptapEditor.isInitialized}focus(){this.headless||this.prosemirrorView.focus()}blur(){var t;this.headless||(t=this.domElement)==null||t.blur()}onUploadStart(t){return this.onUploadStartCallbacks.push(t),()=>{const n=this.onUploadStartCallbacks.indexOf(t);n>-1&&this.onUploadStartCallbacks.splice(n,1)}}onUploadEnd(t){return this.onUploadEndCallbacks.push(t),()=>{const n=this.onUploadEndCallbacks.indexOf(t);n>-1&&this.onUploadEndCallbacks.splice(n,1)}}get topLevelBlocks(){return this.document}get document(){return this._blockManager.document}getBlock(t){return this._blockManager.getBlock(t)}getPrevBlock(t){return this._blockManager.getPrevBlock(t)}getNextBlock(t){return this._blockManager.getNextBlock(t)}getParentBlock(t){return this._blockManager.getParentBlock(t)}forEachBlock(t,n=!1){this._blockManager.forEachBlock(t,n)}onEditorContentChange(t){this._tiptapEditor.on("update",t)}onEditorSelectionChange(t){this._tiptapEditor.on("selectionUpdate",t)}onBeforeChange(t){return this._extensionManager.getExtension(b.BlockChangeExtension).subscribe(t)}getTextCursorPosition(){return this._selectionManager.getTextCursorPosition()}setTextCursorPosition(t,n="start"){return this._selectionManager.setTextCursorPosition(t,n)}getSelection(){return this._selectionManager.getSelection()}getSelectionCutBlocks(t=!1){return this._selectionManager.getSelectionCutBlocks(t)}setSelection(t,n){return this._selectionManager.setSelection(t,n)}get isEditable(){return this._stateManager.isEditable}set isEditable(t){this._stateManager.isEditable=t}insertBlocks(t,n,r="before"){return this._blockManager.insertBlocks(t,n,r)}updateBlock(t,n){return this._blockManager.updateBlock(t,n)}removeBlocks(t){return this._blockManager.removeBlocks(t)}replaceBlocks(t,n){return this._blockManager.replaceBlocks(t,n)}undo(){return this._stateManager.undo()}redo(){return this._stateManager.redo()}insertInlineContent(t,{updateSelection:n=!1}={}){this._styleManager.insertInlineContent(t,{updateSelection:n})}getActiveStyles(){return this._styleManager.getActiveStyles()}addStyles(t){this._styleManager.addStyles(t)}removeStyles(t){this._styleManager.removeStyles(t)}toggleStyles(t){this._styleManager.toggleStyles(t)}getSelectedText(){return this._styleManager.getSelectedText()}getSelectedLinkUrl(){return this._styleManager.getSelectedLinkUrl()}createLink(t,n){this._styleManager.createLink(t,n)}canNestBlock(){return this._blockManager.canNestBlock()}nestBlock(){this._blockManager.nestBlock()}canUnnestBlock(){return this._blockManager.canUnnestBlock()}unnestBlock(){this._blockManager.unnestBlock()}moveBlocksUp(){return this._blockManager.moveBlocksUp()}moveBlocksDown(){return this._blockManager.moveBlocksDown()}blocksToHTMLLossy(t=this.document){return this._exportManager.blocksToHTMLLossy(t)}blocksToFullHTML(t=this.document){return this._exportManager.blocksToFullHTML(t)}tryParseHTMLToBlocks(t){return this._exportManager.tryParseHTMLToBlocks(t)}blocksToMarkdownLossy(t=this.document){return this._exportManager.blocksToMarkdownLossy(t)}tryParseMarkdownToBlocks(t){return this._exportManager.tryParseMarkdownToBlocks(t)}onChange(t,n){return this._eventManager.onChange(t,n)}onSelectionChange(t,n){return this._eventManager.onSelectionChange(t,n)}onMount(t){this._eventManager.onMount(t)}onUnmount(t){this._eventManager.onUnmount(t)}getSelectionBoundingBox(){return this._selectionManager.getSelectionBoundingBox()}get isEmpty(){const t=this.document;return t.length===0||t.length===1&&t[0].type==="paragraph"&&t[0].content.length===0}pasteHTML(t,n=!1){this._exportManager.pasteHTML(t,n)}pasteText(t){return this._exportManager.pasteText(t)}pasteMarkdown(t){return this._exportManager.pasteMarkdown(t)}}class ho{constructor(e,t,n){this.mappings=t,this.options=n}async resolveFile(e){var n;if(!((n=this.options)!=null&&n.resolveFileUrl))return(await fetch(e)).blob();const t=await this.options.resolveFileUrl(e);return t instanceof Blob?t:(await fetch(t)).blob()}mapStyles(e){return Object.entries(e).map(([n,r])=>this.mappings.styleMapping[n](r,this))}mapInlineContent(e){return this.mappings.inlineContentMapping[e.type](e,this)}transformInlineContent(e){return e.map(t=>this.mapInlineContent(t))}async mapBlock(e,t,n,r){return this.mappings.blockMapping[e.type](e,this,t,n,r)}}function mo(o){return{createBlockMapping:e=>e,createInlineContentMapping:e=>e,createStyleMapping:e=>e}}function ko(o,...e){const t=[...o];for(const n of e)for(const r of n){const s=t.findLastIndex(i=>i.group===r.group);s===-1?t.push(r):t.splice(s+1,0,r)}return t}exports.UniqueID=u.UniqueID;exports.UnreachableCaseError=u.UnreachableCaseError;exports.assertEmpty=u.assertEmpty;exports.blockToNode=u.blockToNode;exports.contentNodeToInlineContent=u.contentNodeToInlineContent;exports.contentNodeToTableContent=u.contentNodeToTableContent;exports.docToBlocks=u.docToBlocks;exports.getBlockCache=u.getBlockCache;exports.getBlockInfo=u.getBlockInfo;exports.getBlockInfoFromResolvedPos=u.getBlockInfoFromResolvedPos;exports.getBlockInfoFromSelection=u.getBlockInfoFromSelection;exports.getBlockInfoFromTransaction=u.getBlockInfoFromTransaction;exports.getBlockInfoWithManualOffset=u.getBlockInfoWithManualOffset;exports.getBlockNoteSchema=u.getBlockNoteSchema;exports.getBlockSchema=u.getBlockSchema;exports.getColspan=u.getColspan;exports.getInlineContentSchema=u.getInlineContentSchema;exports.getNearestBlockPos=u.getNearestBlockPos;exports.getPmSchema=u.getPmSchema;exports.getRowspan=u.getRowspan;exports.getStyleSchema=u.getStyleSchema;exports.inlineContentToNodes=u.inlineContentToNodes;exports.isLinkInlineContent=u.isLinkInlineContent;exports.isPartialLinkInlineContent=u.isPartialLinkInlineContent;exports.isPartialTableCell=u.isPartialTableCell;exports.isStyledTextInlineContent=u.isStyledTextInlineContent;exports.isTableCell=u.isTableCell;exports.mapTableCell=u.mapTableCell;exports.nodeToBlock=u.nodeToBlock;exports.nodeToCustomInlineContent=u.nodeToCustomInlineContent;exports.prosemirrorSliceToSlicedBlocks=u.prosemirrorSliceToSlicedBlocks;exports.tableContentToNodes=u.tableContentToNodes;exports.COLORS_DARK_MODE_DEFAULT=c.COLORS_DARK_MODE_DEFAULT;exports.COLORS_DEFAULT=c.COLORS_DEFAULT;exports.EMPTY_CELL_HEIGHT=c.EMPTY_CELL_HEIGHT;exports.EMPTY_CELL_WIDTH=c.EMPTY_CELL_WIDTH;exports.FILE_AUDIO_ICON_SVG=c.FILE_AUDIO_ICON_SVG;exports.FILE_IMAGE_ICON_SVG=c.FILE_IMAGE_ICON_SVG;exports.FILE_VIDEO_ICON_SVG=c.FILE_VIDEO_ICON_SVG;exports.addDefaultPropsExternalHTML=c.addDefaultPropsExternalHTML;exports.addInlineContentAttributes=c.addInlineContentAttributes;exports.addInlineContentKeyboardShortcuts=c.addInlineContentKeyboardShortcuts;exports.addNodeAndExtensionsToSpec=c.addNodeAndExtensionsToSpec;exports.addStyleAttributes=c.addStyleAttributes;exports.applyNonSelectableBlockFix=c.applyNonSelectableBlockFix;exports.audioParse=c.audioParse;exports.audioRender=c.audioRender;exports.audioToExternalHTML=c.audioToExternalHTML;exports.blockHasType=c.blockHasType;exports.camelToDataKebab=c.camelToDataKebab;exports.captureCellAnchor=c.captureCellAnchor;exports.createAudioBlockConfig=c.createAudioBlockConfig;exports.createAudioBlockSpec=c.createAudioBlockSpec;exports.createBlockConfig=c.createBlockConfig;exports.createBlockSpec=c.createBlockSpec;exports.createBlockSpecFromTiptapNode=c.createBlockSpecFromTiptapNode;exports.createBulletListItemBlockConfig=c.createBulletListItemBlockConfig;exports.createBulletListItemBlockSpec=c.createBulletListItemBlockSpec;exports.createCheckListItemBlockSpec=c.createCheckListItemBlockSpec;exports.createCheckListItemConfig=c.createCheckListItemConfig;exports.createCodeBlockConfig=c.createCodeBlockConfig;exports.createCodeBlockSpec=c.createCodeBlockSpec;exports.createDefaultBlockDOMOutputSpec=c.createDefaultBlockDOMOutputSpec;exports.createDividerBlockConfig=c.createDividerBlockConfig;exports.createDividerBlockSpec=c.createDividerBlockSpec;exports.createFileBlockConfig=c.createFileBlockConfig;exports.createFileBlockSpec=c.createFileBlockSpec;exports.createHeadingBlockConfig=c.createHeadingBlockConfig;exports.createHeadingBlockSpec=c.createHeadingBlockSpec;exports.createImageBlockConfig=c.createImageBlockConfig;exports.createImageBlockSpec=c.createImageBlockSpec;exports.createInlineContentSpecFromTipTapNode=c.createInlineContentSpecFromTipTapNode;exports.createInternalInlineContentSpec=c.createInternalInlineContentSpec;exports.createInternalStyleSpec=c.createInternalStyleSpec;exports.createNumberedListItemBlockConfig=c.createNumberedListItemBlockConfig;exports.createNumberedListItemBlockSpec=c.createNumberedListItemBlockSpec;exports.createParagraphBlockConfig=c.createParagraphBlockConfig;exports.createParagraphBlockSpec=c.createParagraphBlockSpec;exports.createQuoteBlockConfig=c.createQuoteBlockConfig;exports.createQuoteBlockSpec=c.createQuoteBlockSpec;exports.createStyleSpec=c.createStyleSpec;exports.createStyleSpecFromTipTapMark=c.createStyleSpecFromTipTapMark;exports.createTableBlockSpec=c.createTableBlockSpec;exports.createToggleListItemBlockConfig=c.createToggleListItemBlockConfig;exports.createToggleListItemBlockSpec=c.createToggleListItemBlockSpec;exports.createToggleWrapper=c.createToggleWrapper;exports.createVideoBlockConfig=c.createVideoBlockConfig;exports.createVideoBlockSpec=c.createVideoBlockSpec;exports.defaultBlockSpecs=c.defaultBlockSpecs;exports.defaultBlockToHTML=c.defaultBlockToHTML;exports.defaultInlineContentSchema=c.defaultInlineContentSchema;exports.defaultInlineContentSpecs=c.defaultInlineContentSpecs;exports.defaultProps=c.defaultProps;exports.defaultStyleSchema=c.defaultStyleSchema;exports.defaultStyleSpecs=c.defaultStyleSpecs;exports.defaultToggledState=c.defaultToggledState;exports.editorHasBlockWithType=c.editorHasBlockWithType;exports.fileParse=c.fileParse;exports.filenameFromURL=c.filenameFromURL;exports.formatKeyboardShortcut=c.formatKeyboardShortcut;exports.getBackgroundColorAttribute=c.getBackgroundColorAttribute;exports.getBlockFromPos=c.getBlockFromPos;exports.getInlineContentSchemaFromSpecs=c.getInlineContentSchemaFromSpecs;exports.getLanguageId=c.getLanguageId;exports.getNodeById=c.getNodeById;exports.getParseRules=c.getParseRules;exports.getStyleParseRules=c.getStyleParseRules;exports.getStyleSchemaFromSpecs=c.getStyleSchemaFromSpecs;exports.getTextAlignmentAttribute=c.getTextAlignmentAttribute;exports.getTextColorAttribute=c.getTextColorAttribute;exports.imageParse=c.imageParse;exports.imageRender=c.imageRender;exports.imageToExternalHTML=c.imageToExternalHTML;exports.isAppleOS=c.isAppleOS;exports.isNodeBlock=c.isNodeBlock;exports.isSafari=c.isSafari;exports.isTableCellSelection=c.isTableCellSelection;exports.isVideoUrl=c.isVideoUrl;exports.mergeCSSClasses=c.mergeCSSClasses;exports.mergeParagraphs=c.mergeParagraphs;exports.parseAudioElement=c.parseAudioElement;exports.parseDefaultProps=c.parseDefaultProps;exports.propsToAttributes=c.propsToAttributes;exports.stylePropsToAttributes=c.stylePropsToAttributes;exports.tablePropSchema=c.tablePropSchema;exports.trackPosition=c.trackPosition;exports.updateBlock=c.updateBlock;exports.updateBlockCommand=c.updateBlockCommand;exports.updateBlockTr=c.updateBlockTr;exports.videoParse=c.videoParse;exports.wrapInBlockStructure=c.wrapInBlockStructure;exports.blocksToMarkdown=b.blocksToMarkdown;exports.cleanHTMLToMarkdown=b.cleanHTMLToMarkdown;exports.createExternalHTMLExporter=b.createExternalHTMLExporter;exports.getBlocksChangedByTransaction=b.getBlocksChangedByTransaction;exports.BlockNoteSchema=M.BlockNoteSchema;exports.CustomBlockNoteSchema=M.CustomBlockNoteSchema;exports.checkPageBreakBlocksInSchema=M.checkPageBreakBlocksInSchema;exports.createPageBreakBlockConfig=M.createPageBreakBlockConfig;exports.createPageBreakBlockSpec=M.createPageBreakBlockSpec;exports.getPageBreakSlashMenuItems=M.getPageBreakSlashMenuItems;exports.uploadToTmpFilesDotOrg_DEV_ONLY=M.uploadToTmpFilesDotOrg_DEV_ONLY;exports.withPageBreak=M.withPageBreak;exports.EventEmitter=R.EventEmitter;exports.createExtension=F.createExtension;exports.createStore=F.createStore;exports.BlockNoteEditor=j;exports.Exporter=ho;exports.HTMLToBlocks=z;exports.combineByGroup=ko;exports.createInlineContentSpec=Ue;exports.createInternalHTMLSerializer=ce;exports.expandPMRangeToWords=Be;exports.fixColumnList=D;exports.getBlock=fe;exports.getInlineContentParseRules=oe;exports.getNextBlock=me;exports.getParentBlock=ke;exports.getPrevBlock=he;exports.insertBlocks=ne;exports.isEmptyColumn=_;exports.mappingFactory=mo;exports.markdownToBlocks=ge;exports.markdownToHTML=G;exports.removeAndInsertBlocks=$;exports.removeEmptyColumns=re;exports.selectedFragmentToHTML=W; + //# sourceMappingURL=blocknote.cjs.map +diff --git a/dist/blocknote.js b/dist/blocknote.js +index 0f97dc48ddedfb9661b81c7469ce504dc974e284..e1af7dbc1e590a4cc7cdd4ec921b7b683c3ebb97 100644 +--- a/dist/blocknote.js ++++ b/dist/blocknote.js +@@ -313,7 +313,7 @@ function Qt(o, e, t, n, s) { + let l = document.createTextNode( + i.textContent + ); +- for (const d of i.marks.toReversed()) ++ for (const d of Array.from(i.marks).reverse()) + if (d.type.name in o.schema.styleSpecs) { + const u = o.schema.styleSpecs[d.type.name].implementation.render(d.attrs.stringValue, o); + u.contentDOM.appendChild(l), l = u.dom; +@@ -2037,7 +2037,9 @@ const de = () => { + ] + }) + ); +-let fe = !1; ++const bnLinkifyProtocolFlag = "__tolariaBlockNoteLinkifyProtocolsRegistered"; ++const bnGlobalObject = typeof globalThis > "u" ? void 0 : globalThis; ++let fe = Boolean(bnGlobalObject && bnGlobalObject[bnLinkifyProtocolFlag]); + function mn(o, e) { + const t = [ + I.ClipboardTextSerializer, +@@ -2062,7 +2064,10 @@ function mn(o, e) { + }).configure({ + defaultProtocol: Ct, + // only call this once if we have multiple editors installed. Or fix https://github.com/ueberdosis/tiptap/issues/5450 +- protocols: fe ? [] : Bt ++ // Tolaria routes editor link clicks through its guarded native opener. ++ openOnClick: !1, ++ // Tolaria pre-registers BlockNote's non-native protocols before linkify initializes. ++ protocols: [] + }), + ...Object.values(o.schema.styleSpecs).map((n) => n.implementation.mark.configure({ + editor: o +@@ -2112,7 +2117,7 @@ function mn(o, e) { + ), + Do(o) + ]; +- return fe = !0, t; ++ return fe = !0, bnGlobalObject && (bnGlobalObject[bnLinkifyProtocolFlag] = !0), t; + } + function kn(o, e) { + const t = [ +diff --git a/dist/defaultBlocks-DE5GNdJH.js b/dist/defaultBlocks-DE5GNdJH.js +index b2761001278486a8b2ac1d10c82420b4994e96d9..eed9fd6fdb9b076dba499ca5ca619ec496d5154f 100644 +--- a/dist/defaultBlocks-DE5GNdJH.js ++++ b/dist/defaultBlocks-DE5GNdJH.js +@@ -1120,7 +1120,15 @@ const Ut = ({ defaultLanguage: e = "text" }) => ({ + ), i.value = t.props.language || e.defaultLanguage || "text", n.isEditable) { + const l = (u) => { + const d = u.target.value; +- n.updateBlock(t.id, { props: { language: d } }); ++ let p; ++ try { ++ p = n.getBlock(t.id); ++ } catch { ++ return; ++ } ++ if (!p) ++ return; ++ n.updateBlock(p.id, { props: { language: d } }); + }; + i.addEventListener("change", l), s = () => i.removeEventListener("change", l); + } else +@@ -1167,9 +1175,7 @@ const Ut = ({ defaultLanguage: e = "text" }) => ({ + const { block: o, nextBlock: r } = t.getTextCursorPosition(); + if (o.type !== "codeBlock") + return !1; +- const { $from: a } = n.selection, s = a.parentOffset === a.parent.nodeSize - 2, i = a.parent.textContent.endsWith(` +- +-`); ++ const { $from: a } = n.selection, s = a.parentOffset === a.parent.nodeSize - 2, i = a.parent.textContent.endsWith("\n "); + if (s && i) { + if (n.delete(a.pos - 2, a.pos), r) + return t.setTextCursorPosition(r, "start"), !0; +@@ -1801,7 +1807,15 @@ const sn = () => ({ + render(e, t) { + const n = document.createDocumentFragment(), o = document.createElement("input"); + o.type = "checkbox", o.checked = e.props.checked, e.props.checked && o.setAttribute("checked", ""), o.addEventListener("change", () => { +- t.updateBlock(e, { props: { checked: !e.props.checked } }); ++ let r; ++ try { ++ r = t.getBlock(e.id); ++ } catch { ++ return; ++ } ++ if (!r) ++ return; ++ t.updateBlock(r, { props: { checked: !r.props.checked } }); + }); + const r = document.createElement("p"), a = document.createElement("div"); + return a.contentEditable = "false", a.appendChild(o), n.appendChild(a), n.appendChild(r), { +@@ -2621,6 +2635,9 @@ class On { + this.state.referencePos = o.getBoundingClientRect().toJSON(), this.emitUpdate(this.pluginState.triggerCharacter); + } + }); ++ M(this, "hideMenu", (t) => { ++ this.state && (this.state.show = !1, this.emitUpdate(t)); ++ }); + M(this, "closeMenu", () => { + this.editor.transact((t) => t.setMeta(B, null)); + }); +@@ -2634,7 +2651,7 @@ class On { + this.editor = t, this.pluginState = void 0, this.emitUpdate = (a) => { + var s; + if (!this.state) +- throw new Error("Attempting to update uninitialized suggestions menu"); ++ return; + n(a, { + ...this.state, + ignoreQueryLength: (s = this.pluginState) == null ? void 0 : s.ignoreQueryLength +@@ -2649,17 +2666,21 @@ class On { + if (!a && !(o !== void 0 && r !== void 0) && !s) + return; + if (this.pluginState = s ? o : r, s || !this.editor.isEditable) { +- this.state && (this.state.show = !1), this.emitUpdate(this.pluginState.triggerCharacter); ++ this.hideMenu(this.pluginState.triggerCharacter); + return; + } + const c = (l = this.rootEl) == null ? void 0 : l.querySelector( + `[data-decoration-id="${this.pluginState.decorationId}"]` + ); +- this.editor.isEditable && c && (this.state = { ++ if (!c) { ++ this.hideMenu(this.pluginState.triggerCharacter); ++ return; ++ } ++ this.state = { + show: !0, + referencePos: c.getBoundingClientRect().toJSON(), + query: this.pluginState.query +- }, this.emitUpdate(this.pluginState.triggerCharacter)); ++ }, this.emitUpdate(this.pluginState.triggerCharacter); + } + destroy() { + var t; +@@ -2761,7 +2782,7 @@ const B = new Ze("SuggestionMenuPlugin"), _n = k(({ editor: e }) => { + if (a === s) { + const c = r.state.doc; + for (const l of t) { +- const u = l.length > 1 ? c.textBetween(a - l.length, a) + i : i; ++ const u = l.length > 1 ? c.textBetween(a - (l.length - 1), a) + i : i; + if (l === u) + return r.dispatch(r.state.tr.insertText(i)), r.dispatch( + r.state.tr.setMeta(B, { +diff --git a/dist/defaultBlocks-DosClM5E.cjs b/dist/defaultBlocks-DosClM5E.cjs +index ea3664446059a2515aa149d10b1e1ee924388300..b19291aacfede6dcedbf26bbe51a5cd35e65d07b 100644 +--- a/dist/defaultBlocks-DosClM5E.cjs ++++ b/dist/defaultBlocks-DosClM5E.cjs +@@ -1,6 +1,4 @@ + "use strict";var Nt=Object.defineProperty;var It=(e,t,n)=>t in e?Nt(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var P=(e,t,n)=>It(e,typeof t!="symbol"?t+"":t,n);const T=require("prosemirror-tables"),S=require("@tiptap/core"),z=require("prosemirror-state"),$=require("prosemirror-view"),ye=require("prosemirror-transform"),j=require("y-prosemirror"),y=require("./BlockNoteExtension-BWw0r8Gy.cjs"),m=require("./blockToNode-CumVjgem.cjs"),Ht=require("@tiptap/extension-bold"),Dt=require("@tiptap/extension-code"),_t=require("@tiptap/extension-italic"),Ot=require("@tiptap/extension-strike"),Vt=require("@tiptap/extension-underline"),R=require("@tiptap/pm/model"),Ft=require("prosemirror-highlight"),Rt=require("prosemirror-highlight/shiki"),N=require("prosemirror-model"),fe=require("@tiptap/pm/state"),ee=require("@tiptap/pm/view"),q=e=>e&&typeof e=="object"&&"default"in e?e:{default:e},Wt=q(Ht),qt=q(Dt),Ut=q(_t),$t=q(Ot),jt=q(Vt),ve=()=>typeof navigator<"u"&&(/Mac/.test(navigator.platform)||/AppleWebKit/.test(navigator.userAgent)&&/Mobile\/\w+/.test(navigator.userAgent));function B(e,t="Ctrl"){return ve()?e.replace("Mod","⌘"):e.replace("Mod",t)}function V(...e){return[...new Set(e.filter(t=>t).join(" ").split(" "))].join(" ")}const Gt=()=>/^((?!chrome|android).)*safari/i.test(navigator.userAgent);function Se(e,t,n,o){const r=document.createElement("div");r.className=V("bn-block-content",n.class),r.setAttribute("data-content-type",e);for(const[s,i]of Object.entries(n))s!=="class"&&r.setAttribute(s,i);const a=document.createElement(t);a.className=V("bn-inline-content",o.class);for(const[s,i]of Object.entries(o))s!=="class"&&a.setAttribute(s,i);return r.appendChild(a),{dom:r,contentDOM:a}}const te=(e,t)=>{let n=m.blockToNode(e,t.pmSchema);n.type.name==="blockContainer"&&(n=n.firstChild);const o=t.pmSchema.nodes[n.type.name].spec.toDOM;if(o===void 0)throw new Error("This block has no default HTML serialization as its corresponding TipTap node doesn't implement `renderHTML`.");const r=o(n);if(typeof r!="object"||!("dom"in r))throw new Error("Cannot use this block's default HTML serialization as its corresponding TipTap node's `renderHTML` function does not return an object with the `dom` property.");return r};function Ee(e,t="
"){const n=e.querySelectorAll("p");if(n.length>1){const o=n[0];for(let r=1;r{t[n]={default:o.default,keepOnSplit:!0,parseHTML:r=>{const a=r.getAttribute(W(n));if(a===null)return null;if(o.default===void 0&&o.type==="boolean"||o.default!==void 0&&typeof o.default=="boolean")return a==="true"?!0:a==="false"?!1:null;if(o.default===void 0&&o.type==="number"||o.default!==void 0&&typeof o.default=="number"){const s=parseFloat(a);return!Number.isNaN(s)&&Number.isFinite(s)?s:null}return a},renderHTML:r=>r[n]!==o.default?{[W(n)]:r[n]}:{}}}),t}function Me(e,t,n,o){const r=e();if(r===void 0)throw new Error("Cannot find node position");const s=n.state.doc.resolve(r).node().attrs.id;if(!s)throw new Error("Block doesn't have id");const i=t.getBlock(s);if(i.type!==o)throw new Error("Block type does not match");return i}function Z(e,t,n,o,r=!1,a){const s=document.createElement("div");if(a!==void 0)for(const[i,c]of Object.entries(a))i!=="class"&&s.setAttribute(i,c);s.className=V("bn-block-content",(a==null?void 0:a.class)||""),s.setAttribute("data-content-type",t);for(const[i,c]of Object.entries(n)){const u=o[i].default;c!==u&&s.setAttribute(W(i),c)}return r&&s.setAttribute("data-file-block",""),s.appendChild(e.dom),e.contentDOM&&(e.contentDOM.className=V("bn-inline-content",e.contentDOM.className)),{...e,dom:s}}function Le(e,t,n){return{config:{type:e.type,content:e.content,propSchema:t},implementation:{node:e.node,render:te,toExternalHTML:te},extensions:n}}function we(e,t){e.stopEvent=n=>(n.type==="mousedown"&&setTimeout(()=>{t.view.dom.blur()},10),!0)}function Be(e,t){const n=[{tag:"[data-content-type="+e.type+"]",contentElement:".bn-inline-content"}];return t.parse&&n.push({tag:"*",getAttrs(o){var a;if(typeof o=="string")return!1;const r=(a=t.parse)==null?void 0:a.call(t,o);return r===void 0?!1:r},preserveWhitespace:!0,getContent:e.content==="inline"||e.content==="none"?(o,r)=>{var a;if(t.parseContent)return t.parseContent({el:o,schema:r});if(e.content==="inline"){const i=o.cloneNode(!0);return Ee(i,(a=t.meta)!=null&&a.code?` +-`:"
"),R.DOMParser.fromSchema(r).parse(i,{topNode:r.nodes.paragraph.create(),preserveWhitespace:!0}).content}return R.Fragment.empty}:void 0}),n}function Kt(e,t,n,o){var a,s,i,c;const r=t.node||S.Node.create({name:e.type,content:e.content==="inline"?"inline*":e.content==="none"?"":e.content,group:"blockContent",selectable:((a=t.meta)==null?void 0:a.selectable)??!0,isolating:((s=t.meta)==null?void 0:s.isolating)??!0,code:((i=t.meta)==null?void 0:i.code)??!1,defining:((c=t.meta)==null?void 0:c.defining)??!0,priority:o,addAttributes(){return xe(e.propSchema)},parseHTML(){return Be(e,t)},renderHTML({HTMLAttributes:l}){var d;const u=document.createElement("div");return Z({dom:u,contentDOM:e.content==="inline"?u:void 0},e.type,{},e.propSchema,((d=t.meta)==null?void 0:d.fileBlockAccept)!==void 0,l)},addNodeView(){return l=>{var f,x;const u=this.options.editor,d=Me(l.getPos,u,this.editor,e.type),p=((f=this.options.domAttributes)==null?void 0:f.blockContent)||{},h=t.render.call({blockContentDOMAttributes:p,props:l,renderType:"nodeView"},d,u);return((x=t.meta)==null?void 0:x.selectable)===!1&&we(h,this.editor),h}}});if(r.name!==e.type)throw new Error("Node name does not match block type. This is a bug in BlockNote.");return{config:e,implementation:{...t,node:r,render(l,u){var p;const d=((p=r.options.domAttributes)==null?void 0:p.blockContent)||{};return t.render.call({blockContentDOMAttributes:d,props:void 0,renderType:"dom"},l,u)},toExternalHTML:(l,u,d)=>{var h,f;const p=((h=r.options.domAttributes)==null?void 0:h.blockContent)||{};return((f=t.toExternalHTML)==null?void 0:f.call({blockContentDOMAttributes:p},l,u,d))??t.render.call({blockContentDOMAttributes:p,renderType:"dom",props:void 0},l,u)}},extensions:n}}function Xt(e){return e}function E(e,t,n){return(o={})=>{const r=typeof e=="function"?e(o):e,a=typeof t=="function"?t(o):t,s=n?typeof n=="function"?n(o):n:void 0;return{config:r,implementation:{...a,toExternalHTML(i,c,l){var d,p;const u=(d=a.toExternalHTML)==null?void 0:d.call({blockContentDOMAttributes:this.blockContentDOMAttributes},i,c,l);if(u!==void 0)return Z(u,i.type,i.props,r.propSchema,((p=a.meta)==null?void 0:p.fileBlockAccept)!==void 0)},render(i,c){var d;const l=a.render.call({blockContentDOMAttributes:this.blockContentDOMAttributes,renderType:this.renderType,props:this.props},i,c);return Z(l,i.type,i.props,r.propSchema,((d=a.meta)==null?void 0:d.fileBlockAccept)!==void 0,this.blockContentDOMAttributes)}},extensions:s}}}function Qt(e,t,n,o){return e.dom.setAttribute("data-inline-content-type",t),Object.entries(n).filter(([r,a])=>{const s=o[r];return a!==s.default}).map(([r,a])=>[W(r),a]).forEach(([r,a])=>e.dom.setAttribute(r,a)),e.contentDOM&&e.contentDOM.setAttribute("data-editable",""),e}function Yt(e){return{Backspace:({editor:t})=>{const n=t.state.selection.$from;return t.state.selection.empty&&n.node().type.name===e.type&&n.parentOffset===0}}}function Te(e,t){return{config:e,implementation:t}}function Jt(e,t,n){return Te({type:e.name,propSchema:t,content:e.config.content==="inline*"?"styled":"none"},{...n,node:e})}function Ae(e){return Object.fromEntries(Object.entries(e).map(([t,n])=>[t,n.config]))}function Pe(e){return e==="boolean"?{}:{stringValue:{default:void 0,keepOnSplit:!0,parseHTML:t=>t.getAttribute("data-value"),renderHTML:t=>t.stringValue!==void 0?{"data-value":t.stringValue}:{}}}}function F(e,t,n,o){return e.dom.setAttribute("data-style-type",t),o==="string"&&e.dom.setAttribute("data-value",n),e.contentDOM&&e.contentDOM.setAttribute("data-editable",""),e}function oe(e,t){return{config:e,implementation:t}}function _(e,t){return oe({type:e.name,propSchema:t},{mark:e,render(n,o){const r=o.pmSchema.marks[e.name].spec.toDOM;if(r===void 0)throw new Error("This block has no default HTML serialization as its corresponding TipTap node doesn't implement `renderHTML`.");const a=o.pmSchema.mark(e.name,{stringValue:n}),s=R.DOMSerializer.renderSpec(document,r(a,!0));if(typeof s!="object"||!("dom"in s))throw new Error("Cannot use this block's default HTML serialization as its corresponding TipTap mark's `renderHTML` function does not return an object with the `dom` property.");return s},toExternalHTML(n,o){const r=o.pmSchema.marks[e.name].spec.toDOM;if(r===void 0)throw new Error("This block has no default HTML serialization as its corresponding TipTap node doesn't implement `renderHTML`.");const a=o.pmSchema.mark(e.name,{stringValue:n}),s=R.DOMSerializer.renderSpec(document,r(a,!0));if(typeof s!="object"||!("dom"in s))throw new Error("Cannot use this block's default HTML serialization as its corresponding TipTap mark's `renderHTML` function does not return an object with the `dom` property.");return s}})}function Ne(e){return Object.fromEntries(Object.entries(e).map(([t,n])=>[t,n.config]))}function Ie(e,t){const n=[{tag:`[data-style-type="${e.type}"]`,contentElement:o=>{const r=o;return r.matches("[data-editable]")?r:r.querySelector("[data-editable]")||r}}];return t&&n.push({tag:"*",consuming:!1,getAttrs(o){if(typeof o=="string")return!1;const r=t==null?void 0:t(o);return r===void 0?!1:{stringValue:r}}}),n}function re(e,t){const n=S.Mark.create({name:e.type,addAttributes(){return Pe(e.propSchema)},parseHTML(){return Ie(e,t.parse)},renderHTML({mark:o}){const r=(t.toExternalHTML||t.render)(o.attrs.stringValue);return F(r,e.type,o.attrs.stringValue,e.propSchema)},addMarkView(){return({mark:o})=>{const r=t.render(o.attrs.stringValue);return F(r,e.type,o.attrs.stringValue,e.propSchema)}}});return oe(e,{...t,mark:n,render:o=>{const r=t.render(o);return F(r,e.type,o,e.propSchema)},toExternalHTML:o=>{const r=(t.toExternalHTML||t.render)(o);return F(r,e.type,o,e.propSchema)}})}function He(e,t){let n,o;if(t.firstChild.descendants((r,a)=>n?!1:!De(r)||r.attrs.id!==e?!0:(n=r,o=a+1,!1)),!(n===void 0||o===void 0))return{node:n,posBeforeNode:o}}function De(e){return e.type.isInGroup("bnBlock")}const en=(e,t)=>({tr:n,dispatch:o})=>(o&&K(n,e,t),!0);function K(e,t,n,o,r){const a=m.getBlockInfoFromResolvedPos(e.doc.resolve(t));let s=null;a.blockNoteType==="table"&&(s=_e(e));const i=m.getPmSchema(e);if(o!==void 0&&r!==void 0&&o>r)throw new Error("Invalid replaceFromPos or replaceToPos");const c=i.nodes[a.blockNoteType],l=i.nodes[n.type||a.blockNoteType],u=l.isInGroup("bnBlock")?l:i.nodes.blockContainer;if(a.isBlockContainer&&l.isInGroup("blockContent")){const d=o!==void 0&&o>a.blockContent.beforePos&&oa.blockContent.beforePos&&r0){const r=e.children.map(a=>m.blockToNode(a,o));if(n.childContainer)t.step(new ye.ReplaceStep(n.childContainer.beforePos+1,n.childContainer.afterPos-1,new N.Slice(N.Fragment.from(r),0,0)));else{if(!n.isBlockContainer)throw new Error("impossible");t.insert(n.blockContent.afterPos,o.nodes.blockGroup.createChecked({},r))}}}function nn(e,t,n,o,r){const a=typeof t=="string"?t:t.id,s=He(a,e.doc);if(!s)throw new Error(`Block with ID ${a} not found`);K(e,s.posBeforeNode,n,o,r);const i=e.doc.resolve(s.posBeforeNode+1).node(),c=m.getPmSchema(e);return m.nodeToBlock(i,c)}function _e(e){const t="selection"in e?e.selection:null;if(!(t instanceof z.TextSelection))return null;const n=e.doc.resolve(t.head);let o=-1,r=-1;for(let L=n.depth;L>=0;L--){const M=n.node(L).type.name;if(o<0&&(M==="tableCell"||M==="tableHeader")&&(o=L),M==="table"){r=L;break}}if(o<0||r<0)return null;const a=n.before(o),s=n.before(r),i=e.doc.nodeAt(s);if(!i||i.type.name!=="table")return null;const c=T.TableMap.get(i),l=a-(s+1),u=c.map.indexOf(l);if(u<0)return null;const d=Math.floor(u/c.width),p=u%c.width,f=a+1+1,x=Math.max(0,t.head-f);return{row:d,col:p,offset:x}}function on(e,t,n){var L;if(t.blockNoteType!=="table")return!1;let o=-1;if(t.isBlockContainer)o=e.mapping.map(t.blockContent.beforePos);else{const M=e.mapping.map(t.bnBlock.beforePos),D=M+(((L=e.doc.nodeAt(M))==null?void 0:L.nodeSize)||0);e.doc.nodesBetween(M,D,(b,w)=>b.type.name==="table"?(o=w,!1):!0)}const r=o>=0?e.doc.nodeAt(o):null;if(!r||r.type.name!=="table")return!1;const a=T.TableMap.get(r),s=Math.max(0,Math.min(n.row,a.height-1)),i=Math.max(0,Math.min(n.col,a.width-1)),c=s*a.width+i,l=a.map[c];if(l==null)return!1;const d=o+1+l+1,p=e.doc.nodeAt(d),h=d+1,f=p?p.content.size:0,x=h+Math.max(0,Math.min(n.offset,f));return"selection"in e&&e.setSelection(z.TextSelection.create(e.doc,x)),!0}const A={gray:{text:"#9b9a97",background:"#ebeced"},brown:{text:"#64473a",background:"#e9e5e3"},red:{text:"#e03e3e",background:"#fbe4e4"},orange:{text:"#d9730d",background:"#f6e9d9"},yellow:{text:"#dfab01",background:"#fbf3db"},green:{text:"#4d6461",background:"#ddedea"},blue:{text:"#0b6e99",background:"#ddebf1"},purple:{text:"#6940a5",background:"#eae4f2"},pink:{text:"#ad1a72",background:"#f4dfeb"}},rn={gray:{text:"#bebdb8",background:"#9b9a97"},brown:{text:"#8e6552",background:"#64473a"},red:{text:"#ec4040",background:"#be3434"},orange:{text:"#e3790d",background:"#b7600a"},yellow:{text:"#dfab01",background:"#b58b00"},green:{text:"#6b8b87",background:"#4d6461"},blue:{text:"#0e87bc",background:"#0b6e99"},purple:{text:"#8552d7",background:"#6940a5"},pink:{text:"#da208f",background:"#ad1a72"}},g={backgroundColor:{default:"default"},textColor:{default:"default"},textAlignment:{default:"left",values:["left","center","right","justify"]}},v=e=>{const t={};return e.hasAttribute("data-background-color")?t.backgroundColor=e.getAttribute("data-background-color"):e.style.backgroundColor&&(t.backgroundColor=e.style.backgroundColor),e.hasAttribute("data-text-color")?t.textColor=e.getAttribute("data-text-color"):e.style.color&&(t.textColor=e.style.color),t.textAlignment=g.textAlignment.values.includes(e.style.textAlign)?e.style.textAlign:void 0,t},I=(e,t)=>{e.backgroundColor&&e.backgroundColor!==g.backgroundColor.default&&(t.style.backgroundColor=e.backgroundColor in A?A[e.backgroundColor].background:e.backgroundColor),e.textColor&&e.textColor!==g.textColor.default&&(t.style.color=e.textColor in A?A[e.textColor].text:e.textColor),e.textAlignment&&e.textAlignment!==g.textAlignment.default&&(t.style.textAlign=e.textAlignment)},an=(e="backgroundColor")=>({default:g.backgroundColor.default,parseHTML:t=>t.hasAttribute("data-background-color")?t.getAttribute("data-background-color"):t.style.backgroundColor?t.style.backgroundColor:g.backgroundColor.default,renderHTML:t=>t[e]===g.backgroundColor.default?{}:{"data-background-color":t[e]}}),sn=(e="textColor")=>({default:g.textColor.default,parseHTML:t=>t.hasAttribute("data-text-color")?t.getAttribute("data-text-color"):t.style.color?t.style.color:g.textColor.default,renderHTML:t=>t[e]===g.textColor.default?{}:{"data-text-color":t[e]}}),cn=(e="textAlignment")=>({default:g.textAlignment.default,parseHTML:t=>t.hasAttribute("data-text-alignment")?t.getAttribute("data-text-alignment"):t.style.textAlign?t.style.textAlign:g.textAlignment.default,renderHTML:t=>t[e]===g.textAlignment.default?{}:{"data-text-alignment":t[e]}}),X=(e,t)=>{const n=e.querySelector(t);if(!n)return;const o=e.querySelector("figcaption"),r=(o==null?void 0:o.textContent)??void 0;return{targetElement:n,caption:r}},O=y.createExtension(({editor:e})=>{const t=y.createStore(void 0);function n(){t.setState(void 0)}return{key:"filePanel",store:t,mount({signal:o}){const r=e.onChange(n,!1),a=e.onSelectionChange(n,!1);o.addEventListener("abort",()=>{r(),a()})},closeMenu:n,showMenu(o){t.setState(o)}}}),ln=(e,t,n)=>{const o=document.createElement("div");o.className="bn-add-file-button";const r=document.createElement("div");r.className="bn-add-file-button-icon",n?r.appendChild(n):r.innerHTML='',o.appendChild(r);const a=document.createElement("p");a.className="bn-add-file-button-text",a.innerHTML=e.type in t.dictionary.file_blocks.add_button_text?t.dictionary.file_blocks.add_button_text[e.type]:t.dictionary.file_blocks.add_button_text.file,o.appendChild(a);const s=c=>{c.preventDefault(),c.stopPropagation()},i=()=>{var c;t.isEditable&&((c=t.getExtension(O))==null||c.showMenu(e.id))};return o.addEventListener("mousedown",s,!0),o.addEventListener("click",i,!0),{dom:o,destroy:()=>{o.removeEventListener("mousedown",s,!0),o.removeEventListener("click",i,!0)}}},dn='',un=e=>{const t=document.createElement("div");t.className="bn-file-name-with-icon";const n=document.createElement("div");n.className="bn-file-icon",n.innerHTML=dn,t.appendChild(n);const o=document.createElement("p");return o.className="bn-file-name",o.textContent=e.props.name,t.appendChild(o),{dom:t}},ae=(e,t,n,o)=>{const r=document.createElement("div");if(r.className="bn-file-block-content-wrapper",e.props.url===""){const s=ln(e,t,o);r.appendChild(s.dom);const i=t.onUploadStart(c=>{if(c===e.id){r.removeChild(s.dom);const l=document.createElement("div");l.className="bn-file-loading-preview",l.textContent="Loading...",r.appendChild(l)}});return{dom:r,destroy:()=>{i(),s.destroy()}}}const a={dom:r};if(e.props.showPreview===!1||!n){const s=un(e);r.appendChild(s.dom),a.destroy=()=>{var i;(i=s.destroy)==null||i.call(s)}}else r.appendChild(n.dom);if(e.props.caption){const s=document.createElement("p");s.className="bn-file-caption",s.textContent=e.props.caption,r.appendChild(s)}return a},se=(e,t)=>{const n=document.createElement("figure"),o=document.createElement("figcaption");return o.textContent=t,n.appendChild(e),n.appendChild(o),{dom:n}},Q=(e,t)=>{const n=document.createElement("div"),o=document.createElement("p");return o.textContent=t,n.appendChild(e),n.appendChild(o),{dom:n}},ne=e=>({url:e.src||void 0}),Oe='',Ve=e=>({type:"audio",propSchema:{backgroundColor:g.backgroundColor,name:{default:""},url:{default:""},caption:{default:""},showPreview:{default:!0}},content:"none"}),Fe=(e={})=>t=>{if(t.tagName==="AUDIO"){if(t.closest("figure"))return;const{backgroundColor:n}=v(t);return{...ne(t),backgroundColor:n}}if(t.tagName==="FIGURE"){const n=X(t,"audio");if(!n)return;const{targetElement:o,caption:r}=n,{backgroundColor:a}=v(t);return{...ne(o),backgroundColor:a,caption:r}}},Re=(e={})=>(t,n)=>{const o=document.createElement("div");o.innerHTML=e.icon??Oe;const r=document.createElement("audio");return r.className="bn-audio",n.resolveFileUrl?n.resolveFileUrl(t.props.url).then(a=>{r.src=a}):r.src=t.props.url,r.controls=!0,r.contentEditable="false",r.draggable=!1,ae(t,n,{dom:r},o.firstElementChild)},We=(e={})=>(t,n)=>{if(!t.props.url){const r=document.createElement("p");return r.textContent="Add audio",{dom:r}}let o;return t.props.showPreview?(o=document.createElement("audio"),o.src=t.props.url):(o=document.createElement("a"),o.href=t.props.url,o.textContent=t.props.name||t.props.url),t.props.caption?t.props.showPreview?se(o,t.props.caption):Q(o,t.props.caption):{dom:o}},qe=E(Ve,e=>({meta:{fileBlockAccept:["audio/*"]},parse:Fe(e),render:Re(e),toExternalHTML:We(e),runsBefore:["file"]})),he=Symbol.for("blocknote.shikiParser"),J=Symbol.for("blocknote.shikiHighlighterPromise");function pn(e){const t=globalThis;let n,o,r=!1;const a=s=>{if(!e.createHighlighter)return process.env.NODE_ENV==="development"&&!r&&(console.log("For syntax highlighting of code blocks, you must provide a `createCodeBlockSpec({ createHighlighter: () => ... })` function"),r=!0),[];if(!n)return t[J]=t[J]||e.createHighlighter(),t[J].then(c=>{n=c});const i=ie(e,s.language);return!i||i==="text"||i==="none"||i==="plaintext"||i==="txt"?[]:n.getLoadedLanguages().includes(i)?(o||(o=t[he]||Rt.createParser(n),t[he]=o),o(s)):n.loadLanguage(i)};return Ft.createHighlightPlugin({parser:a,languageExtractor:s=>s.attrs.language,nodeTypes:["codeBlock"]})}const Ue=({defaultLanguage:e="text"})=>({type:"codeBlock",propSchema:{language:{default:e}},content:"inline"}),$e=E(Ue,e=>({meta:{code:!0,defining:!0,isolating:!1},parse:t=>{var r,a;if(t.tagName!=="PRE"||t.childElementCount!==1||((r=t.firstElementChild)==null?void 0:r.tagName)!=="CODE")return;const n=t.firstElementChild;return{language:n.getAttribute("data-language")||((a=n.className.split(" ").find(s=>s.includes("language-")))==null?void 0:a.replace("language-",""))}},parseContent:({el:t,schema:n})=>{const o=R.DOMParser.fromSchema(n),r=t.firstElementChild;return o.parse(r,{preserveWhitespace:"full",topNode:n.nodes.codeBlock.create()}).content},render(t,n){const o=document.createDocumentFragment(),r=document.createElement("pre"),a=document.createElement("code");r.appendChild(a);let s;if(e.supportedLanguages){const i=document.createElement("select");if(Object.entries(e.supportedLanguages??{}).forEach(([l,{name:u}])=>{const d=document.createElement("option");d.value=l,d.text=u,i.appendChild(d)}),i.value=t.props.language||e.defaultLanguage||"text",n.isEditable){const l=u=>{const d=u.target.value;n.updateBlock(t.id,{props:{language:d}})};i.addEventListener("change",l),s=()=>i.removeEventListener("change",l)}else i.disabled=!0;const c=document.createElement("div");c.contentEditable="false",c.appendChild(i),o.appendChild(c)}return o.appendChild(r),{dom:o,contentDOM:a,destroy:()=>{s==null||s()}}},toExternalHTML(t){const n=document.createElement("pre"),o=document.createElement("code");return o.className=`language-${t.props.language}`,o.dataset.language=t.props.language,n.appendChild(o),{dom:n,contentDOM:o}}}),e=>[y.createExtension({key:"code-block-highlighter",prosemirrorPlugins:[pn(e)]}),y.createExtension({key:"code-block-keyboard-shortcuts",keyboardShortcuts:{Delete:({editor:t})=>t.transact(n=>{const{block:o}=t.getTextCursorPosition();if(o.type!=="codeBlock")return!1;const{$from:r}=n.selection;return r.parent.textContent?!1:(t.removeBlocks([o]),!0)}),Tab:({editor:t})=>e.indentLineWithTab===!1?!1:t.transact(n=>{const{block:o}=t.getTextCursorPosition();return o.type==="codeBlock"?(n.insertText(" "),!0):!1}),Enter:({editor:t})=>t.transact(n=>{const{block:o,nextBlock:r}=t.getTextCursorPosition();if(o.type!=="codeBlock")return!1;const{$from:a}=n.selection,s=a.parentOffset===a.parent.nodeSize-2,i=a.parent.textContent.endsWith(` +- +-`);if(s&&i){if(n.delete(a.pos-2,a.pos),r)return t.setTextCursorPosition(r,"start"),!0;const[c]=t.insertBlocks([{type:"paragraph"}],o,"after");return t.setTextCursorPosition(c,"start"),!0}return n.insertText(` +-`),!0}),"Shift-Enter":({editor:t})=>t.transact(()=>{const{block:n}=t.getTextCursorPosition();if(n.type!=="codeBlock")return!1;const[o]=t.insertBlocks([{type:"paragraph"}],n,"after");return t.setTextCursorPosition(o,"start"),!0})},inputRules:[{find:/^```(.*?)\s$/,replace:({match:t})=>{const n=t[1].trim();return{type:"codeBlock",props:{language:{language:ie(e,n)??n}.language},content:[]}}}]})]);function ie(e,t){var n;return(n=Object.entries(e.supportedLanguages??{}).find(([o,{aliases:r}])=>(r==null?void 0:r.includes(t))||o===t))==null?void 0:n[0]}const je=()=>({type:"divider",propSchema:{},content:"none"}),Ge=E(je,{meta:{isolating:!1},parse(e){if(e.tagName==="HR")return{}},render(){return{dom:document.createElement("hr")}}},[y.createExtension({key:"divider-block-shortcuts",inputRules:[{find:new RegExp("^---$"),replace(){return{type:"divider",props:{},content:[]}}}]})]),me=e=>({url:e.src||void 0}),Ze=()=>({type:"file",propSchema:{backgroundColor:g.backgroundColor,name:{default:""},url:{default:""},caption:{default:""}},content:"none"}),ze=()=>e=>{if(e.tagName==="EMBED"){if(e.closest("figure"))return;const{backgroundColor:t}=v(e);return{...me(e),backgroundColor:t}}if(e.tagName==="FIGURE"){const t=X(e,"embed");if(!t)return;const{targetElement:n,caption:o}=t,{backgroundColor:r}=v(e);return{...me(n),backgroundColor:r,caption:o}}},Ke=E(Ze,{meta:{fileBlockAccept:["*/*"]},parse:ze(),render(e,t){return ae(e,t)},toExternalHTML(e){if(!e.props.url){const n=document.createElement("p");return n.textContent="Add file",{dom:n}}const t=document.createElement("a");return t.href=e.props.url,t.textContent=e.props.name||e.props.url,e.props.caption?Q(t,e.props.caption):{dom:t}}}),Xe={set:(e,t)=>window.localStorage.setItem(`toggle-${e.id}`,t?"true":"false"),get:e=>window.localStorage.getItem(`toggle-${e.id}`)==="true"},ce=(e,t,n,o=Xe)=>{if("isToggleable"in e.props&&!e.props.isToggleable)return{dom:n};const r=document.createElement("div"),a=document.createElement("div");a.className="bn-toggle-wrapper";const s=document.createElement("button");s.className="bn-toggle-button",s.type="button",s.innerHTML='';const i=f=>f.preventDefault();s.addEventListener("mousedown",i);const c=()=>{var f;a.getAttribute("data-show-children")==="true"?(a.setAttribute("data-show-children","false"),o.set(t.getBlock(e),!1),r.contains(l)&&r.removeChild(l)):(a.setAttribute("data-show-children","true"),o.set(t.getBlock(e),!0),t.isEditable&&((f=t.getBlock(e))==null?void 0:f.children.length)===0&&!r.contains(l)&&r.appendChild(l))};s.addEventListener("click",c),a.appendChild(s),a.appendChild(n);const l=document.createElement("button");l.className="bn-toggle-add-block-button",l.type="button",l.textContent=t.dictionary.toggle_blocks.add_block_button;const u=f=>f.preventDefault();l.addEventListener("mousedown",u);const d=()=>{t.transact(()=>{const f=t.updateBlock(e,{children:[{}]});t.setTextCursorPosition(f.children[0].id,"end"),t.focus()})};l.addEventListener("click",d),r.appendChild(a);let p=e.children.length;const h=t.onChange(()=>{var x;const f=((x=t.getBlock(e))==null?void 0:x.children.length)??0;f>p?(a.getAttribute("data-show-children")==="false"&&(a.setAttribute("data-show-children","true"),o.set(t.getBlock(e),!0)),r.contains(l)&&r.removeChild(l)):f===0&&ff instanceof MutationRecord&&(f.type==="attributes"&&f.target===a&&f.attributeName==="data-show-children"||f.type==="childList"&&(f.addedNodes[0]===l||f.removedNodes[0]===l)),destroy:()=>{s.removeEventListener("mousedown",i),s.removeEventListener("click",c),l.removeEventListener("mousedown",u),l.removeEventListener("click",d),h==null||h()}}},Qe=[1,2,3,4,5,6],fn=e=>({editor:t})=>{const n=t.getTextCursorPosition();return t.schema.blockSchema[n.block.type].content!=="inline"?!1:(t.updateBlock(n.block,{type:"heading",props:{level:e}}),!0)},Ye=({defaultLevel:e=1,levels:t=Qe,allowToggleHeadings:n=!0}={})=>({type:"heading",propSchema:{...g,level:{default:e,values:t},...n?{isToggleable:{default:!1,optional:!0}}:{}},content:"inline"}),Je=E(Ye,({allowToggleHeadings:e=!0}={})=>({meta:{isolating:!1},parse(t){let n;switch(t.tagName){case"H1":n=1;break;case"H2":n=2;break;case"H3":n=3;break;case"H4":n=4;break;case"H5":n=5;break;case"H6":n=6;break;default:return}return{...v(t),level:n}},render(t,n){const o=document.createElement(`h${t.props.level}`);return e?{...ce(t,n,o),contentDOM:o}:{dom:o,contentDOM:o}},toExternalHTML(t){const n=document.createElement(`h${t.props.level}`);return I(t.props,n),{dom:n,contentDOM:n}}}),({levels:e=Qe}={})=>[y.createExtension({key:"heading-shortcuts",keyboardShortcuts:Object.fromEntries(e.map(t=>[`Mod-Alt-${t}`,fn(t)])??[]),inputRules:e.map(t=>({find:new RegExp(`^(#{${t}})\\s$`),replace({match:n}){return{type:"heading",props:{level:n[1].length}}}}))})]),et=(e,t,n,o,r)=>{const{dom:a,destroy:s}=ae(e,t,n,r),i=a;i.style.position="relative",e.props.url&&e.props.showPreview&&(e.props.previewWidth?i.style.width=`${e.props.previewWidth}px`:i.style.width="fit-content");const c=document.createElement("div");c.className="bn-resize-handle",c.style.left="4px";const l=document.createElement("div");l.className="bn-resize-handle",l.style.right="4px";const u=document.createElement("div");u.style.position="absolute",u.style.height="100%",u.style.width="100%";let d,p=e.props.previewWidth;const h=b=>{var ue,pe;if(!d){!t.isEditable&&o.contains(c)&&o.contains(l)&&(o.removeChild(c),o.removeChild(l));return}let w;const U="touches"in b?b.touches[0].clientX:b.clientX;e.props.textAlignment==="center"?d.handleUsed==="left"?w=d.initialWidth+(d.initialClientX-U)*2:w=d.initialWidth+(U-d.initialClientX)*2:d.handleUsed==="left"?w=d.initialWidth+d.initialClientX-U:w=d.initialWidth+U-d.initialClientX,p=Math.min(Math.max(w,64),((pe=(ue=t.domElement)==null?void 0:ue.firstElementChild)==null?void 0:pe.clientWidth)||Number.MAX_VALUE),i.style.width=`${p}px`},f=b=>{(!b.target||!i.contains(b.target)||!t.isEditable)&&o.contains(c)&&o.contains(l)&&(o.removeChild(c),o.removeChild(l)),d&&(d=void 0,i.contains(u)&&i.removeChild(u),t.updateBlock(e,{props:{previewWidth:p}}))},x=()=>{t.isEditable&&(o.appendChild(c),o.appendChild(l))},L=b=>{b.relatedTarget===c||b.relatedTarget===l||d||t.isEditable&&o.contains(c)&&o.contains(l)&&(o.removeChild(c),o.removeChild(l))},M=b=>{b.preventDefault(),i.contains(u)||i.appendChild(u);const w="touches"in b?b.touches[0].clientX:b.clientX;d={handleUsed:"left",initialWidth:i.clientWidth,initialClientX:w}},D=b=>{b.preventDefault(),i.contains(u)||i.appendChild(u);const w="touches"in b?b.touches[0].clientX:b.clientX;d={handleUsed:"right",initialWidth:i.clientWidth,initialClientX:w}};return window.addEventListener("mousemove",h),window.addEventListener("touchmove",h),window.addEventListener("mouseup",f),window.addEventListener("touchend",f),i.addEventListener("mouseenter",x),i.addEventListener("mouseleave",L),c.addEventListener("mousedown",M),c.addEventListener("touchstart",M),l.addEventListener("mousedown",D),l.addEventListener("touchstart",D),{dom:i,destroy:()=>{s==null||s(),window.removeEventListener("mousemove",h),window.removeEventListener("touchmove",h),window.removeEventListener("mouseup",f),window.removeEventListener("touchend",f),i.removeEventListener("mouseenter",x),i.removeEventListener("mouseleave",L),c.removeEventListener("mousedown",M),c.removeEventListener("touchstart",M),l.removeEventListener("mousedown",D),l.removeEventListener("touchstart",D)}}},be=e=>{const t=e.src||void 0,n=e.width||void 0,o=e.alt||void 0;return{url:t,previewWidth:n,name:o}},tt='',nt=(e={})=>({type:"image",propSchema:{textAlignment:g.textAlignment,backgroundColor:g.backgroundColor,name:{default:""},url:{default:""},caption:{default:""},showPreview:{default:!0},previewWidth:{default:void 0,type:"number"}},content:"none"}),ot=(e={})=>t=>{if(t.tagName==="IMG"){if(t.closest("figure"))return;const{backgroundColor:n}=v(t);return{...be(t),backgroundColor:n}}if(t.tagName==="FIGURE"){const n=X(t,"img");if(!n)return;const{targetElement:o,caption:r}=n,{backgroundColor:a}=v(t);return{...be(o),backgroundColor:a,caption:r}}},rt=(e={})=>(t,n)=>{const o=document.createElement("div");o.innerHTML=e.icon??tt;const r=document.createElement("div");r.className="bn-visual-media-wrapper";const a=document.createElement("img");return a.className="bn-visual-media",n.resolveFileUrl?n.resolveFileUrl(t.props.url).then(s=>{a.src=s}):a.src=t.props.url,a.alt=t.props.name||t.props.caption||"BlockNote image",a.contentEditable="false",a.draggable=!1,r.appendChild(a),et(t,n,{dom:r},r,o.firstElementChild)},at=(e={})=>(t,n)=>{if(!t.props.url){const r=document.createElement("p");return r.textContent="Add image",{dom:r}}let o;return t.props.showPreview?(o=document.createElement("img"),o.src=t.props.url,o.alt=t.props.name||t.props.caption||"BlockNote image",t.props.previewWidth&&(o.width=t.props.previewWidth)):(o=document.createElement("a"),o.href=t.props.url,o.textContent=t.props.name||t.props.url),t.props.caption?t.props.showPreview?se(o,t.props.caption):Q(o,t.props.caption):{dom:o}},st=E(nt,e=>({meta:{fileBlockAccept:["image/*"]},parse:ot(e),render:rt(e),toExternalHTML:at(e),runsBefore:["file"]})),gn=(e,t,n)=>({state:o,dispatch:r})=>r?it(o.tr,e,t,n):!0,it=(e,t,n,o)=>{const r=m.getNearestBlockPos(e.doc,t),a=m.getBlockInfo(r);if(!a.isBlockContainer)return!1;const s=m.getPmSchema(e),i=[{type:a.bnBlock.node.type,attrs:o?{...a.bnBlock.node.attrs,id:void 0}:{}},{type:n?a.blockContent.node.type:s.nodes.paragraph,attrs:o?{...a.blockContent.node.attrs}:{}}];return e.split(t,2,i),!0},Y=(e,t)=>{const{blockInfo:n,selectionEmpty:o}=e.transact(s=>({blockInfo:m.getBlockInfoFromTransaction(s),selectionEmpty:s.selection.anchor===s.selection.head}));if(!n.isBlockContainer)return!1;const{bnBlock:r,blockContent:a}=n;return a.node.type.name!==t||!o?!1:a.node.childCount===0?(e.transact(s=>{K(s,r.beforePos,{type:"paragraph",props:{}})}),!0):a.node.childCount>0?e.transact(s=>(s.deleteSelection(),it(s,s.selection.from,!0))):!1};function le(e,t,n){var d,p,h;const o=N.DOMParser.fromSchema(t),r=e,a=document.createElement("div");a.setAttribute("data-node-type","blockGroup");for(const f of Array.from(r.childNodes))a.appendChild(f.cloneNode(!0));let s=o.parse(a,{topNode:t.nodes.blockGroup.create()});((p=(d=s.firstChild)==null?void 0:d.firstChild)==null?void 0:p.type.name)==="checkListItem"&&(s=s.copy(s.content.cut(s.firstChild.firstChild.nodeSize+2)));const i=(h=s.firstChild)==null?void 0:h.firstChild;if(!(i!=null&&i.isTextblock))return N.Fragment.from(s);const c=t.nodes[n].create({},i.content),l=s.content.cut(i.nodeSize+2);if(l.size>0){const f=s.copy(l);return c.content.addToEnd(f)}return c.content}const ct=()=>({type:"bulletListItem",propSchema:{...g},content:"inline"}),lt=E(ct,{meta:{isolating:!1},parse(e){var n;if(e.tagName!=="LI")return;const t=e.parentElement;if(t!==null&&(t.tagName==="UL"||t.tagName==="DIV"&&((n=t.parentElement)==null?void 0:n.tagName)==="UL"))return v(e)},parseContent:({el:e,schema:t})=>le(e,t,"bulletListItem"),render(){const e=document.createElement("p");return{dom:e,contentDOM:e}},toExternalHTML(e){const t=document.createElement("li"),n=document.createElement("p");return I(e.props,t),t.appendChild(n),{dom:t,contentDOM:n}}},[y.createExtension({key:"bullet-list-item-shortcuts",keyboardShortcuts:{Enter:({editor:e})=>Y(e,"bulletListItem"),"Mod-Shift-8":({editor:e})=>{const t=e.getTextCursorPosition();return e.schema.blockSchema[t.block.type].content!=="inline"?!1:(e.updateBlock(t.block,{type:"bulletListItem",props:{}}),!0)}},inputRules:[{find:/^\s?[-+*]\s$/,replace({editor:e}){if(m.getBlockInfoFromSelection(e.prosemirrorState).blockNoteType!=="heading")return{type:"bulletListItem",props:{}}}}]})]),dt=()=>({type:"checkListItem",propSchema:{...g,checked:{default:!1,type:"boolean"}},content:"inline"}),ut=E(dt,{meta:{isolating:!1},parse(e){var n;if(e.tagName==="input")return e.closest("[data-content-type]")||e.closest("li")?void 0:e.type==="checkbox"?{checked:e.checked}:void 0;if(e.tagName!=="LI")return;const t=e.parentElement;if(t!==null&&(t.tagName==="UL"||t.tagName==="DIV"&&((n=t.parentElement)==null?void 0:n.tagName)==="UL")){const o=e.querySelector("input[type=checkbox]")||null;return o===null?void 0:{...v(e),checked:o.checked}}},parseContent:({el:e,schema:t})=>le(e,t,"checkListItem"),render(e,t){const n=document.createDocumentFragment(),o=document.createElement("input");o.type="checkbox",o.checked=e.props.checked,e.props.checked&&o.setAttribute("checked",""),o.addEventListener("change",()=>{t.updateBlock(e,{props:{checked:!e.props.checked}})});const r=document.createElement("p"),a=document.createElement("div");return a.contentEditable="false",a.appendChild(o),n.appendChild(a),n.appendChild(r),{dom:n,contentDOM:r}},toExternalHTML(e){const t=document.createElement("li"),n=document.createElement("input");n.type="checkbox",n.checked=e.props.checked,e.props.checked&&n.setAttribute("checked","");const o=document.createElement("p");return I(e.props,t),t.appendChild(n),t.appendChild(o),{dom:t,contentDOM:o}},runsBefore:["bulletListItem"]},[y.createExtension({key:"check-list-item-shortcuts",keyboardShortcuts:{Enter:({editor:e})=>Y(e,"checkListItem"),"Mod-Shift-9":({editor:e})=>{const t=e.getTextCursorPosition();return e.schema.blockSchema[t.block.type].content!=="inline"?!1:(e.updateBlock(t.block,{type:"checkListItem",props:{}}),!0)}},inputRules:[{find:/^\s?\[\s*\]\s$/,replace(){return{type:"checkListItem",props:{checked:!1}}}},{find:/^\s?\[[Xx]\]\s$/,replace(){return{type:"checkListItem",props:{checked:!0}}}}]})]);function pt(e,t,n,o){let r=e.firstChild.attrs.start||1,a=!0;const s=!!e.firstChild.attrs.start,i=m.getBlockInfo({posBeforeNode:t,node:e});if(!i.isBlockContainer)throw new Error("impossible");const c=n.doc.resolve(i.bnBlock.beforePos).nodeBefore,l=c?o.get(c):void 0;return l!==void 0?(r=l+1,a=!1):c&&m.getBlockInfo({posBeforeNode:i.bnBlock.beforePos-c.nodeSize,node:c}).blockNoteType==="numberedListItem"&&(r=pt(c,i.bnBlock.beforePos-c.nodeSize,n,o).index+1,a=!1),o.set(e,r),{index:r,isFirst:a,hasStart:s}}function Ce(e,t){const n=new Map,o=t.decorations.map(e.mapping,e.doc),r=[];e.doc.nodesBetween(0,e.doc.nodeSize-2,(s,i)=>{if(s.type.name==="blockContainer"&&s.firstChild.type.name==="numberedListItem"){const{index:c,isFirst:l,hasStart:u}=pt(s,i,e,n);if(o.find(i,i+s.nodeSize,p=>p.index===c&&p.isFirst===l&&p.hasStart===u).length===0){const p=e.doc.nodeAt(i+1);r.push(ee.Decoration.node(i+1,i+1+p.nodeSize,{"data-index":c.toString()}))}}});const a=r.flatMap(s=>o.find(s.from,s.to));return{decorations:o.remove(a).add(e.doc,r)}}const hn=()=>new fe.Plugin({key:new fe.PluginKey("numbered-list-indexing-decorations"),state:{init(e,t){return Ce(t.tr,{decorations:ee.DecorationSet.empty})},apply(e,t){return!e.docChanged&&!e.selectionSet&&t.decorations?t:Ce(e,t)}},props:{decorations(e){var t;return((t=this.getState(e))==null?void 0:t.decorations)??ee.DecorationSet.empty}}}),ft=()=>({type:"numberedListItem",propSchema:{...g,start:{default:void 0,type:"number"}},content:"inline"}),gt=E(ft,{meta:{isolating:!1},parse(e){var n;if(e.tagName!=="LI")return;const t=e.parentElement;if(t!==null&&(t.tagName==="OL"||t.tagName==="DIV"&&((n=t.parentElement)==null?void 0:n.tagName)==="OL")){const o=parseInt(t.getAttribute("start")||"1"),r=v(e);return e.previousElementSibling||o===1?r:{...r,start:o}}},parseContent:({el:e,schema:t})=>le(e,t,"numberedListItem"),render(){const e=document.createElement("p");return{dom:e,contentDOM:e}},toExternalHTML(e){const t=document.createElement("li"),n=document.createElement("p");return I(e.props,t),t.appendChild(n),{dom:t,contentDOM:n}}},[y.createExtension({key:"numbered-list-item-shortcuts",inputRules:[{find:/^\s?(\d+)\.\s$/,replace({match:e,editor:t}){if(m.getBlockInfoFromSelection(t.prosemirrorState).blockNoteType==="heading")return;const o=parseInt(e[1]);return{type:"numberedListItem",props:{start:o!==1?o:void 0}}}}],keyboardShortcuts:{Enter:({editor:e})=>Y(e,"numberedListItem"),"Mod-Shift-7":({editor:e})=>{const t=e.getTextCursorPosition();return e.schema.blockSchema[t.block.type].content!=="inline"?!1:(e.updateBlock(t.block,{type:"numberedListItem",props:{}}),!0)}},prosemirrorPlugins:[hn()]})]),ht=()=>({type:"toggleListItem",propSchema:{...g},content:"inline"}),mt=E(ht,{meta:{isolating:!1},render(e,t){const n=document.createElement("p");return{...ce(e,t,n),contentDOM:n}},toExternalHTML(e){const t=document.createElement("li"),n=document.createElement("p");return I(e.props,t),t.appendChild(n),{dom:t,contentDOM:n}}},[y.createExtension({key:"toggle-list-item-shortcuts",keyboardShortcuts:{Enter:({editor:e})=>Y(e,"toggleListItem"),"Mod-Shift-6":({editor:e})=>{const t=e.getTextCursorPosition();return e.schema.blockSchema[t.block.type].content!=="inline"?!1:(e.updateBlock(t.block,{type:"toggleListItem",props:{}}),!0)}}})]),bt=()=>({type:"paragraph",propSchema:g,content:"inline"}),Ct=E(bt,{meta:{isolating:!1},parse:e=>{var t;if(e.tagName==="P"&&(t=e.textContent)!=null&&t.trim())return v(e)},render:()=>{const e=document.createElement("p");return{dom:e,contentDOM:e}},toExternalHTML:e=>{const t=document.createElement("p");return I(e.props,t),{dom:t,contentDOM:t}},runsBefore:["default"]},[y.createExtension({key:"paragraph-shortcuts",keyboardShortcuts:{"Mod-Alt-0":({editor:e})=>{const t=e.getTextCursorPosition();return e.schema.blockSchema[t.block.type].content!=="inline"?!1:(e.updateBlock(t.block,{type:"paragraph",props:{}}),!0)}}})]),kt=()=>({type:"quote",propSchema:{backgroundColor:g.backgroundColor,textColor:g.textColor},content:"inline"}),yt=E(kt,{meta:{isolating:!1},parse(e){if(e.tagName==="BLOCKQUOTE"){const{backgroundColor:t,textColor:n}=v(e);return{backgroundColor:t,textColor:n}}},render(){const e=document.createElement("blockquote");return{dom:e,contentDOM:e}},toExternalHTML(e){const t=document.createElement("blockquote");return I(e.props,t),{dom:t,contentDOM:t}}},[y.createExtension({key:"quote-block-shortcuts",keyboardShortcuts:{"Mod-Alt-q":({editor:e})=>{const t=e.getTextCursorPosition();return e.schema.blockSchema[t.block.type].content!=="inline"?!1:(e.updateBlock(t.block,{type:"quote",props:{}}),!0)}},inputRules:[{find:new RegExp("^>\\s$"),replace(){return{type:"quote",props:{}}}}]})]),mn=35,de=120,bn=31,Cn=S.Extension.create({name:"BlockNoteTableExtension",addProseMirrorPlugins:()=>[T.columnResizing({cellMinWidth:mn,defaultCellMinWidth:de,View:null}),T.tableEditing()],addKeyboardShortcuts(){return{Enter:()=>this.editor.state.selection.empty&&this.editor.state.selection.$head.parent.type.name==="tableParagraph"?(this.editor.commands.insertContent({type:"hardBreak"}),!0):!1,Backspace:()=>{const e=this.editor.state.selection,t=e.empty,n=e.$head.parentOffset===0,o=e.$head.node().type.name==="tableParagraph";return t&&n&&o},Tab:()=>this.editor.commands.command(({state:e,dispatch:t,view:n})=>T.goToNextCell(1)(e,t,n)),"Shift-Tab":()=>this.editor.commands.command(({state:e,dispatch:t,view:n})=>T.goToNextCell(-1)(e,t,n))}},extendNodeSchema(e){const t={name:e.name,options:e.options,storage:e.storage};return{tableRole:S.callOrReturn(S.getExtensionField(e,"tableRole",t))}}}),vt={textColor:g.textColor},kn=S.Node.create({name:"tableHeader",addOptions(){return{HTMLAttributes:{}}},content:"tableContent+",addAttributes(){return{colspan:{default:1},rowspan:{default:1},colwidth:{default:null,parseHTML:e=>{const t=e.getAttribute("colwidth");return t?t.split(",").map(o=>parseInt(o,10)):null}}}},tableRole:"header_cell",isolating:!0,parseHTML(){return[{tag:"th",getContent:(e,t)=>St(e,t)}]},renderHTML({HTMLAttributes:e}){return["th",S.mergeAttributes(this.options.HTMLAttributes,e),0]}}),yn=S.Node.create({name:"tableCell",addOptions(){return{HTMLAttributes:{}}},content:"tableContent+",addAttributes(){return{colspan:{default:1},rowspan:{default:1},colwidth:{default:null,parseHTML:e=>{const t=e.getAttribute("colwidth");return t?t.split(",").map(o=>parseInt(o,10)):null}}}},tableRole:"cell",isolating:!0,parseHTML(){return[{tag:"td",getContent:(e,t)=>St(e,t)}]},renderHTML({HTMLAttributes:e}){return["td",S.mergeAttributes(this.options.HTMLAttributes,e),0]}}),vn=S.Node.create({name:"table",content:"tableRow+",group:"blockContent",tableRole:"table",marks:"deletion insertion modification",isolating:!0,parseHTML(){return[{tag:"table"}]},renderHTML({node:e,HTMLAttributes:t}){var r,a,s;const n=Se(this.name,"table",{...((r=this.options.domAttributes)==null?void 0:r.blockContent)||{},...t},((a=this.options.domAttributes)==null?void 0:a.inlineContent)||{}),o=document.createElement("colgroup");for(const i of e.children[0].children)if(i.attrs.colwidth)for(const l of i.attrs.colwidth){const u=document.createElement("col");l&&(u.style=`width: ${l}px`),o.appendChild(u)}else o.appendChild(document.createElement("col"));return(s=n.dom.firstChild)==null||s.appendChild(o),n},addNodeView(){return({node:e,HTMLAttributes:t})=>{var o;class n extends T.TableView{constructor(a,s,i){super(a,s),this.node=a,this.cellMinWidth=s,this.blockContentHTMLAttributes=i;const c=document.createElement("div");c.className=V("bn-block-content",i.class),c.setAttribute("data-content-type","table");for(const[p,h]of Object.entries(i))p!=="class"&&c.setAttribute(p,h);const l=this.dom,u=document.createElement("div");u.className="tableWrapper-inner",u.appendChild(l.firstChild),l.appendChild(u),c.appendChild(l);const d=document.createElement("div");d.className="table-widgets-container",d.style.position="relative",l.appendChild(d),this.dom=c}ignoreMutation(a){return!a.target.closest(".tableWrapper-inner")||super.ignoreMutation(a)}}return new n(e,de,{...((o=this.options.domAttributes)==null?void 0:o.blockContent)||{},...t})}}}),Sn=S.Node.create({name:"tableParagraph",group:"tableContent",content:"inline*",parseHTML(){return[{tag:"p",getAttrs:e=>{if(typeof e=="string"||!e.textContent||!e.closest("[data-content-type]"))return!1;const t=e.parentElement;return t===null?!1:t.tagName==="TD"||t.tagName==="TH"?{}:!1},node:"tableParagraph"}]},renderHTML({HTMLAttributes:e}){return["p",e,0]}}),En=S.Node.create({name:"tableRow",addOptions(){return{HTMLAttributes:{}}},content:"(tableCell | tableHeader)+",tableRole:"row",marks:"deletion insertion modification",parseHTML(){return[{tag:"tr"}]},renderHTML({HTMLAttributes:e}){return["tr",S.mergeAttributes(this.options.HTMLAttributes,e),0]}});function St(e,t){const o=N.DOMParser.fromSchema(t).parse(e,{topNode:t.nodes.blockGroup.create()}),r=[];return o.content.descendants(a=>{if(a.isInline)return r.push(a),!1}),N.Fragment.fromArray(r)}const Et=()=>Le({node:vn,type:"table",content:"table"},vt,[y.createExtension({key:"table-extensions",tiptapExtensions:[Cn,Sn,kn,yn,En]}),y.createExtension({key:"table-keyboard-delete",keyboardShortcuts:{Backspace:({editor:e})=>{if(!(e.prosemirrorState.selection instanceof T.CellSelection))return!1;const t=e.getTextCursorPosition().block,n=t.content;let o=0;for(const a of n.rows)for(const s of a.cells){if("type"in s&&s.content.length>0||!("type"in s)&&s.length>0)return!1;o++}let r=0;return e.prosemirrorState.selection.forEachCell(()=>{r++}),r{(e.getPrevBlock(t)||e.getNextBlock(t))&&e.setTextCursorPosition(t),e.removeBlocks([t])}),!0)}}})]),ke=e=>{const t=e.src||void 0,n=e.width||void 0;return{url:t,previewWidth:n}},xt='',Mt=e=>({type:"video",propSchema:{textAlignment:g.textAlignment,backgroundColor:g.backgroundColor,name:{default:""},url:{default:""},caption:{default:""},showPreview:{default:!0},previewWidth:{default:void 0,type:"number"}},content:"none"}),Lt=e=>t=>{if(t.tagName==="VIDEO"){if(t.closest("figure"))return;const{backgroundColor:n}=v(t);return{...ke(t),backgroundColor:n}}if(t.tagName==="FIGURE"){const n=X(t,"video");if(!n)return;const{targetElement:o,caption:r}=n,{backgroundColor:a}=v(t);return{...ke(o),backgroundColor:a,caption:r}}},wt=E(Mt,e=>({meta:{fileBlockAccept:["video/*"]},parse:Lt(),render(t,n){const o=document.createElement("div");o.innerHTML=e.icon??xt;const r=document.createElement("div");r.className="bn-visual-media-wrapper";const a=document.createElement("video");return a.className="bn-visual-media",n.resolveFileUrl?n.resolveFileUrl(t.props.url).then(s=>{a.src=s}):a.src=t.props.url,a.controls=!0,a.contentEditable="false",a.draggable=!1,a.width=t.props.previewWidth,r.appendChild(a),et(t,n,{dom:r},r,o.firstElementChild)},toExternalHTML(t){if(!t.props.url){const o=document.createElement("p");return o.textContent="Add video",{dom:o}}let n;return t.props.showPreview?(n=document.createElement("video"),n.src=t.props.url,t.props.previewWidth&&(n.width=t.props.previewWidth)):(n=document.createElement("a"),n.href=t.props.url,n.textContent=t.props.name||t.props.url),t.props.caption?t.props.showPreview?se(n,t.props.caption):Q(n,t.props.caption):{dom:n}},runsBefore:["file"]}));function C(e,t,n){if(!(t in e.schema.blockSpecs))return!1;if(!n)return!0;for(const[o,r]of Object.entries(n)){if(!(o in e.schema.blockSpecs[t].config.propSchema))return!1;if(typeof r=="string"){if(e.schema.blockSpecs[t].config.propSchema[o].default!==void 0&&typeof e.schema.blockSpecs[t].config.propSchema[o].default!==r||e.schema.blockSpecs[t].config.propSchema[o].type!==void 0&&e.schema.blockSpecs[t].config.propSchema[o].type!==r)return!1}else{if(e.schema.blockSpecs[t].config.propSchema[o].default!==r.default||e.schema.blockSpecs[t].config.propSchema[o].default===void 0&&r.default===void 0&&e.schema.blockSpecs[t].config.propSchema[o].type!==r.type||typeof e.schema.blockSpecs[t].config.propSchema[o].values!=typeof r.values)return!1;if(typeof e.schema.blockSpecs[t].config.propSchema[o].values=="object"&&typeof r.values=="object"){for(const a of r.values)if(!e.schema.blockSpecs[t].config.propSchema[o].values.includes(a))return!1}}}return!0}function xn(e,t,n,o){return C(t,n,o)&&e.type===n}function Mn(e){return e instanceof T.CellSelection}const G=new Map;function Ln(e){if(G.has(e))return G.get(e);const t=new ye.Mapping;return e._tiptapEditor.on("transaction",({transaction:n})=>{t.appendMapping(n.mapping)}),e._tiptapEditor.on("destroy",()=>{G.delete(e)}),G.set(e,t),t}function Bt(e,t,n="left"){const o=j.ySyncPluginKey.getState(e.prosemirrorState);if(!o){const a=Ln(e),s=a.maps.length;return()=>a.slice(s).map(t,n==="left"?-1:1)}const r=j.absolutePositionToRelativePosition(t+(n==="right"?1:-1),o.binding.type,o.binding.mapping);return()=>{const a=j.ySyncPluginKey.getState(e.prosemirrorState),s=j.relativePositionToAbsolutePosition(a.doc,a.binding.type,r,a.binding.mapping);if(s===null)throw new Error("Position not found, cannot track positions");return s+(n==="right"?-1:1)}}const wn=S.findParentNode(e=>e.type.name==="blockContainer");class Bn{constructor(t,n,o){P(this,"state");P(this,"emitUpdate");P(this,"rootEl");P(this,"pluginState");P(this,"handleScroll",()=>{var t,n;if((t=this.state)!=null&&t.show){const o=(n=this.rootEl)==null?void 0:n.querySelector(`[data-decoration-id="${this.pluginState.decorationId}"]`);if(!o)return;this.state.referencePos=o.getBoundingClientRect().toJSON(),this.emitUpdate(this.pluginState.triggerCharacter)}});P(this,"closeMenu",()=>{this.editor.transact(t=>t.setMeta(H,null))});P(this,"clearQuery",()=>{this.pluginState!==void 0&&this.editor._tiptapEditor.chain().focus().deleteRange({from:this.pluginState.queryStartPos()-(this.pluginState.deleteTriggerCharacter?this.pluginState.triggerCharacter.length:0),to:this.editor.transact(t=>t.selection.from)}).run()});var r;this.editor=t,this.pluginState=void 0,this.emitUpdate=a=>{var s;if(!this.state)throw new Error("Attempting to update uninitialized suggestions menu");n(a,{...this.state,ignoreQueryLength:(s=this.pluginState)==null?void 0:s.ignoreQueryLength})},this.rootEl=o.root,(r=this.rootEl)==null||r.addEventListener("scroll",this.handleScroll,!0)}update(t,n){var l;const o=H.getState(n),r=H.getState(t.state),a=o===void 0&&r!==void 0,s=o!==void 0&&r===void 0;if(!a&&!(o!==void 0&&r!==void 0)&&!s)return;if(this.pluginState=s?o:r,s||!this.editor.isEditable){this.state&&(this.state.show=!1),this.emitUpdate(this.pluginState.triggerCharacter);return}const c=(l=this.rootEl)==null?void 0:l.querySelector(`[data-decoration-id="${this.pluginState.decorationId}"]`);this.editor.isEditable&&c&&(this.state={show:!0,referencePos:c.getBoundingClientRect().toJSON(),query:this.pluginState.query},this.emitUpdate(this.pluginState.triggerCharacter))}destroy(){var t;(t=this.rootEl)==null||t.removeEventListener("scroll",this.handleScroll,!0)}}const H=new z.PluginKey("SuggestionMenuPlugin"),Tt=y.createExtension(({editor:e})=>{const t=[];let n;const o=y.createStore(void 0);return{key:"suggestionMenu",store:o,addTriggerCharacter:r=>{t.push(r)},removeTriggerCharacter:r=>{t.splice(t.indexOf(r),1)},closeMenu:()=>{n==null||n.closeMenu()},clearQuery:()=>{n==null||n.clearQuery()},shown:()=>{var r;return((r=n==null?void 0:n.state)==null?void 0:r.show)||!1},openSuggestionMenu:(r,a)=>{e.headless||(e.focus(),e.transact(s=>{a!=null&&a.deleteTriggerCharacter&&s.insertText(r),s.scrollIntoView().setMeta(H,{triggerCharacter:r,deleteTriggerCharacter:(a==null?void 0:a.deleteTriggerCharacter)||!1,ignoreQueryLength:(a==null?void 0:a.ignoreQueryLength)||!1})}))},prosemirrorPlugins:[new z.Plugin({key:H,view:r=>(n=new Bn(e,(a,s)=>{o.setState({...s,triggerCharacter:a})},r),n),state:{init(){},apply:(r,a,s,i)=>{if(r.selection.$from.parent.type.spec.code)return a;const c=r.getMeta(H);if(typeof c=="object"&&c!==null){a&&(n==null||n.closeMenu());const u=Bt(e,i.selection.from-c.triggerCharacter.length);return{triggerCharacter:c.triggerCharacter,deleteTriggerCharacter:c.deleteTriggerCharacter!==!1,queryStartPos:()=>u()+c.triggerCharacter.length,query:"",decorationId:`id_${Math.floor(Math.random()*4294967295)}`,ignoreQueryLength:c==null?void 0:c.ignoreQueryLength}}if(a===void 0)return a;if(i.selection.from!==i.selection.to||c===null||r.getMeta("focus")||r.getMeta("blur")||r.getMeta("pointer")||a.triggerCharacter!==void 0&&i.selection.from1?c.textBetween(a-l.length,a)+i:i;if(l===u)return r.dispatch(r.state.tr.insertText(i)),r.dispatch(r.state.tr.setMeta(H,{triggerCharacter:u}).scrollIntoView()),!0}}return!1},decorations(r){const a=this.getState(r);if(a===void 0)return null;if(!a.deleteTriggerCharacter){const s=wn(r.selection);if(s)return $.DecorationSet.create(r.doc,[$.Decoration.node(s.pos,s.pos+s.node.nodeSize,{nodeName:"span",class:"bn-suggestion-decorator","data-decoration-id":a.decorationId})])}return $.DecorationSet.create(r.doc,[$.Decoration.inline(a.queryStartPos()-a.triggerCharacter.length,a.queryStartPos(),{nodeName:"span",class:"bn-suggestion-decorator","data-decoration-id":a.decorationId})])}}})]}});function Tn(e){let t=e.getTextCursorPosition().block,n=e.schema.blockSchema[t.type].content;for(;n==="none";){if(t=e.getTextCursorPosition().nextBlock,t===void 0)return;n=e.schema.blockSchema[t.type].content,e.setTextCursorPosition(t,"end")}}function k(e,t){const n=e.getTextCursorPosition().block;if(n.content===void 0)throw new Error("Slash Menu open in a block that doesn't contain content.");let o;return Array.isArray(n.content)&&(n.content.length===1&&m.isStyledTextInlineContent(n.content[0])&&n.content[0].type==="text"&&n.content[0].text==="/"||n.content.length===0)?(o=e.updateBlock(n,t),e.setTextCursorPosition(o)):(o=e.insertBlocks([t],n,"after")[0],e.setTextCursorPosition(e.getTextCursorPosition().nextBlock)),Tn(e),o}function An(e){const t=[];return C(e,"heading",{level:"number"})&&(e.schema.blockSchema.heading.propSchema.level.values||[]).filter(n=>n<=3).forEach(n=>{t.push({onItemClick:()=>{k(e,{type:"heading",props:{level:n}})},badge:B(`Mod-Alt-${n}`),key:n===1?"heading":`heading_${n}`,...e.dictionary.slash_menu[n===1?"heading":`heading_${n}`]})}),C(e,"quote")&&t.push({onItemClick:()=>{k(e,{type:"quote"})},key:"quote",...e.dictionary.slash_menu.quote}),C(e,"toggleListItem")&&t.push({onItemClick:()=>{k(e,{type:"toggleListItem"})},badge:B("Mod-Shift-6"),key:"toggle_list",...e.dictionary.slash_menu.toggle_list}),C(e,"numberedListItem")&&t.push({onItemClick:()=>{k(e,{type:"numberedListItem"})},badge:B("Mod-Shift-7"),key:"numbered_list",...e.dictionary.slash_menu.numbered_list}),C(e,"bulletListItem")&&t.push({onItemClick:()=>{k(e,{type:"bulletListItem"})},badge:B("Mod-Shift-8"),key:"bullet_list",...e.dictionary.slash_menu.bullet_list}),C(e,"checkListItem")&&t.push({onItemClick:()=>{k(e,{type:"checkListItem"})},badge:B("Mod-Shift-9"),key:"check_list",...e.dictionary.slash_menu.check_list}),C(e,"paragraph")&&t.push({onItemClick:()=>{k(e,{type:"paragraph"})},badge:B("Mod-Alt-0"),key:"paragraph",...e.dictionary.slash_menu.paragraph}),C(e,"codeBlock")&&t.push({onItemClick:()=>{k(e,{type:"codeBlock"})},badge:B("Mod-Alt-c"),key:"code_block",...e.dictionary.slash_menu.code_block}),C(e,"divider")&&t.push({onItemClick:()=>{k(e,{type:"divider"})},key:"divider",...e.dictionary.slash_menu.divider}),C(e,"table")&&t.push({onItemClick:()=>{k(e,{type:"table",content:{type:"tableContent",rows:[{cells:["","",""]},{cells:["","",""]}]}})},badge:void 0,key:"table",...e.dictionary.slash_menu.table}),C(e,"image",{url:"string"})&&t.push({onItemClick:()=>{var o;const n=k(e,{type:"image"});(o=e.getExtension(O))==null||o.showMenu(n.id)},key:"image",...e.dictionary.slash_menu.image}),C(e,"video",{url:"string"})&&t.push({onItemClick:()=>{var o;const n=k(e,{type:"video"});(o=e.getExtension(O))==null||o.showMenu(n.id)},key:"video",...e.dictionary.slash_menu.video}),C(e,"audio",{url:"string"})&&t.push({onItemClick:()=>{var o;const n=k(e,{type:"audio"});(o=e.getExtension(O))==null||o.showMenu(n.id)},key:"audio",...e.dictionary.slash_menu.audio}),C(e,"file",{url:"string"})&&t.push({onItemClick:()=>{var o;const n=k(e,{type:"file"});(o=e.getExtension(O))==null||o.showMenu(n.id)},key:"file",...e.dictionary.slash_menu.file}),C(e,"heading",{level:"number",isToggleable:"boolean"})&&(e.schema.blockSchema.heading.propSchema.level.values||[]).filter(n=>n<=3).forEach(n=>{t.push({onItemClick:()=>{k(e,{type:"heading",props:{level:n,isToggleable:!0}})},key:n===1?"toggle_heading":`toggle_heading_${n}`,...e.dictionary.slash_menu[n===1?"toggle_heading":`toggle_heading_${n}`]})}),C(e,"heading",{level:"number"})&&(e.schema.blockSchema.heading.propSchema.level.values||[]).filter(n=>n>3).forEach(n=>{t.push({onItemClick:()=>{k(e,{type:"heading",props:{level:n}})},badge:B(`Mod-Alt-${n}`),key:`heading_${n}`,...e.dictionary.slash_menu[`heading_${n}`]})}),t.push({onItemClick:()=>{var n;(n=e.getExtension(Tt))==null||n.openSuggestionMenu(":",{deleteTriggerCharacter:!0,ignoreQueryLength:!0})},key:"emoji",...e.dictionary.slash_menu.emoji}),t}function Pn(e,t){return e.filter(({title:n,aliases:o})=>n.toLowerCase().includes(t.toLowerCase())||o&&o.filter(r=>r.toLowerCase().includes(t.toLowerCase())).length!==0)}const Nn={audio:qe(),bulletListItem:lt(),checkListItem:ut(),codeBlock:$e(),divider:Ge(),file:Ke(),heading:Je(),image:st(),numberedListItem:gt(),paragraph:Ct(),quote:yt(),table:Et(),toggleListItem:mt(),video:wt()},In=re({type:"textColor",propSchema:"string"},{render:()=>{const e=document.createElement("span");return{dom:e,contentDOM:e}},toExternalHTML:e=>{const t=document.createElement("span");return e!==g.textColor.default&&(t.style.color=e in A?A[e].text:e),{dom:t,contentDOM:t}},parse:e=>{if(e.tagName==="SPAN"&&e.style.color)return e.style.color}}),Hn=re({type:"backgroundColor",propSchema:"string"},{render:()=>{const e=document.createElement("span");return{dom:e,contentDOM:e}},toExternalHTML:e=>{const t=document.createElement("span");return e!==g.backgroundColor.default&&(t.style.backgroundColor=e in A?A[e].background:e),{dom:t,contentDOM:t}},parse:e=>{if(e.tagName==="SPAN"&&e.style.backgroundColor)return e.style.backgroundColor}}),At={bold:_(Wt.default,"boolean"),italic:_(Ut.default,"boolean"),underline:_(jt.default,"boolean"),strike:_($t.default,"boolean"),code:_(qt.default,"boolean"),textColor:In,backgroundColor:Hn},Dn=Ne(At),Pt={text:{config:"text",implementation:{}},link:{config:"link",implementation:{}}},_n=Ae(Pt);exports.COLORS_DARK_MODE_DEFAULT=rn;exports.COLORS_DEFAULT=A;exports.EMPTY_CELL_HEIGHT=bn;exports.EMPTY_CELL_WIDTH=de;exports.FILE_AUDIO_ICON_SVG=Oe;exports.FILE_IMAGE_ICON_SVG=tt;exports.FILE_VIDEO_ICON_SVG=xt;exports.FilePanelExtension=O;exports.SuggestionMenu=Tt;exports.addDefaultPropsExternalHTML=I;exports.addInlineContentAttributes=Qt;exports.addInlineContentKeyboardShortcuts=Yt;exports.addNodeAndExtensionsToSpec=Kt;exports.addStyleAttributes=F;exports.applyNonSelectableBlockFix=we;exports.audioParse=Fe;exports.audioRender=Re;exports.audioToExternalHTML=We;exports.blockHasType=xn;exports.camelToDataKebab=W;exports.captureCellAnchor=_e;exports.createAudioBlockConfig=Ve;exports.createAudioBlockSpec=qe;exports.createBlockConfig=Xt;exports.createBlockSpec=E;exports.createBlockSpecFromTiptapNode=Le;exports.createBulletListItemBlockConfig=ct;exports.createBulletListItemBlockSpec=lt;exports.createCheckListItemBlockSpec=ut;exports.createCheckListItemConfig=dt;exports.createCodeBlockConfig=Ue;exports.createCodeBlockSpec=$e;exports.createDefaultBlockDOMOutputSpec=Se;exports.createDividerBlockConfig=je;exports.createDividerBlockSpec=Ge;exports.createFileBlockConfig=Ze;exports.createFileBlockSpec=Ke;exports.createHeadingBlockConfig=Ye;exports.createHeadingBlockSpec=Je;exports.createImageBlockConfig=nt;exports.createImageBlockSpec=st;exports.createInlineContentSpecFromTipTapNode=Jt;exports.createInternalInlineContentSpec=Te;exports.createInternalStyleSpec=oe;exports.createNumberedListItemBlockConfig=ft;exports.createNumberedListItemBlockSpec=gt;exports.createParagraphBlockConfig=bt;exports.createParagraphBlockSpec=Ct;exports.createQuoteBlockConfig=kt;exports.createQuoteBlockSpec=yt;exports.createStyleSpec=re;exports.createStyleSpecFromTipTapMark=_;exports.createTableBlockSpec=Et;exports.createToggleListItemBlockConfig=ht;exports.createToggleListItemBlockSpec=mt;exports.createToggleWrapper=ce;exports.createVideoBlockConfig=Mt;exports.createVideoBlockSpec=wt;exports.defaultBlockSpecs=Nn;exports.defaultBlockToHTML=te;exports.defaultInlineContentSchema=_n;exports.defaultInlineContentSpecs=Pt;exports.defaultProps=g;exports.defaultStyleSchema=Dn;exports.defaultStyleSpecs=At;exports.defaultToggledState=Xe;exports.editorHasBlockWithType=C;exports.fileParse=ze;exports.filenameFromURL=Zt;exports.filterSuggestionItems=Pn;exports.formatKeyboardShortcut=B;exports.getBackgroundColorAttribute=an;exports.getBlockFromPos=Me;exports.getDefaultSlashMenuItems=An;exports.getInlineContentSchemaFromSpecs=Ae;exports.getLanguageId=ie;exports.getNodeById=He;exports.getParseRules=Be;exports.getStyleParseRules=Ie;exports.getStyleSchemaFromSpecs=Ne;exports.getTextAlignmentAttribute=cn;exports.getTextColorAttribute=sn;exports.imageParse=ot;exports.imageRender=rt;exports.imageToExternalHTML=at;exports.insertOrUpdateBlockForSlashMenu=k;exports.isAppleOS=ve;exports.isNodeBlock=De;exports.isSafari=Gt;exports.isTableCellSelection=Mn;exports.isVideoUrl=zt;exports.mergeCSSClasses=V;exports.mergeParagraphs=Ee;exports.parseAudioElement=ne;exports.parseDefaultProps=v;exports.propsToAttributes=xe;exports.splitBlockCommand=gn;exports.stylePropsToAttributes=Pe;exports.tablePropSchema=vt;exports.trackPosition=Bt;exports.updateBlock=nn;exports.updateBlockCommand=en;exports.updateBlockTr=K;exports.videoParse=Lt;exports.wrapInBlockStructure=Z; ++`:"
"),R.DOMParser.fromSchema(r).parse(i,{topNode:r.nodes.paragraph.create(),preserveWhitespace:!0}).content}return R.Fragment.empty}:void 0}),n}function Kt(e,t,n,o){var a,s,i,c;const r=t.node||S.Node.create({name:e.type,content:e.content==="inline"?"inline*":e.content==="none"?"":e.content,group:"blockContent",selectable:((a=t.meta)==null?void 0:a.selectable)??!0,isolating:((s=t.meta)==null?void 0:s.isolating)??!0,code:((i=t.meta)==null?void 0:i.code)??!1,defining:((c=t.meta)==null?void 0:c.defining)??!0,priority:o,addAttributes(){return xe(e.propSchema)},parseHTML(){return Be(e,t)},renderHTML({HTMLAttributes:l}){var d;const u=document.createElement("div");return Z({dom:u,contentDOM:e.content==="inline"?u:void 0},e.type,{},e.propSchema,((d=t.meta)==null?void 0:d.fileBlockAccept)!==void 0,l)},addNodeView(){return l=>{var f,x;const u=this.options.editor,d=Me(l.getPos,u,this.editor,e.type),p=((f=this.options.domAttributes)==null?void 0:f.blockContent)||{},h=t.render.call({blockContentDOMAttributes:p,props:l,renderType:"nodeView"},d,u);return((x=t.meta)==null?void 0:x.selectable)===!1&&we(h,this.editor),h}}});if(r.name!==e.type)throw new Error("Node name does not match block type. This is a bug in BlockNote.");return{config:e,implementation:{...t,node:r,render(l,u){var p;const d=((p=r.options.domAttributes)==null?void 0:p.blockContent)||{};return t.render.call({blockContentDOMAttributes:d,props:void 0,renderType:"dom"},l,u)},toExternalHTML:(l,u,d)=>{var h,f;const p=((h=r.options.domAttributes)==null?void 0:h.blockContent)||{};return((f=t.toExternalHTML)==null?void 0:f.call({blockContentDOMAttributes:p},l,u,d))??t.render.call({blockContentDOMAttributes:p,renderType:"dom",props:void 0},l,u)}},extensions:n}}function Xt(e){return e}function E(e,t,n){return(o={})=>{const r=typeof e=="function"?e(o):e,a=typeof t=="function"?t(o):t,s=n?typeof n=="function"?n(o):n:void 0;return{config:r,implementation:{...a,toExternalHTML(i,c,l){var d,p;const u=(d=a.toExternalHTML)==null?void 0:d.call({blockContentDOMAttributes:this.blockContentDOMAttributes},i,c,l);if(u!==void 0)return Z(u,i.type,i.props,r.propSchema,((p=a.meta)==null?void 0:p.fileBlockAccept)!==void 0)},render(i,c){var d;const l=a.render.call({blockContentDOMAttributes:this.blockContentDOMAttributes,renderType:this.renderType,props:this.props},i,c);return Z(l,i.type,i.props,r.propSchema,((d=a.meta)==null?void 0:d.fileBlockAccept)!==void 0,this.blockContentDOMAttributes)}},extensions:s}}}function Qt(e,t,n,o){return e.dom.setAttribute("data-inline-content-type",t),Object.entries(n).filter(([r,a])=>{const s=o[r];return a!==s.default}).map(([r,a])=>[W(r),a]).forEach(([r,a])=>e.dom.setAttribute(r,a)),e.contentDOM&&e.contentDOM.setAttribute("data-editable",""),e}function Yt(e){return{Backspace:({editor:t})=>{const n=t.state.selection.$from;return t.state.selection.empty&&n.node().type.name===e.type&&n.parentOffset===0}}}function Te(e,t){return{config:e,implementation:t}}function Jt(e,t,n){return Te({type:e.name,propSchema:t,content:e.config.content==="inline*"?"styled":"none"},{...n,node:e})}function Ae(e){return Object.fromEntries(Object.entries(e).map(([t,n])=>[t,n.config]))}function Pe(e){return e==="boolean"?{}:{stringValue:{default:void 0,keepOnSplit:!0,parseHTML:t=>t.getAttribute("data-value"),renderHTML:t=>t.stringValue!==void 0?{"data-value":t.stringValue}:{}}}}function F(e,t,n,o){return e.dom.setAttribute("data-style-type",t),o==="string"&&e.dom.setAttribute("data-value",n),e.contentDOM&&e.contentDOM.setAttribute("data-editable",""),e}function oe(e,t){return{config:e,implementation:t}}function _(e,t){return oe({type:e.name,propSchema:t},{mark:e,render(n,o){const r=o.pmSchema.marks[e.name].spec.toDOM;if(r===void 0)throw new Error("This block has no default HTML serialization as its corresponding TipTap node doesn't implement `renderHTML`.");const a=o.pmSchema.mark(e.name,{stringValue:n}),s=R.DOMSerializer.renderSpec(document,r(a,!0));if(typeof s!="object"||!("dom"in s))throw new Error("Cannot use this block's default HTML serialization as its corresponding TipTap mark's `renderHTML` function does not return an object with the `dom` property.");return s},toExternalHTML(n,o){const r=o.pmSchema.marks[e.name].spec.toDOM;if(r===void 0)throw new Error("This block has no default HTML serialization as its corresponding TipTap node doesn't implement `renderHTML`.");const a=o.pmSchema.mark(e.name,{stringValue:n}),s=R.DOMSerializer.renderSpec(document,r(a,!0));if(typeof s!="object"||!("dom"in s))throw new Error("Cannot use this block's default HTML serialization as its corresponding TipTap mark's `renderHTML` function does not return an object with the `dom` property.");return s}})}function Ne(e){return Object.fromEntries(Object.entries(e).map(([t,n])=>[t,n.config]))}function Ie(e,t){const n=[{tag:`[data-style-type="${e.type}"]`,contentElement:o=>{const r=o;return r.matches("[data-editable]")?r:r.querySelector("[data-editable]")||r}}];return t&&n.push({tag:"*",consuming:!1,getAttrs(o){if(typeof o=="string")return!1;const r=t==null?void 0:t(o);return r===void 0?!1:{stringValue:r}}}),n}function re(e,t){const n=S.Mark.create({name:e.type,addAttributes(){return Pe(e.propSchema)},parseHTML(){return Ie(e,t.parse)},renderHTML({mark:o}){const r=(t.toExternalHTML||t.render)(o.attrs.stringValue);return F(r,e.type,o.attrs.stringValue,e.propSchema)},addMarkView(){return({mark:o})=>{const r=t.render(o.attrs.stringValue);return F(r,e.type,o.attrs.stringValue,e.propSchema)}}});return oe(e,{...t,mark:n,render:o=>{const r=t.render(o);return F(r,e.type,o,e.propSchema)},toExternalHTML:o=>{const r=(t.toExternalHTML||t.render)(o);return F(r,e.type,o,e.propSchema)}})}function He(e,t){let n,o;if(t.firstChild.descendants((r,a)=>n?!1:!De(r)||r.attrs.id!==e?!0:(n=r,o=a+1,!1)),!(n===void 0||o===void 0))return{node:n,posBeforeNode:o}}function De(e){return e.type.isInGroup("bnBlock")}const en=(e,t)=>({tr:n,dispatch:o})=>(o&&K(n,e,t),!0);function K(e,t,n,o,r){const a=m.getBlockInfoFromResolvedPos(e.doc.resolve(t));let s=null;a.blockNoteType==="table"&&(s=_e(e));const i=m.getPmSchema(e);if(o!==void 0&&r!==void 0&&o>r)throw new Error("Invalid replaceFromPos or replaceToPos");const c=i.nodes[a.blockNoteType],l=i.nodes[n.type||a.blockNoteType],u=l.isInGroup("bnBlock")?l:i.nodes.blockContainer;if(a.isBlockContainer&&l.isInGroup("blockContent")){const d=o!==void 0&&o>a.blockContent.beforePos&&oa.blockContent.beforePos&&r0){const r=e.children.map(a=>m.blockToNode(a,o));if(n.childContainer)t.step(new ye.ReplaceStep(n.childContainer.beforePos+1,n.childContainer.afterPos-1,new N.Slice(N.Fragment.from(r),0,0)));else{if(!n.isBlockContainer)throw new Error("impossible");t.insert(n.blockContent.afterPos,o.nodes.blockGroup.createChecked({},r))}}}function nn(e,t,n,o,r){const a=typeof t=="string"?t:t.id,s=He(a,e.doc);if(!s)throw new Error(`Block with ID ${a} not found`);K(e,s.posBeforeNode,n,o,r);const i=e.doc.resolve(s.posBeforeNode+1).node(),c=m.getPmSchema(e);return m.nodeToBlock(i,c)}function _e(e){const t="selection"in e?e.selection:null;if(!(t instanceof z.TextSelection))return null;const n=e.doc.resolve(t.head);let o=-1,r=-1;for(let L=n.depth;L>=0;L--){const M=n.node(L).type.name;if(o<0&&(M==="tableCell"||M==="tableHeader")&&(o=L),M==="table"){r=L;break}}if(o<0||r<0)return null;const a=n.before(o),s=n.before(r),i=e.doc.nodeAt(s);if(!i||i.type.name!=="table")return null;const c=T.TableMap.get(i),l=a-(s+1),u=c.map.indexOf(l);if(u<0)return null;const d=Math.floor(u/c.width),p=u%c.width,f=a+1+1,x=Math.max(0,t.head-f);return{row:d,col:p,offset:x}}function on(e,t,n){var L;if(t.blockNoteType!=="table")return!1;let o=-1;if(t.isBlockContainer)o=e.mapping.map(t.blockContent.beforePos);else{const M=e.mapping.map(t.bnBlock.beforePos),D=M+(((L=e.doc.nodeAt(M))==null?void 0:L.nodeSize)||0);e.doc.nodesBetween(M,D,(b,w)=>b.type.name==="table"?(o=w,!1):!0)}const r=o>=0?e.doc.nodeAt(o):null;if(!r||r.type.name!=="table")return!1;const a=T.TableMap.get(r),s=Math.max(0,Math.min(n.row,a.height-1)),i=Math.max(0,Math.min(n.col,a.width-1)),c=s*a.width+i,l=a.map[c];if(l==null)return!1;const d=o+1+l+1,p=e.doc.nodeAt(d),h=d+1,f=p?p.content.size:0,x=h+Math.max(0,Math.min(n.offset,f));return"selection"in e&&e.setSelection(z.TextSelection.create(e.doc,x)),!0}const A={gray:{text:"#9b9a97",background:"#ebeced"},brown:{text:"#64473a",background:"#e9e5e3"},red:{text:"#e03e3e",background:"#fbe4e4"},orange:{text:"#d9730d",background:"#f6e9d9"},yellow:{text:"#dfab01",background:"#fbf3db"},green:{text:"#4d6461",background:"#ddedea"},blue:{text:"#0b6e99",background:"#ddebf1"},purple:{text:"#6940a5",background:"#eae4f2"},pink:{text:"#ad1a72",background:"#f4dfeb"}},rn={gray:{text:"#bebdb8",background:"#9b9a97"},brown:{text:"#8e6552",background:"#64473a"},red:{text:"#ec4040",background:"#be3434"},orange:{text:"#e3790d",background:"#b7600a"},yellow:{text:"#dfab01",background:"#b58b00"},green:{text:"#6b8b87",background:"#4d6461"},blue:{text:"#0e87bc",background:"#0b6e99"},purple:{text:"#8552d7",background:"#6940a5"},pink:{text:"#da208f",background:"#ad1a72"}},g={backgroundColor:{default:"default"},textColor:{default:"default"},textAlignment:{default:"left",values:["left","center","right","justify"]}},v=e=>{const t={};return e.hasAttribute("data-background-color")?t.backgroundColor=e.getAttribute("data-background-color"):e.style.backgroundColor&&(t.backgroundColor=e.style.backgroundColor),e.hasAttribute("data-text-color")?t.textColor=e.getAttribute("data-text-color"):e.style.color&&(t.textColor=e.style.color),t.textAlignment=g.textAlignment.values.includes(e.style.textAlign)?e.style.textAlign:void 0,t},I=(e,t)=>{e.backgroundColor&&e.backgroundColor!==g.backgroundColor.default&&(t.style.backgroundColor=e.backgroundColor in A?A[e.backgroundColor].background:e.backgroundColor),e.textColor&&e.textColor!==g.textColor.default&&(t.style.color=e.textColor in A?A[e.textColor].text:e.textColor),e.textAlignment&&e.textAlignment!==g.textAlignment.default&&(t.style.textAlign=e.textAlignment)},an=(e="backgroundColor")=>({default:g.backgroundColor.default,parseHTML:t=>t.hasAttribute("data-background-color")?t.getAttribute("data-background-color"):t.style.backgroundColor?t.style.backgroundColor:g.backgroundColor.default,renderHTML:t=>t[e]===g.backgroundColor.default?{}:{"data-background-color":t[e]}}),sn=(e="textColor")=>({default:g.textColor.default,parseHTML:t=>t.hasAttribute("data-text-color")?t.getAttribute("data-text-color"):t.style.color?t.style.color:g.textColor.default,renderHTML:t=>t[e]===g.textColor.default?{}:{"data-text-color":t[e]}}),cn=(e="textAlignment")=>({default:g.textAlignment.default,parseHTML:t=>t.hasAttribute("data-text-alignment")?t.getAttribute("data-text-alignment"):t.style.textAlign?t.style.textAlign:g.textAlignment.default,renderHTML:t=>t[e]===g.textAlignment.default?{}:{"data-text-alignment":t[e]}}),X=(e,t)=>{const n=e.querySelector(t);if(!n)return;const o=e.querySelector("figcaption"),r=(o==null?void 0:o.textContent)??void 0;return{targetElement:n,caption:r}},O=y.createExtension(({editor:e})=>{const t=y.createStore(void 0);function n(){t.setState(void 0)}return{key:"filePanel",store:t,mount({signal:o}){const r=e.onChange(n,!1),a=e.onSelectionChange(n,!1);o.addEventListener("abort",()=>{r(),a()})},closeMenu:n,showMenu(o){t.setState(o)}}}),ln=(e,t,n)=>{const o=document.createElement("div");o.className="bn-add-file-button";const r=document.createElement("div");r.className="bn-add-file-button-icon",n?r.appendChild(n):r.innerHTML='',o.appendChild(r);const a=document.createElement("p");a.className="bn-add-file-button-text",a.innerHTML=e.type in t.dictionary.file_blocks.add_button_text?t.dictionary.file_blocks.add_button_text[e.type]:t.dictionary.file_blocks.add_button_text.file,o.appendChild(a);const s=c=>{c.preventDefault(),c.stopPropagation()},i=()=>{var c;t.isEditable&&((c=t.getExtension(O))==null||c.showMenu(e.id))};return o.addEventListener("mousedown",s,!0),o.addEventListener("click",i,!0),{dom:o,destroy:()=>{o.removeEventListener("mousedown",s,!0),o.removeEventListener("click",i,!0)}}},dn='',un=e=>{const t=document.createElement("div");t.className="bn-file-name-with-icon";const n=document.createElement("div");n.className="bn-file-icon",n.innerHTML=dn,t.appendChild(n);const o=document.createElement("p");return o.className="bn-file-name",o.textContent=e.props.name,t.appendChild(o),{dom:t}},ae=(e,t,n,o)=>{const r=document.createElement("div");if(r.className="bn-file-block-content-wrapper",e.props.url===""){const s=ln(e,t,o);r.appendChild(s.dom);const i=t.onUploadStart(c=>{if(c===e.id){r.removeChild(s.dom);const l=document.createElement("div");l.className="bn-file-loading-preview",l.textContent="Loading...",r.appendChild(l)}});return{dom:r,destroy:()=>{i(),s.destroy()}}}const a={dom:r};if(e.props.showPreview===!1||!n){const s=un(e);r.appendChild(s.dom),a.destroy=()=>{var i;(i=s.destroy)==null||i.call(s)}}else r.appendChild(n.dom);if(e.props.caption){const s=document.createElement("p");s.className="bn-file-caption",s.textContent=e.props.caption,r.appendChild(s)}return a},se=(e,t)=>{const n=document.createElement("figure"),o=document.createElement("figcaption");return o.textContent=t,n.appendChild(e),n.appendChild(o),{dom:n}},Q=(e,t)=>{const n=document.createElement("div"),o=document.createElement("p");return o.textContent=t,n.appendChild(e),n.appendChild(o),{dom:n}},ne=e=>({url:e.src||void 0}),Oe='',Ve=e=>({type:"audio",propSchema:{backgroundColor:g.backgroundColor,name:{default:""},url:{default:""},caption:{default:""},showPreview:{default:!0}},content:"none"}),Fe=(e={})=>t=>{if(t.tagName==="AUDIO"){if(t.closest("figure"))return;const{backgroundColor:n}=v(t);return{...ne(t),backgroundColor:n}}if(t.tagName==="FIGURE"){const n=X(t,"audio");if(!n)return;const{targetElement:o,caption:r}=n,{backgroundColor:a}=v(t);return{...ne(o),backgroundColor:a,caption:r}}},Re=(e={})=>(t,n)=>{const o=document.createElement("div");o.innerHTML=e.icon??Oe;const r=document.createElement("audio");return r.className="bn-audio",n.resolveFileUrl?n.resolveFileUrl(t.props.url).then(a=>{r.src=a}):r.src=t.props.url,r.controls=!0,r.contentEditable="false",r.draggable=!1,ae(t,n,{dom:r},o.firstElementChild)},We=(e={})=>(t,n)=>{if(!t.props.url){const r=document.createElement("p");return r.textContent="Add audio",{dom:r}}let o;return t.props.showPreview?(o=document.createElement("audio"),o.src=t.props.url):(o=document.createElement("a"),o.href=t.props.url,o.textContent=t.props.name||t.props.url),t.props.caption?t.props.showPreview?se(o,t.props.caption):Q(o,t.props.caption):{dom:o}},qe=E(Ve,e=>({meta:{fileBlockAccept:["audio/*"]},parse:Fe(e),render:Re(e),toExternalHTML:We(e),runsBefore:["file"]})),he=Symbol.for("blocknote.shikiParser"),J=Symbol.for("blocknote.shikiHighlighterPromise");function pn(e){const t=globalThis;let n,o,r=!1;const a=s=>{if(!e.createHighlighter)return process.env.NODE_ENV==="development"&&!r&&(console.log("For syntax highlighting of code blocks, you must provide a `createCodeBlockSpec({ createHighlighter: () => ... })` function"),r=!0),[];if(!n)return t[J]=t[J]||e.createHighlighter(),t[J].then(c=>{n=c});const i=ie(e,s.language);return!i||i==="text"||i==="none"||i==="plaintext"||i==="txt"?[]:n.getLoadedLanguages().includes(i)?(o||(o=t[he]||Rt.createParser(n),t[he]=o),o(s)):n.loadLanguage(i)};return Ft.createHighlightPlugin({parser:a,languageExtractor:s=>s.attrs.language,nodeTypes:["codeBlock"]})}const Ue=({defaultLanguage:e="text"})=>({type:"codeBlock",propSchema:{language:{default:e}},content:"inline"}),$e=E(Ue,e=>({meta:{code:!0,defining:!0,isolating:!1},parse:t=>{var r,a;if(t.tagName!=="PRE"||t.childElementCount!==1||((r=t.firstElementChild)==null?void 0:r.tagName)!=="CODE")return;const n=t.firstElementChild;return{language:n.getAttribute("data-language")||((a=n.className.split(" ").find(s=>s.includes("language-")))==null?void 0:a.replace("language-",""))}},parseContent:({el:t,schema:n})=>{const o=R.DOMParser.fromSchema(n),r=t.firstElementChild;return o.parse(r,{preserveWhitespace:"full",topNode:n.nodes.codeBlock.create()}).content},render(t,n){const o=document.createDocumentFragment(),r=document.createElement("pre"),a=document.createElement("code");r.appendChild(a);let s;if(e.supportedLanguages){const i=document.createElement("select");if(Object.entries(e.supportedLanguages??{}).forEach(([l,{name:u}])=>{const d=document.createElement("option");d.value=l,d.text=u,i.appendChild(d)}),i.value=t.props.language||e.defaultLanguage||"text",n.isEditable){const l=u=>{const d=u.target.value;let p;try{p=n.getBlock(t.id)}catch{return}if(!p)return;n.updateBlock(p.id,{props:{language:d}})};i.addEventListener("change",l),s=()=>i.removeEventListener("change",l)}else i.disabled=!0;const c=document.createElement("div");c.contentEditable="false",c.appendChild(i),o.appendChild(c)}return o.appendChild(r),{dom:o,contentDOM:a,destroy:()=>{s==null||s()}}},toExternalHTML(t){const n=document.createElement("pre"),o=document.createElement("code");return o.className=`language-${t.props.language}`,o.dataset.language=t.props.language,n.appendChild(o),{dom:n,contentDOM:o}}}),e=>[y.createExtension({key:"code-block-highlighter",prosemirrorPlugins:[pn(e)]}),y.createExtension({key:"code-block-keyboard-shortcuts",keyboardShortcuts:{Delete:({editor:t})=>t.transact(n=>{const{block:o}=t.getTextCursorPosition();if(o.type!=="codeBlock")return!1;const{$from:r}=n.selection;return r.parent.textContent?!1:(t.removeBlocks([o]),!0)}),Tab:({editor:t})=>e.indentLineWithTab===!1?!1:t.transact(n=>{const{block:o}=t.getTextCursorPosition();return o.type==="codeBlock"?(n.insertText(" "),!0):!1}),Enter:({editor:t})=>t.transact(n=>{const{block:o,nextBlock:r}=t.getTextCursorPosition();if(o.type!=="codeBlock")return!1;const{$from:a}=n.selection,s=a.parentOffset===a.parent.nodeSize-2,i=a.parent.textContent.endsWith("\n ");if(s&&i){if(n.delete(a.pos-2,a.pos),r)return t.setTextCursorPosition(r,"start"),!0;const[c]=t.insertBlocks([{type:"paragraph"}],o,"after");return t.setTextCursorPosition(c,"start"),!0}return n.insertText(` ++`),!0}),"Shift-Enter":({editor:t})=>t.transact(()=>{const{block:n}=t.getTextCursorPosition();if(n.type!=="codeBlock")return!1;const[o]=t.insertBlocks([{type:"paragraph"}],n,"after");return t.setTextCursorPosition(o,"start"),!0})},inputRules:[{find:/^```(.*?)\s$/,replace:({match:t})=>{const n=t[1].trim();return{type:"codeBlock",props:{language:{language:ie(e,n)??n}.language},content:[]}}}]})]);function ie(e,t){var n;return(n=Object.entries(e.supportedLanguages??{}).find(([o,{aliases:r}])=>(r==null?void 0:r.includes(t))||o===t))==null?void 0:n[0]}const je=()=>({type:"divider",propSchema:{},content:"none"}),Ge=E(je,{meta:{isolating:!1},parse(e){if(e.tagName==="HR")return{}},render(){return{dom:document.createElement("hr")}}},[y.createExtension({key:"divider-block-shortcuts",inputRules:[{find:new RegExp("^---$"),replace(){return{type:"divider",props:{},content:[]}}}]})]),me=e=>({url:e.src||void 0}),Ze=()=>({type:"file",propSchema:{backgroundColor:g.backgroundColor,name:{default:""},url:{default:""},caption:{default:""}},content:"none"}),ze=()=>e=>{if(e.tagName==="EMBED"){if(e.closest("figure"))return;const{backgroundColor:t}=v(e);return{...me(e),backgroundColor:t}}if(e.tagName==="FIGURE"){const t=X(e,"embed");if(!t)return;const{targetElement:n,caption:o}=t,{backgroundColor:r}=v(e);return{...me(n),backgroundColor:r,caption:o}}},Ke=E(Ze,{meta:{fileBlockAccept:["*/*"]},parse:ze(),render(e,t){return ae(e,t)},toExternalHTML(e){if(!e.props.url){const n=document.createElement("p");return n.textContent="Add file",{dom:n}}const t=document.createElement("a");return t.href=e.props.url,t.textContent=e.props.name||e.props.url,e.props.caption?Q(t,e.props.caption):{dom:t}}}),Xe={set:(e,t)=>window.localStorage.setItem(`toggle-${e.id}`,t?"true":"false"),get:e=>window.localStorage.getItem(`toggle-${e.id}`)==="true"},ce=(e,t,n,o=Xe)=>{if("isToggleable"in e.props&&!e.props.isToggleable)return{dom:n};const r=document.createElement("div"),a=document.createElement("div");a.className="bn-toggle-wrapper";const s=document.createElement("button");s.className="bn-toggle-button",s.type="button",s.innerHTML='';const i=f=>f.preventDefault();s.addEventListener("mousedown",i);const c=()=>{var f;a.getAttribute("data-show-children")==="true"?(a.setAttribute("data-show-children","false"),o.set(t.getBlock(e),!1),r.contains(l)&&r.removeChild(l)):(a.setAttribute("data-show-children","true"),o.set(t.getBlock(e),!0),t.isEditable&&((f=t.getBlock(e))==null?void 0:f.children.length)===0&&!r.contains(l)&&r.appendChild(l))};s.addEventListener("click",c),a.appendChild(s),a.appendChild(n);const l=document.createElement("button");l.className="bn-toggle-add-block-button",l.type="button",l.textContent=t.dictionary.toggle_blocks.add_block_button;const u=f=>f.preventDefault();l.addEventListener("mousedown",u);const d=()=>{t.transact(()=>{const f=t.updateBlock(e,{children:[{}]});t.setTextCursorPosition(f.children[0].id,"end"),t.focus()})};l.addEventListener("click",d),r.appendChild(a);let p=e.children.length;const h=t.onChange(()=>{var x;const f=((x=t.getBlock(e))==null?void 0:x.children.length)??0;f>p?(a.getAttribute("data-show-children")==="false"&&(a.setAttribute("data-show-children","true"),o.set(t.getBlock(e),!0)),r.contains(l)&&r.removeChild(l)):f===0&&ff instanceof MutationRecord&&(f.type==="attributes"&&f.target===a&&f.attributeName==="data-show-children"||f.type==="childList"&&(f.addedNodes[0]===l||f.removedNodes[0]===l)),destroy:()=>{s.removeEventListener("mousedown",i),s.removeEventListener("click",c),l.removeEventListener("mousedown",u),l.removeEventListener("click",d),h==null||h()}}},Qe=[1,2,3,4,5,6],fn=e=>({editor:t})=>{const n=t.getTextCursorPosition();return t.schema.blockSchema[n.block.type].content!=="inline"?!1:(t.updateBlock(n.block,{type:"heading",props:{level:e}}),!0)},Ye=({defaultLevel:e=1,levels:t=Qe,allowToggleHeadings:n=!0}={})=>({type:"heading",propSchema:{...g,level:{default:e,values:t},...n?{isToggleable:{default:!1,optional:!0}}:{}},content:"inline"}),Je=E(Ye,({allowToggleHeadings:e=!0}={})=>({meta:{isolating:!1},parse(t){let n;switch(t.tagName){case"H1":n=1;break;case"H2":n=2;break;case"H3":n=3;break;case"H4":n=4;break;case"H5":n=5;break;case"H6":n=6;break;default:return}return{...v(t),level:n}},render(t,n){const o=document.createElement(`h${t.props.level}`);return e?{...ce(t,n,o),contentDOM:o}:{dom:o,contentDOM:o}},toExternalHTML(t){const n=document.createElement(`h${t.props.level}`);return I(t.props,n),{dom:n,contentDOM:n}}}),({levels:e=Qe}={})=>[y.createExtension({key:"heading-shortcuts",keyboardShortcuts:Object.fromEntries(e.map(t=>[`Mod-Alt-${t}`,fn(t)])??[]),inputRules:e.map(t=>({find:new RegExp(`^(#{${t}})\\s$`),replace({match:n}){return{type:"heading",props:{level:n[1].length}}}}))})]),et=(e,t,n,o,r)=>{const{dom:a,destroy:s}=ae(e,t,n,r),i=a;i.style.position="relative",e.props.url&&e.props.showPreview&&(e.props.previewWidth?i.style.width=`${e.props.previewWidth}px`:i.style.width="fit-content");const c=document.createElement("div");c.className="bn-resize-handle",c.style.left="4px";const l=document.createElement("div");l.className="bn-resize-handle",l.style.right="4px";const u=document.createElement("div");u.style.position="absolute",u.style.height="100%",u.style.width="100%";let d,p=e.props.previewWidth;const h=b=>{var ue,pe;if(!d){!t.isEditable&&o.contains(c)&&o.contains(l)&&(o.removeChild(c),o.removeChild(l));return}let w;const U="touches"in b?b.touches[0].clientX:b.clientX;e.props.textAlignment==="center"?d.handleUsed==="left"?w=d.initialWidth+(d.initialClientX-U)*2:w=d.initialWidth+(U-d.initialClientX)*2:d.handleUsed==="left"?w=d.initialWidth+d.initialClientX-U:w=d.initialWidth+U-d.initialClientX,p=Math.min(Math.max(w,64),((pe=(ue=t.domElement)==null?void 0:ue.firstElementChild)==null?void 0:pe.clientWidth)||Number.MAX_VALUE),i.style.width=`${p}px`},f=b=>{(!b.target||!i.contains(b.target)||!t.isEditable)&&o.contains(c)&&o.contains(l)&&(o.removeChild(c),o.removeChild(l)),d&&(d=void 0,i.contains(u)&&i.removeChild(u),t.updateBlock(e,{props:{previewWidth:p}}))},x=()=>{t.isEditable&&(o.appendChild(c),o.appendChild(l))},L=b=>{b.relatedTarget===c||b.relatedTarget===l||d||t.isEditable&&o.contains(c)&&o.contains(l)&&(o.removeChild(c),o.removeChild(l))},M=b=>{b.preventDefault(),i.contains(u)||i.appendChild(u);const w="touches"in b?b.touches[0].clientX:b.clientX;d={handleUsed:"left",initialWidth:i.clientWidth,initialClientX:w}},D=b=>{b.preventDefault(),i.contains(u)||i.appendChild(u);const w="touches"in b?b.touches[0].clientX:b.clientX;d={handleUsed:"right",initialWidth:i.clientWidth,initialClientX:w}};return window.addEventListener("mousemove",h),window.addEventListener("touchmove",h),window.addEventListener("mouseup",f),window.addEventListener("touchend",f),i.addEventListener("mouseenter",x),i.addEventListener("mouseleave",L),c.addEventListener("mousedown",M),c.addEventListener("touchstart",M),l.addEventListener("mousedown",D),l.addEventListener("touchstart",D),{dom:i,destroy:()=>{s==null||s(),window.removeEventListener("mousemove",h),window.removeEventListener("touchmove",h),window.removeEventListener("mouseup",f),window.removeEventListener("touchend",f),i.removeEventListener("mouseenter",x),i.removeEventListener("mouseleave",L),c.removeEventListener("mousedown",M),c.removeEventListener("touchstart",M),l.removeEventListener("mousedown",D),l.removeEventListener("touchstart",D)}}},be=e=>{const t=e.src||void 0,n=e.width||void 0,o=e.alt||void 0;return{url:t,previewWidth:n,name:o}},tt='',nt=(e={})=>({type:"image",propSchema:{textAlignment:g.textAlignment,backgroundColor:g.backgroundColor,name:{default:""},url:{default:""},caption:{default:""},showPreview:{default:!0},previewWidth:{default:void 0,type:"number"}},content:"none"}),ot=(e={})=>t=>{if(t.tagName==="IMG"){if(t.closest("figure"))return;const{backgroundColor:n}=v(t);return{...be(t),backgroundColor:n}}if(t.tagName==="FIGURE"){const n=X(t,"img");if(!n)return;const{targetElement:o,caption:r}=n,{backgroundColor:a}=v(t);return{...be(o),backgroundColor:a,caption:r}}},rt=(e={})=>(t,n)=>{const o=document.createElement("div");o.innerHTML=e.icon??tt;const r=document.createElement("div");r.className="bn-visual-media-wrapper";const a=document.createElement("img");return a.className="bn-visual-media",n.resolveFileUrl?n.resolveFileUrl(t.props.url).then(s=>{a.src=s}):a.src=t.props.url,a.alt=t.props.name||t.props.caption||"BlockNote image",a.contentEditable="false",a.draggable=!1,r.appendChild(a),et(t,n,{dom:r},r,o.firstElementChild)},at=(e={})=>(t,n)=>{if(!t.props.url){const r=document.createElement("p");return r.textContent="Add image",{dom:r}}let o;return t.props.showPreview?(o=document.createElement("img"),o.src=t.props.url,o.alt=t.props.name||t.props.caption||"BlockNote image",t.props.previewWidth&&(o.width=t.props.previewWidth)):(o=document.createElement("a"),o.href=t.props.url,o.textContent=t.props.name||t.props.url),t.props.caption?t.props.showPreview?se(o,t.props.caption):Q(o,t.props.caption):{dom:o}},st=E(nt,e=>({meta:{fileBlockAccept:["image/*"]},parse:ot(e),render:rt(e),toExternalHTML:at(e),runsBefore:["file"]})),gn=(e,t,n)=>({state:o,dispatch:r})=>r?it(o.tr,e,t,n):!0,it=(e,t,n,o)=>{const r=m.getNearestBlockPos(e.doc,t),a=m.getBlockInfo(r);if(!a.isBlockContainer)return!1;const s=m.getPmSchema(e),i=[{type:a.bnBlock.node.type,attrs:o?{...a.bnBlock.node.attrs,id:void 0}:{}},{type:n?a.blockContent.node.type:s.nodes.paragraph,attrs:o?{...a.blockContent.node.attrs}:{}}];return e.split(t,2,i),!0},Y=(e,t)=>{const{blockInfo:n,selectionEmpty:o}=e.transact(s=>({blockInfo:m.getBlockInfoFromTransaction(s),selectionEmpty:s.selection.anchor===s.selection.head}));if(!n.isBlockContainer)return!1;const{bnBlock:r,blockContent:a}=n;return a.node.type.name!==t||!o?!1:a.node.childCount===0?(e.transact(s=>{K(s,r.beforePos,{type:"paragraph",props:{}})}),!0):a.node.childCount>0?e.transact(s=>(s.deleteSelection(),it(s,s.selection.from,!0))):!1};function le(e,t,n){var d,p,h;const o=N.DOMParser.fromSchema(t),r=e,a=document.createElement("div");a.setAttribute("data-node-type","blockGroup");for(const f of Array.from(r.childNodes))a.appendChild(f.cloneNode(!0));let s=o.parse(a,{topNode:t.nodes.blockGroup.create()});((p=(d=s.firstChild)==null?void 0:d.firstChild)==null?void 0:p.type.name)==="checkListItem"&&(s=s.copy(s.content.cut(s.firstChild.firstChild.nodeSize+2)));const i=(h=s.firstChild)==null?void 0:h.firstChild;if(!(i!=null&&i.isTextblock))return N.Fragment.from(s);const c=t.nodes[n].create({},i.content),l=s.content.cut(i.nodeSize+2);if(l.size>0){const f=s.copy(l);return c.content.addToEnd(f)}return c.content}const ct=()=>({type:"bulletListItem",propSchema:{...g},content:"inline"}),lt=E(ct,{meta:{isolating:!1},parse(e){var n;if(e.tagName!=="LI")return;const t=e.parentElement;if(t!==null&&(t.tagName==="UL"||t.tagName==="DIV"&&((n=t.parentElement)==null?void 0:n.tagName)==="UL"))return v(e)},parseContent:({el:e,schema:t})=>le(e,t,"bulletListItem"),render(){const e=document.createElement("p");return{dom:e,contentDOM:e}},toExternalHTML(e){const t=document.createElement("li"),n=document.createElement("p");return I(e.props,t),t.appendChild(n),{dom:t,contentDOM:n}}},[y.createExtension({key:"bullet-list-item-shortcuts",keyboardShortcuts:{Enter:({editor:e})=>Y(e,"bulletListItem"),"Mod-Shift-8":({editor:e})=>{const t=e.getTextCursorPosition();return e.schema.blockSchema[t.block.type].content!=="inline"?!1:(e.updateBlock(t.block,{type:"bulletListItem",props:{}}),!0)}},inputRules:[{find:/^\s?[-+*]\s$/,replace({editor:e}){if(m.getBlockInfoFromSelection(e.prosemirrorState).blockNoteType!=="heading")return{type:"bulletListItem",props:{}}}}]})]),dt=()=>({type:"checkListItem",propSchema:{...g,checked:{default:!1,type:"boolean"}},content:"inline"}),ut=E(dt,{meta:{isolating:!1},parse(e){var n;if(e.tagName==="input")return e.closest("[data-content-type]")||e.closest("li")?void 0:e.type==="checkbox"?{checked:e.checked}:void 0;if(e.tagName!=="LI")return;const t=e.parentElement;if(t!==null&&(t.tagName==="UL"||t.tagName==="DIV"&&((n=t.parentElement)==null?void 0:n.tagName)==="UL")){const o=e.querySelector("input[type=checkbox]")||null;return o===null?void 0:{...v(e),checked:o.checked}}},parseContent:({el:e,schema:t})=>le(e,t,"checkListItem"),render(e,t){const n=document.createDocumentFragment(),o=document.createElement("input");o.type="checkbox",o.checked=e.props.checked,e.props.checked&&o.setAttribute("checked",""),o.addEventListener("change",()=>{let r;try{r=t.getBlock(e.id)}catch{return}if(!r)return;t.updateBlock(r,{props:{checked:!r.props.checked}})});const r=document.createElement("p"),a=document.createElement("div");return a.contentEditable="false",a.appendChild(o),n.appendChild(a),n.appendChild(r),{dom:n,contentDOM:r}},toExternalHTML(e){const t=document.createElement("li"),n=document.createElement("input");n.type="checkbox",n.checked=e.props.checked,e.props.checked&&n.setAttribute("checked","");const o=document.createElement("p");return I(e.props,t),t.appendChild(n),t.appendChild(o),{dom:t,contentDOM:o}},runsBefore:["bulletListItem"]},[y.createExtension({key:"check-list-item-shortcuts",keyboardShortcuts:{Enter:({editor:e})=>Y(e,"checkListItem"),"Mod-Shift-9":({editor:e})=>{const t=e.getTextCursorPosition();return e.schema.blockSchema[t.block.type].content!=="inline"?!1:(e.updateBlock(t.block,{type:"checkListItem",props:{}}),!0)}},inputRules:[{find:/^\s?\[\s*\]\s$/,replace(){return{type:"checkListItem",props:{checked:!1}}}},{find:/^\s?\[[Xx]\]\s$/,replace(){return{type:"checkListItem",props:{checked:!0}}}}]})]);function pt(e,t,n,o){let r=e.firstChild.attrs.start||1,a=!0;const s=!!e.firstChild.attrs.start,i=m.getBlockInfo({posBeforeNode:t,node:e});if(!i.isBlockContainer)throw new Error("impossible");const c=n.doc.resolve(i.bnBlock.beforePos).nodeBefore,l=c?o.get(c):void 0;return l!==void 0?(r=l+1,a=!1):c&&m.getBlockInfo({posBeforeNode:i.bnBlock.beforePos-c.nodeSize,node:c}).blockNoteType==="numberedListItem"&&(r=pt(c,i.bnBlock.beforePos-c.nodeSize,n,o).index+1,a=!1),o.set(e,r),{index:r,isFirst:a,hasStart:s}}function Ce(e,t){const n=new Map,o=t.decorations.map(e.mapping,e.doc),r=[];e.doc.nodesBetween(0,e.doc.nodeSize-2,(s,i)=>{if(s.type.name==="blockContainer"&&s.firstChild.type.name==="numberedListItem"){const{index:c,isFirst:l,hasStart:u}=pt(s,i,e,n);if(o.find(i,i+s.nodeSize,p=>p.index===c&&p.isFirst===l&&p.hasStart===u).length===0){const p=e.doc.nodeAt(i+1);r.push(ee.Decoration.node(i+1,i+1+p.nodeSize,{"data-index":c.toString()}))}}});const a=r.flatMap(s=>o.find(s.from,s.to));return{decorations:o.remove(a).add(e.doc,r)}}const hn=()=>new fe.Plugin({key:new fe.PluginKey("numbered-list-indexing-decorations"),state:{init(e,t){return Ce(t.tr,{decorations:ee.DecorationSet.empty})},apply(e,t){return!e.docChanged&&!e.selectionSet&&t.decorations?t:Ce(e,t)}},props:{decorations(e){var t;return((t=this.getState(e))==null?void 0:t.decorations)??ee.DecorationSet.empty}}}),ft=()=>({type:"numberedListItem",propSchema:{...g,start:{default:void 0,type:"number"}},content:"inline"}),gt=E(ft,{meta:{isolating:!1},parse(e){var n;if(e.tagName!=="LI")return;const t=e.parentElement;if(t!==null&&(t.tagName==="OL"||t.tagName==="DIV"&&((n=t.parentElement)==null?void 0:n.tagName)==="OL")){const o=parseInt(t.getAttribute("start")||"1"),r=v(e);return e.previousElementSibling||o===1?r:{...r,start:o}}},parseContent:({el:e,schema:t})=>le(e,t,"numberedListItem"),render(){const e=document.createElement("p");return{dom:e,contentDOM:e}},toExternalHTML(e){const t=document.createElement("li"),n=document.createElement("p");return I(e.props,t),t.appendChild(n),{dom:t,contentDOM:n}}},[y.createExtension({key:"numbered-list-item-shortcuts",inputRules:[{find:/^\s?(\d+)\.\s$/,replace({match:e,editor:t}){if(m.getBlockInfoFromSelection(t.prosemirrorState).blockNoteType==="heading")return;const o=parseInt(e[1]);return{type:"numberedListItem",props:{start:o!==1?o:void 0}}}}],keyboardShortcuts:{Enter:({editor:e})=>Y(e,"numberedListItem"),"Mod-Shift-7":({editor:e})=>{const t=e.getTextCursorPosition();return e.schema.blockSchema[t.block.type].content!=="inline"?!1:(e.updateBlock(t.block,{type:"numberedListItem",props:{}}),!0)}},prosemirrorPlugins:[hn()]})]),ht=()=>({type:"toggleListItem",propSchema:{...g},content:"inline"}),mt=E(ht,{meta:{isolating:!1},render(e,t){const n=document.createElement("p");return{...ce(e,t,n),contentDOM:n}},toExternalHTML(e){const t=document.createElement("li"),n=document.createElement("p");return I(e.props,t),t.appendChild(n),{dom:t,contentDOM:n}}},[y.createExtension({key:"toggle-list-item-shortcuts",keyboardShortcuts:{Enter:({editor:e})=>Y(e,"toggleListItem"),"Mod-Shift-6":({editor:e})=>{const t=e.getTextCursorPosition();return e.schema.blockSchema[t.block.type].content!=="inline"?!1:(e.updateBlock(t.block,{type:"toggleListItem",props:{}}),!0)}}})]),bt=()=>({type:"paragraph",propSchema:g,content:"inline"}),Ct=E(bt,{meta:{isolating:!1},parse:e=>{var t;if(e.tagName==="P"&&(t=e.textContent)!=null&&t.trim())return v(e)},render:()=>{const e=document.createElement("p");return{dom:e,contentDOM:e}},toExternalHTML:e=>{const t=document.createElement("p");return I(e.props,t),{dom:t,contentDOM:t}},runsBefore:["default"]},[y.createExtension({key:"paragraph-shortcuts",keyboardShortcuts:{"Mod-Alt-0":({editor:e})=>{const t=e.getTextCursorPosition();return e.schema.blockSchema[t.block.type].content!=="inline"?!1:(e.updateBlock(t.block,{type:"paragraph",props:{}}),!0)}}})]),kt=()=>({type:"quote",propSchema:{backgroundColor:g.backgroundColor,textColor:g.textColor},content:"inline"}),yt=E(kt,{meta:{isolating:!1},parse(e){if(e.tagName==="BLOCKQUOTE"){const{backgroundColor:t,textColor:n}=v(e);return{backgroundColor:t,textColor:n}}},render(){const e=document.createElement("blockquote");return{dom:e,contentDOM:e}},toExternalHTML(e){const t=document.createElement("blockquote");return I(e.props,t),{dom:t,contentDOM:t}}},[y.createExtension({key:"quote-block-shortcuts",keyboardShortcuts:{"Mod-Alt-q":({editor:e})=>{const t=e.getTextCursorPosition();return e.schema.blockSchema[t.block.type].content!=="inline"?!1:(e.updateBlock(t.block,{type:"quote",props:{}}),!0)}},inputRules:[{find:new RegExp("^>\\s$"),replace(){return{type:"quote",props:{}}}}]})]),mn=35,de=120,bn=31,Cn=S.Extension.create({name:"BlockNoteTableExtension",addProseMirrorPlugins:()=>[T.columnResizing({cellMinWidth:mn,defaultCellMinWidth:de,View:null}),T.tableEditing()],addKeyboardShortcuts(){return{Enter:()=>this.editor.state.selection.empty&&this.editor.state.selection.$head.parent.type.name==="tableParagraph"?(this.editor.commands.insertContent({type:"hardBreak"}),!0):!1,Backspace:()=>{const e=this.editor.state.selection,t=e.empty,n=e.$head.parentOffset===0,o=e.$head.node().type.name==="tableParagraph";return t&&n&&o},Tab:()=>this.editor.commands.command(({state:e,dispatch:t,view:n})=>T.goToNextCell(1)(e,t,n)),"Shift-Tab":()=>this.editor.commands.command(({state:e,dispatch:t,view:n})=>T.goToNextCell(-1)(e,t,n))}},extendNodeSchema(e){const t={name:e.name,options:e.options,storage:e.storage};return{tableRole:S.callOrReturn(S.getExtensionField(e,"tableRole",t))}}}),vt={textColor:g.textColor},kn=S.Node.create({name:"tableHeader",addOptions(){return{HTMLAttributes:{}}},content:"tableContent+",addAttributes(){return{colspan:{default:1},rowspan:{default:1},colwidth:{default:null,parseHTML:e=>{const t=e.getAttribute("colwidth");return t?t.split(",").map(o=>parseInt(o,10)):null}}}},tableRole:"header_cell",isolating:!0,parseHTML(){return[{tag:"th",getContent:(e,t)=>St(e,t)}]},renderHTML({HTMLAttributes:e}){return["th",S.mergeAttributes(this.options.HTMLAttributes,e),0]}}),yn=S.Node.create({name:"tableCell",addOptions(){return{HTMLAttributes:{}}},content:"tableContent+",addAttributes(){return{colspan:{default:1},rowspan:{default:1},colwidth:{default:null,parseHTML:e=>{const t=e.getAttribute("colwidth");return t?t.split(",").map(o=>parseInt(o,10)):null}}}},tableRole:"cell",isolating:!0,parseHTML(){return[{tag:"td",getContent:(e,t)=>St(e,t)}]},renderHTML({HTMLAttributes:e}){return["td",S.mergeAttributes(this.options.HTMLAttributes,e),0]}}),vn=S.Node.create({name:"table",content:"tableRow+",group:"blockContent",tableRole:"table",marks:"deletion insertion modification",isolating:!0,parseHTML(){return[{tag:"table"}]},renderHTML({node:e,HTMLAttributes:t}){var r,a,s;const n=Se(this.name,"table",{...((r=this.options.domAttributes)==null?void 0:r.blockContent)||{},...t},((a=this.options.domAttributes)==null?void 0:a.inlineContent)||{}),o=document.createElement("colgroup");for(const i of e.children[0].children)if(i.attrs.colwidth)for(const l of i.attrs.colwidth){const u=document.createElement("col");l&&(u.style=`width: ${l}px`),o.appendChild(u)}else o.appendChild(document.createElement("col"));return(s=n.dom.firstChild)==null||s.appendChild(o),n},addNodeView(){return({node:e,HTMLAttributes:t})=>{var o;class n extends T.TableView{constructor(a,s,i){super(a,s),this.node=a,this.cellMinWidth=s,this.blockContentHTMLAttributes=i;const c=document.createElement("div");c.className=V("bn-block-content",i.class),c.setAttribute("data-content-type","table");for(const[p,h]of Object.entries(i))p!=="class"&&c.setAttribute(p,h);const l=this.dom,u=document.createElement("div");u.className="tableWrapper-inner",u.appendChild(l.firstChild),l.appendChild(u),c.appendChild(l);const d=document.createElement("div");d.className="table-widgets-container",d.style.position="relative",l.appendChild(d),this.dom=c}ignoreMutation(a){return!a.target.closest(".tableWrapper-inner")||super.ignoreMutation(a)}}return new n(e,de,{...((o=this.options.domAttributes)==null?void 0:o.blockContent)||{},...t})}}}),Sn=S.Node.create({name:"tableParagraph",group:"tableContent",content:"inline*",parseHTML(){return[{tag:"p",getAttrs:e=>{if(typeof e=="string"||!e.textContent||!e.closest("[data-content-type]"))return!1;const t=e.parentElement;return t===null?!1:t.tagName==="TD"||t.tagName==="TH"?{}:!1},node:"tableParagraph"}]},renderHTML({HTMLAttributes:e}){return["p",e,0]}}),En=S.Node.create({name:"tableRow",addOptions(){return{HTMLAttributes:{}}},content:"(tableCell | tableHeader)+",tableRole:"row",marks:"deletion insertion modification",parseHTML(){return[{tag:"tr"}]},renderHTML({HTMLAttributes:e}){return["tr",S.mergeAttributes(this.options.HTMLAttributes,e),0]}});function St(e,t){const o=N.DOMParser.fromSchema(t).parse(e,{topNode:t.nodes.blockGroup.create()}),r=[];return o.content.descendants(a=>{if(a.isInline)return r.push(a),!1}),N.Fragment.fromArray(r)}const Et=()=>Le({node:vn,type:"table",content:"table"},vt,[y.createExtension({key:"table-extensions",tiptapExtensions:[Cn,Sn,kn,yn,En]}),y.createExtension({key:"table-keyboard-delete",keyboardShortcuts:{Backspace:({editor:e})=>{if(!(e.prosemirrorState.selection instanceof T.CellSelection))return!1;const t=e.getTextCursorPosition().block,n=t.content;let o=0;for(const a of n.rows)for(const s of a.cells){if("type"in s&&s.content.length>0||!("type"in s)&&s.length>0)return!1;o++}let r=0;return e.prosemirrorState.selection.forEachCell(()=>{r++}),r{(e.getPrevBlock(t)||e.getNextBlock(t))&&e.setTextCursorPosition(t),e.removeBlocks([t])}),!0)}}})]),ke=e=>{const t=e.src||void 0,n=e.width||void 0;return{url:t,previewWidth:n}},xt='',Mt=e=>({type:"video",propSchema:{textAlignment:g.textAlignment,backgroundColor:g.backgroundColor,name:{default:""},url:{default:""},caption:{default:""},showPreview:{default:!0},previewWidth:{default:void 0,type:"number"}},content:"none"}),Lt=e=>t=>{if(t.tagName==="VIDEO"){if(t.closest("figure"))return;const{backgroundColor:n}=v(t);return{...ke(t),backgroundColor:n}}if(t.tagName==="FIGURE"){const n=X(t,"video");if(!n)return;const{targetElement:o,caption:r}=n,{backgroundColor:a}=v(t);return{...ke(o),backgroundColor:a,caption:r}}},wt=E(Mt,e=>({meta:{fileBlockAccept:["video/*"]},parse:Lt(),render(t,n){const o=document.createElement("div");o.innerHTML=e.icon??xt;const r=document.createElement("div");r.className="bn-visual-media-wrapper";const a=document.createElement("video");return a.className="bn-visual-media",n.resolveFileUrl?n.resolveFileUrl(t.props.url).then(s=>{a.src=s}):a.src=t.props.url,a.controls=!0,a.contentEditable="false",a.draggable=!1,a.width=t.props.previewWidth,r.appendChild(a),et(t,n,{dom:r},r,o.firstElementChild)},toExternalHTML(t){if(!t.props.url){const o=document.createElement("p");return o.textContent="Add video",{dom:o}}let n;return t.props.showPreview?(n=document.createElement("video"),n.src=t.props.url,t.props.previewWidth&&(n.width=t.props.previewWidth)):(n=document.createElement("a"),n.href=t.props.url,n.textContent=t.props.name||t.props.url),t.props.caption?t.props.showPreview?se(n,t.props.caption):Q(n,t.props.caption):{dom:n}},runsBefore:["file"]}));function C(e,t,n){if(!(t in e.schema.blockSpecs))return!1;if(!n)return!0;for(const[o,r]of Object.entries(n)){if(!(o in e.schema.blockSpecs[t].config.propSchema))return!1;if(typeof r=="string"){if(e.schema.blockSpecs[t].config.propSchema[o].default!==void 0&&typeof e.schema.blockSpecs[t].config.propSchema[o].default!==r||e.schema.blockSpecs[t].config.propSchema[o].type!==void 0&&e.schema.blockSpecs[t].config.propSchema[o].type!==r)return!1}else{if(e.schema.blockSpecs[t].config.propSchema[o].default!==r.default||e.schema.blockSpecs[t].config.propSchema[o].default===void 0&&r.default===void 0&&e.schema.blockSpecs[t].config.propSchema[o].type!==r.type||typeof e.schema.blockSpecs[t].config.propSchema[o].values!=typeof r.values)return!1;if(typeof e.schema.blockSpecs[t].config.propSchema[o].values=="object"&&typeof r.values=="object"){for(const a of r.values)if(!e.schema.blockSpecs[t].config.propSchema[o].values.includes(a))return!1}}}return!0}function xn(e,t,n,o){return C(t,n,o)&&e.type===n}function Mn(e){return e instanceof T.CellSelection}const G=new Map;function Ln(e){if(G.has(e))return G.get(e);const t=new ye.Mapping;return e._tiptapEditor.on("transaction",({transaction:n})=>{t.appendMapping(n.mapping)}),e._tiptapEditor.on("destroy",()=>{G.delete(e)}),G.set(e,t),t}function Bt(e,t,n="left"){const o=j.ySyncPluginKey.getState(e.prosemirrorState);if(!o){const a=Ln(e),s=a.maps.length;return()=>a.slice(s).map(t,n==="left"?-1:1)}const r=j.absolutePositionToRelativePosition(t+(n==="right"?1:-1),o.binding.type,o.binding.mapping);return()=>{const a=j.ySyncPluginKey.getState(e.prosemirrorState),s=j.relativePositionToAbsolutePosition(a.doc,a.binding.type,r,a.binding.mapping);if(s===null)throw new Error("Position not found, cannot track positions");return s+(n==="right"?-1:1)}}const wn=S.findParentNode(e=>e.type.name==="blockContainer");class Bn{constructor(t,n,o){P(this,"state");P(this,"emitUpdate");P(this,"rootEl");P(this,"pluginState");P(this,"handleScroll",()=>{var t,n;if((t=this.state)!=null&&t.show){const o=(n=this.rootEl)==null?void 0:n.querySelector(`[data-decoration-id="${this.pluginState.decorationId}"]`);if(!o)return;this.state.referencePos=o.getBoundingClientRect().toJSON(),this.emitUpdate(this.pluginState.triggerCharacter)}});P(this,"closeMenu",()=>{this.editor.transact(t=>t.setMeta(H,null))});P(this,"clearQuery",()=>{this.pluginState!==void 0&&this.editor._tiptapEditor.chain().focus().deleteRange({from:this.pluginState.queryStartPos()-(this.pluginState.deleteTriggerCharacter?this.pluginState.triggerCharacter.length:0),to:this.editor.transact(t=>t.selection.from)}).run()});var r;this.editor=t,this.pluginState=void 0,this.emitUpdate=a=>{var s;if(!this.state)throw new Error("Attempting to update uninitialized suggestions menu");n(a,{...this.state,ignoreQueryLength:(s=this.pluginState)==null?void 0:s.ignoreQueryLength})},this.rootEl=o.root,(r=this.rootEl)==null||r.addEventListener("scroll",this.handleScroll,!0)}update(t,n){var l;const o=H.getState(n),r=H.getState(t.state),a=o===void 0&&r!==void 0,s=o!==void 0&&r===void 0;if(!a&&!(o!==void 0&&r!==void 0)&&!s)return;if(this.pluginState=s?o:r,s||!this.editor.isEditable){this.state&&(this.state.show=!1),this.emitUpdate(this.pluginState.triggerCharacter);return}const c=(l=this.rootEl)==null?void 0:l.querySelector(`[data-decoration-id="${this.pluginState.decorationId}"]`);this.editor.isEditable&&c&&(this.state={show:!0,referencePos:c.getBoundingClientRect().toJSON(),query:this.pluginState.query},this.emitUpdate(this.pluginState.triggerCharacter))}destroy(){var t;(t=this.rootEl)==null||t.removeEventListener("scroll",this.handleScroll,!0)}}const H=new z.PluginKey("SuggestionMenuPlugin"),Tt=y.createExtension(({editor:e})=>{const t=[];let n;const o=y.createStore(void 0);return{key:"suggestionMenu",store:o,addTriggerCharacter:r=>{t.push(r)},removeTriggerCharacter:r=>{t.splice(t.indexOf(r),1)},closeMenu:()=>{n==null||n.closeMenu()},clearQuery:()=>{n==null||n.clearQuery()},shown:()=>{var r;return((r=n==null?void 0:n.state)==null?void 0:r.show)||!1},openSuggestionMenu:(r,a)=>{e.headless||(e.focus(),e.transact(s=>{a!=null&&a.deleteTriggerCharacter&&s.insertText(r),s.scrollIntoView().setMeta(H,{triggerCharacter:r,deleteTriggerCharacter:(a==null?void 0:a.deleteTriggerCharacter)||!1,ignoreQueryLength:(a==null?void 0:a.ignoreQueryLength)||!1})}))},prosemirrorPlugins:[new z.Plugin({key:H,view:r=>(n=new Bn(e,(a,s)=>{o.setState({...s,triggerCharacter:a})},r),n),state:{init(){},apply:(r,a,s,i)=>{if(r.selection.$from.parent.type.spec.code)return a;const c=r.getMeta(H);if(typeof c=="object"&&c!==null){a&&(n==null||n.closeMenu());const u=Bt(e,i.selection.from-c.triggerCharacter.length);return{triggerCharacter:c.triggerCharacter,deleteTriggerCharacter:c.deleteTriggerCharacter!==!1,queryStartPos:()=>u()+c.triggerCharacter.length,query:"",decorationId:`id_${Math.floor(Math.random()*4294967295)}`,ignoreQueryLength:c==null?void 0:c.ignoreQueryLength}}if(a===void 0)return a;if(i.selection.from!==i.selection.to||c===null||r.getMeta("focus")||r.getMeta("blur")||r.getMeta("pointer")||a.triggerCharacter!==void 0&&i.selection.from1?c.textBetween(a-l.length,a)+i:i;if(l===u)return r.dispatch(r.state.tr.insertText(i)),r.dispatch(r.state.tr.setMeta(H,{triggerCharacter:u}).scrollIntoView()),!0}}return!1},decorations(r){const a=this.getState(r);if(a===void 0)return null;if(!a.deleteTriggerCharacter){const s=wn(r.selection);if(s)return $.DecorationSet.create(r.doc,[$.Decoration.node(s.pos,s.pos+s.node.nodeSize,{nodeName:"span",class:"bn-suggestion-decorator","data-decoration-id":a.decorationId})])}return $.DecorationSet.create(r.doc,[$.Decoration.inline(a.queryStartPos()-a.triggerCharacter.length,a.queryStartPos(),{nodeName:"span",class:"bn-suggestion-decorator","data-decoration-id":a.decorationId})])}}})]}});function Tn(e){let t=e.getTextCursorPosition().block,n=e.schema.blockSchema[t.type].content;for(;n==="none";){if(t=e.getTextCursorPosition().nextBlock,t===void 0)return;n=e.schema.blockSchema[t.type].content,e.setTextCursorPosition(t,"end")}}function k(e,t){const n=e.getTextCursorPosition().block;if(n.content===void 0)throw new Error("Slash Menu open in a block that doesn't contain content.");let o;return Array.isArray(n.content)&&(n.content.length===1&&m.isStyledTextInlineContent(n.content[0])&&n.content[0].type==="text"&&n.content[0].text==="/"||n.content.length===0)?(o=e.updateBlock(n,t),e.setTextCursorPosition(o)):(o=e.insertBlocks([t],n,"after")[0],e.setTextCursorPosition(e.getTextCursorPosition().nextBlock)),Tn(e),o}function An(e){const t=[];return C(e,"heading",{level:"number"})&&(e.schema.blockSchema.heading.propSchema.level.values||[]).filter(n=>n<=3).forEach(n=>{t.push({onItemClick:()=>{k(e,{type:"heading",props:{level:n}})},badge:B(`Mod-Alt-${n}`),key:n===1?"heading":`heading_${n}`,...e.dictionary.slash_menu[n===1?"heading":`heading_${n}`]})}),C(e,"quote")&&t.push({onItemClick:()=>{k(e,{type:"quote"})},key:"quote",...e.dictionary.slash_menu.quote}),C(e,"toggleListItem")&&t.push({onItemClick:()=>{k(e,{type:"toggleListItem"})},badge:B("Mod-Shift-6"),key:"toggle_list",...e.dictionary.slash_menu.toggle_list}),C(e,"numberedListItem")&&t.push({onItemClick:()=>{k(e,{type:"numberedListItem"})},badge:B("Mod-Shift-7"),key:"numbered_list",...e.dictionary.slash_menu.numbered_list}),C(e,"bulletListItem")&&t.push({onItemClick:()=>{k(e,{type:"bulletListItem"})},badge:B("Mod-Shift-8"),key:"bullet_list",...e.dictionary.slash_menu.bullet_list}),C(e,"checkListItem")&&t.push({onItemClick:()=>{k(e,{type:"checkListItem"})},badge:B("Mod-Shift-9"),key:"check_list",...e.dictionary.slash_menu.check_list}),C(e,"paragraph")&&t.push({onItemClick:()=>{k(e,{type:"paragraph"})},badge:B("Mod-Alt-0"),key:"paragraph",...e.dictionary.slash_menu.paragraph}),C(e,"codeBlock")&&t.push({onItemClick:()=>{k(e,{type:"codeBlock"})},badge:B("Mod-Alt-c"),key:"code_block",...e.dictionary.slash_menu.code_block}),C(e,"divider")&&t.push({onItemClick:()=>{k(e,{type:"divider"})},key:"divider",...e.dictionary.slash_menu.divider}),C(e,"table")&&t.push({onItemClick:()=>{k(e,{type:"table",content:{type:"tableContent",rows:[{cells:["","",""]},{cells:["","",""]}]}})},badge:void 0,key:"table",...e.dictionary.slash_menu.table}),C(e,"image",{url:"string"})&&t.push({onItemClick:()=>{var o;const n=k(e,{type:"image"});(o=e.getExtension(O))==null||o.showMenu(n.id)},key:"image",...e.dictionary.slash_menu.image}),C(e,"video",{url:"string"})&&t.push({onItemClick:()=>{var o;const n=k(e,{type:"video"});(o=e.getExtension(O))==null||o.showMenu(n.id)},key:"video",...e.dictionary.slash_menu.video}),C(e,"audio",{url:"string"})&&t.push({onItemClick:()=>{var o;const n=k(e,{type:"audio"});(o=e.getExtension(O))==null||o.showMenu(n.id)},key:"audio",...e.dictionary.slash_menu.audio}),C(e,"file",{url:"string"})&&t.push({onItemClick:()=>{var o;const n=k(e,{type:"file"});(o=e.getExtension(O))==null||o.showMenu(n.id)},key:"file",...e.dictionary.slash_menu.file}),C(e,"heading",{level:"number",isToggleable:"boolean"})&&(e.schema.blockSchema.heading.propSchema.level.values||[]).filter(n=>n<=3).forEach(n=>{t.push({onItemClick:()=>{k(e,{type:"heading",props:{level:n,isToggleable:!0}})},key:n===1?"toggle_heading":`toggle_heading_${n}`,...e.dictionary.slash_menu[n===1?"toggle_heading":`toggle_heading_${n}`]})}),C(e,"heading",{level:"number"})&&(e.schema.blockSchema.heading.propSchema.level.values||[]).filter(n=>n>3).forEach(n=>{t.push({onItemClick:()=>{k(e,{type:"heading",props:{level:n}})},badge:B(`Mod-Alt-${n}`),key:`heading_${n}`,...e.dictionary.slash_menu[`heading_${n}`]})}),t.push({onItemClick:()=>{var n;(n=e.getExtension(Tt))==null||n.openSuggestionMenu(":",{deleteTriggerCharacter:!0,ignoreQueryLength:!0})},key:"emoji",...e.dictionary.slash_menu.emoji}),t}function Pn(e,t){return e.filter(({title:n,aliases:o})=>n.toLowerCase().includes(t.toLowerCase())||o&&o.filter(r=>r.toLowerCase().includes(t.toLowerCase())).length!==0)}const Nn={audio:qe(),bulletListItem:lt(),checkListItem:ut(),codeBlock:$e(),divider:Ge(),file:Ke(),heading:Je(),image:st(),numberedListItem:gt(),paragraph:Ct(),quote:yt(),table:Et(),toggleListItem:mt(),video:wt()},In=re({type:"textColor",propSchema:"string"},{render:()=>{const e=document.createElement("span");return{dom:e,contentDOM:e}},toExternalHTML:e=>{const t=document.createElement("span");return e!==g.textColor.default&&(t.style.color=e in A?A[e].text:e),{dom:t,contentDOM:t}},parse:e=>{if(e.tagName==="SPAN"&&e.style.color)return e.style.color}}),Hn=re({type:"backgroundColor",propSchema:"string"},{render:()=>{const e=document.createElement("span");return{dom:e,contentDOM:e}},toExternalHTML:e=>{const t=document.createElement("span");return e!==g.backgroundColor.default&&(t.style.backgroundColor=e in A?A[e].background:e),{dom:t,contentDOM:t}},parse:e=>{if(e.tagName==="SPAN"&&e.style.backgroundColor)return e.style.backgroundColor}}),At={bold:_(Wt.default,"boolean"),italic:_(Ut.default,"boolean"),underline:_(jt.default,"boolean"),strike:_($t.default,"boolean"),code:_(qt.default,"boolean"),textColor:In,backgroundColor:Hn},Dn=Ne(At),Pt={text:{config:"text",implementation:{}},link:{config:"link",implementation:{}}},_n=Ae(Pt);exports.COLORS_DARK_MODE_DEFAULT=rn;exports.COLORS_DEFAULT=A;exports.EMPTY_CELL_HEIGHT=bn;exports.EMPTY_CELL_WIDTH=de;exports.FILE_AUDIO_ICON_SVG=Oe;exports.FILE_IMAGE_ICON_SVG=tt;exports.FILE_VIDEO_ICON_SVG=xt;exports.FilePanelExtension=O;exports.SuggestionMenu=Tt;exports.addDefaultPropsExternalHTML=I;exports.addInlineContentAttributes=Qt;exports.addInlineContentKeyboardShortcuts=Yt;exports.addNodeAndExtensionsToSpec=Kt;exports.addStyleAttributes=F;exports.applyNonSelectableBlockFix=we;exports.audioParse=Fe;exports.audioRender=Re;exports.audioToExternalHTML=We;exports.blockHasType=xn;exports.camelToDataKebab=W;exports.captureCellAnchor=_e;exports.createAudioBlockConfig=Ve;exports.createAudioBlockSpec=qe;exports.createBlockConfig=Xt;exports.createBlockSpec=E;exports.createBlockSpecFromTiptapNode=Le;exports.createBulletListItemBlockConfig=ct;exports.createBulletListItemBlockSpec=lt;exports.createCheckListItemBlockSpec=ut;exports.createCheckListItemConfig=dt;exports.createCodeBlockConfig=Ue;exports.createCodeBlockSpec=$e;exports.createDefaultBlockDOMOutputSpec=Se;exports.createDividerBlockConfig=je;exports.createDividerBlockSpec=Ge;exports.createFileBlockConfig=Ze;exports.createFileBlockSpec=Ke;exports.createHeadingBlockConfig=Ye;exports.createHeadingBlockSpec=Je;exports.createImageBlockConfig=nt;exports.createImageBlockSpec=st;exports.createInlineContentSpecFromTipTapNode=Jt;exports.createInternalInlineContentSpec=Te;exports.createInternalStyleSpec=oe;exports.createNumberedListItemBlockConfig=ft;exports.createNumberedListItemBlockSpec=gt;exports.createParagraphBlockConfig=bt;exports.createParagraphBlockSpec=Ct;exports.createQuoteBlockConfig=kt;exports.createQuoteBlockSpec=yt;exports.createStyleSpec=re;exports.createStyleSpecFromTipTapMark=_;exports.createTableBlockSpec=Et;exports.createToggleListItemBlockConfig=ht;exports.createToggleListItemBlockSpec=mt;exports.createToggleWrapper=ce;exports.createVideoBlockConfig=Mt;exports.createVideoBlockSpec=wt;exports.defaultBlockSpecs=Nn;exports.defaultBlockToHTML=te;exports.defaultInlineContentSchema=_n;exports.defaultInlineContentSpecs=Pt;exports.defaultProps=g;exports.defaultStyleSchema=Dn;exports.defaultStyleSpecs=At;exports.defaultToggledState=Xe;exports.editorHasBlockWithType=C;exports.fileParse=ze;exports.filenameFromURL=Zt;exports.filterSuggestionItems=Pn;exports.formatKeyboardShortcut=B;exports.getBackgroundColorAttribute=an;exports.getBlockFromPos=Me;exports.getDefaultSlashMenuItems=An;exports.getInlineContentSchemaFromSpecs=Ae;exports.getLanguageId=ie;exports.getNodeById=He;exports.getParseRules=Be;exports.getStyleParseRules=Ie;exports.getStyleSchemaFromSpecs=Ne;exports.getTextAlignmentAttribute=cn;exports.getTextColorAttribute=sn;exports.imageParse=ot;exports.imageRender=rt;exports.imageToExternalHTML=at;exports.insertOrUpdateBlockForSlashMenu=k;exports.isAppleOS=ve;exports.isNodeBlock=De;exports.isSafari=Gt;exports.isTableCellSelection=Mn;exports.isVideoUrl=zt;exports.mergeCSSClasses=V;exports.mergeParagraphs=Ee;exports.parseAudioElement=ne;exports.parseDefaultProps=v;exports.propsToAttributes=xe;exports.splitBlockCommand=gn;exports.stylePropsToAttributes=Pe;exports.tablePropSchema=vt;exports.trackPosition=Bt;exports.updateBlock=nn;exports.updateBlockCommand=en;exports.updateBlockTr=K;exports.videoParse=Lt;exports.wrapInBlockStructure=Z; + //# sourceMappingURL=defaultBlocks-DosClM5E.cjs.map +diff --git a/src/api/exporters/html/util/serializeBlocksExternalHTML.ts b/src/api/exporters/html/util/serializeBlocksExternalHTML.ts +index fb993f8309d9509df9b2dea85456dd587157d13a..d6dc4a08714d441ccadc297189a4ff8b20bc26ad 100644 +--- a/src/api/exporters/html/util/serializeBlocksExternalHTML.ts ++++ b/src/api/exporters/html/util/serializeBlocksExternalHTML.ts +@@ -116,7 +116,7 @@ export function serializeInlineContentExternalHTML< + node.textContent, + ); + // Reverse the order of marks to maintain the correct priority. +- for (const mark of node.marks.toReversed()) { ++ for (const mark of Array.from(node.marks).reverse()) { + if (mark.type.name in editor.schema.styleSpecs) { + const newDom = ( + editor.schema.styleSpecs[mark.type.name].implementation +diff --git a/src/api/exporters/html/util/serializeBlocksInternalHTML.ts b/src/api/exporters/html/util/serializeBlocksInternalHTML.ts +index 0f890b77abec5ec6271b1280368a0efdd1be2aa5..9f0d95e21b9758bd04fd70d7fc542d31dff58d25 100644 +--- a/src/api/exporters/html/util/serializeBlocksInternalHTML.ts ++++ b/src/api/exporters/html/util/serializeBlocksInternalHTML.ts +@@ -97,7 +97,7 @@ export function serializeInlineContentInternalHTML< + node.textContent, + ); + // Reverse the order of marks to maintain the correct priority. +- for (const mark of node.marks.toReversed()) { ++ for (const mark of Array.from(node.marks).reverse()) { + if (mark.type.name in editor.schema.styleSpecs) { + const newDom = editor.schema.styleSpecs[ + mark.type.name +diff --git a/src/blocks/Code/block.ts b/src/blocks/Code/block.ts +index dbb7fc33a9add7a96488349876bc56ad60111a3f..299586cf484d5ba8a693478d7b3487ee1da4a12f 100644 +--- a/src/blocks/Code/block.ts ++++ b/src/blocks/Code/block.ts +@@ -134,7 +134,18 @@ export const createCodeBlockSpec = createBlockSpec( + const handleLanguageChange = (event: Event) => { + const language = (event.target as HTMLSelectElement).value; + +- editor.updateBlock(block.id, { props: { language } }); ++ let liveBlock; ++ try { ++ liveBlock = editor.getBlock(block.id); ++ } catch { ++ return; ++ } ++ ++ if (!liveBlock) { ++ return; ++ } ++ ++ editor.updateBlock(liveBlock.id, { props: { language } }); + }; + select.addEventListener("change", handleLanguageChange); + removeSelectChangeListener = () => +diff --git a/src/blocks/ListItem/CheckListItem/block.ts b/src/blocks/ListItem/CheckListItem/block.ts +index b32ee408eaf81ccbabb39e5b0f95a2fa73358b0f..da424ac4f051b1de81b919c9d03897d0989b78e2 100644 +--- a/src/blocks/ListItem/CheckListItem/block.ts ++++ b/src/blocks/ListItem/CheckListItem/block.ts +@@ -83,7 +83,18 @@ export const createCheckListItemBlockSpec = createBlockSpec( + checkbox.setAttribute("checked", ""); + } + checkbox.addEventListener("change", () => { +- editor.updateBlock(block, { props: { checked: !block.props.checked } }); ++ let liveBlock; ++ try { ++ liveBlock = editor.getBlock(block.id); ++ } catch { ++ return; ++ } ++ ++ if (!liveBlock) { ++ return; ++ } ++ ++ editor.updateBlock(liveBlock, { props: { checked: !liveBlock.props.checked } }); + }); + // We use a

tag, because for

  • tags we'd need a