Bladeren bron

feat(editor): replace drag grip with draggable bullet and zone-based drop

- Merge grip handle and bullet into a single draggable dot per block
- Change bullet from hidden-on-hover to always-visible with color
  transitions (muted → primary on hover, primary + size bump on drag)
- Move drop targets from block wrappers to insertion zones,
  highlighting the zone with a primary-colored bar on dragover
- Add @dragover.prevent to editor-body for consistent move cursor
- Add blank line separation in editor-block demo page
- Update READMEs (root, editor package, dev app) to reflect
  new drag/drop UX and correct stale page listings
- Update TESTING.md with detailed drag/drop scenarios
Zander Hawke 2 dagen geleden
bovenliggende
commit
10ef13a829
6 gewijzigde bestanden met toevoegingen van 358 en 67 verwijderingen
  1. 3 3
      README.md
  2. 288 0
      TESTING.md
  3. 13 23
      apps/dev/README.md
  4. 1 0
      apps/dev/src/pages/editor-block.vue
  5. 9 4
      packages/editor/README.md
  6. 44 37
      packages/editor/src/components/Editor.vue

+ 3 - 3
README.md

@@ -65,9 +65,9 @@ Enesis is being built as a complete block-editing substrate:
 │       ├── src/
 │       │   ├── index.ts               Public API (Block + Editor + EditorToolbar components)
 │       │   ├── components/
-│       │   │   ├── EditorBlock.vue     Self-contained ProseMirror block editor
-│       │   │   ├── Editor.vue          Multi-block shell with insertion zones, undo/redo
-│       │   │   ├── EditorInsertionZone.vue   32px hit target between blocks, hover-reveal
+│   │   │   ├── EditorBlock.vue     Self-contained ProseMirror block editor with draggable bullet
+│   │   │   ├── 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
 │       │   ├── composables/
 │       │   │   ├── useMarkdownDecorations.ts   ProseMirror decoration plugin

+ 288 - 0
TESTING.md

