@voithos-labs/aragonite 0.10.2 → 0.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/README.md +6 -21
  2. package/THIRD-PARTY-NOTICES.md +25 -0
  3. package/dist/a11y-strings.d.ts +19 -0
  4. package/dist/a11y-strings.js +19 -0
  5. package/dist/action-contracts.d.ts +7 -1
  6. package/dist/ambient/ambient-dom.js +5 -1
  7. package/dist/block-component.d.ts +14 -0
  8. package/dist/components/BlockDragHandle.svelte +37 -24
  9. package/dist/components/BlockHost.svelte +18 -11
  10. package/dist/components/Editor.svelte +434 -198
  11. package/dist/components/Editor.svelte.d.ts +1 -1
  12. package/dist/components/SelectionOverlay.svelte +25 -17
  13. package/dist/components/SelectionOverlay.svelte.d.ts +1 -1
  14. package/dist/components/TailInsert.svelte +107 -0
  15. package/dist/components/TailInsert.svelte.d.ts +17 -0
  16. package/dist/components/block-content-selector.d.ts +6 -2
  17. package/dist/components/block-content-selector.js +6 -2
  18. package/dist/components/blocks/ThematicBreakBlock.svelte +19 -6
  19. package/dist/components/blocks/ThematicBreakBlock.svelte.d.ts +1 -0
  20. package/dist/components/blocks/code/CodeBlock.svelte +211 -30
  21. package/dist/components/blocks/code/CodeBlockRail.svelte +704 -0
  22. package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
  23. package/dist/components/blocks/code/code-bootstrap.js +16 -0
  24. package/dist/components/blocks/code/code-context-actions.d.ts +1 -0
  25. package/dist/components/blocks/code/code-context-actions.js +24 -0
  26. package/dist/components/blocks/code/code-fence-exit.d.ts +15 -0
  27. package/dist/components/blocks/code/code-fence-exit.js +26 -0
  28. package/dist/components/blocks/code/code-languages.d.ts +6 -0
  29. package/dist/components/blocks/code/code-languages.js +22 -3
  30. package/dist/components/blocks/code/code-renderer.js +11 -0
  31. package/dist/components/blocks/directive/DirectiveContainerBlock.svelte +1 -1
  32. package/dist/components/blocks/editable-leaf.d.ts +37 -6
  33. package/dist/components/blocks/editable-leaf.js +248 -30
  34. package/dist/components/blocks/editable-surface.d.ts +9 -0
  35. package/dist/components/blocks/editable-surface.js +22 -3
  36. package/dist/components/blocks/list/ListBlock.svelte +1 -0
  37. package/dist/components/blocks/list/ListItemBlock.svelte +14 -5
  38. package/dist/components/blocks/list/ListItemBlock.svelte.d.ts +2 -0
  39. package/dist/components/blocks/list/task-checkbox.d.ts +2 -0
  40. package/dist/components/blocks/list/task-checkbox.js +11 -2
  41. package/dist/components/blocks/surface-wiring.svelte.d.ts +4 -0
  42. package/dist/components/blocks/surface-wiring.svelte.js +8 -1
  43. package/dist/components/blocks/table/TableActionMenu.svelte +210 -86
  44. package/dist/components/blocks/table/TableActionMenu.svelte.d.ts +5 -0
  45. package/dist/components/blocks/table/TableBlock.svelte +205 -186
  46. package/dist/components/blocks/table/TableBlock.svelte.d.ts +1 -0
  47. package/dist/components/blocks/table/TableCellBlock.svelte +87 -22
  48. package/dist/components/blocks/table/TableRowBlock.svelte +4 -18
  49. package/dist/components/blocks/table/TableRowBlock.svelte.d.ts +0 -5
  50. package/dist/components/blocks/table/cell-clipboard.d.ts +10 -0
  51. package/dist/components/blocks/table/cell-clipboard.js +34 -1
  52. package/dist/components/blocks/table/cell-keydown-plan.d.ts +1 -1
  53. package/dist/components/blocks/table/cell-keydown-plan.js +3 -1
  54. package/dist/components/blocks/table/table-cell-paste.js +2 -1
  55. package/dist/components/blocks/table/table-menu-model.d.ts +35 -8
  56. package/dist/components/blocks/table/table-menu-model.js +36 -17
  57. package/dist/components/blocks/text/TextEditableBlock.svelte +58 -11
  58. package/dist/components/blocks/text/click-snap-guard.d.ts +3 -0
  59. package/dist/components/blocks/text/click-snap-guard.js +8 -0
  60. package/dist/components/blocks/text/delimiter-autopair.d.ts +71 -0
  61. package/dist/components/blocks/text/delimiter-autopair.js +216 -0
  62. package/dist/components/blocks/text/edge-policy-dispatch.d.ts +4 -0
  63. package/dist/components/blocks/text/edge-policy-dispatch.js +89 -20
  64. package/dist/components/blocks/text/live-selection-edit.d.ts +5 -5
  65. package/dist/components/blocks/text/live-selection-edit.js +37 -7
  66. package/dist/components/blocks/text/text-clipboard.js +2 -2
  67. package/dist/components/blocks/text/text-keydown.d.ts +7 -1
  68. package/dist/components/blocks/text/text-keydown.js +11 -1
  69. package/dist/components/blocks/text/text-render.d.ts +1 -1
  70. package/dist/components/blocks/text/text-render.js +3 -1
  71. package/dist/components/blocks/text/widget-interaction.d.ts +3 -0
  72. package/dist/components/blocks/text/widget-interaction.js +181 -37
  73. package/dist/components/drag-handle.d.ts +41 -0
  74. package/dist/components/drag-handle.js +134 -0
  75. package/dist/components/editor-root-focus.d.ts +19 -0
  76. package/dist/components/editor-root-focus.js +67 -0
  77. package/dist/components/editor-root-geometry.d.ts +39 -0
  78. package/dist/components/editor-root-geometry.js +91 -0
  79. package/dist/components/editor-root-keydown.d.ts +1 -1
  80. package/dist/components/editor-root-keydown.js +12 -3
  81. package/dist/components/editor-root-listeners.d.ts +3 -6
  82. package/dist/components/editor-root-listeners.js +3 -24
  83. package/dist/components/editor-root-mode-flip.d.ts +36 -0
  84. package/dist/components/editor-root-mode-flip.js +92 -0
  85. package/dist/components/image/ImageOverlayHost.svelte +14 -6
  86. package/dist/components/image/ImageProperties.svelte +512 -70
  87. package/dist/components/image/ImageProperties.svelte.d.ts +6 -1
  88. package/dist/components/image/ImageResizeHandles.svelte +67 -34
  89. package/dist/components/image/image-crop.d.ts +39 -0
  90. package/dist/components/image/image-crop.js +74 -0
  91. package/dist/components/image/image-edit-commit.d.ts +1 -0
  92. package/dist/components/image/image-edit-commit.js +21 -5
  93. package/dist/components/image/image-source-bytes.js +10 -3
  94. package/dist/components/image/image-widget-editing.js +1 -0
  95. package/dist/components/image/widget-dom.js +5 -1
  96. package/dist/components/link-card/link-card-commit.js +1 -1
  97. package/dist/components/lrd-map-gate.js +1 -1
  98. package/dist/components/menu/BlockMenu.svelte +315 -0
  99. package/dist/components/menu/BlockMenu.svelte.d.ts +34 -0
  100. package/dist/components/menu/MenuIcon.svelte +153 -0
  101. package/dist/components/menu/MenuIcon.svelte.d.ts +51 -0
  102. package/dist/components/menu/SelectionToolbar.svelte +385 -0
  103. package/dist/components/menu/SelectionToolbar.svelte.d.ts +16 -0
  104. package/dist/components/menu/clipboard-actions.d.ts +12 -0
  105. package/dist/components/menu/clipboard-actions.js +42 -0
  106. package/dist/components/menu/default-context-actions.d.ts +15 -0
  107. package/dist/components/menu/default-context-actions.js +76 -0
  108. package/dist/components/menu/flyout-placement.d.ts +6 -0
  109. package/dist/components/menu/flyout-placement.js +25 -0
  110. package/dist/core/inline/format-toggle.d.ts +12 -4
  111. package/dist/core/inline/format-toggle.js +94 -40
  112. package/dist/core/inline/image-dimensions.d.ts +3 -0
  113. package/dist/core/inline/image-dimensions.js +46 -10
  114. package/dist/core/inline/inline-widgets.d.ts +19 -0
  115. package/dist/core/inline/inline-widgets.js +5 -0
  116. package/dist/core/inline/scan/brackets.js +1 -0
  117. package/dist/core/inline/scan/plugin-syntax.d.ts +8 -0
  118. package/dist/core/inline/scan/plugin-syntax.js +15 -1
  119. package/dist/core/inline/transparency.js +3 -3
  120. package/dist/core/inline-render.d.ts +6 -0
  121. package/dist/core/inline-render.js +32 -0
  122. package/dist/core/nodes.d.ts +14 -0
  123. package/dist/cursor/edge-affinity.js +2 -1
  124. package/dist/cursor/height-oracle.d.ts +2 -3
  125. package/dist/cursor/height-oracle.js +0 -1
  126. package/dist/cursor/overlay-remeasure.js +8 -0
  127. package/dist/cursor/reveal-source.js +7 -2
  128. package/dist/cursor/scroll-hold.d.ts +10 -0
  129. package/dist/cursor/scroll-hold.js +22 -0
  130. package/dist/cursor/scrollport.d.ts +9 -0
  131. package/dist/cursor/scrollport.js +30 -1
  132. package/dist/cursor/visual-lines.d.ts +5 -4
  133. package/dist/cursor/visual-lines.js +56 -11
  134. package/dist/cursor/widget-edge-snap.d.ts +28 -0
  135. package/dist/cursor/widget-edge-snap.js +38 -0
  136. package/dist/cursor/widget-offset.d.ts +9 -0
  137. package/dist/cursor/widget-offset.js +71 -4
  138. package/dist/debug/interaction-trace.d.ts +4 -0
  139. package/dist/debug/interaction-trace.js +15 -0
  140. package/dist/decorations/decoration-state.svelte.js +1 -1
  141. package/dist/decorations/reserved-attrs.js +1 -0
  142. package/dist/editor-actions/ancestry-folds.d.ts +2 -2
  143. package/dist/editor-actions/ancestry-folds.js +1 -1
  144. package/dist/editor-actions/block-edit-scope.js +1 -1
  145. package/dist/editor-actions/commit/text-batch.d.ts +3 -2
  146. package/dist/editor-actions/commit/text-batch.js +1 -1
  147. package/dist/editor-actions/commit/undo-controller.js +4 -2
  148. package/dist/editor-actions/container-edit.js +2 -1
  149. package/dist/editor-actions/enter-completion.d.ts +2 -0
  150. package/dist/editor-actions/enter-completion.js +23 -2
  151. package/dist/editor-actions/focus/focus-dispatch.js +7 -4
  152. package/dist/editor-actions/focus/focus-landing.d.ts +10 -1
  153. package/dist/editor-actions/focus/focus-landing.js +20 -6
  154. package/dist/editor-actions/inline-range-commit.js +1 -1
  155. package/dist/editor-actions/plugin/container.d.ts +7 -0
  156. package/dist/editor-actions/plugin/container.js +3 -1
  157. package/dist/editor-actions/reorder-action.js +19 -10
  158. package/dist/editor-actions/reorder-drag.js +29 -1
  159. package/dist/editor-actions/replacement-focus.d.ts +1 -1
  160. package/dist/editor-actions/replacement-focus.js +1 -1
  161. package/dist/editor-actions/search-replace.js +1 -1
  162. package/dist/editor-actions/table-context.d.ts +4 -1
  163. package/dist/editor-actions/table-context.js +57 -1
  164. package/dist/editor-events.d.ts +3 -0
  165. package/dist/editor-keys.d.ts +33 -0
  166. package/dist/editor-props.d.ts +24 -12
  167. package/dist/index.d.ts +1 -1
  168. package/dist/plugin.d.ts +7 -0
  169. package/dist/plugin.js +16 -0
  170. package/dist/plugins/latex/BlockMath.svelte +264 -29
  171. package/dist/plugins/latex/BlockMath.svelte.d.ts +2 -0
  172. package/dist/plugins/latex/index.d.ts +2 -1
  173. package/dist/plugins/latex/latex-kind.js +60 -10
  174. package/dist/plugins/latex/math-completion.js +4 -1
  175. package/dist/plugins/latex/math-layout.d.ts +15 -0
  176. package/dist/plugins/latex/math-layout.js +13 -0
  177. package/dist/plugins/latex/math-source.d.ts +19 -0
  178. package/dist/plugins/latex/math-source.js +103 -0
  179. package/dist/plugins/latex/register.d.ts +11 -2
  180. package/dist/plugins/latex/register.js +3 -1
  181. package/dist/plugins/latex/renderer.d.ts +3 -3
  182. package/dist/plugins/latex/renderer.js +13 -6
  183. package/dist/plugins/mermaid/MermaidBlock.svelte +19 -9
  184. package/dist/plugins/parrot/ParrotBlock.svelte +3 -1
  185. package/dist/reactivity/list-windowing.svelte.d.ts +8 -8
  186. package/dist/reactivity/list-windowing.svelte.js +75 -30
  187. package/dist/schema/block-completions.d.ts +8 -0
  188. package/dist/schema/block-completions.js +11 -0
  189. package/dist/schema/commands.d.ts +9 -5
  190. package/dist/schema/commands.js +12 -7
  191. package/dist/schema/context-actions.d.ts +31 -0
  192. package/dist/schema/context-actions.js +23 -0
  193. package/dist/schema/fenced-code-raw.js +31 -1
  194. package/dist/schema/operations.d.ts +8 -1
  195. package/dist/schema/reserved-chords.js +37 -4
  196. package/dist/schema/table-cell-raw.d.ts +1 -1
  197. package/dist/schema/table-cell-raw.js +1 -1
  198. package/dist/selection/block-hit-test.js +3 -2
  199. package/dist/selection/char-endpoint-snap.js +1 -1
  200. package/dist/selection/clipboard-text.js +6 -1
  201. package/dist/selection/covered-block.d.ts +10 -0
  202. package/dist/selection/covered-block.js +24 -0
  203. package/dist/selection/cross-block/dispatch.d.ts +3 -0
  204. package/dist/selection/cross-block/dispatch.js +13 -1
  205. package/dist/selection/cross-block/format-range.d.ts +1 -1
  206. package/dist/selection/cross-block/format-range.js +4 -15
  207. package/dist/selection/cross-block/format-toggle.js +1 -1
  208. package/dist/selection/cross-block/keydown.js +3 -29
  209. package/dist/selection/cross-block/ops.js +1 -1
  210. package/dist/selection/cross-block/paste.js +31 -35
  211. package/dist/selection/cross-block/type-replace.d.ts +3 -2
  212. package/dist/selection/cross-block/type-replace.js +52 -13
  213. package/dist/selection/dead-space-caret.d.ts +13 -0
  214. package/dist/selection/dead-space-caret.js +57 -1
  215. package/dist/selection/drag-pointer.d.ts +26 -2
  216. package/dist/selection/drag-pointer.js +103 -2
  217. package/dist/selection/gap-caret.js +1 -1
  218. package/dist/selection/keyboard-extend.d.ts +3 -2
  219. package/dist/selection/keyboard-extend.js +4 -3
  220. package/dist/selection/multi-click.d.ts +42 -0
  221. package/dist/selection/multi-click.js +203 -0
  222. package/dist/selection/native-bridge.d.ts +3 -0
  223. package/dist/selection/native-bridge.js +31 -2
  224. package/dist/selection/path-lookup.d.ts +10 -2
  225. package/dist/selection/path-lookup.js +11 -4
  226. package/dist/selection/pointer-gesture.d.ts +9 -0
  227. package/dist/selection/pointer-gesture.js +11 -0
  228. package/dist/selection/primitives.d.ts +7 -0
  229. package/dist/selection/primitives.js +19 -1
  230. package/dist/selection/range-delete-ceremony.js +4 -2
  231. package/dist/selection/range-delete-chrome.js +2 -1
  232. package/dist/selection/range-delete-table-coverage.js +2 -1
  233. package/dist/selection/range-delete-table.js +3 -2
  234. package/dist/selection/range-delete.d.ts +4 -0
  235. package/dist/selection/range-delete.js +31 -3
  236. package/dist/selection/selection-drop.d.ts +34 -0
  237. package/dist/selection/selection-drop.js +253 -0
  238. package/dist/selection/selection-restore.js +1 -1
  239. package/dist/selection/selection-state.svelte.d.ts +6 -0
  240. package/dist/selection/selection-state.svelte.js +36 -1
  241. package/dist/selection/table-endpoint-snap.js +1 -1
  242. package/dist/selection/table-rect-extend.js +1 -1
  243. package/dist/styles/editor-theme.css +75 -31
  244. package/dist/styles/editor.css +268 -18
  245. package/dist/testing/container-conformance.js +2 -2
  246. package/dist/testing/inline-conformance.js +2 -1
  247. package/dist/tree-operations/blockquote.js +1 -1
  248. package/dist/tree-operations/chain-rebuild.d.ts +63 -0
  249. package/dist/tree-operations/chain-rebuild.js +142 -0
  250. package/dist/tree-operations/children.d.ts +1 -1
  251. package/dist/tree-operations/children.js +1 -1
  252. package/dist/tree-operations/cleanup.js +1 -1
  253. package/dist/tree-operations/content-write.d.ts +50 -0
  254. package/dist/tree-operations/content-write.js +263 -0
  255. package/dist/tree-operations/index.d.ts +8 -3
  256. package/dist/tree-operations/index.js +6 -2
  257. package/dist/tree-operations/list/exit-replacement.js +1 -1
  258. package/dist/tree-operations/list/unwrap-merge.js +3 -3
  259. package/dist/tree-operations/node-ops.d.ts +17 -234
  260. package/dist/tree-operations/node-ops.js +47 -1113
  261. package/dist/tree-operations/node-primitives.d.ts +75 -0
  262. package/dist/tree-operations/node-primitives.js +117 -0
  263. package/dist/tree-operations/paste/apply.js +1 -1
  264. package/dist/tree-operations/paste/body-write.d.ts +1 -1
  265. package/dist/tree-operations/paste/body-write.js +2 -2
  266. package/dist/tree-operations/paste/container-match.js +4 -2
  267. package/dist/tree-operations/paste/dispatch.js +2 -1
  268. package/dist/tree-operations/paste/find-enclosing-list.js +1 -1
  269. package/dist/tree-operations/paste/focus-target.d.ts +1 -1
  270. package/dist/tree-operations/paste/list-absorb.js +1 -1
  271. package/dist/tree-operations/paste/list-break-out.js +1 -1
  272. package/dist/tree-operations/paste/parent-scope.js +1 -1
  273. package/dist/tree-operations/paste/paste-replacement.js +1 -1
  274. package/dist/tree-operations/paste/replace-block-at-parent.d.ts +3 -1
  275. package/dist/tree-operations/paste/replace-block-at-parent.js +5 -2
  276. package/dist/tree-operations/paste/replacement-parse.d.ts +17 -0
  277. package/dist/tree-operations/paste/replacement-parse.js +22 -0
  278. package/dist/tree-operations/path-mutate.d.ts +1 -1
  279. package/dist/tree-operations/path-mutate.js +2 -1
  280. package/dist/tree-operations/reorder-unit.js +1 -1
  281. package/dist/tree-operations/reorder.d.ts +5 -2
  282. package/dist/tree-operations/reorder.js +55 -2
  283. package/dist/tree-operations/settle.d.ts +105 -0
  284. package/dist/tree-operations/settle.js +660 -0
  285. package/dist/tree-operations/table-grid-clipboard.d.ts +21 -0
  286. package/dist/tree-operations/table-grid-clipboard.js +90 -0
  287. package/dist/tree-operations/unshare.d.ts +15 -77
  288. package/dist/tree-operations/unshare.js +15 -166
  289. package/docs/guide/consumer-guide.md +136 -104
  290. package/docs/guide/plugin-api.md +73 -29
  291. package/docs/guide/plugin-guide.md +55 -23
  292. package/package.json +9 -7
  293. package/dist/components/blocks/code/CodeLanguageChip.svelte +0 -127
  294. package/dist/components/blocks/code/CodeLanguageChip.svelte.d.ts +0 -14
  295. package/dist/components/blocks/table/TableGrip.svelte +0 -91
  296. package/dist/components/blocks/table/TableGrip.svelte.d.ts +0 -8
  297. package/dist/components/blocks/table/table-drop-target.d.ts +0 -1
  298. package/dist/components/blocks/table/table-drop-target.js +0 -16
  299. package/dist/components/blocks/table/table-reorder-drag.d.ts +0 -78
  300. package/dist/components/blocks/table/table-reorder-drag.js +0 -97
