Просмотр исходного кода

docs: update all READMEs; add pipeline perf test

Packages:
  - editor/README: Block → EditorBlock component name, EditorFocusTarget
    union type for focused prop, hidden-at-rest grip description
  - root README: fix monorepo tree (6 pages, not 10), grip not bullet

Dev app:
  - editor-block.vue: fix "one line" → "a block"; add resolveAsset prop,
    pattern-update, content-change-op, first-para-height events
  - editor.vue: focused prop type boolean → EditorFocusTarget;
    add debug and testMode props
  - dev/README: Block → EditorBlock, 6 pages not 10

Tauri app:
  - README: replace generic template with Enesis-specific docs
  - Welcome.md: remove default Tauri welcome file

Testing:
  - Add parse-pipeline.perf.test.ts: 4 tests asserting full decoration
    pipeline (apply + decorations) averages under 16ms per keystroke
Zander Hawke 13 часов назад
Родитель
Сommit
9fdc006fda

+ 11 - 15
README.md

@@ -38,28 +38,24 @@ The multi-block architecture is the other major friction point. Each `EditorBloc
 │       │   │   ├── LiveExample.vue     Editable demo wrapper with source view
 │       │   │   └── AppLogo.vue         Enesis brand mark
 │       │   ├── pages/
-│       │   │   ├── index.vue           Overview
-│       │   │   ├── basic-editing.vue   Headings, paragraphs, rules
-│       │   │   ├── inline-marks.vue    Bold, italic, code, strike, highlight
-│       │   │   ├── tasks.vue           Task states & priorities
-│       │   │   ├── blockquotes.vue     Blockquotes & callouts
-│       │   │   ├── links.vue           Markdown links
-│       │   │   ├── refs-tags.vue       Page refs, block refs, tags
-│       │   │   ├── code-blocks.vue     Fenced code blocks
-│       │   │   ├── properties.vue      Key::value properties & dates
-│       │   │   └── math.vue            Inline & display LaTeX
-│       │   ├── App.vue                 Shell layout (Nuxt UI) with sidebar
-│       │   ├── main.ts                 App bootstrap (10 routes)
+│       │   │   ├── index.vue           Overview / key concepts
+│       │   │   ├── editor.vue          Multi-block Editor shell
+│       │   │   ├── editor-block.vue    Standalone EditorBlock demo
+│       │   │   ├── toolbar.vue         Toolbar integration
+│       │   │   ├── suggestion-menu.vue Pattern completion menu
+│       │   │   └── themes.vue          Theme presets
+│       │   ├── App.vue                 Shell layout with sidebar
+│       │   ├── main.ts                 App bootstrap (6 routes)
 │       │   └── style.css               Tailwind v4 + Nuxt UI theme
-│       ├── public/                     Static assets
+│       ├── public/                     Static assets (icons.svg sprite, favicon)
 │       └── vite.config.ts              Vite with Nuxt UI plugin
 ├── packages/
 │   └── editor/                Core headless engine
 │       ├── src/
-│       │   ├── index.ts               Public API (Block + Editor + EditorToolbar components)
+│       │   ├── index.ts               Public API (Editor + EditorBlock + EditorToolbar + EditorSuggestionMenu)
 │       │   ├── components/
-│       │   │   ├── EditorBlock.vue     Self-contained ProseMirror block editor with draggable bullet
+│       │   │   ├── EditorBlock.vue     Self-contained ProseMirror block editor with draggable grip
 │       │   │   ├── Editor.vue          Multi-block shell with insertion zones, drag-to-reorder, undo/redo
 │       │   │   ├── EditorInsertionZone.vue   32px hit target between blocks, hover-reveal, drag target
 │       │   │   └── EditorToolbar.vue   Shared toolbar bound to active block

+ 2 - 2
apps/dev/README.md

@@ -1,6 +1,6 @@
 # @enesis/dev
 
-Documentation-style development sandbox for the Enesis Editor. A Vite + Vue 3 app that exercises every `@enesis/editor` `<Block>` feature through live, editable examples organized across 10 catalog pages.
+Documentation-style development sandbox for the Enesis Editor. A Vite + Vue 3 app that exercises every `@enesis/editor` feature through live, editable examples organized across 6 catalog pages.
 
 ## Quick Start
 
@@ -37,7 +37,7 @@ App.vue                     Shell layout — UHeader + UPage + sidebar navigatio
 └── pages/themes.vue        Theme presets
 
 components/
-├── LiveExample.vue         <Block> wrapper with title, description, source code panel
+├── LiveExample.vue         <EditorBlock> wrapper with title, description, source code panel
 └── AppLogo.vue             Enesis brand mark
 ```
 