@@ -0,0 +1,288 @@
+# Manual Testing Checklist
+
+Comprehensive manual test scenarios organized by functional area. Each test targets either a feature not covered by automated tests, a cross-component interaction that is hard to isolate, or a visual/rendering concern requiring human inspection.
+
+---
+
+## 1. Block Management (Multi-Block Editor)
+
+- [ ] **1.1 Block creation** — Type in an empty block; verify new paragraph block appears
+- [ ] **1.2 Insertion zones** — Click between blocks; verify zone activates, typing creates a new block
+- [ ] **1.3 Insertion zone keyboard** — ArrowUp/Down into zone, Enter to create block, character key to create block with typed text
+- [ ] **1.4 Paste into insertion zone** — Paste multi-line content into an insertion zone; verify one block per line is created
+- [ ] **1.5 Block split (Enter)** — Press Enter mid-paragraph; verify block splits with cursor in new block
+- [ ] **1.6 Block split with Shift+Enter** — Press Shift+Enter; verify soft line break within same paragraph (not a block split)
+- [ ] **1.7 Block split (multi-paragraph)** — Type two paragraphs in one block, press Enter; verify only current paragraph splits
+- [ ] **1.8 Merge with Backspace** — Press Backspace at start of block; verify merges into previous block
+- [ ] **1.9 Delete-if-empty** — Press Backspace in an empty block; verify block is deleted
+- [ ] **1.10 Delete last remaining block** — Verify last block cannot be deleted (stays as empty paragraph)
+- [ ] **1.11 ArrowUp from first block** — Press ArrowUp at block start; verify no crash (emits boundary event)
+- [ ] **1.12 ArrowDown from last block** — Press ArrowDown at block end; verify no crash (emits boundary event)
+- [ ] **1.13 Drag bullet visible** — Verify each block has a visible bullet dot (always shown, not hidden)
+- [ ] **1.14 Bullet hover feedback** — Hover over a bullet; verify color changes from muted to primary
+- [ ] **1.15 Bullet drag feedback** — Start dragging a bullet; verify it turns primary color and grows slightly (size-6 → size-7)
+- [ ] **1.16 Drag-to-reorder** — Grab the bullet dot, drag to a zone line between two other blocks; verify block order changes
+- [ ] **1.17 Drop zone indicator** — During a drag, hover over an insertion zone; verify the zone shows a primary-colored bar
+- [ ] **1.18 Drop zone between blocks** — Drop a block into a zone between two blocks; verify it lands in between
+- [ ] **1.19 Drop zone at top/bottom** — Drop a block into the zone before the first block or after the last block; verify it lands at the edge
+- [ ] **1.20 Drag reorder undo** — Drag reorder, then undo; verify block order restores
+- [ ] **1.21 Indent (Tab)** — Press Tab; verify block indents (depth++)
+- [ ] **1.22 Outdent (Shift+Tab)** — Press Shift+Tab; verify block outdents (depth--)
+- [ ] **1.23 Indent cascade** — Indent a parent block; verify all child blocks also indent
+- [ ] **1.24 Outdent cascade** — Outdent; verify children follow
+- [ ] **1.25 Indent max depth** — Indent repeatedly to depth 10; verify clamped
+- [ ] **1.26 Outdent min depth** — Outdent at depth 0; verify stays at depth 0
+
+---
+
+## 2. Inline Formatting (Markdown Decorations)
+
+- [ ] **2.1 Bold `**text**`** — Type `**bold**`; verify bold text rendered with hidden markers
+- [ ] **2.2 Italic `*text*`** — Type `*italic*`; verify italic rendered
+- [ ] **2.3 Strikethrough `~~text~~`** — Type `~~strike~~`; verify strikethrough rendered
+- [ ] **2.4 Highlight `^^text^^`** — Type `^^highlight^^`; verify highlight rendered
+- [ ] **2.5 Inline Code `` `code` ``** — Type `` `code` ``; verify inline code rendered
+- [ ] **2.6 Nested formatting** — Type `***italicbold***`, `**bold `code`**`; verify both formats apply
+- [ ] **2.7 Link `[text](url)`** — Type `[link](https://example.com)`; verify link rendered, URL hidden
+- [ ] **2.8 Unmatched delimiters** — Type `**unmatched`; verify renders as literal text
+- [ ] **2.9 Combined bold+link** — Type `**[text](url)**`; verify bold linked text
+
+---
+
+## 3. References & Tags
+
+- [ ] **3.1 Page ref `[[Name]]`** — Type `[[Page Name]]`; verify rendered as page reference
+- [ ] **3.2 Page ref with alias `[[Real|Display]]`** — Type `[[Real Page|Display Name]]`; verify alias rendered
+- [ ] **3.3 Block ref `((block-id))`** — Type `((block-id-1234))`; verify block reference rendered
+- [ ] **3.4 Tag `#tag` at word boundary** — Type `#tag` after space; verify tag rendered
+- [ ] **3.5 Tag NOT in middle of word** — Type `some#tag`; verify NOT rendered as tag
+
+---
+
+## 4. Suggestion Menu (Pattern Plugin)
+
+- [ ] **4.1 Slash `/` menu opens** — Type `/`; verify suggestion menu appears at cursor position
+- [ ] **4.2 Slash menu filters by query** — Type `/he`; verify menu filters to heading options
+- [ ] **4.3 Slash menu keyboard nav** — ArrowDown/ArrowUp through items; verify highlight moves and wraps at ends
+- [ ] **4.4 Slash menu Enter selects** — Select "Heading 1"; verify `# ` inserted
+- [ ] **4.5 Slash menu Escape cancels** — Press Escape; verify menu closes with no text change
+- [ ] **4.6 `[[` menu opens** — Type `[[`; verify page reference menu appears
+- [ ] **4.7 `[[` menu Enter selects** — Select a page; verify `[[Page Name]]` inserted
+- [ ] **4.8 `((` menu opens** — Type `((`; verify block embed menu appears
+- [ ] **4.9 `((` menu completion** — Select a block; verify `((block-id))` inserted
+- [ ] **4.10 `#` tag menu** — Type `#`; verify tag completion menu appears
+- [ ] **4.11 Menu position on scroll** — Scroll the page; verify menu repositions to follow cursor
+- [ ] **4.12 Menu closes on completion** — After selecting an item; verify menu closes
+
+---
+
+## 5. Toolbar
+
+- [ ] **5.1 Toolbar disabled without focus** — No block focused; verify toolbar buttons disabled
+- [ ] **5.2 Bold button** — Select text, click Bold; verify `**text**` wraps selection
+- [ ] **5.3 Italic button** — Select text, click Italic; verify `*text*` wraps
+- [ ] **5.4 Code button** — Select text, click Code; verify `` `text` `` wraps
+- [ ] **5.5 Strikethrough button** — Select text, click Strike; verify `~~text~~` wraps
+- [ ] **5.6 Highlight button** — Select text, click Highlight; verify `^^text^^` wraps
+- [ ] **5.7 Heading dropdown** — Select heading level; verify `# ` prefix inserted
+- [ ] **5.8 Link button** — Click link, modal opens, enter URL, Enter; verify `[selected](url)`
+- [ ] **5.9 Page ref button** — Click, modal, enter name, Enter; verify `[[name]]`
+- [ ] **5.10 Block ref button** — Click, modal, enter ID, Enter; verify `((id))`
+- [ ] **5.11 Tag button** — Click, modal, enter tag, Enter; verify `#tag`
+- [ ] **5.12 Inline math button** — Click; verify `$ $` inserted with cursor inside
+- [ ] **5.13 Block math button** — Click; verify `$$\n\n$$` inserted with cursor between
+- [ ] **5.14 Escape closes toolbar modal** — Open any modal, press Escape; verify modal closes
+
+---
+
+## 6. Auto-Close Plugin
+
+- [ ] **6.1 Parentheses** — Type `(`; verify `()` with cursor between
+- [ ] **6.2 Square brackets** — Type `[`; verify `[]` with cursor between
+- [ ] **6.3 Curly braces** — Type `{`; verify `{}` with cursor between
+- [ ] **6.4 Double quotes** — Type `"` after space; verify `""` with cursor between
+- [ ] **6.5 Double quotes mid-word** — Type `"` in middle of word; verify NOT auto-closed
+- [ ] **6.6 Single quotes** — Type `'` after space; verify `''`
+- [ ] **6.7 Wrap selection in parens** — Select text, type `(`; verify `(selected)`
+- [ ] **6.8 Skip-over close** — Cursor between `()`, type `)`; verify cursor moves past `)`
+- [ ] **6.9 Delete pair (Backspace)** — Cursor between `()`, press Backspace; verify both parens removed
+- [ ] **6.10 Bold auto-close `**`** — Type `*` then `*` after word; verify `****` with cursor between
+- [ ] **6.11 Strike auto-close `~~`** — Type `~~`; verify `~~~~` with cursor between
+- [ ] **6.12 Highlight auto-close `^^`** — Type `^^`; verify `^^^^` with cursor between
+- [ ] **6.13 Inline math auto-close `$`** — Type `$` at word boundary; verify `$$` with cursor between
+- [ ] **6.14 Block math `$$` conversion** — Type `$$` at line start; verify converts to math_block
+- [ ] **6.15 Code fence `` ``` ``** — Type `` ``` `` at line start; verify converts to code_block
+- [ ] **6.16 Underscore italic `_`** — Type `_` at word boundary; verify `__` with cursor between; NOT mid-word
+
+---
+
+## 7. Headings
+
+- [ ] **7.1 H1 through H6** — Type `#` through `######`; verify heading rendered with correct level
+- [ ] **7.2 Leading whitespace before `#`** — Type ` # heading`; verify heading still recognized
+- [ ] **7.3 Heading in middle of paragraph** — Type `text # not heading`; verify NOT rendered as heading
+- [ ] **7.4 Heading live preview** — Focus on a heading; verify hash marks hidden/faded; blur, verify hash marks visible
+
+---
+
+## 8. Tasks
+
+- [ ] **8.1 Task state TODO** — Type `TODO task text`; verify task rendered with TODO indicator
+- [ ] **8.2 Task state DOING** — Type `DOING task`; verify DOING indicator
+- [ ] **8.3 Task state DONE** — Type `DONE task`; verify DONE indicator
+- [ ] **8.4 Task state LATER** — Type `LATER task`; verify LATER indicator
+- [ ] **8.5 Task state NOW** — Type `NOW task`; verify NOW indicator
+- [ ] **8.6 Task state WAITING** — Type `WAITING task`; verify WAITING indicator
+- [ ] **8.7 Task state CANCELLED** — Type `CANCELLED task`; verify CANCELLED indicator
+- [ ] **8.8 Task toggle via toolbar** — Click task toggle button; verify state cycles through TODO → DOING → DONE
+
+---
+
+## 9. Blockquotes & Callouts
+
+- [ ] **9.1 Blockquote `> text`** — Type `> quote`; verify blockquote rendered
+- [ ] **9.2 Callout `> [!NOTE]`** — Type `> [!NOTE] Description`; verify callout with NOTE style
+- [ ] **9.3 Callout `> [!WARNING]`** — Verify WARNING style
+- [ ] **9.4 Callout `> [!TIP]`** — Verify TIP style
+- [ ] **9.5 Callout `> [!DANGER]`** — Verify DANGER style
+- [ ] **9.6 Callout `> [!INFO]`** — Verify INFO style
+- [ ] **9.7 Callout continuation lines** — Type two lines: `> [!NOTE] header` then `> continuation`; verify both styled as callout
+- [ ] **9.8 Blockquote continuation** — Multiple `> ` lines; verify they merge into one blockquote
+
+---
+
+## 10. Tables
+
+- [ ] **10.1 Simple table rendering** — Type `| A | B |\n|---|---|\n| 1 | 2 |`; verify rendered as table when blurred
+- [ ] **10.2 Click to focus and edit** — Click rendered table; verify block focuses, pipe syntax becomes editable
+- [ ] **10.3 Table in live preview** — Verify pipes hidden/faded when focused, fully visible when blurred
+- [ ] **10.4 Table header row** — Verify header row recognized and styled differently
+
+---
+
+## 11. Horizontal Rules
+
+- [ ] **11.1 HR `---`** — Type `---` on its own line; verify horizontal rule rendered
+- [ ] **11.2 HR `***`** — Type `***`; verify horizontal rule rendered (odd-length `***`)
+- [ ] **11.3 HR `----`** — Type `----`; verify HR (even-length also valid)
+- [ ] **11.4 NOT HR `*****`** — Type `*****`; verify NOT HR (recognized as bold)
+
+---
+
+## 12. Properties
+
+- [ ] **12.1 Property at line start** — Type `key:: value`; verify key styled as property, value visible
+- [ ] **12.2 Property NOT mid-line** — Type `not a key:: value`; verify NOT rendered as property
+- [ ] **12.3 Multiple properties** — Multiple `key:: value` lines; verify each styled correctly
+
+---
+
+## 13. Code Blocks
+
+- [ ] **13.1 Create via triple backtick** — Type `` ``` `` at line start; verify code block created
+- [ ] **13.2 Language detection** — Type `` ```typescript ``; verify language detected with correct syntax highlighting
+- [ ] **13.3 Language switching** — Click language dropdown, change from JS to Python; verify highlighting updates
+- [ ] **13.4 Typing inside code block** — Type code; verify CM6 editor handles keystrokes, PM sees fence-wrapped content
+- [ ] **13.5 ArrowUp at code block start** — Cursor at first line, press ArrowUp; verify exits to previous block
+- [ ] **13.6 ArrowDown at code block end** — Cursor at last line, press ArrowDown; verify exits to next block
+- [ ] **13.7 Backspace empty code block** — Delete all content, press Backspace; verify block removed
+- [ ] **13.8 Shift+Enter in code block** — Press Shift+Enter; verify new paragraph inserted after code block
+- [ ] **13.9 Code block with no language** — Type `` ``` `` alone; verify code block with no syntax highlighting
+- [ ] **13.10 Tilde fences `~~~`** — Type `~~~js`; verify code block created with tilde fences
+
+---
+
+## 14. Math Blocks
+
+- [ ] **14.1 Create via `$$`** — Type `$$` at line start; verify math_block created
+- [ ] **14.2 KaTeX rendering** — Type `$$\nE=mc^2\n$$`; verify rendered LaTeX preview
+- [ ] **14.3 Inline math `$x^2$`** — Type `$x^2+1$`; verify inline KaTeX rendered when blurred
+- [ ] **14.4 Inline math editing** — Focus on inline math; verify raw `$math$` visible and editable
+- [ ] **14.5 Math block empty** — Create empty math block; verify no render errors
+- [ ] **14.6 Navigation at math block boundaries** — ArrowUp/Down at math block edges; verify focus moves correctly
+
+---
+
+## 15. Undo / Redo
+
+- [ ] **15.1 Typing undo** — Type text, press Ctrl+Z (or ⌘Z); verify text reverted
+- [ ] **15.2 Typing redo** — After undo, press Ctrl+Shift+Z; verify text restored
+- [ ] **15.3 Block split undo** — Split a block with Enter, then undo; verify blocks merge back
+- [ ] **15.4 Block merge undo** — Merge with backspace, then undo; verify split restored
+- [ ] **15.5 Block indent undo** — Indent with Tab, then undo; verify depth restored
+- [ ] **15.6 Block delete undo** — Delete an empty block, undo; verify block reappears
+- [ ] **15.7 History coalescing** — Type rapidly; verify single undo step reverts all, not character-by-character
+- [ ] **15.8 History separate for pause** — Type, pause >500ms, type more; verify two separate undo steps
+- [ ] **15.9 Toolbar undo/redo buttons** — Click undo/redo buttons; verify same behavior as keyboard
+
+---
+
+## 16. Live Preview & Marker Visibility
+
+- [ ] **16.1 Markers hidden when blurred** — Click outside editor; verify `**`, `*`, `` ` ``, `~~`, `^^` markers are hidden
+- [ ] **16.2 Markers visible near cursor** — Click inside formatted text; verify markers fade or show near cursor
+- [ ] **16.3 `always-visible` mode** — Set `markerMode="always-visible"`; verify markers never hide
+- [ ] **16.4 `live-preview` mode default** — Default mode; verify markers hide on blur
+- [ ] **16.5 Table source hidden on blur** — Blur a table block; verify raw pipe syntax hidden, rendered table shown
+- [ ] **16.6 Inline math KaTeX on blur** — Blur inline math; verify rendered math shown, `$` hidden
+
+---
+
+## 17. Paste Handling
+
+- [ ] **17.1 Single-line paste** — Paste one line of text; verify it stays in current block
+- [ ] **17.2 Multi-line paste (same type)** — Paste multiple paragraphs; verify they stay in current block
+- [ ] **17.3 Multi-line paste (mixed types)** — Paste `para\npara\n> blockquote`; verify block splits with each type in own block
+- [ ] **17.4 Paste with code fences** — Paste text containing `` ``` ``; verify code block created correctly
+
+---
+
+## 18. Focus Management
+
+- [ ] **18.1 Focus one block at a time** — Click different blocks; verify only one block has focus
+- [ ] **18.2 Focus registry tracking** — Check focusedId updates as different blocks are clicked
+- [ ] **18.3 Column-preserving navigation** — ArrowUp/Down between blocks; verify cursor aligns to same X coordinate
+- [ ] **18.4 Programmatic focus** — Set cursorPosition prop; verify block focuses at correct position
+
+---
+
+## 19. Extensibility & Plugins
+
+- [ ] **19.1 Custom pattern triggers** — Add `extraPatterns` prop; verify custom trigger opens suggestion menu
+- [ ] **19.2 Custom inline rules** — Register `extraInlineRules`; verify new syntax is decorated
+- [ ] **19.3 Custom NodeViews** — Register custom nodeView; verify it renders correctly
+
+---
+
+## 20. Theme System
+
+- [ ] **20.1 Default theme** — Verify default CSS variables applied
+- [ ] **20.2 Kanagawa theme** — Apply kanagawa preset; verify colors change
+- [ ] **20.3 Custom theme** — Provide custom ThemeInput; verify CSS variables override
+- [ ] **20.4 Theme switching at runtime** — Switch themes; verify all components update instantly
+
+---
+
+## 21. Debug Logging
+
+- [ ] **21.1 Global debug `*`** — Set `localStorage.setItem("editor:debug", "*")`; verify all namespaces logged
+- [ ] **21.2 Filtered namespaces** — Set `"Block,ParseEngine"`; verify only those namespaces logged
+- [ ] **21.3 Per-component debug** — Set `debug` prop on EditorBlock; verify scoped filtering
+
+---
+
+## 22. Error Recovery
+
+- [ ] **22.1 EditorBlock mount failure** — Deliberately break state; verify block falls back to textarea
+- [ ] **22.2 Invalid markdown** — Type extremely malformed markdown; verify no crash
+- [ ] **22.3 Empty document** — Start with empty markdown; verify editor renders one empty paragraph block
+
+---
+
+## 23. Edge Cases & Cross-Cutting
+
+- [ ] **23.1 Unicode/emoji in content** — Type emoji and CJK characters; verify roundtrip through parse/serialize
+- [ ] **23.2 Very long content in a block** — Type >1000 characters in one block; verify no performance regression
+- [ ] **23.3 Rapid typing** — Type very fast; verify coalescing works with no duplicate blocks
+- [ ] **23.4 Keyboard shortcut collision** — Verify Mod+b (bold) does not conflict with browser behavior
+- [ ] **23.5 Vim keybindings** — Switch to vim bindings; verify `h/j/k/l` navigation within block

+ 13 - 23
apps/dev/README.md

@@ -17,38 +17,28 @@ Opens at `http://localhost:5173`.
 | Page | Route | Features Demonstrated |
 |---|---|---|
 | Overview | `/` | Key concepts, Block + Editor API tables (props + events) |