@@ -1,20 +1,26 @@
1
1
  /**
2
- * The editable-leaf seam a plugin block component builds on: a text-editing block surface with
3
- * native caret/IME/undo/cross-block-selection parity, in one factory so a plugin never touches an
4
- * editor context key. Two modes: `plain` commits per keystroke, `render-primary` reveals its
5
- * source and commits on blur. Call synchronously during init. Contract: plugin-guide § The
6
- * editable leaf.
2
+ * The editable-leaf seam a plugin block component builds on: native caret/IME/undo/selection
3
+ * parity in one factory, so a plugin never touches an editor context key. `plain` commits per
4
+ * keystroke; `render-primary` reveals its source and commits on blur. A painted source
5
+ * (`renderSource`) edits only its own DOM: every byte still enters the CST through
6
+ * `commitReveal`'s one `updateBlockContent`. Call synchronously during init. Contract:
7
+ * plugin-guide § The editable leaf.
7
8
  */
8
9
  import { getContext } from 'svelte';
9
10
  import { createAttachmentKey } from 'svelte/attachments';
10
11
  import { EDITOR_POLICIES_KEY, EDITOR_SERVICES_KEY } from '../../editor-keys';
11
12
  import { asDomTextOffset } from '../../cursor/coordinate-spaces';
12
- import { setCursorOffset, getCursorOffset, getSelectionOffsets } from '../../cursor/content-offsets';
13
+ import { setCursorOffset, getCursorOffset, getSelectionOffsets, getRangeOffsets } from '../../cursor/content-offsets';
13
14
  import { handleSharedKeydown } from '../../selection/shared-keydown';