+ 9 - 5
apps/dev/src/pages/editor-block.vue

@@ -64,7 +64,7 @@ function onTagClick({ tag }: { tag: string }) {
       <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.
+            Each <code class="text-xs font-mono px-1 py-0.5 rounded bg-elevated border border-default">EditorBlock</code> manages a block of markdown in its own ProseMirror instance.
             Decorations render bold, italic, links, tables, code blocks, callouts, references, tags, math, and properties.
           </p>
 
@@ -96,13 +96,14 @@ function onTagClick({ tag }: { tag: string }) {
               </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">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 the block and show active decorations; re-focuses when the prop changes</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>
                   <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">resolvedRefs</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">Set&lt;string&gt;</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">Known refs — missing entries render with dashed underline</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">resolvedRefsVersion</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">Bump to re-check refs across all blocks</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">resolvedRefsVersion</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">Bump to re-check refs across all blocks</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">resolveAsset</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">(file: File) => Promise&lt;string&gt;</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">Upload handler — resolves dropped/pasted files to a URL</td></tr>
               </tbody>
             </table>
           </div>
@@ -123,14 +124,17 @@ function onTagClick({ tag }: { tag: string }) {
                  <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">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">content-change-op</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">{ blockId, previousContent, newContent, timestamp }</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 change metadata for history tracking</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">first-para-height</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">Height of first paragraph, for drag-handle alignment</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-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-update</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">PatternUpdatePayload</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 query/range updated as user types</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">page-ref-clicked</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">{ pageName, alias? }</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">Clicked a [[page]] chip</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-ref-clicked</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">{ 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">Clicked a ((block)) chip</td></tr>

+ 5 - 3
apps/dev/src/pages/editor.vue

@@ -163,14 +163,16 @@ function onTagClick({ tag }: { tag: string }) {
               </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">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">EditorFocusTarget</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">Focus target on mount: <code>true</code> (first block), <code>"first-block"</code>, <code>"last-block"</code>, <code>"first-zone"</code>, <code>"last-zone"</code>, or <code>{ kind: "block"; id }</code> / <code>{ kind: "zone"; index }</code></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">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>
                   <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">resolvedRefs</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">Set&lt;string&gt;</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">Known refs — missing entries render dashed</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">resolvedRefsVersion</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">Bump to re-check refs across blocks</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">resolveAsset</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">(file) => Promise&lt;string&gt;</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">Upload handler — drops/pastes insert as ![]()</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">resolveAsset</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">(file) => Promise&lt;string&gt;</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">Upload handler — drops/pastes insert as ![]()</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">testMode</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">Expose window.__testUtils__ for Playwright E2E tests</td></tr>
               </tbody>
             </table>
           </div>

+ 58 - 4
apps/tauri/README.md

@@ -1,7 +1,61 @@
-# Tauri + Vue + TypeScript
+# Enesis
 
-This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+A warm, literary journal app. Built with Tauri, Nuxt UI, and the `@enesis/editor` block markdown engine.
 
-## Recommended IDE Setup
+Daily notes are flat markdown files (`journals/YYYY-MM-DD.md`) — readable anywhere, no lock-in. A SQLite index provides full-text search and backlinks.
 
-- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
+The visual identity (Hearthside theme) leans into warmth: Lora serif, clay primary, stone neutrals, earthy semantics.
+
+## Quick start
+
+```bash
+pnpm install
+pnpm generate  # static build
+pnpm tauri dev  # or: pnpm tauri build
+```
+
+## Stack
+
+| Layer | Technology |
+|---|---|
+| Desktop shell | Tauri v2 (Rust) |
+| Frontend | Nuxt 3 + Vue 3 + TypeScript |
+| UI framework | Nuxt UI v4 |
+| Editor engine | `@enesis/editor` (Lezer-based block markdown) |
+| Index | SQLite via `tauri-plugin-sql`, FTS5 |
+| File watching | `notify` crate |
+
+## Theme — Hearthside
+
+Warm, earthy, literary. Lora serif throughout, clay primary, orange secondary, stone neutral base, rose/emerald/amber/sky for semantics. Configured in `app.config.ts` and `assets/main.css`.
+
+## Development
+
+```bash
+pnpm dev              # Vite dev server
+pnpm tauri dev        # Tauri desktop dev
+pnpm generate         # Static pre-render (for Tauri)
+pnpm test:e2e         # Playwright E2E (in packages/editor)
+```
+
+## Project structure
+
+```
+src/
+  assets/main.css              # Tailwind + theme tokens + font
+  app.config.ts                # Nuxt UI color mappings
+  components/
+    JournalDay.vue             # One Editor per day
+    JournalView.vue            # Reverse-chronological feed
+    TimelineRail.vue           # Density dot timeline
+    AppRightSidebar.vue        # References + search
+  layouts/default.vue          # Sidebars + editor shell
+  composables/                 # useFileSystem, useDates, etc.
+  stores/                      # Pinia: workspace, ui
+  lib/                         # Indexer, schema, parse helpers
+src-tauri/
+  src/lib.rs                   # Rust commands + plugins
+  icons/                       # App icons (clay block logo)
+public/
+  favicon.svg                  # Clay block stacking logo
+```

+ 0 - 47
apps/tauri/Welcome.md

@@ -1,47 +0,0 @@
-* # Welcome to Enesis Editor
-
-  Each **block** between blank lines is its own independent editor — click anywhere to start typing.
-
-  * ## Text Formatting
-
-    Try **bold**, *italic*, `code`, ~~strikethrough~~, ^^highlight^^, and $\LaTeX$ math.
-
-  * ## References
-
-    [[Known Page]] resolves. [[Missing Page]] shows via dashed border.
-
-    ((unknown-block-id))
-
-    Use #tags for categorization.
-
-  * ## Tasks
-
-    * TODO Write the documentation
-    * DOING Implement the parser
-    * DONE Set up the build pipeline
-
-  * ## Table
-
-    | Feature | Status |
-    | --- | --- |
-    | Blocks | Done |
-    | Tables | Done |
-
-
-  * ## Code
-
-    ```typescript
-    function greet(name: string): string {
-      return `Hello, ${name}!`
-    }
-    ```
-
-  * ## Callout
-
-    > [!NOTE] Useful info for the reader
-    > Plus multi-line functionality
-
-  * ## Properties
-
-    author:: Enesis Editor
-    version:: 0.1.0

+ 7 - 14
packages/editor/README.md

@@ -14,14 +14,14 @@ Requires peer dependencies: `vue`, `@nuxt/ui`, `@vueuse/core`, `pinia`.
 
 ```vue
 <script setup lang="ts">
-import { Block } from "@enesis/editor"
+import { Editor, EditorBlock } from "@enesis/editor"
 import { ref } from "vue"
 
 const content = ref("# Hello\n\nThis is **bold** text.\n\n> [!NOTE] A callout")
 </script>
 
 <template>
-  <Block v-model:content="content" focused cursor-position="end" />
+  <EditorBlock v-model:content="content" :focused="true" cursor-position="end" />
 </template>
 ```
 
@@ -30,10 +30,10 @@ const content = ref("# Hello\n\nThis is **bold** text.\n\n> [!NOTE] A callout")
 ```ts
 import EnesisEditor from "@enesis/editor"
 app.use(EnesisEditor)
-// → <Block /> registered globally
+// → <EditorBlock /> registered globally
 ```
 
-## `<Block>` API
+## `<EditorBlock>` API
 
 ### Props
 
@@ -85,9 +85,9 @@ The `Editor` component manages a list of blocks with insertion zones between the
 ### Props
 
 | Prop | Type | Default | Description |
-|---|---|---|---|
+|---|---|---|---|---|
 | `content` (v-model) | `string` | required | Full document markdown (all blocks serialized). |
-| `focused` | `boolean` | `false` | Whether the first block receives focus on mount. |
+| `focused` | `"first-block" \| "last-block" \| "first-zone" \| "last-zone" \| { kind: "block"; id: string } \| { kind: "zone"; index: number } \| true` | — | Where to place focus on mount. String values target positions; object values target specific blocks or zones; `true` focuses the first block. |
 | `markerMode` | `"live-preview" \| "always-visible"` | `"live-preview"` | When to show markdown delimiters. |
 | `debug` | `string` | — | Namespace filter for debug logs. |
 | `theme` | `ThemeInput` | — | CSS-variable theme preset or overrides (see `theme.ts`). |
@@ -337,7 +337,7 @@ Each pattern spec defines:
 
 ## Drag-to-Reorder
 
-Each block has a draggable bullet in its left gutter. The bullet is always visible, transitions from muted to primary on hover, and grows (size-6 → size-7) with primary color while being dragged. Drop targets are the insertion zones between blocks — a primary-colored bar appears on the zone during a valid hover. Dragging to a zone before the first block, between any two blocks, or after the last block inserts the block at that position.
+Each block has a draggable grip in its left gutter. The grip is hidden at rest (`opacity-0`), fades in on block hover, and transitions from muted to primary color while being dragged. Drop targets are the insertion zones between blocks — a primary-tinted bar appears on the zone during a valid hover. Dragging to a zone before the first block, between any two blocks, or after the last block inserts the block at that position.
 
 - **Native HTML5 drag** — no external drag library required
 - **Zone-based drop** — drop on any insertion zone, not on the block itself
@@ -352,13 +352,6 @@ import type { MoveBlockOp } from "@enesis/editor"
 const reordered = moveBlock(blocks, 0, 2) // move block 0 to index 2
 ```
 
-```typescript
-import { moveBlock } from "@enesis/editor"
-import type { MoveBlockOp } from "@enesis/editor"
-
-const reordered = moveBlock(blocks, 0, 2) // move block 0 to index 2
-```
-
 ## Development
 
 ```bash

+ 55 - 0
packages/editor/src/lib/__tests__/parse-pipeline.perf.test.ts

@@ -0,0 +1,55 @@
+import { EditorState } from "prosemirror-state"
+import { EditorView } from "prosemirror-view"
+import { describe, test, expect } from "vitest"
+import { schema } from "@/lib/schema"
+import { createMarkdownDecorationsPlugin } from "@/composables/useMarkdownDecorations"
+
+const ITERATIONS = 50
+
+const SAMPLES: Record<string, string> = {
+  plain: "hello world",
+  bold: "this is **bold** and **more bold** text",
+  mixed:
+    "**bold** *italic* `code` ~~strike~~ ^^highlight^^ and [[Page Ref]] ((block-id)) #tag",
+  kitchenSink: [
+    "# Documentation\n\n",
+    "This is a **paragraph** with *multiple* `inline` ~~styles~~.\n\n",
+    "## Features\n\n",
+    "> [!TIP] Use callouts for **important** notes\n\n",
+    "TODO Add [[Page Reference]] and ((block-123)) support\n\n",
+    "priority:: high\nauthor:: John\n",
+  ].join(""),
+}
+
+function makeDoc(content: string) {
+  const textNode = content ? schema.text(content) : undefined
+  const para = textNode
+    ? schema.node("paragraph", {}, [textNode])
+    : schema.node("paragraph")
+  return schema.node("doc", {}, [para])
+}
+
+describe("per-keystroke pipeline budget", () => {
+  for (const [name, content] of Object.entries(SAMPLES)) {
+    test(`${name} averages under 16ms`, () => {
+      const { plugin } = createMarkdownDecorationsPlugin()
+      const state = EditorState.create({
+        doc: makeDoc(content),
+        schema,
+        plugins: [plugin],
+      })
+      const view = new EditorView(document.createElement("div"), { state })
+
+      const start = performance.now()
+      for (let i = 0; i < ITERATIONS; i++) {
+        const pos = view.state.doc.content.size
+        view.dispatch(view.state.tr.insertText("x", pos))
+      }
+      const avg = (performance.now() - start) / ITERATIONS
+
+      view.destroy()
+
+      expect(avg).toBeLessThan(16)
+    })
+  }
+})