-| Basic Editing | `/basic-editing` | Headings `#`–`######`, paragraphs, horizontal rules |
-| Inline Marks | `/inline-marks` | Bold, italic, inline code, strikethrough, highlight, combined marks |
-| Tasks | `/tasks` | Task states (TODO→DONE cycle) |
-| Blockquotes & Callouts | `/blockquotes` | Blockquote `>`, 5 callout types (NOTE, WARNING, TIP, DANGER, INFO) |
-| Links | `/links` | Standard markdown `[text](url)` links |
-| Page Refs & Tags | `/refs-tags` | Page refs `[[Page]]`, block refs `((id))`, tags `#tag` |
-| Code Blocks | `/code-blocks` | Fenced code blocks with language labels and CM6 syntax highlighting |
-| Properties & Dates | `/properties` | `key:: value` properties, date parsing |
-| Math | `/math` | Inline `$...$`, display `$$...$$`, mixed content |
-| Editor Shell | `/editor-shell` | Multi-block Editor component with insertion zones, split/merge |
+| Editor Shell | `/editor` | Multi-block Editor with insertion zones, split/merge, drag-to-reorder, undo/redo |
+| Block Demo | `/editor-block` | Standalone EditorBlock with all syntax features |
 | Toolbar | `/toolbar` | Shared EditorToolbar bound to active block via `#toolbar` slot |