14
- import { createEditableSurface, createClipboardHandlers, consumePendingRestore } from './editable-surface';
15
+ import { createEditableSurface, createClipboardHandlers, consumePendingRestore, withKeydownVerdict } from './editable-surface';
15
16
  import { wireSurfaceContexts } from './surface-wiring.svelte';
16
17
  import { createContentOffsetBackend, anchorTrailingNewline } from './plain-text-backend';
18
+ import { CONTENT_EMPTY_ATTR, chromeFreeText, clampToLandableRaw, holdsOnlyMarkerChrome } from '../../cursor/widget-offset';
17
19
  import { parkFocusOnEditorRoot } from '../../selection/native-bridge';
20
+ import { assertInvariant } from '../../assert';
21
+ import { checkRenderedTextFidelity } from '../../invariants/render-fidelity';
22
+ import { resolveBinding } from '../../schema/commands';
23
+ import { eventToChord } from '../../schema/keybindings';
18
24
  import { resetForPointerDown } from '../../selection/cross-block/pointer';
19
25
  import { placeCaret } from '../../selection/caret-doors';
20
26
  import { createSourceReveal } from '../../cursor/reveal-source';
@@ -25,6 +31,7 @@ import {} from '../../schema/commands';
25
31
  import {} from '../../schema/block-commands';
