@voithos-labs/aragonite 0.10.2 → 0.10.3

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 (255) hide show
  1. package/README.md +6 -21
  2. package/THIRD-PARTY-NOTICES.md +25 -0
  3. package/dist/a11y-strings.d.ts +18 -0
  4. package/dist/a11y-strings.js +18 -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 +35 -24
  9. package/dist/components/BlockHost.svelte +16 -9
  10. package/dist/components/Editor.svelte +381 -198
  11. package/dist/components/Editor.svelte.d.ts +1 -1
  12. package/dist/components/SelectionOverlay.svelte +17 -3
  13. package/dist/components/TailInsert.svelte +107 -0
  14. package/dist/components/TailInsert.svelte.d.ts +17 -0
  15. package/dist/components/block-content-selector.d.ts +6 -2
  16. package/dist/components/block-content-selector.js +6 -2
  17. package/dist/components/blocks/ThematicBreakBlock.svelte +19 -6
  18. package/dist/components/blocks/ThematicBreakBlock.svelte.d.ts +1 -0
  19. package/dist/components/blocks/code/CodeBlock.svelte +211 -30
  20. package/dist/components/blocks/code/CodeBlockRail.svelte +686 -0
  21. package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
  22. package/dist/components/blocks/code/code-bootstrap.js +4 -0
  23. package/dist/components/blocks/code/code-context-actions.d.ts +1 -0
  24. package/dist/components/blocks/code/code-context-actions.js +24 -0
  25. package/dist/components/blocks/code/code-fence-exit.d.ts +15 -0
  26. package/dist/components/blocks/code/code-fence-exit.js +26 -0
  27. package/dist/components/blocks/code/code-languages.d.ts +6 -0
  28. package/dist/components/blocks/code/code-languages.js +11 -0
  29. package/dist/components/blocks/code/code-renderer.js +11 -0
  30. package/dist/components/blocks/directive/DirectiveContainerBlock.svelte +1 -1
  31. package/dist/components/blocks/editable-leaf.d.ts +37 -6
  32. package/dist/components/blocks/editable-leaf.js +243 -29
  33. package/dist/components/blocks/editable-surface.d.ts +9 -0
  34. package/dist/components/blocks/editable-surface.js +22 -3
  35. package/dist/components/blocks/list/ListItemBlock.svelte +3 -3
  36. package/dist/components/blocks/list/task-checkbox.d.ts +2 -0
  37. package/dist/components/blocks/list/task-checkbox.js +11 -2
  38. package/dist/components/blocks/surface-wiring.svelte.d.ts +4 -0
  39. package/dist/components/blocks/surface-wiring.svelte.js +8 -1
  40. package/dist/components/blocks/table/TableActionMenu.svelte +210 -86
  41. package/dist/components/blocks/table/TableActionMenu.svelte.d.ts +5 -0
  42. package/dist/components/blocks/table/TableBlock.svelte +205 -186
  43. package/dist/components/blocks/table/TableBlock.svelte.d.ts +1 -0
  44. package/dist/components/blocks/table/TableCellBlock.svelte +87 -22
  45. package/dist/components/blocks/table/TableRowBlock.svelte +4 -18
  46. package/dist/components/blocks/table/TableRowBlock.svelte.d.ts +0 -5
  47. package/dist/components/blocks/table/cell-clipboard.d.ts +10 -0
  48. package/dist/components/blocks/table/cell-clipboard.js +34 -1
  49. package/dist/components/blocks/table/cell-keydown-plan.d.ts +1 -1
  50. package/dist/components/blocks/table/cell-keydown-plan.js +3 -1
  51. package/dist/components/blocks/table/table-cell-paste.js +2 -1
  52. package/dist/components/blocks/table/table-menu-model.d.ts +35 -8
  53. package/dist/components/blocks/table/table-menu-model.js +36 -17
  54. package/dist/components/blocks/text/TextEditableBlock.svelte +58 -11
  55. package/dist/components/blocks/text/delimiter-autopair.d.ts +71 -0
  56. package/dist/components/blocks/text/delimiter-autopair.js +216 -0
  57. package/dist/components/blocks/text/edge-policy-dispatch.d.ts +4 -0
  58. package/dist/components/blocks/text/edge-policy-dispatch.js +56 -2
  59. package/dist/components/blocks/text/live-selection-edit.js +27 -0
  60. package/dist/components/blocks/text/text-keydown.d.ts +7 -1
  61. package/dist/components/blocks/text/text-keydown.js +11 -1
  62. package/dist/components/blocks/text/text-render.d.ts +1 -1
  63. package/dist/components/blocks/text/text-render.js +3 -1
  64. package/dist/components/blocks/text/widget-interaction.d.ts +3 -0
  65. package/dist/components/blocks/text/widget-interaction.js +144 -22
  66. package/dist/components/drag-handle.d.ts +35 -0
  67. package/dist/components/drag-handle.js +126 -0
  68. package/dist/components/editor-root-focus.d.ts +19 -0
  69. package/dist/components/editor-root-focus.js +67 -0
  70. package/dist/components/editor-root-geometry.d.ts +39 -0
  71. package/dist/components/editor-root-geometry.js +91 -0
  72. package/dist/components/editor-root-keydown.d.ts +1 -1
  73. package/dist/components/editor-root-keydown.js +12 -3
  74. package/dist/components/editor-root-listeners.d.ts +10 -6
  75. package/dist/components/editor-root-listeners.js +23 -22
  76. package/dist/components/editor-root-mode-flip.d.ts +36 -0
  77. package/dist/components/editor-root-mode-flip.js +92 -0
  78. package/dist/components/image/ImageOverlayHost.svelte +14 -6
  79. package/dist/components/image/ImageProperties.svelte +512 -70
  80. package/dist/components/image/ImageProperties.svelte.d.ts +6 -1
  81. package/dist/components/image/ImageResizeHandles.svelte +59 -34
  82. package/dist/components/image/image-crop.d.ts +39 -0
  83. package/dist/components/image/image-crop.js +74 -0
  84. package/dist/components/image/image-edit-commit.d.ts +1 -0
  85. package/dist/components/image/image-edit-commit.js +21 -5
  86. package/dist/components/image/image-source-bytes.js +10 -3
  87. package/dist/components/image/image-widget-editing.js +1 -0
  88. package/dist/components/image/widget-dom.js +5 -1
  89. package/dist/components/link-card/link-card-commit.js +1 -1
  90. package/dist/components/lrd-map-gate.js +1 -1
  91. package/dist/components/menu/BlockMenu.svelte +315 -0
  92. package/dist/components/menu/BlockMenu.svelte.d.ts +34 -0
  93. package/dist/components/menu/MenuIcon.svelte +153 -0
  94. package/dist/components/menu/MenuIcon.svelte.d.ts +51 -0
  95. package/dist/components/menu/clipboard-actions.d.ts +12 -0
  96. package/dist/components/menu/clipboard-actions.js +42 -0
  97. package/dist/components/menu/default-context-actions.d.ts +15 -0
  98. package/dist/components/menu/default-context-actions.js +76 -0
  99. package/dist/components/menu/flyout-placement.d.ts +6 -0
  100. package/dist/components/menu/flyout-placement.js +25 -0
  101. package/dist/core/inline/format-toggle.d.ts +12 -4
  102. package/dist/core/inline/format-toggle.js +94 -40
  103. package/dist/core/inline/image-dimensions.d.ts +3 -0
  104. package/dist/core/inline/image-dimensions.js +46 -10
  105. package/dist/core/inline/inline-widgets.d.ts +11 -0
  106. package/dist/core/inline/scan/brackets.js +1 -0
  107. package/dist/core/inline/scan/plugin-syntax.d.ts +8 -0
  108. package/dist/core/inline/scan/plugin-syntax.js +15 -1
  109. package/dist/core/inline-render.d.ts +6 -0
  110. package/dist/core/inline-render.js +32 -0
  111. package/dist/core/nodes.d.ts +14 -0
  112. package/dist/cursor/edge-affinity.js +2 -1
  113. package/dist/cursor/overlay-remeasure.js +8 -0
  114. package/dist/cursor/reveal-source.js +7 -2
  115. package/dist/cursor/widget-offset.d.ts +6 -0
  116. package/dist/cursor/widget-offset.js +61 -4
  117. package/dist/debug/interaction-trace.d.ts +4 -0
  118. package/dist/debug/interaction-trace.js +15 -0
  119. package/dist/decorations/decoration-state.svelte.js +1 -1
  120. package/dist/editor-actions/ancestry-folds.d.ts +2 -2
  121. package/dist/editor-actions/ancestry-folds.js +1 -1
  122. package/dist/editor-actions/block-edit-scope.js +1 -1
  123. package/dist/editor-actions/commit/text-batch.d.ts +3 -2
  124. package/dist/editor-actions/commit/text-batch.js +1 -1
  125. package/dist/editor-actions/commit/undo-controller.js +4 -2
  126. package/dist/editor-actions/container-edit.js +2 -1
  127. package/dist/editor-actions/enter-completion.d.ts +2 -0
  128. package/dist/editor-actions/enter-completion.js +23 -2
  129. package/dist/editor-actions/inline-range-commit.js +1 -1
  130. package/dist/editor-actions/reorder-action.js +19 -10
  131. package/dist/editor-actions/reorder-drag.js +29 -1
  132. package/dist/editor-actions/replacement-focus.d.ts +1 -1
  133. package/dist/editor-actions/replacement-focus.js +1 -1
  134. package/dist/editor-actions/search-replace.js +1 -1
  135. package/dist/editor-actions/table-context.d.ts +4 -1
  136. package/dist/editor-actions/table-context.js +57 -1
  137. package/dist/editor-events.d.ts +3 -0
  138. package/dist/editor-keys.d.ts +33 -0
  139. package/dist/editor-props.d.ts +21 -12
  140. package/dist/index.d.ts +1 -1
  141. package/dist/plugin.d.ts +6 -0
  142. package/dist/plugin.js +11 -0
  143. package/dist/plugins/latex/BlockMath.svelte +264 -29
  144. package/dist/plugins/latex/BlockMath.svelte.d.ts +2 -0
  145. package/dist/plugins/latex/index.d.ts +2 -1
  146. package/dist/plugins/latex/latex-kind.js +36 -3
  147. package/dist/plugins/latex/math-completion.js +4 -1
  148. package/dist/plugins/latex/math-layout.d.ts +15 -0
  149. package/dist/plugins/latex/math-layout.js +13 -0
  150. package/dist/plugins/latex/math-source.d.ts +19 -0
  151. package/dist/plugins/latex/math-source.js +97 -0
  152. package/dist/plugins/latex/register.d.ts +11 -2
  153. package/dist/plugins/latex/register.js +3 -1
  154. package/dist/plugins/latex/renderer.d.ts +3 -3
  155. package/dist/plugins/latex/renderer.js +13 -6
  156. package/dist/reactivity/list-windowing.svelte.d.ts +8 -8
  157. package/dist/reactivity/list-windowing.svelte.js +49 -24
  158. package/dist/schema/block-completions.d.ts +8 -0
  159. package/dist/schema/block-completions.js +11 -0
  160. package/dist/schema/context-actions.d.ts +31 -0
  161. package/dist/schema/context-actions.js +23 -0
  162. package/dist/schema/fenced-code-raw.js +31 -1
  163. package/dist/schema/operations.d.ts +8 -1
  164. package/dist/schema/reserved-chords.js +25 -4
  165. package/dist/schema/table-cell-raw.d.ts +1 -1
  166. package/dist/schema/table-cell-raw.js +1 -1
  167. package/dist/selection/block-hit-test.js +3 -2
  168. package/dist/selection/char-endpoint-snap.js +1 -1
  169. package/dist/selection/clipboard-text.js +6 -1
  170. package/dist/selection/covered-block.d.ts +10 -0
  171. package/dist/selection/covered-block.js +24 -0
  172. package/dist/selection/cross-block/dispatch.d.ts +3 -0
  173. package/dist/selection/cross-block/dispatch.js +13 -1
  174. package/dist/selection/cross-block/format-range.d.ts +1 -1
  175. package/dist/selection/cross-block/format-range.js +4 -15
  176. package/dist/selection/cross-block/format-toggle.js +1 -1
  177. package/dist/selection/cross-block/keydown.js +1 -1
  178. package/dist/selection/cross-block/ops.js +1 -1
  179. package/dist/selection/cross-block/paste.js +32 -30
  180. package/dist/selection/cross-block/type-replace.d.ts +3 -2
  181. package/dist/selection/cross-block/type-replace.js +62 -13
  182. package/dist/selection/dead-space-caret.d.ts +10 -0
  183. package/dist/selection/dead-space-caret.js +47 -1
  184. package/dist/selection/double-click-trim.d.ts +17 -0
  185. package/dist/selection/double-click-trim.js +57 -0
  186. package/dist/selection/drag-pointer.d.ts +7 -2
  187. package/dist/selection/drag-pointer.js +61 -2
  188. package/dist/selection/gap-caret.js +1 -1
  189. package/dist/selection/keyboard-extend.js +1 -1
  190. package/dist/selection/path-lookup.js +1 -1
  191. package/dist/selection/range-delete-ceremony.js +4 -2
  192. package/dist/selection/range-delete-chrome.js +2 -1
  193. package/dist/selection/range-delete-table-coverage.js +2 -1
  194. package/dist/selection/range-delete-table.js +3 -2
  195. package/dist/selection/range-delete.js +30 -2
  196. package/dist/selection/selection-restore.js +1 -1
  197. package/dist/selection/selection-state.svelte.d.ts +6 -0
  198. package/dist/selection/selection-state.svelte.js +36 -1
  199. package/dist/selection/table-endpoint-snap.js +1 -1
  200. package/dist/selection/table-rect-extend.js +1 -1
  201. package/dist/styles/editor-theme.css +57 -28
  202. package/dist/styles/editor.css +217 -18
  203. package/dist/testing/container-conformance.js +2 -2
  204. package/dist/testing/inline-conformance.js +2 -1
  205. package/dist/tree-operations/blockquote.js +1 -1
  206. package/dist/tree-operations/chain-rebuild.d.ts +63 -0
  207. package/dist/tree-operations/chain-rebuild.js +142 -0
  208. package/dist/tree-operations/children.d.ts +1 -1
  209. package/dist/tree-operations/children.js +1 -1
  210. package/dist/tree-operations/cleanup.js +1 -1
  211. package/dist/tree-operations/content-write.d.ts +50 -0
  212. package/dist/tree-operations/content-write.js +263 -0
  213. package/dist/tree-operations/index.d.ts +8 -3
  214. package/dist/tree-operations/index.js +6 -2
  215. package/dist/tree-operations/list/exit-replacement.js +1 -1
  216. package/dist/tree-operations/list/unwrap-merge.js +3 -3
  217. package/dist/tree-operations/node-ops.d.ts +17 -234
  218. package/dist/tree-operations/node-ops.js +47 -1113
  219. package/dist/tree-operations/node-primitives.d.ts +75 -0
  220. package/dist/tree-operations/node-primitives.js +117 -0
  221. package/dist/tree-operations/paste/apply.js +1 -1
  222. package/dist/tree-operations/paste/body-write.d.ts +1 -1
  223. package/dist/tree-operations/paste/body-write.js +2 -2
  224. package/dist/tree-operations/paste/container-match.js +4 -2
  225. package/dist/tree-operations/paste/dispatch.js +2 -1
  226. package/dist/tree-operations/paste/find-enclosing-list.js +1 -1
  227. package/dist/tree-operations/paste/focus-target.d.ts +1 -1
  228. package/dist/tree-operations/paste/list-absorb.js +1 -1
  229. package/dist/tree-operations/paste/list-break-out.js +1 -1
  230. package/dist/tree-operations/paste/parent-scope.js +1 -1
  231. package/dist/tree-operations/paste/paste-replacement.js +1 -1
  232. package/dist/tree-operations/paste/replace-block-at-parent.js +1 -1
  233. package/dist/tree-operations/path-mutate.d.ts +1 -1
  234. package/dist/tree-operations/path-mutate.js +2 -1
  235. package/dist/tree-operations/reorder-unit.js +1 -1
  236. package/dist/tree-operations/reorder.d.ts +5 -2
  237. package/dist/tree-operations/reorder.js +55 -2
  238. package/dist/tree-operations/settle.d.ts +105 -0
  239. package/dist/tree-operations/settle.js +660 -0
  240. package/dist/tree-operations/table-grid-clipboard.d.ts +21 -0
  241. package/dist/tree-operations/table-grid-clipboard.js +90 -0
  242. package/dist/tree-operations/unshare.d.ts +15 -77
  243. package/dist/tree-operations/unshare.js +15 -166
  244. package/docs/guide/consumer-guide.md +126 -96
  245. package/docs/guide/plugin-api.md +31 -3
  246. package/docs/guide/plugin-guide.md +26 -3
  247. package/package.json +4 -2
  248. package/dist/components/blocks/code/CodeLanguageChip.svelte +0 -127
  249. package/dist/components/blocks/code/CodeLanguageChip.svelte.d.ts +0 -14
  250. package/dist/components/blocks/table/TableGrip.svelte +0 -91
  251. package/dist/components/blocks/table/TableGrip.svelte.d.ts +0 -8
  252. package/dist/components/blocks/table/table-drop-target.d.ts +0 -1
  253. package/dist/components/blocks/table/table-drop-target.js +0 -16
  254. package/dist/components/blocks/table/table-reorder-drag.d.ts +0 -78
  255. package/dist/components/blocks/table/table-reorder-drag.js +0 -97
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # <img alt="header" src="docs/assets/header.png" />
2
2
 
