Capability clusters
C2 · Authoring & knowledge UX
This cluster is the entire interactive surface of eli: a three-tier markdown editor (BlockNote block editor by default, raw textarea fallback, server-side env-flag switch), the vault navigation chrome (collapsible sidebar with buckets/types/folders/tags/favorites, breadcrumb header, cmdk command palette), the note metadata inspector (frontmatter properties, backlinks, version history, pin/star/archive/trash actions), a kanban projects board, daily notes, and the cross-cutting UX systems: a rebindable keybinding registry, FOUC-free theming, mobile adaptations, and keyboard accessibility. Every persistence path funnels through Next.js Server Actions in app/_actions/ against the storage abstraction (getStorage()), so the same components serve both Local (SQLite index + .md vault files) and SaaS (Supabase) modes — the SaaS note page calls this the ADR-0004 proof point (app/(saas)/(app)/w/[workspace]/n/[...path]/page.tsx:25-30).
Modes: Local + SaaS (shared editor/inspector/board; sidebar, palette, daily note, keybinding overrides are local-only) · Key dirs: components/editor/, components/sidebar/, components/note-list/, components/inspector/, components/projects/, components/ui/, lib/keybindings/, app/_actions/, app/(local)/v/, app/(saas)/(app)/w/ · Depends on: C1 storage, C4 AI routes, C7 realtime · Feeds: C3 embeds, C6 versions/diff, C5 agent setup, C9 cron
Capabilities
| Capability | What it does | Entry points | Modes |
|---|---|---|---|
| BlockNote block editor (default) | Notion-style editor on @blocknote/react + @blocknote/shadcn; hydrates markdown via tryParseMarkdownToBlocks (single-paragraph fallback on parse failure, components/editor/blocknote-editor.tsx:91-102), serialises with blocksToMarkdownLossy, 1500 ms debounced autosave, manual Save + Cmd/Ctrl+S, Revert re-parses last saved body; mounted ssr:false with skeleton (blocknote-editor-dynamic.tsx:28-43) | /v/n/[...path], /w/[workspace]/n/[...path] | both |
| Custom slash menu with AI group | slashMenu={false} replaced by a SuggestionMenuController on / merging BlockNote defaults with 3 AI items — Continue writing (last 8000 chars to /api/ai/draft tier:fast), Generate from prompt, Summarise (blocknote-editor.tsx:371-404); tolerant JSON-lines/SSE text-delta stream reader (:433-462) | / inside BlockNote | both |
| Wikilink autocomplete | Second SuggestionMenuController on [[ queries searchNotesAction (top 8 FTS hits) and inserts literal [[Title]] text — link resolution happens at save/index time, not in the editor (blocknote-editor.tsx:405-417) | [[ inside BlockNote | both |
| Raw textarea editor (legacy/mobile) | Monospace markdown textarea with the same save/OCC/AI-toolbar contract (components/editor/note-editor.tsx); selected when ELI_LEGACY_EDITOR=1 (everywhere) or ELI_MOBILE_EDITOR=raw (≤767 px via matchMedia, mobile-editor-switch.tsx:29); decision tree is server-only (note-editor-root.tsx:33-35) | env flags | both |
| Debounced autosave + optimistic concurrency | Both editors POST FormData {id, bodyMd, message?, expectedVersionId?, workspaceId?} to saveNoteAction after 1500 ms idle (SAVE_DEBOUNCE_MS, note-editor.tsx:34, blocknote-editor.tsx:52); stale expectedVersionId parks the loser’s body as a version instead of rejecting; save revalidates paths and fire-and-forgets scheduleEmbed (app/_actions/notes.ts:76-105) | saveNoteAction | both |
| Conflict banner + snapshot review | snapshotVersionId renders an amber role=status aria-live=polite banner; “Review snapshot” sets window.location.hash to #version-<id>, PropertiesPanel’s hashchange listener highlights the History row (conflict-banner.tsx:41-79, properties-panel.tsx:72-81,261-277) | editor after conflicting save | both |
| AI transform toolbar menu | Rewrite (tone hint), Shorten, Expand, Fix grammar, Translate (default ‘French’) on selection or whole body via /api/ai/transform (ai-transform-menu.tsx:64-104, ≥3 chars); Summarise toolbar button → /api/ai/summarize toast (note-editor.tsx:157-180) | editor toolbar | both |
| Note inspector (properties panel) | Right aside (hidden below lg): pin/star/archive/download/delete actions; Type/Status/Tags blur-commit via updateFrontmatterAction; System card for _-prefixed frontmatter; Info stats; Backlinks with context; History (top 8 of 12 versions, Compare links to diff route) (properties-panel.tsx) | note pages | both |
| Inbox re-classification toast | Mutations compute bucket before/after; leftInbox triggers one-shot “Moved out of Inbox” toast (notes.ts:148-154, properties-panel.tsx:98-100) | inspector edits | both |
| Two-pane vault shell, URL-driven filters | Server component parses ?bucket/type/tag/folder/favorites/q into a NoteFilter (default = all except archived), storage.listNotes limit 200, renders NoteList beside content in section#main-content (two-pane-shell.tsx:35-66) | /v + filter URLs | local |
| Note list | Title, 200-char first-non-heading-line preview, type badge, ≤3 tag badges, relative time, star, selected highlight (note-list.tsx:49-84); search input debounces 250 ms then router.replace rewrites ?q= for server-side FTS (note-list-search.tsx:26-36) | middle pane of /v* | local |
| App sidebar | shadcn Sidebar variant=inset collapsible=icon: buckets Inbox/All/Archive with counts, Favorites, Views (Today, Starred), Types, Folders, Tags — each a filter URL; data loaded per-request force-dynamic (app-sidebar.tsx:80, app/(local)/v/layout.tsx:18-24) | /v* layout | local |
| Vault header | Sticky breadcrumbs parsed from /v/n/<path> (intermediate segments → ?folder= links, vault-header.tsx:30-50), palette trigger, Re-scan (refreshVaultAction → cache reset + scanVault, app/_actions/vault.ts:121-133), theme toggle | /v* header | local |
| Command palette (cmdk) | Cmd/Ctrl+K; live searchNotesAction per keystroke (empty query = 20 recent, notes.ts:16-29); groups for notes, new note, navigation, vault ops (re-scan, external-tools setup, export zip, trash), theme; navigation is deliberately hard (window.location.assign, command-palette.tsx:131-140) | Cmd+K, header button | local |
| New note dialog (quick capture) | Title + optional path; title-only lands at inbox/<slug>.md via NFKD slugifier (new-note-dialog.tsx:42,67-76); createNoteAction then hard navigation to /v/n/<path> | Cmd+Shift+N, sidebar/list +, palette | both (navigates to /v/n/*) |
| Daily note (Today) | /v/today server-redirects to storage.todayNote() which returns-or-creates daily/YYYY-MM-DD.md with {type: daily, created} (app/(local)/v/today/page.tsx:4-8, lib/storage/local/writes.ts:482-494) | /v/today | local |
| Projects kanban board | type:project notes (limit 500) grouped by status into backlog/todo/in-progress/done + free-form columns, synonyms normalised (project-board.tsx:50-58); native HTML5 DnD, optimistic move with revert-on-error; setProjectStatusAction validates type and merges frontmatter (app/_actions/projects.ts:29-62) | /v/projects, /w/[ws]/projects | both |
| Keybinding registry + provider | 5 actions with defaults (lib/keybindings/registry.ts:39-49); one window keydown listener; per-action handler stacks, last-registered wins, return false chains down (keybinding-provider.tsx:84-100); single-combo only, no chords | global keydown, useKeybinding | both |
| Keybinding overrides + recorder | Local root layout merges ~/.eli/config.json#keybindings over defaults (app/layout.tsx:59-67); /keybindings page with capture-phase recorder (bare Escape cancels, keybinding-recorder.tsx:63-91); persisted via server actions + revalidatePath('/','layout'); no uniqueness validation | /keybindings | local |
| Shortcuts help overlay | Shift+? global dialog listing every action with override-aware combo, mac vs Ctrl detected post-mount (keybindings-help-dialog.tsx:44-46) | ? anywhere | both |
| Theming | Inline pre-hydration script reads localStorage eli-theme and stamps class + colorScheme on <html> before paint (theme-script.tsx:18-32); provider follows OS matchMedia for ‘system’ and syncs tabs via the storage event (theme-provider.tsx:70-78) | header toggle, palette | both |
| Resizable editor/inspector split | ≥1024 px: react-resizable-panels with layout persisted to localStorage id eli-editor-inspector (editor default 75%/min 40%, inspector min 15%, resizable-shell.tsx:57-58,117-133); SSR/first paint renders editor-only until mounted since both branches touch window (:79-80); below lg, inspector becomes a bottom Drawer behind a floating button (:156-178) | note pages | both |
| Mobile adaptations (P6-MOBILE) | SaaS hamburger Sheet <640 px with 44 px rows (components/saas/mobile-nav.tsx:35-72); toolbar buttons get [&>button]:max-sm:min-h-11; ELI_MOBILE_EDITOR=raw swaps editors ≤767 px; shadcn sidebar becomes offcanvas Sheet <768 px (hooks/use-mobile.ts:3); PWA metadata + PwaRegister (registers /sw.js only when NODE_ENV==='production', pwa-register.tsx:15; app/layout.tsx:26-51,94) | viewport <640/768/1024 px | both |
| Accessibility skip-link | First-tab “Skip to content” anchor, sr-only until focused, targets #main-content (app/layout.tsx:82-87, two-pane-shell.tsx:87, SaaS layout main); aria-labels on icon buttons, aria-live banner | Tab from load | both |
| Live vault refresh (SSE) | VaultEventsBridge subscribes to /api/events (EventSource) and debounces router.refresh() 500 ms so external FS edits/agent writes/clipper drops appear live (vault-events-bridge.tsx:26-48); disable via ELI_DISABLE_LIVE_EVENTS=1 or window.__ELI_DISABLE_EVENTS__ | (local)/v/layout.tsx | local |
| SaaS realtime co-edit toast | RealtimeNoteWatcher on Supabase postgres_changes UPDATE filtered by note id; remote version change → non-destructive “changed elsewhere” toast with Refresh; own saves suppressed via markVersionAsLocal globalThis-pinned Set (max 32) (realtime/note-watcher.tsx:49-56,92-108) | SaaS note page | saas |
| External AI tools setup dialog | Multi-select for Claude Code/Cursor/Codex CLI/Gemini CLI/OpenClaw; setupExternalToolsAction merges an eli MCP stdio entry pointing at bin/eli-mcp.mjs into each agent’s config; idempotent, per-agent result rows (components/setup/external-tools-dialog.tsx:43-51,89-99) | palette command | local |
| Vault picker / sample vault | / redirects to /v when a vault is active; otherwise dialog registers a ~-expanded path in ~/.eli/config.json + immediate scan with counts (vault-picker.tsx:62-108, app/_actions/vault.ts:30-67); sample vault seeds ~/eli-sample with 3 wikilinked notes, written with the wx flag so existing files are never clobbered (vault.ts:69-108) | / (local) | local |
| Note lifecycle server actions | createNoteAction, saveNoteAction (OCC + embed), archive/unarchive, deleteNoteAction (soft-trash default, {hard:true} permanent, cancels pending embed, notes.ts:225-245), restoreNoteAction (SaaS clears trashed_at under RLS), emptyTrashAction({olderThanDays?}) shared with cron, pin/favorite toggles, updateFrontmatterAction, searchNotesAction | app/_actions/notes.ts | both |
| shadcn/ui primitive inventory | 55 vendored primitives in components/ui/ incl. command (cmdk), sidebar (cookie-persisted, built-in Mod+B toggle), resizable (react-resizable-panels), drawer/sheet, sonner, kbd | components/ui/* | both |
System view — simplified
C2 is the app users actually touch: a markdown editor, a filterable note list with sidebar navigation, an inspector for metadata, and a kanban board — all client chrome around a thin server-action layer. Standalone (with AI, SaaS, and agents turned off) it is a complete offline Obsidian-style workspace: browse a vault by bucket/type/folder/tag, edit notes with versioned autosave and conflict snapshots, capture quickly with Cmd+Shift+N, keep a daily note, and drag projects across a board. Its only hard dependency is the C1 storage interface; AI menu items degrade to error toasts if /api/ai/* is unreachable. Every filter, search, and list state lives in the URL, so the server re-renders the tree — there is no client-side state store for navigation.
System view — detailed
| Component | Role | Files |
|---|---|---|
| Editor stack | Server-side variant switch, BlockNote with slash-menu AI + wikilink autocomplete, raw textarea fallback, SSR-safe mount, mobile switch, conflict banner, AI transform menu | components/editor/note-editor-root.tsx, blocknote-editor.tsx, blocknote-editor-dynamic.tsx, note-editor.tsx, mobile-editor-switch.tsx, conflict-banner.tsx, ai-transform-menu.tsx |
| Note actions layer | Server actions for all note mutations + search: save (OCC, embed scheduling), create, archive, trash/restore/empty, pin, favorite, frontmatter patch, inbox-exit detection, project status | app/_actions/notes.ts, favorites.ts, projects.ts, vault.ts, keybindings.ts |
| Inspector | Pin/star/archive/export/delete actions, blur-commit Type/Status/Tags, system-field display, info stats, backlinks, version history with diff links + conflict-snapshot highlight | components/inspector/properties-panel.tsx, backlinks-panel.tsx |
| Navigation shell (local) | Sidebar filter tree, breadcrumb header with re-scan, two-pane filtered list layout, live SSE refresh bridge | components/sidebar/app-sidebar.tsx, vault-header.tsx, new-note-sidebar-button.tsx, components/two-pane-shell.tsx, components/note-list/*, components/vault-events-bridge.tsx, hooks/use-vault-events.ts, app/(local)/v/layout.tsx, app/(local)/layout.tsx |
| Command palette | cmdk dialog: live FTS search, quick actions, navigation, theme switching | components/command-palette.tsx, new-note-dialog.tsx, components/ui/command.tsx |
| Keybinding system | Action registry with defaults, combo matcher, config-file overrides merge, global provider with handler stacks, help overlay, rebind recorder + settings page | lib/keybindings/registry.ts, overrides.ts, components/keybinding-provider.tsx, keybindings-help-dialog.tsx, keybindings-table.tsx, keybinding-recorder.tsx, app/(local)/keybindings/page.tsx |
| Theming | FOUC-free boot script, light/dark/system provider with OS-follow and cross-tab sync, toggle dropdown; design-token stylesheet (class-based dark variant, oklch palette, radius scale) + cn() | components/theme-script.tsx, theme-provider.tsx, theme-toggle.tsx, lib/theme/constants.ts, app/globals.css, lib/utils.ts |
| Note pages | Route-level composition of editor + inspector + comments; local adds TwoPaneShell/favorites/today, SaaS adds workspace resolution, realtime watcher, share dialog | app/(local)/v/n/[...path]/page.tsx, app/(local)/v/page.tsx, app/(local)/v/today/page.tsx, app/(saas)/(app)/w/[workspace]/n/[...path]/page.tsx, components/resizable-shell.tsx, components/realtime/note-watcher.tsx |
| Projects board | Kanban of type:project notes grouped by status, native DnD with optimistic moves, shared across modes | components/projects/project-board.tsx, app/(local)/v/projects/page.tsx, app/(saas)/(app)/w/[workspace]/projects/page.tsx |
| Setup & onboarding | Vault picker + sample vault seeding; external AI agent MCP-config writer dialog | components/vault-picker.tsx, components/setup/external-tools-dialog.tsx, app/(local)/page.tsx |
| Mobile & a11y chrome | SaaS hamburger sheet nav, mobile breakpoint hook, skip-link + PWA registration in root layout | components/saas/mobile-nav.tsx, hooks/use-mobile.ts, app/layout.tsx, app/(saas)/(app)/layout.tsx |
| UI primitives | 55 vendored shadcn-style components (cmdk command, cookie-persisted sidebar with Mod+B, resizable panels, drawer/sheet, sonner, kbd) | components/ui/ |
Data flow — simplified
The autosave loop is the load-bearing runtime path of the whole platform: markdown comes out of storage, becomes BlockNote blocks, and every keystroke re-serialises it back to markdown, restarting a 1500 ms debounce timer. When the timer fires, saveNoteAction writes through the storage abstraction with an optimistic-concurrency token; a raced write is never rejected — the previous body is parked as a conflict-snapshot version and the editor shows a review banner. The save also revalidates the RSC tree and schedules a semantic re-embed without blocking the user.
Data flow — detailed
Open-and-edit-note autosave loop, including the markdown round-trip, both debounce values, the OCC divergence, and the local-vs-SaaS post-save divergence:
Quick capture → hard navigation (why the app deliberately abandons soft routing here):
Works separately / works together
Standalone
With every other cluster disabled except C1 storage, C2 is a complete offline markdown workspace: vault picker and sample-vault onboarding, sidebar navigation over buckets/types/folders/tags, URL-driven filtering and 250 ms-debounced search, the full BlockNote editing loop with versioned autosave and conflict snapshots, the inspector, quick capture, daily notes, the projects kanban, rebindable keybindings, and FOUC-free theming. AI features degrade gracefully (fetch failures surface as toasts); SaaS-only chrome (realtime watcher, share dialog, hamburger nav) simply never mounts.
Composed
- → C1 storage: every UI mutation goes through
getStorage()(lib/storage) soLocalAdapterandSupabaseAdapterserve identical components — ADR-0004, called out atapp/(saas)/(app)/w/[workspace]/n/[...path]/page.tsx:25-30. The/api/eventsSSE feed (FS watcher vialib/watch/events-bus) drivesVaultEventsBridgerefreshes. - → C3 semantic search:
saveNoteActionfire-and-forgetsscheduleEmbedfromlib/ai/auto-embed-queue(app/_actions/notes.ts:96-105);deleteNoteActioncancels pending embeds viacancelEmbed(notes.ts:225-245). - → C4 AI platform: slash-menu items POST
/api/ai/draftand/api/ai/summarize; the transform menu POSTs/api/ai/transform(blocknote-editor.tsx:371-404,ai-transform-menu.tsx:64-104). - → C6 versioning: the History card links Compare to
/v/diff/<path>?a&b(local) or/w/<slug>/diff(SaaS, parameteriseddiffRouteBase,properties-panel.tsx:56); conflict snapshots created by the storage adapter surface in this card via the hash deep-link. - ↔ C7 SaaS collaboration: SaaS note page adds
RealtimeNoteWatcher(Supabasepostgres_changes) andShareLinkDialog(mounted when the editor toolbar receivesworkspaceId,note-editor.tsx:27-31); both note pages mountCommentsThreadwith section anchors fromlib/storage/helpers/section-diff. - → C5 agent surface:
SetupExternalToolsDialogwrites MCP configs (~/.claude/mcp.json,~/.cursor/mcp.json,~/.codex/config.toml,~/.gemini/settings.json,~/.openclaw/openclaw.json) pointing atbin/eli-mcp.mjs;storage.todayNote()is shared with the MCP daily tool (lib/mcp/tools/daily.ts:54). - ← C8 capture/templates: clipper drops and template-created notes appear live via the SSE bridge without a manual re-scan.
- ↔ C9 platform ops:
/api/cron/trash-prunereusesemptyTrashAction({olderThanDays})(notes.ts:254-287); project status changes participate in git auto-commit via the standardupdateNotepath (app/_actions/projects.ts:12-16); e2e disables SSE viaELI_DISABLE_LIVE_EVENTS=1/window.__ELI_DISABLE_EVENTS__. - → Export: inspector download hits
/api/export/note/[id]; the palette’s “Export vault as zip” hits/api/export/vault. - → Config: keybinding overrides and the vault registry both live in
~/.eli/config.jsonvialib/config; the root layout re-reads it on every render in local mode (app/layout.tsx:59-67).
Load-bearing details
- Editor selection is server-side and env-gated:
ELI_LEGACY_EDITOR=1forces the textarea everywhere,ELI_MOBILE_EDITOR=rawgives BlockNote-on-desktop/textarea-on-mobile, default is BlockNote everywhere (components/editor/note-editor-root.tsx:33-35;env.ts:56,72). Env vars never reach the client bundle. - The markdown round-trip is explicitly lossy by design:
tryParseMarkdownToBlockson load,blocksToMarkdownLossyon save; adversarial parse failures fall back to one paragraph block containing the raw markdown so no body is ever lost (blocknote-editor.tsx:91-102,135). - Saves are last-writer-wins-with-snapshot, not reject-on-conflict: a stale
expectedVersionIdstill commits, and the previous head’s body is parked as an auto-conflict-snapshot version whose id rides back inSaveState.snapshotVersionId(app/_actions/notes.ts:35-47,76-89;conflict-banner.tsx:7-31). - Both debounce constants matter: autosave is 1500 ms (
SAVE_DEBOUNCE_MS,note-editor.tsx:34,blocknote-editor.tsx:52), list search is 250 ms (note-list-search.tsx:9), SSE-driven refresh is 500 ms (vault-events-bridge.tsx:7). - Wikilink autocomplete inserts plain text
[[Title]]— resolution and backlink extraction happen at save/index time in the storage layer, never in the editor (blocknote-editor.tsx:405-417). - Editor identity key is
`${note.id}:${note.versionId}`so any external version change remounts the editor with fresh content (app/(local)/v/n/[...path]/page.tsx:102; SaaS page:152). - Palette and NewNoteDialog use
window.location.assigninstead ofrouter.pushbecause soft navigation races the layout revalidation triggered byrevalidatePath('/v','layout')(command-palette.tsx:131-140;new-note-dialog.tsx:55-60). - Keybinding handler stacks give modal behaviour for free: last-registered handler wins per action, unmount pops the stack, returning
falsechains downward (components/keybinding-provider.tsx:63-100). The rebind recorder wins over the global provider by listening in the capture phase (keybinding-recorder.tsx:89).mergeKeybindingOverridessilently drops overrides for unknown action ids, so stale~/.eli/config.jsonentries can never break the table (lib/keybindings/overrides.ts:19-34). - Frontmatter is partitioned by underscore prefix:
_-prefixed keys are system fields (read-only System card, preserved on property edits, used for_pinnedand_child_count); everything else is user-editable and fully replaced byupdateFrontmatterAction(lib/storage/helpers/frontmatter.ts:92-102;notes.ts:354-360). - Trash is soft-delete by default;
emptyTrashAction’solderThanDaysusesupdatedAtas the trashed-at proxy and powers both the manual button and the daily cron (notes.ts:225-287). The inspector’s delete confirm reveals the local trash location.eli/trash/(properties-panel.tsx:146-155). - Mobile tap-target compliance is a single Tailwind selector:
[&>button]:max-sm:min-h-11gives every toolbar button a 44 px minimum below 640 px (note-editor.tsx:216;blocknote-editor.tsx:318); MobileNav rows useh-11(mobile-nav.tsx:64). - SaaS self-save suppression is
globalThis.__ELI_LOCAL_VERSIONS__— a Set of version ids capped at 32 with oldest-first eviction, stamped bymarkVersionAsLocalafter each save; the realtime watcher never auto-replaces the buffer, only offers a Refresh toast (realtime/note-watcher.tsx:34-56,92-108). - Theme boots without FOUC via an inline
<head>script that stamps the class before first paint from localStorage keyeli-theme(lib/theme/constants.ts:12;theme-script.tsx:18-32); cross-tab sync rides thestorageevent (theme-provider.tsx:70-78). Browser chrome follows via the viewportthemeColorpair#fafafa/#0a0a0a(app/layout.tsx:45-47). - Design tokens live in
app/globals.css(Tailwind v4): dark mode is class-based,@custom-variant dark (&:is(.dark *))— noprefers-color-schememedia query in CSS, so the ThemeScript/provider.darkclass is the single switch (globals.css:5).@theme inlinemaps shadcn/sidebar/chart oklch variables to utilities; the palette is zero-chroma grayscale except--destructiveand dark-mode--sidebar-primary(blueoklch(0.488 0.243 264.376)), and the entire radius scale derives from one--radius: 0.625remtoken (sm 0.6x → 4xl 2.6x).cn()inlib/utils.tsis exactlytwMerge(clsx(...)). - Two cross-cutting guards sit in
@layer base(globals.css:119-160):body{overflow-x:hidden}is the P6-MOBILE no-horizontal-scroll guard (wide content must opt into its ownoverflow-x-auto), and a P6-A11Y focus-visible safety net draws a 2pxvar(--ring)outline on raw interactive elements — the:not([data-slot])predicate excludes shadcn primitives that already ship their own focus ring, avoiding double indicators.
Gaps and partially wired
toggle-sidebar(Mod+\) is registered but dead: it appears in the registry, help overlay, and rebind table, but no component ever mounts a handler for it — the working sidebar shortcut is the shadcn built-in Mod+B ad-hoc listener (lib/keybindings/registry.ts:43;components/ui/sidebar.tsx:33,97-110). Verified:toggle-sidebaroccurs nowhere outside registry/help/table.- No chord support:
matchActionis single-combo only; the palette’sg tnext to “Go to today” is a display-onlyCommandShortcutlabel (command-palette.tsx:187;lib/keybindings/registry.ts:70-84). - BlockNote ignores dark mode:
BlockNoteViewis pinned totheme="light"regardless of app theme (blocknote-editor.tsx:364); the textarea editor inherits app theming correctly. - SaaS backlink links are wrong-mode: PropertiesPanel backlink hrefs are hardcoded to the local
/v/n/prefix even when rendered inside/w/<slug>/n/..., so SaaS backlink clicks navigate to a local-mode route (properties-panel.tsx:241); contrastdiffRouteBasewhich IS parameterised (:56). - SaaS favorites stubbed: the SaaS note page hardcodes favorite state to
falsepending auser_favoritestable (app/(saas)/(app)/w/[workspace]/n/[...path]/page.tsx:88-89,135). - Keybinding overrides are local-only: the SaaS root layout deliberately skips the
~/.eli/config.jsonread (“per-user keybindings in saas would be a future profiles-row column”,app/layout.tsx:54-67);/keybindingsreturnsnotFoundin SaaS (app/(local)/keybindings/page.tsx:21). - No rebind uniqueness validation: assigning the same combo to two actions is silently resolved by handler precedence, not flagged as an error.
- No SaaS daily note:
/v/todayis local-only; no/w/<slug>/todayroute exists. - Daily-note day boundary is UTC:
todayNote()computes the date asnew Date().toISOString().slice(0,10)(lib/storage/local/writes.ts:483), so “Today” rolls over at UTC midnight — an evening user in the Americas gets tomorrow’s note. - No SaaS command palette: the palette mounts in the local vault header; SaaS reaches notes via top-bar links instead.
- Trash age proxy:
emptyTrashAction({olderThanDays})measures againstupdatedAtrather than a real trashed-at timestamp (notes.ts:254-287), so a note edited just before trashing ages faster than expected. - Undebounced hook exposed: a public
useVaultEventshook exists alongside the debounced bridge (hooks/use-vault-events.ts:17-40) but nothing else consumes it yet.
See also