26
32
  import { owningPluginEditor } from '../../schema/plugin-install';
27
33
  import { reorderRunCommand } from '../../editor-actions/reorder-action';
34
+ import { createTextBatch } from '../../editor-actions/commit/text-batch';
28
35
  /**
29
36
  * The node → metadata bridge (plus `commandHooks`) a minted block command resolves
30
37
  * against on the leaf tier — the container factory's `buildContainerKindTarget` sibling.
@@ -48,9 +55,9 @@ export function createEditableLeaf(deps) {
48
55
  // Plain mode's source is always the editable view.
49
56
  const isRevealed = mode === 'render-primary' ? deps.isRevealed : () => true;
50
57
  const wiring = wireSurfaceContexts();
51
- const { blockEdit, stickyColumn, edgeAffinity, selection, getDoc, getBlockElByPath, getEditorRoot, pluginEditor, events: editorEvents } = wiring.deps;
58
+ const { blockEdit, focusActions, stickyColumn, edgeAffinity, selection, getDoc, getBlockElByPath, getEditorRoot, pluginEditor, activePlugins, events: editorEvents } = wiring.deps;
52
59
  const { reorder } = getContext(EDITOR_SERVICES_KEY);
53
- const { presentationMode: getPresentationModeCtx, theme: getThemeCtx, onPasteImage } = getContext(EDITOR_POLICIES_KEY);
60
+ const { presentationMode: getPresentationModeCtx, theme: getThemeCtx, keybindingOverrides, onPasteImage } = getContext(EDITOR_POLICIES_KEY);
54
61
  const getPresentationMode = () => getPresentationModeCtx?.() ?? 'source';
55
62
  const getTheme = () => getThemeCtx?.() ?? 'dark';
56
63
  // Resolved by the kind's recorded owner, like the command context's `editor`.
@@ -123,13 +130,29 @@ export function createEditableLeaf(deps) {
123
130
  showSource: () => {
124
131
  traceRevealOpen('leaf');
125
132
  revealedBase = sourceText();
133
+ clearSourceHistory();
126
134
  deps.setRevealed?.(true);
127
135
  },
128
136
  showRendered: () => {
129
137
  revealedBase = null;
138
+ clearSourceHistory();
130
139
  deps.setRevealed?.(false);
131
140
  }
132
141
  });
142
+ // Every open goes through here rather than the kernel directly, so a chrome-only source is
143
+ // completed (see `EditableLeafDeps.completeBareSource`) whatever door revealed it.
144
+ async function revealSource(atSourceOffset = 0) {
145
+ await revealKernel.reveal(atSourceOffset);
146
+ const el = deps.getEl();
147
+ if (!el || !deps.completeBareSource || !isRevealed() || isReading())
148
+ return;
149
+ const completed = deps.completeBareSource(el.textContent ?? '');
150
+ if (!completed)
151
+ return;
152
+ paintSource(el, completed.text);
153
+ deps.onSourceEdit?.(completed.text);
154
+ setCursorOffset(el, asDomTextOffset(completed.caret));
155
+ }
133
156
  // ── Commit ─────────────────────────────────────────────────────────────────
134
157
  // Returns the commit's own promise, so a caller that has to act on the committed bytes
135
158
  // (a single-line Enter's split) can wait for the write to land.
@@ -138,14 +161,29 @@ export function createEditableLeaf(deps) {
138
161
  // caret follows the edit position.
139
162
  return Promise.resolve(blockEdit.updateBlockContent(deps.getIndex(), edited + trailingLineEnding(deps.getNode().raw), preEditOffset, edited.length));
140
163
  }
141
- async function commitReveal() {
164
+ // A blur that arrives with a cross-block range live is the release of a drag that began in
165
+ // this source and left it: the fold waits one frame, so the range's rects measured real text
166
+ // under the pointer, then folds unless focus came back — else the source stays open with
167
+ // nothing focused in it and no further blur to close it.
168
+ let foldFrame = 0;
169
+ function foldAfterRange() {
170
+ if (foldFrame)
171
+ return;
172
+ foldFrame = requestAnimationFrame(() => {
173
+ foldFrame = 0;
174
+ const el = deps.getEl();
175
+ if (!isRevealed() || (el && el.contains(document.activeElement)))
176
+ return;
177
+ void commitReveal(true);
178
+ });
179
+ }
180
+ async function commitReveal(force = false) {
142
181
  if (mode !== 'render-primary' || !isRevealed())
143
182
  return;
144
- // A cross-block selection sweeping through keeps the source revealed so its rects
145
- // measure real text, and focusout is this fold's only entry — so the leaf stays in
146
- // source view until the user focuses it and leaves again.
147
- if (selection.isCrossBlock)
183
+ if (selection.isCrossBlock && !force) {
184
+ foldAfterRange();
148
185
  return;
186
+ }
149
187
  // Only wired to onFocusOut — the block leaf folds on blur, never Escape-cancel.
150
188
  traceRevealFold('blur');
151
189
  const edited = deps.getEl()?.textContent ?? sourceText();
@@ -169,7 +207,7 @@ export function createEditableLeaf(deps) {
169
207
  // and block-level traversal passes over.
170
208
  if (isReading())
171
209
  return;
172
- void revealKernel.reveal(offset);
210
+ void revealSource(offset);
173
211
  return;
174
212
  }
175
213
  surface.parkCaret(offset);
@@ -184,7 +222,7 @@ export function createEditableLeaf(deps) {
184
222
  return;
185
223
  // Through the kernel like every other open, so this entry gets the same trace pair,
186
224
  // length assert and reveal base; it seats a caret at 0, which the column re-seats.
187
- await revealKernel.reveal();
225
+ await revealSource();
188
226
  }
189
227
  if (!deps.getEl())
190
228
  return;
@@ -196,6 +234,30 @@ export function createEditableLeaf(deps) {
196
234
  }
197
235
  const getCommandContext = () => buildLeafCommandContext(deps, blockEdit, pluginEditor);
198
236
  // ── View sync ──────────────────────────────────────────────────────────────
237
+ // The one place source bytes become DOM, so G1.28 is asserted here for every painter rather
238
+ // than trusted per plugin. A painter's chrome-only case (a fence with no body line) takes the
239
+ // same stamp a code block does, so its markers paint while focused.
240
+ function paintSource(el, text) {
241
+ if (deps.renderSource) {
242
+ const painted = deps.renderSource(text);
243
+ assertInvariant('rendered-text-fidelity', () => checkRenderedTextFidelity(painted.textContent ?? '', text));
244
+ el.replaceChildren(painted);
245
+ el.toggleAttribute(CONTENT_EMPTY_ATTR, holdsOnlyMarkerChrome(el));
246
+ }
247
+ else {
248
+ el.textContent = text;
249
+ }
250
+ anchorTrailingNewline(el);
251
+ }
252
+ function repaintSource() {
253
+ const el = deps.getEl();
254
+ if (!el || !deps.renderSource || composing)
255
+ return;
256
+ const offset = getCursorOffset(el);
257
+ paintSource(el, el.textContent ?? '');
258
+ if (offset !== null)
259
+ setCursorOffset(el, asDomTextOffset(offset));
260
+ }
199
261
  function syncSource() {
200
262
  const text = sourceText();
201
263
  const el = deps.getEl();
@@ -204,23 +266,77 @@ export function createEditableLeaf(deps) {
204
266
  const pending = pendingCursor;
205
267
  pendingCursor = null;
206
268
  if ((el.textContent ?? '') !== text) {
207
- el.textContent = text;
208
- anchorTrailingNewline(el);
269
+ paintSource(el, text);
270
+ // The local entries were taken against bytes an external rewrite just replaced.
271
+ clearSourceHistory();
209
272
  // Restore only under a live caret — an external rewrite (undo, structural
210
273
  // replace) must not steal focus.
211
274
  consumePendingRestore(el, pending, (offset) => setCursorOffset(el, asDomTextOffset(offset)));
212
275
  }
213
276
  }
214
- // ── Event handlers ─────────────────────────────────────────────────────────
277
+ let sourceUndo = [];
278
+ let sourceRedo = [];
279
+ // The document's own keystroke batch, not a second timer: a burst of typing inside the
280
+ // reveal takes one entry there, so it takes one here.
281
+ let batchBaseText = '';
282
+ const sourceBatch = createTextBatch({
283
+ pushSnapshot: (_leafPath, offset) => {
284
+ sourceUndo.push({ text: batchBaseText, caret: offset });
285
+ sourceRedo = [];
286
+ }
287
+ });
288
+ function clearSourceHistory() {
289
+ sourceBatch.interrupt();
290
+ sourceUndo = [];
291
+ sourceRedo = [];
292
+ }
293
+ function restoreSourceEntry(el, from, to) {
294
+ const entry = from.pop();
295
+ if (!entry)
296
+ return;
297
+ // The restored text is what the next keystroke must snapshot, so it opens its own batch.
298
+ sourceBatch.interrupt();
299
+ to.push({ text: el.textContent ?? '', caret: getCursorOffset(el) ?? 0 });
300
+ paintSource(el, entry.text);
301
+ setCursorOffset(el, asDomTextOffset(entry.caret));
302
+ deps.onSourceEdit?.(entry.text);
303
+ }
304
+ /**
305
+ * The one entry every reveal edit crosses, so the batching rule lives here rather than at
306
+ * each gesture: one character replaced by at most one non-newline character is the shape a
307
+ * keystroke has, and nothing else coalesces. Enter, a paste, a cut and a selection replace
308
+ * each take an entry of their own and end the burst before them.
309
+ */
310
+ function recordSourceEdit(text, caret, keystroke) {
311
+ if (!keystroke) {
312
+ sourceBatch.interrupt();
313
+ sourceUndo.push({ text, caret });
314
+ sourceRedo = [];
315
+ return;
316
+ }
317
+ batchBaseText = text;
318
+ sourceBatch.keystroke(deps.getPath(), caret);
319
+ }
215
320
  // Splice `insert` over the source text node's [start, end) and reseat the caret. A