3
- This project is an effort (perhaps in vain) to create a markdown editor that is both open source and not crap. In my book, this means that it has to be lossless, extensible, lean, fast, have a graceful ui/ux, and have a hella good plugin interface. So you know, just some simplistic and easy to achieve goals [^1] [^2].
3
+ > Made for [Limestone Notes](https://limestone.app/) ([source](https://github.com/voithos-labs/limestone)), by [Voithos Labs](https://voithoslabs.com/).
4
+
5
+ This project is an effort (perhaps in vain) to create a markdown editor that is both open source and not crap. In my book, this means that it has to be lossless, extensible, fast, have a graceful ui/ux, and have a hella good plugin interface. So you know, just some simplistic and easy to achieve goals [^1] [^2].
4
6
 
5
7
  Note that aragonite is a work in progress [^3]. It's written in typescript and svelte [^4] [^5] [^6] [^7], and tested on chromium browsers (chrome and edge) [^8]. Yes, there are plans to port to different frontend frameworks and test in different browsers. No, not right now, sometime in the future.
6
8
 
@@ -189,27 +191,12 @@ The one deliberate byte write a plugin makes is its own kind's raw rebuild, whic
189
191
 
190
192
  A plugin component that throws takes down its own block, which degrades to a readable fallback while its siblings keep working. Uninstall a plugin and every document written with it still round trips byte for byte, because unknown syntax is handled gracefully.
191
193
 
192
- Also, shipping a kind forces the boring questions up front: the registration type requires declaring how the kind behaves under all nine cross-cutting subsystems (round-tripping, focus, merge, selection, search, reordering, undo, clipboard, and how the fuzzer drives it), and registering enrolls it in a conformance battery that actually exercises those behaviors.
194
+ Also, shipping a kind forces the boring questions up front: the registration type requires declaring how the kind behaves under all nine cross-cutting subsystems (round-tripping, focus, merge, selection, search, reordering, undo, clipboard, and how the fuzzer drives it), and registering enrolls it in a conformance battery: the headless half runs in your own test suite, the browser half in this repo's e2e sweep.
193
195
 
194
196
  This is the bet. Aragonite cannot top Obsidian in plugin count (in the short term, at least), but what it can try to do is trade plugin count for plugin quality. Score it against my three criterias: reach is the whole own a kind story above, safety is the lossless promise doing double duty, and ergonomics is the part I haven't argued yet, so here it is: svelte and typescript end to end, the entire authoring surface on one import path (`@voithos-labs/aragonite/plugin`), and a public testing seam so your plugin's own test suite isn't an afterthought.
195
197
 
196
198
  Does the design actually work in practice? Well, the nine bundled first party plugins (admonitions, details, footnotes, emoji, math, diagrams, table of contents, occurrence highlighting, and a party parrot) are built on the exact surface third parties get, so I would describe it as "so far, so good".
197
199
 
198
- # Lean
199
-
200
- Let's start by establishing the right context: most editors ship as a toolkit, and you assemble the editor yourself. CodeMirror is seven `@codemirror/*` packages plus a Lezer grammar; ProseMirror is `prosemirror-model` and `-state` and `-view` and `-transform` and however much glue you write to make them a product. Aragonite, on the other hand, is one library you import, with the parser, serializer, block editing, windowing, undo, selection, decorations, presentation modes, and the plugin platform already wired to each other.
201
-
202
- (Yes, there is a small set of dependencies. Two hard runtime dependencies: highlight.js, for code-block syntax colors, and esm-env, a few bytes of bundler-agnostic dev-flag resolution that svelte itself already depends on. Svelte is a peer you already have, and compiles to far less runtime than a virtual-DOM framework; katex and mermaid are optional peers, pulled in only if you use the math or diagram plugins. That is the whole tree.)
203
-
204
- Currently, the codebase lands at around 69k lines of typescript and svelte for the shipped library, roughly 6k of which is the nine bundled plugins [^13]. Here is where the lines actually went:
205
-
206
- <picture>
207
- <source media="(prefers-color-scheme: dark)" srcset="docs/assets/loc-dark.svg">
208
- <img alt="Horizontal bar chart of the shipped library's lines of code by area: block UIs and rendering is the largest slice, then editing/commits/undo, the parser and serializer, selection, and the bundled plugins; the schema registry, invariants, public API, decorations, and windowing each take progressively smaller slices." src="docs/assets/loc-light.svg">
209
- </picture>
210
-
211
- I guess the number itself is nothing special, but the ratio turns out to be quite dense. A whole block editor (a full markdown parser and serializer, structural editing, windowing, cross-block selection, undo, decorations, five presentation modes, and a plugin platform) fits in a codebase one person can still realistically read end to end. The test suite, meanwhile, is ard 2.4x the size of the library (~168k lines), which says more about my paranoia than the leanness.
212
-
213
200
  # Fast
214
201
 
215
202
  Aragonite is fast, and its this way due to one main reason: the editor only mounts what you can see. A 10KB note and a 10MB document have the same number of live components on screen, so typing costs the same in both. This is, as people call it, windowing, and is also one of the reasons why the block editor model earned its keep.
@@ -310,9 +297,9 @@ Now, the numbers here depend on the machine; however, the scale of the numbers a
310
297
 
311
298
  Now, here's a conundrum Finn and I faced early on: we wanted Notion's uiux, but we (by we, I meant Finn) wanted a document, not a pile of blocks. In summary, we wanted the benefits of Notion's uiux in our uiux, not necessarily its look. Which is why even though under the hood aragonite is as much a block editor as Notion is, on the surface it reads like a document you are writing, not a fucking game of tetris.
312
299
 
313
- Notion never lets you forget you are in a builder. Hover any block and a drag grip and a plus button fade into the gutter; the surface is a scaffold, and a stray click + drag can rearrange the page. Obsidian sits at the other pole, and reads as a calm plain document, because under the hood it is one (a text buffer, with all of the limitations I mentioned before). Aragonite wants the best of both world: the calm surface and a real structure.
300
+ Notion never lets you forget you are in a builder. Hover any block, a plain paragraph included, and a drag grip and a plus button fade into the gutter; the surface is a scaffold, and a stray click + drag can rearrange the page. Obsidian sits at the other pole, and reads as a calm plain document, because under the hood it is one (a text buffer, with all of the limitations I mentioned before). Aragonite wants the best of both world: the calm surface and a real structure.
314
301
 
315
- So, if you open aragonite, the blocks are there, mostly invisible. No card chrome, no per block outline, no gutter furniture by default. The reorder handle is off unless you ask for it, and even then only appears on hover; keyboard reorder is always available and shows nothing until you use it.
302
+ So, if you open aragonite, the blocks are there, mostly invisible. No card chrome, no per block outline, and prose never grows a handle. The blocks you would pick up whole (a table, a picture, a code block, a list item, a divider) get one on hover, and you turn that off easily (`blockDragHandles={false}`); keyboard reorder is always available and shows nothing until you use it.
316
303
 
317
304
  Oh, live preview? You think I forgot about it? nah. By default, markdown syntax stays visible but dimmed. Aragonite provide the `presentationMode` prop, which dials the same document along a spectrum, from the raw side to the rendered side:
318
305
 
@@ -361,8 +348,6 @@ Aragonite is free software, released under [AGPL-3.0-or-later](./LICENSE): use i
361
348
 
362
349
  [^12]: ProseMirror friends: yes, this means no `StateField`. The forward-mapping problem it solves is downstream of positions being integers into a flat sequence. Ours aren't.
363
350
 
364
- [^13]: counted from the tracked `src/lib` source, excluding the tests; `scripts/render-loc-chart.mjs` does the counting and draws the chart above. Give or take a refactor.
365
-
366
351
  [^14]: before anyone suggests it: CSS `content-visibility` is not this. It skips paint and layout but leaves the components mounted, and the cost that matters here is script, not layout.
367
352
 
368
353
  [^15]: don't you dare try it lol
@@ -63,6 +63,31 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
63
63
  SOFTWARE.
64
64
  ```
65
65
 
66
+ ## Lucide (ISC)
67
+
68
+ The editor's own chrome embeds icon path data from Lucide: the code block's rail
69
+ (`src/lib/components/blocks/code/CodeBlockRail.svelte`) and the menus, the image toolbar and the
70
+ demo's selection bar (`src/lib/components/menu/MenuIcon.svelte`). The paths are inlined rather
71
+ than taken as a dependency; no Lucide code ships.
72
+
73
+ ```
74
+ ISC License
75
+
76
+ Copyright (c) 2026 Lucide Icons and Contributors
77
+
78
+ Permission to use, copy, modify, and/or distribute this software for any
79
+ purpose with or without fee is hereby granted, provided that the above
80
+ copyright notice and this permission notice appear in all copies.
81
+
82
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
83
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
84
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
85
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
86
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
87
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
88
+ PERFORMANCE OF THIS SOFTWARE.
89
+ ```
90
+
66
91
  ## WHATWG HTML Standard (CC BY 4.0)
67
92
 
68
93
  `src/lib/core/inline/html-entities.ts` vendors the named-character-reference table from <https://html.spec.whatwg.org/entities.json>.
@@ -9,11 +9,23 @@ export declare const FAILED_BLOCK_LABEL = "Block failed to render";
9
9
  export declare const GAP_CARET_LABEL = "Insertion point between blocks";
10
10
  export declare const WHOLE_BLOCK_INPUT_LABEL = "Focused block input";
11
11
  export declare const IMAGE_PROPERTIES_LABEL = "Image properties";
12
+ export declare const IMAGE_ALT_FIELD = "Alt text";
13
+ export declare const IMAGE_ALT_PLACEHOLDER = "Describe the image";
14
+ export declare const IMAGE_CROP = "Crop image";
15
+ export declare const IMAGE_CROP_APPLY = "Apply crop";
16
+ export declare const IMAGE_CROP_CANCEL = "Cancel crop";
17
+ export declare const IMAGE_REMOVE = "Remove image";
12
18
  export declare const LINK_CARD_LABEL = "Link properties";
13
19
  export declare const LINK_CARD_URL = "Link URL";
14
20
  export declare const LINK_CARD_OPEN = "Open link";
15
21
  export declare const LINK_CARD_REMOVE = "Remove link";
16
22
  export declare const CODE_LANGUAGE_FIELD = "Code block language";
23
+ export declare const CODE_RUN_LABEL = "Run code block";
24
+ export declare const CODE_COPY_LABEL = "Copy code";
25
+ export declare const CODE_COPIED_LABEL = "Code copied";
26
+ export declare const CODE_MENU_LABEL = "Code block actions";
27
+ export declare const CODE_RAIL_LABEL = "Code block controls";
28
+ export declare const CODE_LANGUAGE_LIST = "Code block languages";
17
29
  /** Chrome, not an announcement: the language chip's accessible name. */
18
30
  export declare function codeLanguageLabel(language: string): string;
19
31
  export declare const SEARCH_TOGGLE_REPLACE = "Toggle replace";
@@ -30,6 +42,12 @@ export declare const SEARCH_CLOSE_TITLE = "Close";
30
42
  export declare const SEARCH_CLOSE_LABEL = "Close search";
31
43
  export declare const TABLE_ACTIONS = "Table actions";
32
44
  export declare const COLUMN_ALIGNMENT = "Column alignment";
45
+ export declare const ADD_ROW_BELOW = "Add row";
46
+ export declare const ADD_COLUMN_RIGHT = "Add column";
47
+ export declare const TAIL_ADD_ROW = "Add a line below";
48
+ export declare const TAIL_ADD_BLOCK = "Add a block";
49
+ export declare const BLOCK_MENU_LABEL = "Insert a block";
50
+ export declare const BLOCK_ACTIONS_LABEL = "Block actions";
33
51
  export declare const ALIGN_LEFT = "Left";
34
52
  export declare const ALIGN_CENTER = "Center";
35
53
  export declare const ALIGN_RIGHT = "Right";
@@ -9,11 +9,23 @@ export const FAILED_BLOCK_LABEL = 'Block failed to render';
9
9
  export const GAP_CARET_LABEL = 'Insertion point between blocks';
10
10
  export const WHOLE_BLOCK_INPUT_LABEL = 'Focused block input';
11
11
  export const IMAGE_PROPERTIES_LABEL = 'Image properties';
12
+ export const IMAGE_ALT_FIELD = 'Alt text';
13
+ export const IMAGE_ALT_PLACEHOLDER = 'Describe the image';
14
+ export const IMAGE_CROP = 'Crop image';
15
+ export const IMAGE_CROP_APPLY = 'Apply crop';
16
+ export const IMAGE_CROP_CANCEL = 'Cancel crop';
17
+ export const IMAGE_REMOVE = 'Remove image';
12
18
  export const LINK_CARD_LABEL = 'Link properties';
13
19
  export const LINK_CARD_URL = 'Link URL';
14
20
  export const LINK_CARD_OPEN = 'Open link';
15
21
  export const LINK_CARD_REMOVE = 'Remove link';
16
22
  export const CODE_LANGUAGE_FIELD = 'Code block language';
23
+ export const CODE_RUN_LABEL = 'Run code block';
24
+ export const CODE_COPY_LABEL = 'Copy code';
25
+ export const CODE_COPIED_LABEL = 'Code copied';
26
+ export const CODE_MENU_LABEL = 'Code block actions';
27
+ export const CODE_RAIL_LABEL = 'Code block controls';
28
+ export const CODE_LANGUAGE_LIST = 'Code block languages';
17
29
  /** Chrome, not an announcement: the language chip's accessible name. */
18
30
  export function codeLanguageLabel(language) {
19
31
  return `Code language: ${language}`;
@@ -35,6 +47,12 @@ export const SEARCH_CLOSE_LABEL = 'Close search';
35
47
  // ── Table action menu ────────────────────────────────────────────────────────
36
48
  export const TABLE_ACTIONS = 'Table actions';
37
49
  export const COLUMN_ALIGNMENT = 'Column alignment';
50
+ export const ADD_ROW_BELOW = 'Add row';
51
+ export const ADD_COLUMN_RIGHT = 'Add column';
52
+ export const TAIL_ADD_ROW = 'Add a line below';
53
+ export const TAIL_ADD_BLOCK = 'Add a block';
54
+ export const BLOCK_MENU_LABEL = 'Insert a block';
55
+ export const BLOCK_ACTIONS_LABEL = 'Block actions';
38
56
  export const ALIGN_LEFT = 'Left';
39
57
  export const ALIGN_CENTER = 'Center';
40
58
  export const ALIGN_RIGHT = 'Right';
@@ -5,7 +5,7 @@
5
5
  import type { CstNode, TableAlignment } from './core/nodes';
6
6
  import type { NodeView } from './core/node-views';
7
7
  import type { StructuralChange } from './tree-operations/structural-change';
8
- import type { TrackedPosition } from './tree-operations/node-ops';
8
+ import type { TrackedPosition } from './tree-operations/settle';
9
9
  import type { SharingState } from './tree-operations/sharing';
10
10
  import type { BlockComponent, FocusPosition } from './block-component';
11
11
  import type { ScopedOpDescriptor } from './schema/operations';
@@ -290,6 +290,12 @@ export interface TableContext {
290
290
  cycleAlignment(colIdx: number): Promise<void>;
291
291
  /** Set a column's alignment directly — distinct from the cycle step. */
292
292
  setColumnAlignment(colIdx: number, alignment: TableAlignment): Promise<void>;
293
+ /** Write a grid of cell texts from `origin`, appending the rows and columns it needs, as one
294
+ * commit: a spreadsheet-like paste. Empty grids are a no-op. */
295
+ pasteGrid(origin: {
296
+ rowIdx: number;
297
+ colIdx: number;
298
+ }, grid: string[][]): Promise<void>;
293
299
  }
294
300
  /**
295
301
  * The TableContext mutations addressed by ONE axis index, the vocabulary the affordance
@@ -2,6 +2,7 @@
2
2
  * DOM construction and lookup for the ambient marker span — the read-only
3
3
  * prefix container blocks contribute to their first prose child's textContent.
4
4
  */
5
+ import { DRAG_ANCHOR_ATTR } from '../components/block-content-selector';
5
6
  import { domDescendants } from '../cursor/dom-walk';
6
7
  import { isAtomicInlineWidget, isHiddenMarkerText } from '../cursor/widget-offset';
7
8
  import { devWarn } from '../dev-warn';
@@ -10,7 +11,8 @@ export function buildAmbientSpan(prefix) {
10
11
  const outer = document.createElement('span');
11
12
  outer.className = 'md-marker';
12
13
  outer.setAttribute('contenteditable', 'false');
13
- const ranges = normalized.interactive ?? [];
14
+ // Document order, whatever order the host listed them in: the walk below is a single pass.
15
+ const ranges = [...(normalized.interactive ?? [])].sort((a, b) => a.start - b.start);
14
16
  let cursor = 0;
15
17
  for (const range of ranges) {
16
18
  if (range.start < 0 || range.end > normalized.text.length || range.start >= range.end) {
@@ -30,6 +32,8 @@ export function buildAmbientSpan(prefix) {
30
32
  if (range.ariaChecked !== undefined) {
31
33
  inner.setAttribute('aria-checked', String(range.ariaChecked));
32
34
  }
35
+ if (range.dragAnchor)
36
+ inner.setAttribute(DRAG_ANCHOR_ATTR, '');
33
37
  inner.textContent = normalized.text.slice(range.start, range.end);
34
38
  inner.addEventListener('click', range.onClick);
35
39
  outer.appendChild(inner);
@@ -71,6 +71,8 @@ export interface AmbientInteractiveRange {
71
71
  className: string;
72
72
  role?: 'checkbox';
73
73
  ariaChecked?: boolean;
74
+ /** The block's drag grip centres on this span's box rather than on its text line. */
75
+ dragAnchor?: boolean;
74
76
  /** The click lands on the range's own span, before the leaf's caret handling; a handler
75
77
  * reading the chord (`isWidgetActivationClick`) stops propagation to keep the gesture. */
76
78
  onClick: (e: MouseEvent) => void;
@@ -78,6 +80,11 @@ export interface AmbientInteractiveRange {
78
80
  export type AmbientPrefix = string | {
79
81
  text: string;
80
82
  interactive?: AmbientInteractiveRange[];
83
+ /**
84
+ * The hanging indent the rendered prefix needs, when its painted width is not its text
85
+ * width (a task item's `- [ ] ` paints as one box). Defaults to one `ch` per character.
86
+ */
87
+ indent?: string;
81
88
  };
82
89
  /**
83
90
  * The props BlockHost passes every block component; a registry `extraProps` may add
@@ -145,6 +152,13 @@ export interface BlockComponent {
145
152
  * surfaces with no such snap.
146
153
  */
147
154
  snapCaretToPoint?(clientX: number, clientY: number): void;
155
+ /**
156
+ * A press beside the block (the editor's margin, the host's own padding) that may become a
157
+ * drag: start the block's OWN drag anchored at the leaf nearest the point — a table's cell
158
+ * rectangle — exactly as a press on that leaf would. True when it did; false leaves the press
159
+ * to the editor's generic drag.
160
+ */
161
+ startDragAtPoint?(clientX: number, clientY: number, event: PointerEvent): boolean;
148
162
  /**
149
163
  * Descend child indices to the BlockComponent at the leaf, or null if the path
150
164
  * doesn't resolve. Empty `path` returns this component. Containers implement it.
@@ -1,30 +1,35 @@
1
1
  <script lang="ts">
2
2
  import { DRAG_HANDLE_TITLE } from '../a11y-strings';
3
+ import MenuIcon from './menu/MenuIcon.svelte';
4
+ import { alignDragHandle } from './drag-handle';
3
5
  </script>
4
6
 
5
7
  <!-- aria-hidden + non-focusable: keyboard reorder (Alt+Arrow) is the operable,
6
8
  screen-reader-visible path, so this mouse-only grip stays out of the tab/SR flow. -->
7
- <span class="block-drag-handle" aria-hidden="true" title={DRAG_HANDLE_TITLE}>
8
- <span class="grip"><span class="dots"></span></span>
9
+ <span
10
+ class="block-drag-handle"
11
+ aria-hidden="true"
12
+ title={DRAG_HANDLE_TITLE}
13
+ {@attach alignDragHandle}
14
+ >
15
+ <span class="grip"><MenuIcon name="grip-vertical" size={16} /></span>
9
16
  </span>
10
17
 
11
18
  <style>
12
19
  .block-drag-handle {
13
20
  position: absolute;
14
- /* Sits inside the editor's own 1rem padding, so the grip clears the block's left
21
+ /* Sits inside the editor's own left padding, so the grip clears the block's left
15
22
  border instead of being clipped behind it (overflow-x:auto). */
16
- left: -0.85rem;
23
+ left: -1.25rem;
17
24
  /* Spans gutter to content-left (width === |left|) so a pointer gliding from the
18
25
  block never crosses an un-hovered gap, which would hide the handle and — being
19
26
  pointer-events:none once hidden — strand it. Stopping AT content-left keeps
20
27
  line-start caret/marker clicks from being hijacked into a drag. */
21
- width: 0.85rem;
28
+ width: 1.25rem;
22
29
  /* Full-height hit strip so the handle is reachable at ANY height; the visible
23
- grip aligns to the first line. */
30
+ grip sits on the block's first line. */
24
31
  top: 0;
25
32
  bottom: 0;
26
- display: flex;
27
- align-items: flex-start;
28
33
  opacity: 0;
29
34
  pointer-events: none;
30
35
  cursor: grab;
@@ -32,35 +37,41 @@
32
37
  color: var(--color-ui-muted, #a4a4a4);
33
38
  }
34
39
 
35
- /* One line-height box so the dots sit on the first line; a block with a divergent
36
- inner line-height is off by a few px, which is cosmetic — reachability rides the strip. */
40
+ /* Flush left in the strip (the glyph ends a few px clear of the content), centred on the
41
+ measured band that `alignDragHandle` writes as an inline `top`.
42
+
43
+ ALWAYS hittable, unlike the strip around it: a grip reachable only by first hovering its
44
+ block is a flyout you traverse the block to get to. Its own box, not the full-height
45
+ strip, which would swallow every gutter click the block has. */
37
46
  .grip {
47
+ position: absolute;
48
+ left: 0;
49
+ width: 100%;
50
+ height: 1.25rem;
51
+ top: 0.5lh;
52
+ transform: translateY(-50%);
38
53
  display: flex;
39
54
  align-items: center;
40
- height: 1lh;
55
+ justify-content: flex-start;
56
+ pointer-events: auto;
41
57
  }
42
58
 
43
- .dots {
44
- display: block;
45
- width: 0.5rem;
46
- height: 0.85rem;
47
- background-image: radial-gradient(currentColor 40%, transparent 45%);
48
- background-size: 0.25rem 0.28rem;
59
+ /* The hit target is bigger than the glyph: a 16px box in a gutter is a target the pointer
60
+ misses between two rows, and a miss here reads as the grip belonging to the block above.
61
+ Never past the strip's right edge, which is the content's first character. */
62
+ .grip::before {
63
+ content: '';
64
+ position: absolute;
65
+ inset: -8px 0 -8px -4px;
49
66
  }
50
67
 
51
- /* Touch never fires the hover reveal, so the handle shows unasked. The pointer goes to the
52
- grip rather than the full-height strip, which would make the whole gutter unscrollable. */
68
+ /* Touch never fires the hover reveal, so the handle shows unasked. */
53
69
  @media (hover: none) {
54
70
  .block-drag-handle {
55
71
  opacity: 1;
56
72
  }
57
73
  .grip {
58
- pointer-events: auto;
59
74
  touch-action: none;
60
- /* The whole gutter slot, not just the dots: 1rem of editor padding is all the width
61
- there is, and any more would reach over the line's first character. */
62
- width: 100%;
63
- justify-content: center;
64
75
  }
65
76
  }
66
77
  </style>
@@ -13,6 +13,7 @@
13
13
  import SelectionOverlay from './SelectionOverlay.svelte';
14
14
  import DecorationOverlay from './DecorationOverlay.svelte';
15
15
  import BlockDragHandle from './BlockDragHandle.svelte';
16
+ import { showsDragHandle } from './drag-handle';
16
17
  import TextEditableBlock from './blocks/text/TextEditableBlock.svelte';
17
18
  import { defaultRegistryView } from '../schema/registry-view';
18
19
  import { FAILED_BLOCK_LABEL } from '../a11y-strings';
@@ -59,12 +60,16 @@
59
60
  const getDoc = getContext<EditorDoc | undefined>(EDITOR_DOC_KEY)?.doc;
60
61
  // Stable object, so a plain read rather than a getter.
61
62
  const rects = services?.rects;
62
- const getDragHandles = getContext<EditorPolicies | undefined>(
63
- EDITOR_POLICIES_KEY
64
- )?.blockDragHandles;
63
+ const policies = getContext<EditorPolicies | undefined>(EDITOR_POLICIES_KEY);
64
+ const getDragHandles = policies?.blockDragHandles;
65
65
  // $derived, not a mount-time snapshot: a runtime prop toggle must reach blocks
66
66
  // that window in after the change.
67
67
  const dragHandles = $derived(getDragHandles?.() ?? false);
68
+ // The affordance opt-in already folds reading mode in, but a picture's grip does not wait
69
+ // for that opt-in, so reading mode is asked here too.
70
+ const isReading = $derived(policies?.presentationMode?.() === 'reading');
71
+ // A reorder unit without a grip (a paragraph) is still a drop neighbour and keyboard-movable.
72
+ const showsHandle = $derived(reorderable && !isReading && showsDragHandle(node, dragHandles));
68
73
 
69
74
  let myPath = $derived([...parentPath, index]);
70
75
 
@@ -167,7 +172,7 @@
167
172
 
168
173
  // A block can grow after mount without its `raw` changing (async content decoding
169
174
  // in), which the effect above never sees, and overflow-anchor is off so the growth
170
- // would slide the viewport. The scope gates on the height it already recorded.
175
+ // would slide the viewport. The scope gates on the height it last applied.
171
176
  $effect(() => {
172
177
  if (!hostEl || !measureChannel) return;
173
178
  const observer = new ResizeObserver((entries) => {
@@ -233,7 +238,7 @@
233
238
  <div
234
239
  class={[
235
240
  'block-host',
236
- { 'reorder-host': reorderable && dragHandles },
241
+ { 'reorder-host': reorderable && dragHandles, 'handle-host': showsHandle },
237
242
  ...blockDecs.flatMap((d) => d.class ?? [])
238
243
  ]}
239
244
  data-block-path={JSON.stringify(myPath)}
@@ -291,7 +296,7 @@
291
296
  />
292
297
  <!-- Rendered LAST so the block-el lookup still resolves block content as its
293
298
  first match. -->
294
- {#if reorderable && dragHandles}
299
+ {#if showsHandle}
295
300
  <BlockDragHandle />
296
301
  {/if}
297
302
  </div>
@@ -302,9 +307,11 @@
302
307
  }
303
308
 
304
309
  /* Pure-CSS hover reveal: no per-block reactive state on a path whose cost scales
305
- with mounted-component count. Global because reorder hosts nest; the `:not(:has(
306
- ...))` reveals the innermost hovered handle, not a staircase of ancestors. */
307
- :global(.reorder-host:hover:not(:has(.reorder-host:hover)) > .block-drag-handle) {
310
+ with mounted-component count. Global because handle hosts nest; the `:not(:has(
311
+ ...))` reveals the innermost hovered handle, not a staircase of ancestors. A gripless
312
+ unit (a paragraph in a quote) is no host, so hovering it reveals its container's. */
313
+ :global(.handle-host:hover:not(:has(.handle-host:hover)) > .block-drag-handle),
314
+ :global(.block-drag-handle:hover) {
308
315
  opacity: 1;
309
316
  pointer-events: auto;
310
317
  }