+| Suggestion Menu | `/suggestion-menu` | Slash-command and mention pattern menu demo |
 | Themes | `/themes` | CSS-variable theme presets and overrides |
 
-Each page features multiple `<LiveExample>` sections — editable `<Block>` instances with a collapsible source code panel.
+Each page features multiple live, editable examples with a collapsible source code panel.
 
 ## Architecture
 
 ```
-App.vue                     Shell layout — UHeader + UPage + sidebar (UNavigationMenu)
+App.vue                     Shell layout — UHeader + UPage + sidebar navigation
 ├── pages/index.vue         Overview
-├── pages/basic-editing.vue
-├── pages/inline-marks.vue
-├── pages/tasks.vue
-├── pages/blockquotes.vue
-├── pages/links.vue
-├── pages/refs-tags.vue
-├── pages/code-blocks.vue
-├── pages/properties.vue
-└── pages/math.vue
+├── pages/editor.vue        Multi-block Editor shell
+├── pages/editor-block.vue  Standalone EditorBlock demo
+├── pages/toolbar.vue       Toolbar integration
+├── pages/suggestion-menu.vue  Pattern completion menu
+└── pages/themes.vue        Theme presets
 
 components/
-└── LiveExample.vue         <Block> wrapper with title, description, source code panel
+├── LiveExample.vue         <Block> wrapper with title, description, source code panel
+└── AppLogo.vue             Enesis brand mark
 ```
 
 The dev app resolves `@enesis/editor` directly to source (`packages/editor/src/index.ts`) via Vite aliases, enabling hot-reload during development.
@@ -57,7 +47,7 @@ The dev app resolves `@enesis/editor` directly to source (`packages/editor/src/i
 
 - **Framework:** Vue 3 + Vite + TypeScript (`strict: true`)
 - **UI Library:** Nuxt UI v4 (UNavigationMenu, UPage, UHeader, UColorModeButton, UCard, etc.)
-- **Routing:** Vue Router — 10 routes, one per feature page
+- **Routing:** Vue Router — 6 routes, one per feature page
 - **Styling:** Tailwind CSS v4 with Nuxt UI theme
 - **Type Checking:** `vue-tsc` (pre-existing type errors in the editor package do not block the dev server)
 - **Icons:** Iconify (Lucide)

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

@@ -8,6 +8,7 @@ const content = ref(`# Heading 1
 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

+ 9 - 4
packages/editor/README.md

@@ -192,9 +192,6 @@ Formatting is purely decorative — markdown syntax is stored as plain text in P
 | Display Math | `$$...$$` | MathBlock node view (+ KaTeX preview) |
 | Table | `\| A \| B \|` / `\|---|---\|` / `\| 1 \| 2 \|` | Blurred: rendered HTML widget via `renderCellContent()`. Focused: \`\|\` decorators, editable raw source. |
 | Fenced Code Block | `` ```python `` / `~~~js` | CM6 node view |
-| Highlight | `^^text^^` | `md-highlight` |
-
-
 
 ### Table Rendering
 
@@ -340,9 +337,10 @@ Each pattern spec defines:
 
 ## Drag-to-Reorder
 
-Each block has a draggable handle (`i-lucide-grip-vertical`) in its left gutter that appears on hover. Drag it to reorder blocks. A plus button also appears on hover to insert a new empty block at that position.
+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.
 
 - **Native HTML5 drag** — no external drag library required
+- **Zone-based drop** — drop on any insertion zone, not on the block itself
 - **Undo support** — each drag records a `MoveBlockOp` in the shell's `OperationHistory` (inverse swaps `fromIndex` / `toIndex`)
 - **`moveBlock(blocks, fromIndex, toIndex)`** — pure function in `editor-operations.ts`
 - **`MoveBlockOp`** — operation type with `fromIndex`, `toIndex`, `blockId`, `content`, `depth`, and `timestamp`
@@ -354,6 +352,13 @@ 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

+ 44 - 37
packages/editor/src/components/Editor.vue

@@ -107,7 +107,7 @@ const dragState = ref<{
   content: string
   depth: number
 } | null>(null)
-const dropTargetIndex = ref<number | null>(null)
+const dropZoneIndex = ref<number | null>(null)
 
 function onDragStart(
   index: number,
@@ -123,31 +123,42 @@ function onDragStart(
   }
 }
 
-function onDragOver(index: number, event: DragEvent) {
-  if (!dragState.value || dragState.value.fromIndex === index) {
-    dropTargetIndex.value = null
+function onDragOverZone(zoneIndex: number, event: DragEvent) {
+  if (!dragState.value) {
+    dropZoneIndex.value = null
     return
   }
-  dropTargetIndex.value = index
+  dropZoneIndex.value = zoneIndex
   event.dataTransfer!.dropEffect = "move"
 }
 
-function onDragLeave(index: number) {
-  if (dropTargetIndex.value === index) {
-    dropTargetIndex.value = null
+function onDragLeaveZone(zoneIndex: number) {
+  if (dropZoneIndex.value === zoneIndex) {
+    dropZoneIndex.value = null
   }
 }
 
 function onDragEnd() {
   dragState.value = null
-  dropTargetIndex.value = null
+  dropZoneIndex.value = null
 }
 
-function onDrop(targetIndex: number) {
+function onDropOnZone(zoneIndex: number) {
   const state = dragState.value
-  if (!state || state.fromIndex === targetIndex) {
+  if (!state) {
     dragState.value = null
-    dropTargetIndex.value = null
+    dropZoneIndex.value = null
+    return
+  }
+
+  // Zone i = insert before block i.  In post-removal coordinates:
+  // if fromIndex < zoneIndex, the zone shifts left by 1.
+  const targetIndex =
+    state.fromIndex < zoneIndex ? zoneIndex - 1 : zoneIndex
+
+  if (state.fromIndex === targetIndex) {
+    dragState.value = null
+    dropZoneIndex.value = null
     return
   }
 
@@ -166,7 +177,7 @@ function onDrop(targetIndex: number) {
   onBlockContentChange()
 
   dragState.value = null
-  dropTargetIndex.value = null
+  dropZoneIndex.value = null
 }
 
 // ── Undo / Redo history ──────────────────────────────────────────
@@ -718,46 +729,38 @@ defineExpose({
         :redo="redo"
       />
     </div>
-    <div class="editor-body">
+    <div class="editor-body" @dragover.prevent>
       <template v-for="(block, i) in blocks" :key="block.id">
         <EditorInsertionZone
         :ref="(el: any) => { if (el) zoneRefs[i] = el; else delete zoneRefs[i] }"
+        :class="dropZoneIndex === i ? 'bg-(--ui-primary) rounded' : ''"
         @activate="(content: string) => onZoneActivate(i, content)"
         @focus="onZoneFocus(i)"
         @blur="onZoneBlur"
         @navigate-up="onZoneNavigateUp(i)"
         @navigate-down="onZoneNavigateDown(i)"
+        @dragover.prevent="onDragOverZone(i, $event)"
+        @dragleave="onDragLeaveZone(i)"
+        @drop="onDropOnZone(i)"
       />
       <div
         class="flex items-start group/block"
         :data-block-index="i"
-        @dragover.prevent="onDragOver(i, $event)"
-        @dragleave="onDragLeave(i)"
-        @drop="onDrop(i)"
+      >
+      <div
+        class="flex items-center justify-end h-7 select-none"
+        :style="{ width: (block.depth + 1) * 20 + 'px' }"
       >
         <div
-          class="flex items-center justify-end h-7 select-none"
-          :class="dropTargetIndex === i ? 'bg-(--ui-primary)/10 rounded-l' : ''"
-          :style="{ width: (block.depth + 1) * 20 + 'px' }"
+          draggable="true"
+          class="flex items-center justify-center cursor-grab active:cursor-grabbing rounded transition-all"
+          :class="dragState?.blockId === block.id ? 'size-7 text-(--ui-primary)' : 'size-6 text-(--ui-text-muted) hover:text-(--ui-primary)'"
+          @dragstart="onDragStart(i, block.id, block.content, block.depth, $event)"
+          @dragend="onDragEnd"
         >
-          <div
-            draggable="true"
-            class="flex items-center justify-center size-6 opacity-0 group-hover/block:opacity-100 cursor-grab active:cursor-grabbing rounded hover:bg-(--ui-bg-elevated) transition-opacity"
-            @dragstart="onDragStart(i, block.id, block.content, block.depth, $event)"
-            @dragend="onDragEnd"
-          >
-            <UIcon name="i-lucide-grip-vertical" class="size-4 text-(--ui-text-muted)" />
-          </div>
-          <UButton
-            icon="i-lucide-plus"
-            color="neutral"
-            variant="ghost"
-            size="xs"
-            class="opacity-0 group-hover/block:opacity-100 size-5 min-w-0 p-0 transition-opacity"
-            :ui="{ base: 'size-5' }"
-            @click="onZoneActivate(i, '')"
-          />
+          <UIcon name="i-ph-dot-duotone" class="size-6" />
         </div>
+      </div>
 
         <EditorBlock
           :ref="(el: any) => { if (el) blockRefs.set(block.id, el); else blockRefs.delete(block.id) }"
@@ -792,11 +795,15 @@ defineExpose({
     </template>
     <EditorInsertionZone
       :ref="(el: any) => { if (el) zoneRefs[blocks.length] = el; else delete zoneRefs[blocks.length] }"
+      :class="dropZoneIndex === blocks.length ? 'bg-(--ui-primary) rounded' : ''"
       @activate="(content: string) => onZoneActivate(blocks.length, content)"
       @focus="onZoneFocus(blocks.length)"
       @blur="onZoneBlur"
       @navigate-up="onZoneNavigateUp(blocks.length)"
       @navigate-down="onZoneNavigateDown(blocks.length)"
+      @dragover.prevent="onDragOverZone(blocks.length, $event)"
+      @dragleave="onDragLeaveZone(blocks.length)"
+      @drop="onDropOnZone(blocks.length)"
     />
     </div>
   </div>