216
321
  // DOM-text mutation keeps the offset walk exact where a native Enter/cut/paste would
217
322
  // inject <div>/<br> that vanish from textContent.
218
323
  function spliceSourceText(el, start, end, insert) {
219
324
  const text = el.textContent ?? '';
220
- el.textContent = text.slice(0, start) + insert + text.slice(end);
221
- anchorTrailingNewline(el);
325
+ const keystroke = end - start <= 1 && insert.length <= 1 && insert !== '\n';
326
+ if (deps.renderSource)
327
+ recordSourceEdit(text, getCursorOffset(el) ?? start, keystroke);
328
+ const spliced = text.slice(0, start) + insert + text.slice(end);
329
+ // An edit that empties the body leaves the same chrome-only source a bare block arrives
330
+ // as, so the edit door owes the completion the reveal door applies.
331
+ const completed = deps.completeBareSource?.(spliced) ?? null;
332
+ const next = completed?.text ?? spliced;
333
+ paintSource(el, next);
334
+ deps.onSourceEdit?.(next);
222
335
  preEditOffset = start;
223
- setCursorOffset(el, asDomTextOffset(start + insert.length));
336
+ setCursorOffset(el, asDomTextOffset(completed?.caret ?? start + insert.length));
337
+ // Armed once the edit has settled, so the window measures the gap the user leaves.
338
+ if (deps.renderSource && keystroke)
339
+ sourceBatch.armPause();
224
340
  if (mode === 'plain')
225
341
  editableSurface.onInput();
226
342
  }
@@ -267,6 +383,44 @@ export function createEditableLeaf(deps) {
267
383
  if (composing || !el)
268
384
  return;
269
385
  preEditOffset = getCursorOffset(el) ?? 0;
386
+ // Undo INSIDE an open painted reveal steps back through this session's own edits: the
387
+ // document's history sees the session as one entry written on blur, so until the local
388
+ // stack is spent the chord has nothing else to mean. Resolved through the keymap like every
389
+ // chord, so a host's rebinding or disable reaches it.
390
+ if (deps.renderSource && isRevealed()) {
391
+ const command = historyCommandFor(e);
392
+ if (command === 'history.undo' && sourceUndo.length > 0) {
393
+ e.preventDefault();
394
+ restoreSourceEntry(el, sourceUndo, sourceRedo);
395
+ return;
396
+ }
397
+ if (command === 'history.redo' && sourceRedo.length > 0) {
398
+ e.preventDefault();
399
+ restoreSourceEntry(el, sourceRedo, sourceUndo);
400
+ return;
401
+ }
402
+ }
403
+ // Backspace in a painted source that holds nothing but its own chrome deletes the block, as
404
+ // it does in a code block: an empty body has no byte the press could mean, and a seat that
405
+ // only steps out (or eats the one blank line) leaves a block the user just asked to be rid
406
+ // of. In every mode — the gate is the byte BEFORE the caret: whitespace, or nothing, so a
407
+ // press that sits right after a visible marker still edits that marker in source mode.
408
+ if (e.key === 'Backspace' && !e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey) {
409
+ const offset = deps.renderSource ? getCursorOffset(el) : null;
410
+ const text = el.textContent ?? '';
411
+ if (offset !== null &&
412
+ (offset === 0 || /\s/.test(text[offset - 1] ?? '')) &&
413
+ !hasSelectionIn(el) &&
414
+ chromeFreeText(el).trim() === '') {
415
+ e.preventDefault();
416
+ revealedBase = null;
417
+ deps.setRevealed?.(false);
418
+ const index = deps.getIndex();
419
+ await blockEdit.deleteBlock(index);
420
+ void focusActions.moveFocus(index - 1, 'end');
421
+ return;
422
+ }
423
+ }
270
424
  if ((await handleSharedKeydown(e, editableSurface.sharedCtx)) || editableSurface.isDetached())
271
425
  return;
272
426
  if (dispatchChord(e))
@@ -290,6 +444,56 @@ export function createEditableLeaf(deps) {
290
444
  spliceSourceText(el, offset, offset, '\n');
291
445
  }
292
446
  }
447
+ /**
448
+ * A painted source takes plain text edits here, not from the engine: Chromium treats a lone
449
+ * `\n` text node as a placeholder and replaces it on insert (a fresh `$$` block lost the line
450
+ * before its closer), and a native delete has no notion of hidden chrome. The edit range is
451
+ * clamped to the landable span, so nothing reaches a fence line. Composition stays native.
452
+ */
453
+ function onBeforeInput(e) {
454
+ if (!deps.renderSource || composing || e.isComposing)
455
+ return;
456
+ const el = deps.getEl();
457
+ if (!el)
458
+ return;
459
+ let insert;
460
+ switch (e.inputType) {
461
+ case 'insertText':
462
+ insert = e.data ?? '';
463
+ break;
464
+ case 'insertLineBreak':
465
+ case 'insertParagraph':
466
+ insert = '\n';
467
+ break;
468
+ case 'deleteContentBackward':
469
+ case 'deleteContentForward':
470
+ case 'deleteWordBackward':
471
+ case 'deleteWordForward':
472
+ insert = '';
473
+ break;
474
+ default:
475
+ return;
476
+ }
477
+ const target = e.getTargetRanges()[0];
478
+ const range = target ? getRangeOffsets(el, target) : null;
479
+ if (!range)
480
+ return;
481
+ e.preventDefault();
482
+ const start = clampToLandableRaw(el, range.start, 0);
483
+ const end = Math.max(start, clampToLandableRaw(el, range.end, 0));
484
+ spliceSourceText(el, start, end, insert);
485
+ }
486
+ function historyCommandFor(e) {
487
+ const chord = eventToChord(e);
488
+ if (!chord)
489
+ return undefined;
490
+ return resolveBinding(chord, deps.getNode().kind, keybindingOverrides(), activePlugins)
491
+ ?.command;
492
+ }
493
+ function hasSelectionIn(el) {
494
+ const sel = window.getSelection();
495
+ return Boolean(sel && !sel.isCollapsed && el.contains(sel.anchorNode));
496
+ }
293
497
  function onPointerDown(e) {
294
498
  void crossBlock.handlePointerDown(e);
295
499
  }
@@ -303,17 +507,25 @@ export function createEditableLeaf(deps) {
303
507
  const descriptor = tryGetBlockKindDescriptor(deps.getNode().kind);
304
508
  return descriptor?.caretTargetAtPoint?.(host, e.clientX, e.clientY)?.offset ?? 0;
305
509
  }
510
+ // The press only notes where it landed: revealing on the press swallowed every drag that
511
+ // began on a rendered equation. The editor's own drag runs from such a press (its root
512
+ // handler), and the CLICK — a release that did not move — is what reveals.
513
+ let renderPress = null;
306
514
  function onRenderPointerDown(e) {
307
- // Shift-click extends a selection; a plain click reveals. Reading mode: no reveal
308
- // and no preventDefault, so native selection over the rendered view stays live.
309
- if (e.shiftKey || isReading())
515
+ renderPress = e.shiftKey || isReading() ? null : { x: e.clientX, y: e.clientY };
516
+ }
517
+ function onRenderClick(e) {
518
+ const press = renderPress;
519
+ renderPress = null;
520
+ if (!press || e.shiftKey || isReading())
521
+ return;
522
+ if (Math.abs(e.clientX - press.x) > 3 || Math.abs(e.clientY - press.y) > 3)
310
523
  return;
311
- e.preventDefault();
312
524
  // The reveal lands a caret, so this owes the shared preamble. NOT through
313
525
  // crossBlock.handlePointerDown: that hit-tests against the SOURCE text, which the
314
526
  // rendered view is not.
315
- resetForPointerDown(selection, stickyColumn, edgeAffinity, e.shiftKey);
316
- void revealKernel.reveal(revealOffsetAt(e));
527
+ resetForPointerDown(selection, stickyColumn, edgeAffinity, false);
528
+ void revealSource(revealOffsetAt(e));
317
529
  }
318
530
  // The revealed source owns the press and has already spent the chord, so a spread the fold
319
531
  // does not unmount must not re-run either on the way up.
@@ -322,6 +534,10 @@ export function createEditableLeaf(deps) {
322
534
  if (!isRevealed())
323
535
  onRenderPointerDown(e);
324
536
  },
537
+ onclick: (e) => {
538
+ if (!isRevealed())
539
+ onRenderClick(e);
540
+ },
325
541
  onkeydown: (e) => {
326
542
  if (!isRevealed())
327
543
  void dispatchChord(e);
@@ -333,7 +549,8 @@ export function createEditableLeaf(deps) {
333
549
  role: 'textbox',
334
550
  spellcheck: 'false',
335
551
  oninput: editableSurface.onInput,
336
- onkeydown: handleKeydown,
552
+ onbeforeinput: onBeforeInput,
553
+ onkeydown: withKeydownVerdict(handleKeydown),
337
554
  oncopy: clipboard.onCopy,
338
555
  oncut: clipboard.onCut,
339
556
  onpaste: clipboard.onPaste,
@@ -373,6 +590,7 @@ export function createEditableLeaf(deps) {
373
590
  get sourceText() {
374
591
  return sourceText();
375
592
  },
593
+ repaintSource,
376
594
  surfaceProps,
377
595
  renderProps,
378
596
  getPresentationMode,
@@ -403,7 +621,7 @@ export function createEditableLeaf(deps) {
403
621
  reveal: (offset = 0) => {
404
622
  if (mode !== 'render-primary')
405
623
  return Promise.resolve(surface.focus(offset));
406
- return isReading() ? Promise.resolve() : revealKernel.reveal(offset);
624
+ return isReading() ? Promise.resolve() : revealSource(offset);
407
625
  },
408
626
  commitSource: (edited) => void commitSource(edited)
409
627
  };
@@ -22,6 +22,12 @@ import type { SelectionState } from '../../selection/selection-state.svelte';
22
22
  import { type RawOffset } from '../../cursor/coordinate-spaces';
23
23
  import { type CrossBlockHandlers } from '../../selection/cross-block/dispatch';
24
24
  import type { SharedKeydownContext } from '../../selection/shared-keydown';
25
+ /**
26
+ * Binds a surface's keydown handler so the interaction trace records ONE verdict per event,
27
+ * after the handler's own await chain settles. That record is the e2e harness's only positive
28
+ * signal that a gesture which must change nothing has finished. Disabled, one boolean read.
29
+ */
30
+ export declare function withKeydownVerdict(handle: (e: KeyboardEvent) => Promise<void>): (e: KeyboardEvent) => void;
25
31
  /**
26
32
  * Per-surface cursor I/O in raw-content coordinates (ambient marker excluded).
27
33
  * `buildRange` maps a raw-offset span to a DOM Range for selection writes.
@@ -186,6 +192,9 @@ export interface ClipboardSurfaceDeps {
186
192
  copyPreHook?: (e: ClipboardEvent) => boolean;
187
193
  /** Pre-cross-block cut arm (selected-widget splice, intra-table rect cut). */
188
194
  cutPreHook?: (e: ClipboardEvent) => boolean | Promise<boolean>;
195
+ /** Pre-cross-block paste arm, handed the normalized text while a live rectangle is still
196
+ * readable (a grid into a table's cells). True when it consumed the paste. */
197
+ pastePreHook?: (text: string) => boolean | Promise<boolean>;
189
198
  /** The intra-block copy payload; owns its preventDefault. Omit to write the
190
199
  * visible selection string (code, leaf); text and the cell slice their raw. */
191
200
  copyTail?: (e: ClipboardEvent) => void;
@@ -17,9 +17,24 @@ import { createCrossBlockHandlers } from '../../selection/cross-block/dispatch';
17
17
  import { writeCrossBlockCopy, writeCrossBlockCut } from '../../selection/cross-block/clipboard';
18
18
  import { createImagePasteArm } from '../paste-image-arm';
19
19
  import { clampToLandableRaw, revealsNoMarkers } from '../../cursor/widget-offset';
20
- import { traceCompositionStart, traceCompositionEnd } from '../../debug/interaction-trace';
20
+ import { isInteractionTraceEnabled, traceCompositionEnd, traceCompositionStart, traceKeydownVerdict } from '../../debug/interaction-trace';
21
21
  import { assertInvariant } from '../../assert';
22
22
  import { checkCompositionEndPaired } from '../../invariants/inline-transitions';
23
+ // ── Keydown verdict ─────────────────────────────────────────────────────────
24
+ /**
25
+ * Binds a surface's keydown handler so the interaction trace records ONE verdict per event,
26
+ * after the handler's own await chain settles. That record is the e2e harness's only positive
27
+ * signal that a gesture which must change nothing has finished. Disabled, one boolean read.
28
+ */
29
+ export function withKeydownVerdict(handle) {
30
+ return (e) => {
31
+ if (!isInteractionTraceEnabled()) {
32
+ void handle(e);
33
+ return;
34
+ }
35
+ void handle(e).then(() => traceKeydownVerdict(e.key, e.defaultPrevented));
36
+ };
37
+ }
23
38
  /**
24
39
  * Guarded pending-caret restore. Applies only while `el` still holds focus, so a blur
25
40
  * between arming the restore and the render drops it instead of yanking the global
@@ -92,7 +107,9 @@ export function createEditableSurface(deps) {
92
107
  const el = deps.getEl();
93
108
  if (!el)
94
109
  return;
95
- el.focus();
110
+ // `preventScroll`: seating a caret must not move the page — the reveal path scrolls when a
111
+ // target is off-screen; an implicit focus scroll jumped the document on every table edit.
112
+ el.focus({ preventScroll: true });
96
113
  if (offset === CURSOR_EXACT_START) {
97
114
  deps.backend.setRaw(asRawOffset(0));
98
115
  return;
@@ -107,7 +124,7 @@ export function createEditableSurface(deps) {
107
124
  const el = deps.getEl();
108
125
  if (!el)
109
126
  return;
110
- el.focus();
127
+ el.focus({ preventScroll: true });
111
128
  const ambientLength = deps.getAmbientLength();
112
129
  // minOffset = the walk position of raw 0 keeps the scan out of the marker region.
113
130
  const minOffset = toDomTextOffset(asRawOffset(0), ambientLength);
@@ -283,6 +300,8 @@ export function createClipboardHandlers(deps) {
283
300
  async function insertPastedText(text, e) {
284
301
  const fold = deps.foldReveal?.() ?? null;
285
302
  await fold?.settled;
303
+ if (text && deps.pastePreHook && (await deps.pastePreHook(text)))
304
+ return;
286
305
  if (await deps.crossBlock.handlePaste(e, text))
287
306
  return;
288
307
  deps.stickyColumn.reset();
@@ -147,6 +147,7 @@
147
147
  node={item}
148
148
  index={absoluteIndex}
149
149
  myPath={[...myPath, absoluteIndex]}
150
+ itemCount={(node.children ?? []).length}
150
151
  slots={listState.refSlots}
151
152
  />
152
153
  {/each}
@@ -39,16 +39,20 @@
39
39
  import { dispatchKindCommand } from '../../../schema/block-commands';
40
40
  import type { AnyCommandId } from '../../../schema/command-id';
41
41
  import BlockDragHandle from '../../BlockDragHandle.svelte';
42
+ import { showsListItemDragHandle } from '../../drag-handle';
42
43
 
43
44
  let {
44
45
  node,
45
46
  index,
46
47
  myPath = [],
48
+ itemCount,
47
49
  slots
48
50
  }: {
49
51
  node: NodeView;
50
52
  index: number;
51
53
  myPath?: number[];
54
+ /** How many items the enclosing list holds; a lone item has no sibling to reorder past. */
55
+ itemCount: number;
52
56
  slots?: RefSlots<BlockComponent>;
53
57
  } = $props();
54
58
 
@@ -67,6 +71,10 @@
67
71
  // $derived, not a mount-time snapshot: a runtime prop toggle must reach blocks
68
72
  // that window in and out after the change, not just those mounted at mount.
69
73
  const dragHandles = $derived(getDragHandles?.() ?? false);
74
+ // The grip is the only pointer road into a reorder, and a reorder needs a sibling: a lone
75
+ // item stays a reorder host (a dragged sibling never arrives, but the class costs nothing)
76
+ // and simply shows nothing to grab.
77
+ const showsHandle = $derived(showsListItemDragHandle(itemCount, dragHandles));
70
78
  const presentationMode = $derived(getPresentationMode?.() ?? 'source');
71
79
  const readOnly = $derived(presentationMode === 'reading');
72
80
 
@@ -260,6 +268,7 @@
260
268
  <div
261
269
  class="list-item-block"
262
270
  class:reorder-host={dragHandles}
271
+ class:handle-host={showsHandle}
263
272
  data-task-checked={taskCheckedAttr}
264
273
  data-list-marker={presentationMarkerKind}
265
274
  bind:this={boxEl}
@@ -276,8 +285,9 @@
276
285
  />
277
286
  </div>
278
287
  <!-- A list item IS a reorder unit; its inner content BlockList passes the
279
- default reorderable={false}, so the paragraph inside gets no handle. -->
280
- {#if dragHandles}
288
+ default reorderable={false}, so the paragraph inside gets no handle. A lone item
289
+ renders none either: its drag is list-scoped and there is no sibling to pass. -->
290
+ {#if showsHandle}
281
291
  <BlockDragHandle />
282
292
  {/if}
283
293
  </div>
@@ -289,7 +299,7 @@
289
299
  align-items: flex-start;
290
300
  }
291
301
 
292
- /* Hover reveal is the shared global `.reorder-host` rule in BlockHost; it reveals
302
+ /* Hover reveal is the shared global `.handle-host` rule in BlockHost; it reveals
293
303
  only the innermost hovered unit, so a sub-item's hover never lights the parent. */
294
304
 
295
305
  .list-item-content {
@@ -318,7 +328,6 @@
318
328
  > :global(.block-list)
319
329
  > :global(.block-host:first-child)
320
330
  > :global(:not(.list-block)) {
321
- text-decoration: line-through;
322
- color: var(--syntax-task-done, rgba(128, 128, 128, 0.7));
331
+ color: var(--syntax-task-done, var(--color-text-muted, rgba(128, 128, 128, 0.7)));
323
332
  }
324
333
  </style>
@@ -5,6 +5,8 @@ type $$ComponentProps = {
5
5
  node: NodeView;
6
6
  index: number;
7
7
  myPath?: number[];
8
+ /** How many items the enclosing list holds; a lone item has no sibling to reorder past. */
9
+ itemCount: number;
8
10
  slots?: RefSlots<BlockComponent>;
9
11
  };
10
12
  declare const ListItemBlock: import("svelte").Component<$$ComponentProps, {
@@ -1,3 +1,5 @@
1
1
  import type { AmbientPrefix } from '../../../block-component';
2
2
  import type { ListItemMetadata } from '../../../core/nodes';
3
+ /** Matches the `.task-checkbox` slot and gap in editor.css; the two move together. */
4
+ export declare const TASK_HANGING_INDENT = "2.1em";
3
5
  export declare function buildTaskItemAmbient(metadata: ListItemMetadata | undefined, onToggle: () => void): AmbientPrefix;
@@ -1,4 +1,6 @@
1
1
  import { devWarn } from '../../../dev-warn';
2
+ /** Matches the `.task-checkbox` slot and gap in editor.css; the two move together. */
3
+ export const TASK_HANGING_INDENT = '2.1em';
2
4
  function isTaskMarkerChecked(taskMarker) {
3
5
  const c = taskMarker[1];
4
6
  return c === 'x' || c === 'X';
@@ -26,8 +28,15 @@ export function buildTaskItemAmbient(metadata, onToggle) {
26
28
  role: 'checkbox',
27
29
  // single source of truth: derive from the keyed marker (in the render memo key), not parallel taskChecked
28
30
  ariaChecked: isTaskMarkerChecked(metadata.taskMarker),
31
+ // The painted box is taller than the text beside it; the grip centres on the box.
32
+ dragAnchor: true,
29
33
  onClick: onToggle
30
- }
31
- ]
34
+ },
35
+ // The list marker gets a span of its own so the rendered modes can collapse its width:
36
+ // GitHub draws the box at the margin, not indented under a bullet that is not there.
37
+ { start: 0, end: boxStart, className: 'task-list-marker', onClick: () => { } }
38
+ ],
39
+ // The painted box (editor.css): slot + gap + the marker's trailing space.
40
+ indent: TASK_HANGING_INDENT
32
41
  };
33
42
  }
@@ -5,6 +5,8 @@
5
5
  * `createEditableSurface` itself stays context-free (the jsdom harness constructs it bare).
6
6
  */
7
7
  import { type KindCommandTarget } from '../../schema/block-commands';
8
+ import type { AnyBlockKind } from '../../core/nodes';
9
+ import type { AnyCommandId } from '../../schema/command-id';
8
10
  import type { EditableSurfaceDeps } from './editable-surface';
9
11
  /** The context-threaded half of `EditableSurfaceDeps` — the fields every surface passes verbatim. */
10
12
  export type SharedSurfaceDeps = Pick<EditableSurfaceDeps, 'selection' | 'getDoc' | 'getBlockElByPath' | 'focusActions' | 'getEditorRoot' | 'getScrollHost' | 'getEditorLifetime' | 'stickyColumn' | 'edgeAffinity' | 'blockEdit' | 'controller' | 'history' | 'pluginEditor' | 'getKeybindingOverrides' | 'pasteCoordinator' | 'grammar' | 'activePlugins' | 'events' | 'linkRef' | 'onCommandError' | 'crossBlockCommands'>;
@@ -13,6 +15,8 @@ export interface SurfaceWiring {
13
15
  deps: SharedSurfaceDeps;
14
16
  /** Resolve a chord at `target` through the shared gates; consumes the event when spent. */
15
17
  dispatchChord(e: KeyboardEvent, target: KindCommandTarget): boolean;
18
+ /** The command a press names at `kind`, overrides included, without running it. */
19
+ resolveChord(e: KeyboardEvent, kind: AnyBlockKind): AnyCommandId | null;
16
20
  }
17
21
  export declare function wireSurfaceContexts(): SurfaceWiring;
18
22
  export declare function useParkFocusOnUnmount(getEl: () => HTMLElement | null, getEditorRoot: () => HTMLElement | null): void;