Forráskód Böngészése

feat(docs): restructure dev app to component-based pages with Nuxt UI-style API tables

- Consolidate 13 syntax-specific pages into 6 component pages
  (Editor, EditorBlock, Toolbar, SuggestionMenu, Themes, Overview)
- Add API documentation tables (Props, Events, Exposed Methods, Slots)
  using Nuxt UI table classes (border-separate, bg-muted thead, code tags)
- Update editor wrapper styling with p-4 sm:p-8 dark:bg-neutral-950/50
- Add inline content styles to table cells (code, p, ul, ol, li resets)
- Fix pattern plugin: skip delimiter triggers ([[, (() when query
  starts with a space — prevents menu opening on accidental `[[ ` typing
- Auto-close mention menu when no items match the query
- Remove overflow-hidden from suggestion menu cards to prevent clipping
- Fix editor-block example to not auto-focus (shows rendered table)
- Remove old pages: basic-editing, blockquotes, code-blocks, editor-shell,
  inline-marks, links, math, properties, refs-tags, tasks
Zander Hawke 3 napja
szülő
commit
c83c86c04a

+ 28 - 14
apps/dev/src/App.vue

@@ -4,22 +4,36 @@ import { motion } from "motion-v"
 
 const navigationItems = [
   { label: "Overview", icon: "i-lucide-book-open", to: "/" },
-  { label: "Editor Shell", icon: "i-lucide-layers", to: "/editor-shell" },
-  { label: "Basic Editing", icon: "i-lucide-type", to: "/basic-editing" },
-  { label: "Inline Marks", icon: "i-lucide-bold", to: "/inline-marks" },
-  { label: "Tasks & Priorities", icon: "i-lucide-check-square", to: "/tasks" },
   {
-    label: "Blockquotes & Callouts",
-    icon: "i-lucide-quote",
-    to: "/blockquotes",
+    label: "Editor",
+    icon: "i-lucide-layers",
+    to: "/editor",
+    description: "Multi-block shell with toolbar, drag, undo/redo",
+  },
+  {
+    label: "EditorBlock",
+    icon: "i-lucide-file-text",
+    to: "/editor-block",
+    description: "Single block with all markdown syntax",
+  },
+  {
+    label: "Toolbar",
+    icon: "i-lucide-rows-3",
+    to: "/toolbar",
+    description: "Formatting toolbar integration",
+  },
+  {
+    label: "Suggestion Menu",
+    icon: "i-lucide-list-ordered",
+    to: "/suggestion-menu",
+    description: "Slash commands, references, tags",
+  },
+  {
+    label: "Themes",
+    icon: "i-lucide-palette",
+    to: "/themes",
+    description: "CSS-variable theme system",
   },
-  { label: "Links", icon: "i-lucide-link", to: "/links" },
-  { label: "Page Refs & Tags", icon: "i-lucide-hash", to: "/refs-tags" },
-  { label: "Code Blocks", icon: "i-lucide-code", to: "/code-blocks" },
-  { label: "Properties & Dates", icon: "i-lucide-list", to: "/properties" },
-  { label: "Math", icon: "i-lucide-sigma", to: "/math" },
-  { label: "Toolbar", icon: "i-lucide-rows-3", to: "/toolbar" },
-  { label: "Themes", icon: "i-lucide-palette", to: "/themes" },
 ]
 
 const variants: {

+ 7 - 21
apps/dev/src/main.ts

@@ -2,35 +2,21 @@ import ui from "@nuxt/ui/vue-plugin"
 import { createApp } from "vue"
 import { createRouter, createWebHistory } from "vue-router"
 import App from "~/App.vue"
-import BasicEditing from "~/pages/basic-editing.vue"
-import Blockquotes from "~/pages/blockquotes.vue"
-import CodeBlocks from "~/pages/code-blocks.vue"
-import EditorShell from "~/pages/editor-shell.vue"
+import EditorPage from "~/pages/editor.vue"
+import EditorBlockPage from "~/pages/editor-block.vue"
 import IndexPage from "~/pages/index.vue"
-import InlineMarks from "~/pages/inline-marks.vue"
-import Links from "~/pages/links.vue"
-import MathPage from "~/pages/math.vue"
-import Properties from "~/pages/properties.vue"
-import RefsTags from "~/pages/refs-tags.vue"
-import Tasks from "~/pages/tasks.vue"
+import SuggestionMenuPage from "~/pages/suggestion-menu.vue"
 import ThemesPage from "~/pages/themes.vue"
 import ToolbarPage from "~/pages/toolbar.vue"
 import "~/style.css"
 
 const routes = [
   { path: "/", component: IndexPage },
-  { path: "/editor-shell", component: EditorShell },
-  { path: "/basic-editing", component: BasicEditing },
-  { path: "/inline-marks", component: InlineMarks },
-  { path: "/tasks", component: Tasks },
-  { path: "/blockquotes", component: Blockquotes },
-  { path: "/links", component: Links },
-  { path: "/refs-tags", component: RefsTags },
-  { path: "/code-blocks", component: CodeBlocks },
-  { path: "/properties", component: Properties },
-  { path: "/math", component: MathPage },
-  { path: "/themes", component: ThemesPage },
+  { path: "/editor", component: EditorPage },
+  { path: "/editor-block", component: EditorBlockPage },
   { path: "/toolbar", component: ToolbarPage },
+  { path: "/suggestion-menu", component: SuggestionMenuPage },
+  { path: "/themes", component: ThemesPage },
 ]
 
 const app = createApp(App)

+ 0 - 31
apps/dev/src/pages/basic-editing.vue

@@ -1,31 +0,0 @@
-<script setup lang="ts">
-import LiveExample from "~/components/LiveExample.vue"
-</script>
-
-<template>
-  <UPage>
-    <UPageHeader title="Basic Editing" description="Headings, paragraphs, and structure." />
-
-    <UPageBody>
-      <div class="space-y-8">
-        <LiveExample
-          title="Headings"
-          description="Use 1-6 # characters followed by a space."
-          :content="`# Heading 1\n\n## Heading 2\n\n### Heading 3\n\n#### Heading 4`"
-        />
-
-        <LiveExample
-          title="Paragraphs"
-          description="Blank lines separate paragraphs. Each paragraph is its own Block."
-          :content="`First paragraph with some text.\n\nSecond paragraph goes here.\n\nThird paragraph with more content for testing.`"
-        />
-
-        <LiveExample
-          title="Horizontal Rule"
-          description="Three or more dashes create a horizontal rule."
-          :content="`Content above the rule\n\n---\n\nContent below the rule`"
-        />
-      </div>
-    </UPageBody>
-  </UPage>
-</template>

+ 0 - 49
apps/dev/src/pages/blockquotes.vue

@@ -1,49 +0,0 @@
-<script setup lang="ts">
-import LiveExample from "~/components/LiveExample.vue"
-</script>
-
-<template>
-  <UPage>
-    <UPageHeader title="Blockquotes & Callouts" description="Blockquote syntax with optional callout types." />
-
-    <UPageBody>
-      <div class="space-y-8">
-        <LiveExample
-          title="Blockquote"
-          description="Start a line with > followed by a space."
-          :content="`> This is a blockquote.\n> It spans multiple lines.\n> Each line starts with >.`"
-        />
-
-        <LiveExample
-          title="Callouts"
-          description="Use > [!TYPE] to create a styled callout. Valid types: NOTE, WARNING, TIP, DANGER, INFO."
-          :content="`> [!NOTE] This is a note callout\n> Continuation of the note`"
-        />
-
-        <LiveExample
-          title="Warning Callout"
-          description="Warning callouts have a yellow accent."
-          :content="`> [!WARNING] Check your configuration\n> This setting may affect performance.`"
-        />
-
-        <LiveExample
-          title="Tip Callout"
-          description="Tip callouts have a green accent."
-          :content="`> [!TIP] Use keyboard shortcuts\n> Mod+M toggles inline math.`"
-        />
-
-        <LiveExample
-          title="Danger Callout"
-          description="Danger callouts have a red accent."
-          :content="`> [!DANGER] This is destructive\n> This action cannot be undone.`"
-        />
-
-        <LiveExample
-          title="Info Callout"
-          description="Info callouts use the primary color."
-          :content="`> [!INFO] The editor uses ProseMirror\n> Each Block is an independent editor instance.`"
-        />
-      </div>
-    </UPageBody>
-  </UPage>
-</template>

+ 0 - 80
apps/dev/src/pages/code-blocks.vue

@@ -1,80 +0,0 @@
-<script setup lang="ts">
-import LiveExample from "~/components/LiveExample.vue"
-
-const basicCode = `Here is some code:
-
-\`\`\`
-const x = 42
-console.log(x)
-\`\`\``
-
-const languageDetection = `A Python example:
-
-\`\`\`python
-def hello():
-    print("Hello world!")
-    for i in range(3):
-        print(f"Line {i}")
-\`\`\``
-
-const tsCode = `TypeScript example:
-
-\`\`\`typescript
-interface User {
-  name: string
-  age: number
-}
-
-const user: User = {
-  name: "Alice",
-  age: 30
-}
-\`\`\``
-
-const cssCode = `\`\`\`css
-.container {
-  display: flex;
-  gap: 1rem;
-  padding: 2rem;
-}
-
-.title {
-  font-size: 1.5rem;
-  font-weight: 600;
-}
-\`\`\``
-</script>
-
-<template>
-  <UPage>
-    <UPageHeader title="Code Blocks" description="Fenced code blocks with syntax highlighting." />
-
-    <UPageBody>
-      <div class="space-y-8">
-        <LiveExample
-          title="Basic Code Block"
-          description="Use triple backticks ``` to create a code block."
-          :content="basicCode"
-        />
-
-        <LiveExample
-          title="Language Detection"
-          description="Specify a language after the opening backticks for syntax highlighting."
-          :content="languageDetection"
-        />
-
-        <LiveExample
-          title="TypeScript"
-          description="TypeScript with proper syntax highlighting."
-          :content="tsCode"
-        />
-
-        <LiveExample
-          title="CSS"
-          description="CSS with syntax highlighting."
-          :content="cssCode"
-        />
-      </div>
-    </UPageBody>
-  </UPage>
-</template>

+ 132 - 0
apps/dev/src/pages/editor-block.vue

@@ -0,0 +1,132 @@
+<script setup lang="ts">
+import { EditorBlock } from "@enesis/editor"
+import { ref } from "vue"
+
+const content = ref(`# Heading 1
+## Heading 2
+
+Paragraph with **bold**, *italic*, \`code\`, ~~strikethrough~~, ^^highlight^^, $math$, and [links](https://example.com).
+
+> Simple blockquote
+> [!NOTE] Callout with left border accent
+
+TODO Task states are supported
+DONE Completed tasks
+
+| Col A | Col B |
+|---|---|
+| Data | More data |
+
+\`\`\`python
+def hello():
+    print("Hello!")
+\`\`\`
+
+[[Page Reference]] and #tag
+
+author:: Editor Block Demo
+`)
+</script>
+
+<template>
+  <UPage>
+    <UPageHeader
+      title="EditorBlock"
+      description="A single ProseMirror block editor with markdown decoration rendering."
+    />
+
+    <UPageBody>
+      <div class="space-y-8">
+        <section class="space-y-4">
+          <p class="text-sm text-muted leading-relaxed">
+            Each <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">EditorBlock</code> manages one line of markdown in its own ProseMirror instance.
+            Decorations render bold, italic, links, tables, code blocks, callouts, references, tags, math, and properties.
+          </p>
+
+          <div class="rounded-lg border border-default dark:bg-neutral-950/50 p-4 sm:p-8 rounded-t-md">
+            <EditorBlock
+              v-model:content="content"
+              cursor-position="start"
+              marker-mode="always-visible"
+            />
+          </div>
+        </section>
+
+        <section class="space-y-4">
+          <h2 class="text-lg font-semibold text-highlighted">Props</h2>
+          <div class="rounded-lg border border-default overflow-hidden">
+            <table class="w-full border-separate border-spacing-0 rounded-md text-sm">
+              <thead class="bg-muted">
+                <tr>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Prop</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Type</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Default</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Description</th>
+                </tr>
+              </thead>
+              <tbody>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">content</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">string</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">—</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Markdown content (v-model)</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">focused</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">boolean</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">false</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Auto-focus on mount</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">cursorPosition</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">'start' \| 'end' \| number</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">—</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Cursor placement on focus</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">markerMode</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">'live-preview' \| 'always-visible'</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">'live-preview'</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Decorator visibility mode</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">debug</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">string</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">—</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Debug namespace filter</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">extraPatterns</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">PatternSpec[]</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">—</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Custom suggestion triggers</td></tr>
+              </tbody>
+            </table>
+          </div>
+        </section>
+
+        <section class="space-y-4">
+          <h2 class="text-lg font-semibold text-highlighted">Events</h2>
+          <div class="rounded-lg border border-default overflow-hidden">
+            <table class="w-full border-separate border-spacing-0 rounded-md text-sm">
+              <thead class="bg-muted">
+                <tr>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Event</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Payload</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Description</th>
+                </tr>
+              </thead>
+              <tbody>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">change</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">string</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Content changed</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">focus</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">{ view, handlers }</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Block gained focus</td></tr>
+                <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">blur</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">—</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Block lost focus</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">selection-change</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">{ from, to, empty }</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Selection changed</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">split</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">{ before, after }</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Enter split the block</td></tr>
+                <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">merge-previous</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">—</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Backspace at start</td></tr>
+                <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">delete-if-empty</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">—</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Backspace on empty block</td></tr>
+                <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">indent / outdent</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">—</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Tab / Shift+Tab</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">arrow-up-from-start</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">number?</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Cursor at first line</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">arrow-down-from-end</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">number?</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Cursor at last line</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">pattern-open</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">PatternOpenPayload</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Trigger detected (/, [[, \#)</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">pattern-close</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">PatternClosePayload</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Pattern session ended</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">error</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">{ code, message?, blockId? }</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Mount failure, KaTeX error, parser crash</td></tr>
+              </tbody>
+            </table>
+          </div>
+        </section>
+
+        <section class="space-y-4">
+          <h2 class="text-lg font-semibold text-highlighted">Exposed Methods</h2>
+          <div class="rounded-lg border border-default overflow-hidden">
+            <table class="w-full border-separate border-spacing-0 rounded-md text-sm">
+              <thead class="bg-muted">
+                <tr>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Method</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Signature</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Description</th>
+                </tr>
+              </thead>
+              <tbody>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">view</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">EditorView</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Raw ProseMirror view</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">focus</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">(pos?) => void</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Programmatic focus</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">getContent</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">() => string</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Current markdown</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">setContent</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">(md) => void</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Replace content externally</td></tr>
+              </tbody>
+            </table>
+          </div>
+        </section>
+      </div>
+    </UPageBody>
+  </UPage>
+</template>

+ 0 - 66
apps/dev/src/pages/editor-shell.vue

@@ -1,66 +0,0 @@
-<script setup lang="ts">
-import { Editor } from "@enesis/editor"
-import { ref, watch } from "vue"
-
-const content = ref(`* TODO Build the Editor shell
-  * Phase 1: Block parser
-  * Phase 2: Test edge cases
-  * Phase 3: ID preservation
-  * Phase 4: Focus registry
-  * Phase 5: Wire navigation events
-  * Phase 6: InsertionZone component
-* LATER Add drag-and-drop support
-  * Research libraries
-  * Design drop targets
-* DOING Implement toolbar integration
-* DONE Create demo pages`)
-
-const serialized = ref("")
-
-watch(content, (val) => {
-  serialized.value = val
-})
-</script>
-
-<template>
-  <UPage>
-    <UPageHeader
-      title="Editor Shell"
-      description="Multi-block document editor with navigation, split/merge, and task state support."
-    />
-
-    <UPageBody>
-      <div class="space-y-8">
-        <section class="space-y-4">
-          <h2 class="text-lg font-semibold text-highlighted">Multi-Block Document</h2>
-          <p class="text-sm text-muted leading-relaxed">
-            The <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">Editor</code> component manages a list of blocks, each one a <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">* </code> list item.
-            Use <kbd class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">Enter</kbd> to split, <kbd class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">Backspace</kbd> at start to merge,
-            <kbd class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">Tab</kbd> / <kbd class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">Shift+Tab</kbd> to change depth.
-          </p>
-
-          <div class="rounded-lg border border-default dark:bg-neutral-950/50 overflow-hidden">
-            <div class="p-4">
-              <Editor
-                v-model:content="content"
-                :focused="true"
-                marker-mode="always-visible"
-              />
-            </div>
-          </div>
-        </section>
-
-        <section class="space-y-4">
-          <h2 class="text-lg font-semibold text-highlighted">Serialized Output</h2>
-          <p class="text-xs text-muted leading-relaxed">
-            The <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">v-model:content</code> binding updates with the full serialized markdown.
-            Try splitting, merging, or reordering blocks above.
-          </p>
-          <div class="rounded-lg border border-default dark:bg-neutral-950/50 overflow-hidden">
-            <pre class="p-4 overflow-x-auto text-xs leading-relaxed font-mono text-muted"><code>{{ serialized }}</code></pre>
-          </div>
-        </section>
-      </div>
-    </UPageBody>
-  </UPage>
-</template>

+ 164 - 0
apps/dev/src/pages/editor.vue

@@ -0,0 +1,164 @@
+<script setup lang="ts">
+import { Editor, EditorToolbar } from "@enesis/editor"
+import { ref } from "vue"
+
+const content = ref(`# Getting Started with the Editor
+
+This document demonstrates all the features the editor supports.
+
+## Text Formatting
+
+You can use **bold**, *italic*, \`code\`, ~~strikethrough~~, and ^^highlight^^.
+
+## Links & References
+
+Visit [Nuxt UI](https://ui.nuxt.com) for documentation. Reference [[other pages]] and use #tags for categorization.
+
+## Tasks & Tables
+
+* TODO Task tracking with six states
+* DOING Work in progress
+* DONE Completed tasks
+
+| Feature | Status |
+|---|---|
+| Tables | Done |
+| Undo/Redo | Done |
+
+\`\`\`typescript
+function greet(name: string): string {
+  return \`Hello, \${name}!\`
+}
+\`\`\`
+
+> [!NOTE] Use callouts for important information
+
+## Properties
+
+author:: Editor Demo
+status:: published`)
+</script>
+
+<template>
+  <UPage>
+    <UPageHeader
+      title="Editor"
+      description="Multi-block editor shell with insertion zones, toolbar, drag-to-reorder, and unified undo/redo."
+    />
+
+    <UPageBody>
+      <div class="space-y-8">
+        <section class="space-y-4">
+          <p class="text-sm text-muted leading-relaxed">
+            The <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">Editor</code> component manages a list of blocks with insertion zones between them.
+            Each block is a <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">*</code> list item in the unified markdown string.
+            Drag the <span class="i-lucide-grip-vertical size-4 inline-block align-middle text-muted" /> handle to reorder blocks.
+          </p>
+
+          <div class="rounded-lg border border-default dark:bg-neutral-950/50 p-4 sm:p-8 rounded-t-md overflow-hidden">
+            <Editor v-model:content="content" :focused="true" marker-mode="always-visible">
+              <template #toolbar="{ handlers, selectionVersion, canUndo, canRedo, undo, redo }">
+                <EditorToolbar
+                  :handlers="handlers"
+                  :selection-version="selectionVersion"
+                  :can-undo="canUndo"
+                  :can-redo="canRedo"
+                  :undo="undo"
+                  :redo="redo"
+                />
+              </template>
+            </Editor>
+          </div>
+        </section>
+
+        <section class="space-y-4">
+          <h2 class="text-lg font-semibold text-highlighted">Props</h2>
+          <div class="rounded-lg border border-default overflow-hidden">
+            <table class="w-full border-separate border-spacing-0 rounded-md text-sm">
+              <thead class="bg-muted">
+                <tr>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Prop</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Type</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Default</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Description</th>
+                </tr>
+              </thead>
+              <tbody>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">content</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">string</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">—</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Full document markdown (v-model)</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">focused</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">boolean</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">false</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Focus first block on mount</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">markerMode</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">'live-preview' \| 'always-visible'</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">'live-preview'</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Decorator visibility mode</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">theme</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">ThemeInput</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">—</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">CSS-variable theme preset</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">keyBinding</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">KeyBinding</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">default</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Swappable keyboard handler</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">extraPatterns</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">PatternSpec[]</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">—</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Custom suggestion triggers</td></tr>
+              </tbody>
+            </table>
+          </div>
+        </section>
+
+        <section class="space-y-4">
+          <h2 class="text-lg font-semibold text-highlighted">Slot: #toolbar</h2>
+          <div class="rounded-lg border border-default overflow-hidden">
+            <table class="w-full border-separate border-spacing-0 rounded-md text-sm">
+              <thead class="bg-muted">
+                <tr>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Binding</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Type</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Description</th>
+                </tr>
+              </thead>
+              <tbody>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">handlers</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">FormattingHandlers</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Toggle/wrap/insert helpers</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">selectionVersion</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">number</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Bumped on cursor move</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">canUndo / canRedo</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">boolean</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Undo/redo availability</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">undo / redo</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">() => void</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Undo/redo functions</td></tr>
+              </tbody>
+            </table>
+          </div>
+        </section>
+
+        <section class="space-y-4">
+          <h2 class="text-lg font-semibold text-highlighted">Events</h2>
+          <div class="rounded-lg border border-default overflow-hidden">
+            <table class="w-full border-separate border-spacing-0 rounded-md text-sm">
+              <thead class="bg-muted">
+                <tr>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Event</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Payload</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Description</th>
+                </tr>
+              </thead>
+              <tbody>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">focus</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">{ view, handlers }</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Block gained focus</td></tr>
+                <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">blur</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">—</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">All blocks lost focus</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">selection-change</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">{ from, to, empty }</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Selection changed</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">error</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">{ code, message?, blockId? }</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Mount failure or KaTeX error</td></tr>
+              </tbody>
+            </table>
+          </div>
+        </section>
+
+        <section class="space-y-4">
+          <h2 class="text-lg font-semibold text-highlighted">Exposed Methods</h2>
+          <div class="rounded-lg border border-default overflow-hidden">
+            <table class="w-full border-separate border-spacing-0 rounded-md text-sm">
+              <thead class="bg-muted">
+                <tr>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Method</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Signature</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Description</th>
+                </tr>
+              </thead>
+              <tbody>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">undo</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">() => void</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Undo last operation</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">redo</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">() => void</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Redo last undone operation</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">canUndo / canRedo</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">Ref&lt;boolean&gt;</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Undo/redo availability</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">getHistoryState</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">() => { canUndo, canRedo }</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Inspect undo/redo state</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">applyHistory</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">(op) => void</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Push operation to history stack</td></tr>
+              </tbody>
+            </table>
+          </div>
+        </section>
+      </div>
+    </UPageBody>
+  </UPage>
+</template>

+ 40 - 64
apps/dev/src/pages/index.vue

@@ -9,8 +9,7 @@ This is a **block-based markdown editor** built with Vue 3, TypeScript, and Pros
 > [!NOTE] Every paragraph is its own Block component.
 > Blocks are independent editors with their own state.
 
-Try editing any of the examples on the left.
-`)
+Try the examples under each component page.`)
 </script>
 
 <template>
@@ -23,12 +22,7 @@ Try editing any of the examples on the left.
     <UPageBody>
       <div class="space-y-8">
         <section class="space-y-4">
-          <h2 class="text-lg font-semibold text-highlighted">What is this?</h2>
-          <p class="text-sm text-muted leading-relaxed">
-            The editor is a headless engine — each <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">Block</code> component manages a single paragraph with its own ProseMirror instance.
-            Markdown syntax is rendered as rich decorations in live-preview mode: delimiters fade when blurred, syntax elements get contextual styling.
-          </p>
-          <div class="rounded-lg border border-default dark:bg-neutral-950/50 p-4">
+          <div class="rounded-lg border border-default dark:bg-neutral-950/50 p-4 sm:p-8 rounded-t-md">
             <EditorBlock
               v-model:content="example"
               :focused="true"
@@ -39,95 +33,77 @@ Try editing any of the examples on the left.
         </section>
 
         <section class="space-y-4">
-          <h2 class="text-lg font-semibold text-highlighted">Key Concepts</h2>
+          <h2 class="text-lg font-semibold text-highlighted">Components</h2>
+          <p class="text-sm text-muted leading-relaxed">
+            The editor is organised as four main components:
+          </p>
           <div class="grid gap-4 sm:grid-cols-2">
             <div class="rounded-lg border border-default p-4 space-y-2">
               <div class="flex items-center gap-2">
                 <span class="i-lucide-layers size-4 text-primary shrink-0" />
-                <h3 class="text-sm font-medium text-highlighted">Blocks</h3>
+                <h3 class="text-sm font-medium text-highlighted">
+                  <RouterLink to="/editor" class="hover:underline">Editor</RouterLink>
+                </h3>
               </div>
               <p class="text-xs text-muted leading-relaxed">
-                Documents are split into distinct Block components — each one is an independent editor.
-                Blocks communicate through a shared document model.
+                Multi-block shell with insertion zones, shared toolbar, drag-to-reorder, and unified undo/redo.
               </p>
             </div>
             <div class="rounded-lg border border-default p-4 space-y-2">
               <div class="flex items-center gap-2">
-                <span class="i-lucide-palette size-4 text-primary shrink-0" />
-                <h3 class="text-sm font-medium text-highlighted">Decorations</h3>
+                <span class="i-lucide-file-text size-4 text-primary shrink-0" />
+                <h3 class="text-sm font-medium text-highlighted">
+                  <RouterLink to="/editor-block" class="hover:underline">EditorBlock</RouterLink>
+                </h3>
               </div>
               <p class="text-xs text-muted leading-relaxed">
-                Markdown syntax is rendered as ProseMirror decorations — styled spans overlaid on the source text.
-                Delimiters animate in and out based on focus.
+                Single-block ProseMirror editor with all markdown syntax — props, events, and exposed methods.
               </p>
             </div>
             <div class="rounded-lg border border-default p-4 space-y-2">
               <div class="flex items-center gap-2">
-                <span class="i-lucide-parse size-4 text-primary shrink-0" />
-                <h3 class="text-sm font-medium text-highlighted">Parsing Pipeline</h3>
+                <span class="i-lucide-rows-3 size-4 text-primary shrink-0" />
+                <h3 class="text-sm font-medium text-highlighted">
+                  <RouterLink to="/toolbar" class="hover:underline">EditorToolbar</RouterLink>
+                </h3>
               </div>
               <p class="text-xs text-muted leading-relaxed">
-                A 3-stage pipeline classifies blocks, builds a Lezer AST, then extracts decorations.
-                Everything runs in under 16ms per keystroke.
+                Formatting toolbar with active state tracking, undo/redo buttons, and handler integration.
               </p>
             </div>
             <div class="rounded-lg border border-default p-4 space-y-2">
               <div class="flex items-center gap-2">
-                <span class="i-lucide-plug size-4 text-primary shrink-0" />
-                <h3 class="text-sm font-medium text-highlighted">Extensible</h3>
+                <span class="i-lucide-list-ordered size-4 text-primary shrink-0" />
+                <h3 class="text-sm font-medium text-highlighted">
+                  <RouterLink to="/suggestion-menu" class="hover:underline">Suggestion Menu</RouterLink>
+                </h3>
               </div>
               <p class="text-xs text-muted leading-relaxed">
-                Inline and block rules are registered as factories. Add new syntax by creating a rule, registering it, and writing a test.
+                Slash commands, page references, block references, and tag completions.
               </p>
             </div>
           </div>
         </section>
 
         <section class="space-y-4">
-          <h2 class="text-lg font-semibold text-highlighted">Block API</h2>
-
-          <div class="rounded-lg border border-default overflow-hidden">
-            <table class="w-full text-sm">
-              <thead>
-                <tr class="border-b border-default bg-elevated">
-                  <th class="text-left px-4 py-2 font-medium text-muted text-xs uppercase tracking-wider">Prop</th>
-                  <th class="text-left px-4 py-2 font-medium text-muted text-xs uppercase tracking-wider">Type</th>
-                  <th class="text-left px-4 py-2 font-medium text-muted text-xs uppercase tracking-wider">Default</th>
-                  <th class="text-left px-4 py-2 font-medium text-muted text-xs uppercase tracking-wider">Description</th>
-                </tr>
-              </thead>
-              <tbody>
-                <tr class="border-b border-default"><td class="px-4 py-2 font-mono text-xs">content</td><td class="px-4 py-2 text-xs">string</td><td class="px-4 py-2 text-xs">—</td><td class="px-4 py-2 text-xs text-muted">Markdown content (v-model)</td></tr>
-                <tr class="border-b border-default"><td class="px-4 py-2 font-mono text-xs">focused</td><td class="px-4 py-2 text-xs">boolean</td><td class="px-4 py-2 text-xs">false</td><td class="px-4 py-2 text-xs text-muted">Automatically focus the editor</td></tr>
-                <tr class="border-b border-default"><td class="px-4 py-2 font-mono text-xs">cursorPosition</td><td class="px-4 py-2 text-xs">'start' | 'end'</td><td class="px-4 py-2 text-xs">—</td><td class="px-4 py-2 text-xs text-muted">Where to place cursor on focus</td></tr>
-                <tr class="border-b border-default"><td class="px-4 py-2 font-mono text-xs">markerMode</td><td class="px-4 py-2 text-xs">'live-preview' | 'always-visible'</td><td class="px-4 py-2 text-xs">'always-visible'</td><td class="px-4 py-2 text-xs text-muted">How decorator markers behave</td></tr>
-                <tr class="border-b border-default"><td class="px-4 py-2 font-mono text-xs">debug</td><td class="px-4 py-2 text-xs">string</td><td class="px-4 py-2 text-xs">—</td><td class="px-4 py-2 text-xs text-muted">Debug namespace filter</td></tr>
-              </tbody>
-            </table>
-          </div>
+          <h2 class="text-lg font-semibold text-highlighted">Link</h2>
+          <p class="text-sm text-muted leading-relaxed">
+            <RouterLink to="/themes" class="text-primary hover:underline">Theme system</RouterLink> — CSS-variable presets and custom colour overrides.
+          </p>
         </section>
 
         <section class="space-y-4">
-          <h2 class="text-lg font-semibold text-highlighted">Events</h2>
-          <div class="rounded-lg border border-default overflow-hidden">
-            <table class="w-full text-sm">
-              <thead>
-                <tr class="border-b border-default bg-elevated">
-                  <th class="text-left px-4 py-2 font-medium text-muted text-xs uppercase tracking-wider">Event</th>
-                  <th class="text-left px-4 py-2 font-medium text-muted text-xs uppercase tracking-wider">Payload</th>
-                  <th class="text-left px-4 py-2 font-medium text-muted text-xs uppercase tracking-wider">Description</th>
-                </tr>
-              </thead>
-              <tbody>
-                <tr class="border-b border-default"><td class="px-4 py-2 font-mono text-xs">change</td><td class="px-4 py-2 text-xs">string</td><td class="px-4 py-2 text-xs text-muted">Content changed</td></tr>
-                <tr class="border-b border-default"><td class="px-4 py-2 font-mono text-xs">focus</td><td class="px-4 py-2 text-xs">void</td><td class="px-4 py-2 text-xs text-muted">Editor gained focus</td></tr>
-                <tr class="border-b border-default"><td class="px-4 py-2 font-mono text-xs">blur</td><td class="px-4 py-2 text-xs">void</td><td class="px-4 py-2 text-xs text-muted">Editor lost focus</td></tr>
-                <tr class="border-b border-default"><td class="px-4 py-2 font-mono text-xs">selection-change</td><td class="px-4 py-2 text-xs">{ from, to }</td><td class="px-4 py-2 text-xs text-muted">Cursor/selection moved</td></tr>
-                <tr class="border-b border-default"><td class="px-4 py-2 font-mono text-xs">split</td><td class="px-4 py-2 text-xs">string</td><td class="px-4 py-2 text-xs text-muted">Block split on Enter</td></tr>
-                <tr class="border-b border-default"><td class="px-4 py-2 font-mono text-xs">merge-previous</td><td class="px-4 py-2 text-xs">void</td><td class="px-4 py-2 text-xs text-muted">Backspace at start merges blocks</td></tr>
-                <tr class="border-b border-default"><td class="px-4 py-2 font-mono text-xs">delete-if-empty</td><td class="px-4 py-2 text-xs">void</td><td class="px-4 py-2 text-xs text-muted">Backspace on empty block</td></tr>
-              </tbody>
-            </table>
+          <h2 class="text-lg font-semibold text-highlighted">Built with</h2>
+          <div class="flex flex-wrap gap-2">
+            <span class="px-2.5 py-1 text-xs font-medium rounded-md bg-elevated border border-default">Vue 3</span>
+            <span class="px-2.5 py-1 text-xs font-medium rounded-md bg-elevated border border-default">TypeScript</span>
+            <span class="px-2.5 py-1 text-xs font-medium rounded-md bg-elevated border border-default">ProseMirror</span>
+            <span class="px-2.5 py-1 text-xs font-medium rounded-md bg-elevated border border-default">@lezer/markdown</span>
+            <span class="px-2.5 py-1 text-xs font-medium rounded-md bg-elevated border border-default">Nuxt UI</span>
+            <span class="px-2.5 py-1 text-xs font-medium rounded-md bg-elevated border border-default">Tailwind CSS</span>
+            <span class="px-2.5 py-1 text-xs font-medium rounded-md bg-elevated border border-default">CodeMirror 6</span>
+            <span class="px-2.5 py-1 text-xs font-medium rounded-md bg-elevated border border-default">KaTeX</span>
+            <span class="px-2.5 py-1 text-xs font-medium rounded-md bg-elevated border border-default">Vitest</span>
           </div>
         </section>
       </div>

+ 0 - 49
apps/dev/src/pages/inline-marks.vue

@@ -1,49 +0,0 @@
-<script setup lang="ts">
-import LiveExample from "~/components/LiveExample.vue"
-</script>
-
-<template>
-  <UPage>
-    <UPageHeader title="Inline Marks" description="Bold, italic, code, strikethrough, and highlight." />
-
-    <UPageBody>
-      <div class="space-y-8">
-        <LiveExample
-          title="Bold"
-          description="Wrap text in **double asterisks**."
-          :content="`This is **bold text** and this is also **bold** inline.`"
-        />
-
-        <LiveExample
-          title="Italic"
-          description="Wrap text in *single asterisks*."
-          :content="`This is *italic text* and this is also *italic* inline.`"
-        />
-
-        <LiveExample
-          title="Inline Code"
-          description="Wrap text in \`backticks\`."
-          :content="`Use the \`const\` keyword or call \`console.log()\` to debug.`"
-        />
-
-        <LiveExample
-          title="Strikethrough"
-          description="Wrap text in ~~double tildes~~."
-          :content="`This is ~~strikethrough text~~ that has been removed.`"
-        />
-
-        <LiveExample
-          title="Highlight"
-          description="Wrap text in ^^double carets^^."
-          :content="`This is ^^highlighted text^^ that stands out.`"
-        />
-
-        <LiveExample
-          title="Combined Marks"
-          description="Marks can be nested inside each other."
-          :content="`This is **bold with *italic* inside** and \`code with ~~strike~~\`.`"
-        />
-      </div>
-    </UPageBody>
-  </UPage>
-</template>

+ 0 - 31
apps/dev/src/pages/links.vue

@@ -1,31 +0,0 @@
-<script setup lang="ts">
-import LiveExample from "~/components/LiveExample.vue"
-</script>
-
-<template>
-  <UPage>
-    <UPageHeader title="Links" description="Standard markdown links with inline formatting." />
-
-    <UPageBody>
-      <div class="space-y-8">
-        <LiveExample
-          title="Basic Link"
-          description="Use [text](url) syntax."
-          :content="`Visit [Example](https://example.com) for more information.`"
-        />
-
-        <LiveExample
-          title="Links with Bold"
-          description="Link text can contain formatting."
-          :content="`Check out [**important link**](https://example.com) for details.`"
-        />
-
-        <LiveExample
-          title="Multiple Links"
-          description="Multiple links in the same paragraph."
-          :content="`Search on [Google](https://google.com) or [DuckDuckGo](https://duckduckgo.com).`"
-        />
-      </div>
-    </UPageBody>
-  </UPage>
-</template>

+ 0 - 74
apps/dev/src/pages/math.vue

@@ -1,74 +0,0 @@
-<script setup lang="ts">
-import LiveExample from "~/components/LiveExample.vue"
-</script>
-
-<template>
-  <UPage>
-    <UPageHeader title="Math" description="Inline $...$ and display $$...$$ math with KaTeX rendering." />
-
-    <UPageBody>
-      <div class="space-y-8">
-        <section>
-          <h2 class="text-lg font-semibold text-highlighted mb-3">Inline Math</h2>
-          <div class="space-y-6">
-            <LiveExample
-              title="Basic Inline"
-              description="Use single $ delimiters for inline math."
-              :content="`The formula $E = mc^2$ is famous.`"
-            />
-
-            <LiveExample
-              title="Complex Expressions"
-              description="Inline math handles complex LaTeX."
-              :content="`The sum $\\sum_{i=1}^n i = \\frac{n(n+1)}{2}$ converges nicely.`"
-            />
-
-            <LiveExample
-              title="Greek Letters"
-              description="Standard LaTeX Greek letters work inline."
-              :content="`Angles: $\\alpha$, $\\beta$, $\\gamma$ are common variables.`"
-            />
-          </div>
-        </section>
-
-        <USeparator />
-
-        <section>
-          <h2 class="text-lg font-semibold text-highlighted mb-3">Block Math</h2>
-          <div class="space-y-6">
-            <LiveExample
-              title="Display Math"
-              description="Use $$...$$ for display math blocks. Type $$ at the start of an empty paragraph."
-              :content="`Here is a displayed equation:\n\n$$\n\\int_{-\\infty}^{\\infty} e^{-x^2} \\, dx = \\sqrt{\\pi}\n$$`"
-            />
-
-            <LiveExample
-              title="Matrices"
-              description="Block math supports matrices and arrays."
-              :content="`A 2x2 matrix:\n\n$$\n\\begin{bmatrix}\na & b \\\\\nc & d\n\\end{bmatrix}\n$$`"
-            />
-
-            <LiveExample
-              title="Piecewise Functions"
-              description="Block math handles cases and piecewise definitions."
-              :content="`Piecewise function:\n\n$$\nf(x) = \\begin{cases}\nx^2 & \\text{if } x > 0 \\\\\n0 & \\text{otherwise}\n\\end{cases}\n$$`"
-            />
-          </div>
-        </section>
-
-        <USeparator />
-
-        <section>
-          <h2 class="text-lg font-semibold text-highlighted mb-3">Mixed Content</h2>
-          <div class="space-y-6">
-            <LiveExample
-              title="Math with Other Syntax"
-              description="Math works alongside formatting, callouts, refs, and tasks."
-              :content="`## Math Document\n\nYou can use **bold** and $x$ inline math together.\n\n> [!NOTE] Callouts with math: $E = mc^2$\n\n[[Physics Notes]] with $\\theta$ angle.\n\nTODO Review $\\Delta$ formula`"
-            />
-          </div>
-        </section>
-      </div>
-    </UPageBody>
-  </UPage>
-</template>

+ 0 - 25
apps/dev/src/pages/properties.vue

@@ -1,25 +0,0 @@
-<script setup lang="ts">
-import LiveExample from "~/components/LiveExample.vue"
-</script>
-
-<template>
-  <UPage>
-    <UPageHeader title="Properties" description="Key-value metadata." />
-
-    <UPageBody>
-      <div class="space-y-8">
-        <LiveExample
-          title="Properties"
-          description="Use key:: value syntax at the end of a line."
-          :content="`This is a document with metadata.\nauthor:: John Doe\nstatus:: published`"
-        />
-
-        <LiveExample
-          title="Mixed Properties"
-          description="Properties work alongside other syntax."
-          :content="`## Meeting Notes\ntags:: #development #planning\nTODO Review the agenda`"
-        />
-      </div>
-    </UPageBody>
-  </UPage>
-</template>

+ 0 - 37
apps/dev/src/pages/refs-tags.vue

@@ -1,37 +0,0 @@
-<script setup lang="ts">
-import LiveExample from "~/components/LiveExample.vue"
-</script>
-
-<template>
-  <UPage>
-    <UPageHeader title="Page Refs & Tags" description="Wiki-style references and hashtags." />
-
-    <UPageBody>
-      <div class="space-y-8">
-        <LiveExample
-          title="Page References"
-          description="Use [[Page Name]] to create a page ref chip."
-          :content="`See [[Getting Started]] for the tutorial.\n\nReference [[Real Page|Display Name]] to show custom text.`"
-        />
-
-        <LiveExample
-          title="Block References"
-          description="Use ((block-id)) to reference another block."
-          :content="`As mentioned in ((abc123)), the feature is ready.\n\nRefer to ((def456)) for implementation details.`"
-        />
-
-        <LiveExample
-          title="Tags"
-          description="Use #tag to create a tag chip."
-          :content="`This is about #documentation and #development.\n\n#bug #feature #enhancement`"
-        />
-
-        <LiveExample
-          title="Combined References"
-          description="References work with other inline formatting."
-          :content="`The **[[Main Page]]** has been updated with #new features.\n\nSee ((ref-123)) for *details* on #implementation.`"
-        />
-      </div>
-    </UPageBody>
-  </UPage>
-</template>

+ 91 - 0
apps/dev/src/pages/suggestion-menu.vue

@@ -0,0 +1,91 @@
+<script setup lang="ts">
+import { EditorBlock } from "@enesis/editor"
+import { ref } from "vue"
+
+const slashContent = ref(`Start typing / to see the slash command menu.
+
+You can insert headings, formatting, blocks, and references through the menu.`)
+
+const mentionContent = ref(`Type double square brackets to search for page references.
+
+Type double parentheses to search for block references.
+
+Type a hash sign to search for tags.`)
+</script>
+
+<template>
+  <UPage>
+    <UPageHeader
+      title="Suggestion Menu"
+      description="Slash commands, page references, block references, and tag completions."
+    />
+
+    <UPageBody>
+      <div class="space-y-8">
+        <section class="space-y-4">
+          <p class="text-sm text-muted leading-relaxed">
+            Each <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">EditorBlock</code> manages its own <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">EditorSuggestionMenu</code> instance.
+            The menu appears automatically when trigger characters are typed.
+          </p>
+
+          <div class="rounded-lg border border-default dark:bg-neutral-950/50 p-4 sm:p-8 rounded-t-md">
+            <div class="p-4 border-b border-default">
+              <h3 class="text-sm font-semibold text-highlighted mb-1">Slash Commands</h3>
+              <p class="text-xs text-muted">Type <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">/</code> to open headings, formatting, blocks, and insert options.</p>
+            </div>
+            <div class="p-4">
+              <EditorBlock v-model:content="slashContent" :focused="true" cursor-position="end" marker-mode="always-visible" />
+            </div>
+          </div>
+
+          <div class="rounded-lg border border-default dark:bg-neutral-950/50 p-4 sm:p-8 rounded-t-md">
+            <div class="p-4 border-b border-default">
+              <h3 class="text-sm font-semibold text-highlighted mb-1">References & Tags</h3>
+              <p class="text-xs text-muted">
+                <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">[[</code> page references,
+                <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">((</code> block references,
+                <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">#</code> tag suggestions.
+              </p>
+            </div>
+            <div class="p-4">
+              <EditorBlock v-model:content="mentionContent" :focused="true" cursor-position="end" marker-mode="always-visible" />
+            </div>
+          </div>
+        </section>
+
+        <section class="space-y-4">
+          <h2 class="text-lg font-semibold text-highlighted">Built-in Triggers</h2>
+          <div class="rounded-lg border border-default overflow-hidden">
+            <table class="w-full border-separate border-spacing-0 rounded-md text-sm">
+              <thead class="bg-muted">
+                <tr>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Trigger</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Kind</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Behaviour</th>
+                </tr>
+              </thead>
+              <tbody>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">/</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">command</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Slash command palette (headings, formatting, blocks, insert)</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">[[</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">reference</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Page reference completion → [[Page Name]]</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">((</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">embed</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Block reference completion → ((block-id))</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">#</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">tag</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Tag completion → #tagname</td></tr>
+              </tbody>
+            </table>
+          </div>
+        </section>
+
+        <section class="space-y-4">
+          <h2 class="text-lg font-semibold text-highlighted">Extending Triggers</h2>
+          <p class="text-sm text-muted leading-relaxed">
+            Additional triggers can be registered via the <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">extraPatterns</code> prop on <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">EditorBlock</code> or <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">Editor</code>:
+          </p>
+          <pre class="rounded-lg border border-default bg-muted p-4 text-xs font-mono overflow-x-auto">import type { PatternSpec } from "@enesis/editor"
+
+const extraPatterns: PatternSpec[] = [
+  { start: "@", kind: "command", termination: "boundary", priority: 2 },
+]</pre>
+        </section>
+      </div>
+    </UPageBody>
+  </UPage>
+</template>

+ 0 - 31
apps/dev/src/pages/tasks.vue

@@ -1,31 +0,0 @@
-<script setup lang="ts">
-import LiveExample from "~/components/LiveExample.vue"
-</script>
-
-<template>
-  <UPage>
-    <UPageHeader title="Tasks" description="Task states with valid state progression." />
-
-    <UPageBody>
-      <div class="space-y-8">
-        <LiveExample
-          title="Task States"
-          description="Valid states: TODO, DOING, DONE, LATER, NOW, WAITING, CANCELLED."
-          :content="`TODO Fix the login bug\nDOING Working on the API\nDONE Deployed to production\nLATER Review the PR\nWAITING Need design feedback\nCANCELLED Won't implement`"
-        />
-
-        <LiveExample
-          title="Task Cycle"
-          description="TODO → DOING → DONE is the linear cycle. LATER, NOW, WAITING, CANCELLED are orthogonal."
-          :content="`TODO Write documentation\nDOING Refactor tests\nDONE Ship v1.0`"
-        />
-
-        <LiveExample
-          title="Mixed Content"
-          description="Tasks work with inline formatting."
-          :content="`TODO Write **bold** documentation\nDOING Fix the *really* tricky bug\nDONE ~~Remove~~ deprecated code`"
-        />
-      </div>
-    </UPageBody>
-  </UPage>
-</template>

+ 4 - 4
apps/dev/src/pages/themes.vue

@@ -10,10 +10,10 @@ const content = ref(`* TODO Build the Editor shell
   * Define EditorTheme type
   * Create kanagawa preset
   * Wire CSS vars to shell
-* LATER Add more presets
-* DONE Task state support
+ * LATER Add more presets
+ * DONE Task state support
   * Implement task cycling
-  * Add priority display`)
+  * Add completion states`)
 
 const currentTheme = ref<string>("default")
 const customPrimary = ref("#ff6b6b")
@@ -78,7 +78,7 @@ const isCustom = computed(() => currentTheme.value === "custom")
         <section class="space-y-4">
           <h2 class="text-lg font-semibold text-highlighted">Editor Preview</h2>
 
-          <div class="rounded-lg border border-default dark:bg-neutral-950/50 overflow-hidden">
+          <div class="rounded-lg border border-default dark:bg-neutral-950/50 p-4 sm:p-8 rounded-t-md overflow-hidden">
             <div class="p-4">
               <Editor
                 v-model:content="content"

+ 37 - 33
apps/dev/src/pages/toolbar.vue

@@ -6,30 +6,25 @@ const content =
   ref(`* **Bold**, *italic*, \`code\`, ~~strikethrough~~, ^^highlight^^, and $math$ work inline
 * ## Heading 2
 * TODO This task can be toggled from the toolbar
-* [[Page refs]] and #tags are supported inline
-* | Feature | Shortcut |
-  |---|---|
-  | Bold | **⌘B** |
-  | Italic | *⌘I* |`)
+* [[Page refs]] and #tags are supported inline`)
 </script>
 
 <template>
   <UPage>
     <UPageHeader
-      title="Toolbar"
-      description="Formatting toolbar with active state tracking."
+      title="EditorToolbar"
+      description="Formatting toolbar with active state tracking and undo/redo buttons."
     />
 
     <UPageBody>
       <div class="space-y-8">
         <section class="space-y-4">
-          <h2 class="text-lg font-semibold text-highlighted">Editor with Toolbar</h2>
           <p class="text-sm text-muted leading-relaxed">
-            Toolbar and editor are wired automatically via the <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">#toolbar</code> slot — no manual event handling needed.
-            The Editor provides <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">handlers</code> and <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">selectionVersion</code> to the slot scope.
+            The toolbar is rendered via the <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">#toolbar</code> slot on <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">Editor</code>.
+            It receives <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">handlers</code>, <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">canUndo</code>/<code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">canRedo</code>, and <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">undo</code>/<code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">redo</code> from the focused block.
           </p>
 
-          <div class="flex flex-col gap-3 rounded-lg border border-default dark:bg-neutral-950/50 p-4">
+          <div class="rounded-lg border border-default dark:bg-neutral-950/50 p-4 sm:p-8 rounded-t-md">
             <Editor v-model:content="content" :focused="true" marker-mode="always-visible">
               <template #toolbar="{ handlers, selectionVersion, canUndo, canRedo, undo, redo }">
                 <EditorToolbar
@@ -46,38 +41,47 @@ const content =
         </section>
 
         <section class="space-y-4">
-          <h2 class="text-lg font-semibold text-highlighted">Integration Pattern</h2>
+          <h2 class="text-lg font-semibold text-highlighted">Props</h2>
           <div class="rounded-lg border border-default overflow-hidden">
-            <table class="w-full text-sm">
-              <thead>
-                <tr class="border-b border-default bg-elevated">
-                  <th class="text-left px-4 py-2 font-medium text-muted text-xs uppercase tracking-wider">Step</th>
-                  <th class="text-left px-4 py-2 font-medium text-muted text-xs uppercase tracking-wider">Description</th>
+            <table class="w-full border-separate border-spacing-0 rounded-md text-sm">
+              <thead class="bg-muted">
+                <tr>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Prop</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Type</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Default</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Description</th>
                 </tr>
               </thead>
               <tbody>
-                <tr class="border-b border-default">
-                  <td class="px-4 py-2 font-mono text-xs">1</td>
-                  <td class="px-4 py-2 text-xs text-muted">Editor detects <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">#toolbar</code> slot and renders it above the block list</td>
-                </tr>
-                <tr class="border-b border-default">
-                  <td class="px-4 py-2 font-mono text-xs">2</td>
-                  <td class="px-4 py-2 text-xs text-muted">Editor passes <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">handlers</code> and <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">selectionVersion</code> from the focused Block into the slot</td>
-                </tr>
-                <tr class="border-b border-default">
-                  <td class="px-4 py-2 font-mono text-xs">3</td>
-                  <td class="px-4 py-2 text-xs text-muted">Toolbar's <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">computed</code>s re-run, calling <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">handlers.isActive(format)</code> for each button</td>
-                </tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">handlers</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">FormattingHandlers</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">—</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Handlers from focused block</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">selectionVersion</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">number</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">—</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Re-compute active states</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">canUndo / canRedo</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">boolean</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">—</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Enable undo/redo buttons</td></tr>
+                 <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">undo / redo</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">() => void</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">—</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Undo/redo callbacks</td></tr>
               </tbody>
             </table>
           </div>
         </section>
 
         <section class="space-y-4">
-          <h2 class="text-lg font-semibold text-highlighted">Known Limitations</h2>
-          <ul class="text-sm text-muted leading-relaxed space-y-2 list-disc list-inside">
-            <li>Undo/redo (<kbd class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">⌘Z</kbd> / <kbd class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">⌘⇧Z</kbd>) are wired via global keyboard shortcuts. <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">canUndo</code>, <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">canRedo</code>, <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">undo</code>, and <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">redo</code> are provided via the <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">#toolbar</code> slot scope.</li>
-          </ul>
+          <h2 class="text-lg font-semibold text-highlighted">Button Groups</h2>
+          <div class="rounded-lg border border-default overflow-hidden">
+            <table class="w-full border-separate border-spacing-0 rounded-md text-sm">
+              <thead class="bg-muted">
+                <tr>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Group</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Buttons</th>
+                  <th class="py-3 px-4 font-semibold text-sm border-e border-b first:border-s border-t border-muted text-left">Description</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">History</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">Undo, Redo</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Undo/redo stack</td></tr>
+                <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">Block</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">Heading, Task</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Heading level dropdown, task toggle</td></tr>
+                <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">Inline</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">Bold, Italic, Code, Strike, Highlight</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Inline formatting toggles</td></tr>
+                <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">Insert</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">Link, Inline Math, Block Math</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Insert markdown syntax</td></tr>
+                <tr><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left"><code class="md-code">References</code></td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left">Page Ref, Block Ref, Tag</td><td class="py-3 px-4 text-sm align-top border-e border-b first:border-s border-muted text-left text-muted">Prompt-based insertion</td></tr>
+              </tbody>
+            </table>
+          </div>
         </section>
       </div>
     </UPageBody>

+ 20 - 2
packages/editor/src/assets/style.css

@@ -395,7 +395,7 @@
 }
 
 .md-table-rendered {
-  @apply block w-full overflow-x-auto;
+  @apply block w-full overflow-x-auto rounded-md;
 }
 
 .md-table-rendered table {
@@ -418,13 +418,31 @@
 }
 
 .md-table-rendered th {
-  @apply font-semibold;
+  @apply font-semibold text-sm;
 }
 
 .md-table-rendered td {
   @apply align-top;
 }
 
+/* Inline content within table cells */
+.md-table-rendered td code {
+  @apply text-xs/5;
+}
+
+.md-table-rendered td p {
+  @apply my-0 leading-6;
+}
+
+.md-table-rendered td ul,
+.md-table-rendered td ol {
+  @apply my-0 ps-4.5;
+}
+
+.md-table-rendered td li {
+  @apply leading-6 my-0.5;
+}
+
 /* Rounded corners on corner cells (matches Nuxt UI prose style) */
 .md-table-rendered thead tr:first-child th:first-child {
   @apply rounded-tl-md;

+ 15 - 0
packages/editor/src/components/EditorBlock.vue

@@ -12,6 +12,7 @@ import { EditorView } from "prosemirror-view"
 import {
   onMounted,
   onUnmounted,
+  nextTick,
   ref,
   useTemplateRef,
   watch,
@@ -231,6 +232,20 @@ const slashGroups = useSlashGroups(
 )
 const mentionGroups = useMentionGroups(localSession, closeSession)
 
+// Close mention/tag menus when no items match the query.
+watch(localSession, (session) => {
+  if (session && session.kind !== "command") {
+    nextTick(() => {
+      if (!localSession.value) return
+      const q = localSession.value.query.toLowerCase()
+      const hasMatches = mentionGroups.value.some((g) =>
+        g.items?.some((item) => item.label?.toLowerCase().includes(q)),
+      )
+      if (!hasMatches) closeSession()
+    })
+  }
+})
+
 function onCaptureKeydown(e: KeyboardEvent) {
   if (!localSession.value) return
   if (["ArrowDown", "ArrowUp", "Enter", "Escape"].includes(e.key)) {

+ 4 - 0
packages/editor/src/composables/usePatternPlugin.ts

@@ -149,6 +149,10 @@ function findPattern(
     // Don't open tag palette on bare # — it's likely a heading attempt.
     if (spec.kind === "tag" && query.length === 0) continue
 
+    // Don't open delimiter patterns ([[, (() followed immediately by a space
+    // — the user is not starting a reference.
+    if (spec.termination === "delimiter" && /^\s/.test(query)) continue
+
     const startPos = $head.pos - textBefore.length + startIdx
     if (isPatternCompleted(spec, query)) return null