Skip to content

feat(editor): interaction trio host + Notion-style UX (v0.3) - #63

Merged
yexiyue merged 7 commits into
developfrom
feat/interaction-trio-phase-a-slash
May 13, 2026
Merged

feat(editor): interaction trio host + Notion-style UX (v0.3)#63
yexiyue merged 7 commits into
developfrom
feat/interaction-trio-phase-a-slash

Conversation

@yexiyue

@yexiyue yexiyue commented May 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Host-side adoption of editor-core v0.3 interaction trio (slash + wikilink + selectionToolbar) plus the Notion-style polish that came with it.

Highlights

  • CharTriggerPopover shared component — SlashCommandPopover (161) + WikilinkPopover (120) collapsed into thin shells (20 + 21) over a generic 211-line base. ~85% duplication eliminated.
  • Slash popover: groups items by section, MRU-driven "Recent" lift, Notion-like list (Heading 1/2/3, lists, quote, divider, today's date, jump-to-note, 6 built-in plugin items including math/table/mermaid/code/image/admonitions)
  • Wikilink popover: `[[query` triggers note picker, commit replaces with `[[<title>]]`, dismiss preserves `[[` literal
  • Selection toolbar: floats above selection on mouseup with bold/italic/strike/code/link buttons
  • Internal link routing: `onEvent.LinkOpen` now resolves wikilink target + `.md` paths to local notes via `resolveInternalLink`, falling back to system browser for external URLs
  • interactionProviders simplification: basic block catalog → module-level const; matchNotes helper dedupes filter logic
  • Settings UI: 3 new toggle rows (Slash / Wikilink / Selection toolbar)
  • `preferencesStore.EDITOR_PLUGIN_IDS` extended with slash / wikilink / selectionToolbar (default enabled)
  • MRU persistence via localStorage (`swarmnote.slash.mru`, capped at 20)

Cross-repo refs

Sibling `@swarmnote/editor-core` change: `swarm-apps/swarmnote-editor#feat/interaction-trio-phase-a-slash`

Test plan

  • `pnpm exec tsc --noEmit` pass
  • `pnpm lint:ci` pass (1 pre-existing sidebar warning unrelated)
  • `pnpm build` pass
  • Manual: slash menu (keyboard + click), wikilink trigger + commit, selection toolbar appearance after mouseup, link click jump / reveal
  • reviewer: `dev-notes/knowledge/editor.md` has "Interaction trigger 三类" section documenting the design

yexiyue added 7 commits May 13, 2026 14:35
Land host side of Phase A — consume sibling editor-core slash runtime.

- preferencesStore: add `slash` to `EDITOR_PLUGIN_IDS` (enabled by default)
- NoteEditor:
  - opt into `slashCommandPlugin()` when `enabledPlugins` contains 'slash'
  - subscribe to `EditorEventType.SlashTriggerChange`, mirror match into
    component state, drop on `match.active: false`
  - wire `host.getSlashItems` through new `interactionProviders.ts`
- SlashCommandPopover.tsx (new): Radix Popover anchored at `match.screenRect`,
  renders grouped items with `activeIndex` highlighted; captures keydown
  on editor's contentDOM and routes ArrowDown/ArrowUp/Enter/Escape to
  `slash.{next,prev,confirm,dismiss}` via `editorControl.execCommand`
- interactionProviders.ts (new): `getSlashItems(query, signal)` returns
  "Jump to: <title>" items from `fileTreeStore` notes (fuzzy match);
  empty query returns first 8 notes
- Settings general.tsx: add Slash 命令 row in 编辑器插件 section

Depends on sibling commit
  swarm-apps/swarmnote-editor@feat/interaction-trio-phase-a-slash · 0a23111

Frontend typecheck + lint:ci + build pass. Manual `pnpm tauri dev`
smoke test deferred to user.
Build on sibling's expanded slash surface (commandId path + per-item
priority + onItemConfirmed) to deliver a Notion-style picker:

- interactionProviders: add a basic block catalog (Heading 1/2/3,
  Bulleted / Numbered / Check list, Quote, Divider, Today's date).
  Most route through commandId references (toggleHeading via run for
  level arg; toggleList / toggleBlockquote / insertHorizontalRule via
  commandId metadata only).

- MRU: persist recently confirmed item ids to localStorage
  (swarmnote.slash.mru, capped at 20). On each getSlashItems call,
  matching items get item.priority bumped to MRU_PRIORITY_BASE + recency
  rank and re-sectioned as "Recent" so the popover groups them at the
  top. Empty store → no behavior change.

- NoteEditor: pass onItemConfirmed → bumpSlashMru into
  slashCommandPlugin({ ... }) so every Enter on the popover updates MRU.

Frontend typecheck + lint:ci + build pass. Depends on sibling commit
c33a10a (execCommandFacet + per-plugin slash items + SlashItem.priority).
Render each SlashItem as a <button> with onMouseDown (not onClick) +
preventDefault. mousedown fires before the editor's blur event, so the
trigger stays active long enough to dispatch; click would arrive after
SlashTriggerChange { active: false } already unmounted the popover.

On pick, call `editorControl.execCommand("slash.confirmAt", index)`
to atomically jump to the clicked index and commit in one round-trip.
Land host side of Phase B — consume sibling editor-core wikilink runtime.

- preferencesStore: add `wikilink` to EDITOR_PLUGIN_IDS (default enabled)
- NoteEditor:
  - opt into `wikilinkPlugin()` when enabled
  - subscribe to `EditorEventType.WikilinkTriggerChange`, mirror match
    into component state, drop on `match.active: false`
  - wire `host.getWikilinkItems` from interactionProviders
  - render `<WikilinkPopover />` alongside existing slash popover
- WikilinkPopover.tsx (new): Radix Popover anchored at `match.screenRect`,
  identical keyboard / click routing as slash but dispatches `wikilink.*`
  commands; static "Link to note" header
- interactionProviders: `getWikilinkItems(query, signal)` returns matching
  note titles from `fileTreeStore` (empty query returns first 8)
- Settings general.tsx: add Wikilink row in 编辑器插件 section

Frontend typecheck + lint:ci + build pass. Depends on sibling commit
879cb2f (wikilink runtime + types stable).
Land host side of Phase C — the trio is complete on desktop.

- preferencesStore: add `selectionToolbar` to EDITOR_PLUGIN_IDS (default enabled)
- NoteEditor:
  - opt into `selectionToolbarPlugin()` when enabled
  - subscribe to `EditorEventType.SelectionToolbarChange`, mirror match
    into component state, drop on `match.active: false`
  - render `<SelectionToolbar />` alongside the slash + wikilink popovers
- SelectionToolbar.tsx (new): Radix Popover with `side="top"` floating
  above the selection. Renders the merged action buttons (bold / italic /
  strike / inline-code / link by default) as <button onMouseDown> so the
  editor selection is preserved through the click. Each button dispatches
  `action.commandId` via `editorControl.execCommand`.
  Uses lucide icons mapped from action.icon strings (extensible registry).
- Settings general.tsx: add Selection 工具栏 row in 编辑器插件 section

Frontend typecheck + lint:ci + build pass. Depends on sibling commit
b9b2955 (selection toolbar runtime + types stable).

This completes the v0.3 interaction trio (slash + wikilink +
selectionToolbar). Plugin SDK surface is fully stable: all three
register* / get* / event types previously marked @unstable in v0.1 are
now load-bearing and locked.
- dev-notes/knowledge/editor.md: new "Interaction trigger 三类" section
  covering CharTrigger family abstraction, Selection family, payload
  DOM-agnostic + requestMeasure pattern, SDK surface (v0.3 stable),
  execCommandFacet, popover-click vs blur gotcha, Notion-style UX
  (built-in plugin slash items, basic block catalog, MRU), and
  third-party plugin example.
- dev-notes/plans/editor-core-host-boundary.md §二: banner pointing at
  the v0.3 interaction-trio OpenSpec change.
Host-side companion to the editor-core link/wikilink polish.

Extract shared CharTriggerPopover:
- SlashCommandPopover (161) + WikilinkPopover (120) had ~85% duplication
  across keyboard routing, Radix popover anchor, section grouping, and
  item rendering
- New CharTriggerPopover.tsx (211) owns the shared rendering, generic
  over CharTriggerItem (SlashItem / WikilinkItem both satisfy)
- SlashCommandPopover (20) + WikilinkPopover (21) are now thin shells
  forwarding `commandPrefix` / `emptyLabel` / `headerLabel`. Future
  hashtag / mention popovers reuse the same component
- Net code reduction ~29 lines, duplication eliminated

Internal link routing:
- onEvent LinkOpen handler now resolves via `resolveInternalLink(url)`
  first → loadDocument internal note; otherwise openUrl() external
- Previously `openUrl` was called for every url, so wikilink targets
  and `.md` relative paths were never routed to in-app navigation

interactionProviders simplification:
- basicBlockItems() function call → BASIC_BLOCK_ITEMS module-level const
  (no per-call rebuild; closures look up store lazily)
- Heading 1/2/3 three near-duplicate object literals → HEADING_LEVELS
  array + map
- getSlashItems / getWikilinkItems duplicate "filter notes by query"
  → matchNotes(query) helper
- Cleaned up doc blocks (resolveInternalLink doc was sandwiched between
  getSlashItems and getWikilinkItems)
@yexiyue
yexiyue merged commit 786ba44 into develop May 13, 2026
3 of 4 checks passed
@yexiyue
yexiyue deleted the feat/interaction-trio-phase-a-slash branch May 13, 2026 08:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant