@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
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Editor-root geometry observers: the width and viewport-height watchers, the type-scale probe
3
+ * and the header-slot compensation. Each observes the element its installing `$effect`
4
+ * captured and returns the teardown; the reactive signals they feed (`widthVersion`,
5
+ * `typeScale`, `viewportHeightVersion`) stay in `Editor.svelte`, which is what the rest of the
6
+ * root reads. ResizeObserver's per-callback batching is the coalescing; no timer debounce (G4.4).
7
+ */
8
+ import { ESTIMATE_BASE_FONT_SIZE } from '../cursor/typography-estimates';
9
+ import { onRoot, removeAll } from './editor-root-listeners';
10
+ // ── Width and height ────────────────────────────────────────────────
11
+ /** Calls `onWidthChange` when `el`'s width moves. A height-only resize is ignored: only a
12
+ * re-wrap stales the measured heights. */
13
+ export function installWidthWatcher(el, onWidthChange) {
14
+ let lastWidth = el.clientWidth;
15
+ const observer = new ResizeObserver(() => {
16
+ const width = el.clientWidth;
17
+ if (width === lastWidth)
18
+ return;
19
+ lastWidth = width;
20
+ onWidthChange();
21
+ });
22
+ observer.observe(el);
23
+ return () => observer.disconnect();
24
+ }
25
+ /** Calls `bump` on a height change of the resolved scrollport `target`. */
26
+ export function installViewportHeightWatcher(target, bump) {
27
+ if (target === window) {
28
+ // The page viewport has no box to observe, and a visualViewport move (a mobile URL
29
+ // bar retracting) never touches documentElement's height — hence both, ungated.
30
+ const visual = window.visualViewport;
31
+ return removeAll(onRoot(window, 'resize', bump), visual ? onRoot(visual, 'resize', bump) : () => { });
32
+ }
33
+ // Cast, not a narrowing: `UserScrollport` is a union of object types, which `=== window`
34
+ // does not narrow — the same cast `createScrollport` makes on the same split.
35
+ const el = target;
36
+ let lastHeight = el.clientHeight;
37
+ const observer = new ResizeObserver(() => {
38
+ if (el.clientHeight === lastHeight)
39
+ return;
40
+ lastHeight = el.clientHeight;
41
+ bump();
42
+ });
43
+ observer.observe(el);
44
+ return () => observer.disconnect();
45
+ }
46
+ /**
47
+ * The width watcher's sibling: a font-size move puts the height estimates off several-fold, so
48
+ * a document whose true height clears the activation watermark can fail to window at all. No
49
+ * other box in the root reports it, hence the `1em` probe; the scale is relative to the size
50
+ * `HEIGHT_ESTIMATES` were calibrated at.
51
+ */
52
+ export function installTypeScaleProbe(el, deps) {
53
+ const apply = (fontSizePx) => {
54
+ const next = fontSizePx / ESTIMATE_BASE_FONT_SIZE;
55
+ // Sub-percent moves are sub-pixel on a line box — not worth a full rebuild.
56
+ if (!(next > 0) || Math.abs(next - deps.getScale()) < 0.01)
57
+ return;
58
+ deps.onScale(next);
59
+ };
60
+ apply(el.getBoundingClientRect().height);
61
+ const observer = new ResizeObserver((entries) => apply(borderBoxHeight(entries, el)));
62
+ observer.observe(el);
63
+ return () => observer.disconnect();
64
+ }
65
+ // Border boxes throughout, seed and fallback alike, so a browser without `borderBoxSize`
66
+ // computes the same delta.
67
+ function borderBoxHeight(entries, el) {
68
+ const box = entries[0]?.borderBoxSize?.[0];
69
+ return box ? box.blockSize : el.getBoundingClientRect().height;
70
+ }
71
+ /**
72
+ * The header slot's height lives outside the height model, so while the editor owns the
73
+ * correction a growing header would slide the document under the reader. Compensating from
74
+ * the SLOT's own resize composes with `correctAnchor` instead of double-correcting; a reveal
75
+ * already holding the scroll outranks it.
76
+ */
77
+ export function installHeaderSlotCompensation(deps) {
78
+ const { el, port } = deps;
79
+ let lastHeight = el.getBoundingClientRect().height;
80
+ const observer = new ResizeObserver((entries) => {
81
+ const height = borderBoxHeight(entries, el);
82
+ const delta = height - lastHeight;
83
+ lastHeight = height;
84
+ if (delta === 0 || !deps.ownsScrollCorrection() || port.scrollTop() === 0)
85
+ return;
86
+ if (!deps.revealHoldsScroll())
87
+ port.setScrollTop(port.scrollTop() + delta);
88
+ });
89
+ observer.observe(el);
90
+ return () => observer.disconnect();
91
+ }
@@ -31,7 +31,7 @@ export interface EditorRootKeydownDeps {
31
31
  * chords. `undefined` = every installed plugin. */
32
32
  activation: PluginActivation | undefined;
33
33
  onCommandError: CommandErrorSink;
34
- crossBlock: Pick<CrossBlockHandlers, 'handleKeyDown'>;
34
+ crossBlock: Pick<CrossBlockHandlers, 'handleKeyDown' | 'insertText'>;
35
35
  /** True for nodes in the host's `header` slot: they sit inside `root.contains`
36
36
  * without being the editor's own content. */
37
37
  isHostChrome(node: Node | null): boolean;
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { claimsBodyChord, isForeignTextEntry } from '../active-editor';
10
10
  import { isReservedUiChord, runGlobalChord } from '../schema/commands';
11
- import { eventToChord } from '../schema/keybindings';
11
+ import { eventToChord, isCharacterKey } from '../schema/keybindings';
12
12
  export function createEditorRootKeydown(deps) {
13
13
  /**
14
14
  * Search / Escape: focus inside this editor, or a search chord this instance
@@ -81,8 +81,17 @@ export function createEditorRootKeydown(deps) {
81
81
  event.preventDefault();
82
82
  return;
83
83
  }
84
- if (deps.isCrossBlock)
85
- void deps.crossBlock.handleKeyDown(event);
84
+ if (!deps.isCrossBlock)
85
+ return;
86
+ // A range whose blocks host no character position leaves no editing surface focused, so
87
+ // no `beforeinput` ever fires for a typed character and this is the only door it has.
88
+ // Composition still belongs to the browser, and a chorded key is not text.
89
+ if (isCharacterKey(event.key) && !event.isComposing && !event.ctrlKey && !event.metaKey) {
90
+ event.preventDefault();
91
+ void deps.crossBlock.insertText(event.key);
92
+ return;
93
+ }
94
+ void deps.crossBlock.handleKeyDown(event);
86
95
  }
87
96
  };
88
97
  }
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * Editor-root ambient listeners: the mod-active cursor tracker, the selectionchange
3
- * bridge, and the viewport-height watcher. Pure dispatch over live getters; each
4
- * installing `$effect` stays in `Editor.svelte` as a guard plus one install call,
5
- * returning the teardown. `onRoot`/`removeAll` capture that add/remove pair once.
3
+ * bridge, the blur announcer and the double-click word select. Pure dispatch over live
4
+ * getters; each installing `$effect` stays in `Editor.svelte` as a guard plus one install
5
+ * call, returning the teardown. `onRoot`/`removeAll` capture that add/remove pair once.
6
6
  */
7
- import type { UserScrollport } from '../cursor/scroll-ancestors';
8
7
  export declare function onRoot<E extends Event>(target: EventTarget, type: string, handler: (event: E) => void, options?: {
9
8
  capture?: boolean;
10
9
  passive?: boolean;
@@ -40,5 +39,10 @@ export declare function installEditorBlurAnnouncer(deps: {
40
39
  root: HTMLElement;
41
40
  emit: () => void;
42
41
  }): () => void;
43
- /** Calls `bump` on a height change of the resolved scrollport `target`. */
44
- export declare function installViewportHeightWatcher(target: UserScrollport, bump: () => void): () => void;
42
+ /**
43
+ * Windows Chromium's double-click takes the space after the word, so the word is selected on
44
+ * the SECOND press, trimmed, with the native selection suppressed; the dblclick trim is the
45
+ * fallback. A press on an inline widget is that widget's own gesture (a footnote's double-click
46
+ * takes its whole token), so the root leaves it to the widget.
47
+ */
48
+ export declare function installDoubleClickWordSelect(root: HTMLElement): () => void;
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Editor-root ambient listeners: the mod-active cursor tracker, the selectionchange
3
- * bridge, and the viewport-height watcher. Pure dispatch over live getters; each
4
- * installing `$effect` stays in `Editor.svelte` as a guard plus one install call,
5
- * returning the teardown. `onRoot`/`removeAll` capture that add/remove pair once.
3
+ * bridge, the blur announcer and the double-click word select. Pure dispatch over live
4
+ * getters; each installing `$effect` stays in `Editor.svelte` as a guard plus one install
5
+ * call, returning the teardown. `onRoot`/`removeAll` capture that add/remove pair once.
6
6
  */
7
7
  import { tick } from 'svelte';
8
+ import { isEditableEventTarget } from '../editor-actions/whole-block-focus-surface';
9
+ import { selectWordAtPoint, trimDoubleClickSelection } from '../selection/double-click-trim';
8
10
  // ── Listener plumbing ───────────────────────────────────────────────
9
11
  // Typed off `Event`, not the per-target event maps: those names are type-only,
10
12
  // and `target` ranges over every EventTarget the root effects listen on.
@@ -89,24 +91,23 @@ export function installEditorBlurAnnouncer(deps) {
89
91
  };
90
92
  return onRoot(deps.root, 'focusout', handler);
91
93
  }
92
- /** Calls `bump` on a height change of the resolved scrollport `target`. */
93
- export function installViewportHeightWatcher(target, bump) {
94
- if (target === window) {
95
- // The page viewport has no box to observe, and a visualViewport move (a mobile URL
96
- // bar retracting) never touches documentElement's height — hence both, ungated.
97
- const visual = window.visualViewport;
98
- return removeAll(onRoot(window, 'resize', bump), visual ? onRoot(visual, 'resize', bump) : () => { });
99
- }
100
- // Cast, not a narrowing: `UserScrollport` is a union of object types, which `=== window`
101
- // does not narrow — the same cast `createScrollport` makes on the same split.
102
- const el = target;
103
- let lastHeight = el.clientHeight;
104
- const observer = new ResizeObserver(() => {
105
- if (el.clientHeight === lastHeight)
94
+ /**
95
+ * Windows Chromium's double-click takes the space after the word, so the word is selected on
96
+ * the SECOND press, trimmed, with the native selection suppressed; the dblclick trim is the
97
+ * fallback. A press on an inline widget is that widget's own gesture (a footnote's double-click
98
+ * takes its whole token), so the root leaves it to the widget.
99
+ */
100
+ export function installDoubleClickWordSelect(root) {
101
+ const onSecondPress = (e) => {
102
+ if (e.detail !== 2 || e.button !== 0 || e.shiftKey || e.ctrlKey || e.metaKey)
103
+ return;
104
+ if (!isEditableEventTarget(e.target) || pressesInlineWidget(e.target))
106
105
  return;
107
- lastHeight = el.clientHeight;
108
- bump();
109
- });
110
- observer.observe(el);
111
- return () => observer.disconnect();
106
+ if (selectWordAtPoint(root.ownerDocument, e.clientX, e.clientY))
107
+ e.preventDefault();
108
+ };
109
+ return removeAll(onRoot(root, 'mousedown', onSecondPress), onRoot(root, 'dblclick', () => trimDoubleClickSelection(root.ownerDocument)));
110
+ }
111
+ function pressesInlineWidget(target) {
112
+ return target instanceof Element && target.closest('[data-inline-widget]') !== null;
112
113
  }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Editor-root presentation-mode flip: carry the caret across a mode change. The pre phase is
3
+ * the last moment the outgoing mode owns the DOM (past it the mode's render key has rebuilt
4
+ * every block from its own CST bytes), so the caret is captured and the surface blurred there;
5
+ * the post phase drops the geometry the old markers set and re-seats the caret through the
6
+ * reveal road. The two `$effect`s stay in `Editor.svelte` as the mode read plus one call each.
7
+ */
8
+ import type { EdgeAffinityState } from '../cursor/edge-affinity';
9
+ import type { HeightOracle } from '../cursor/height-oracle';
10
+ import type { EditorEvents } from '../editor-events';
11
+ import type { BlockElLookup } from '../editor-keys';
12
+ import type { PresentationMode } from '../presentation-mode';
13
+ import type { EditorSelection } from '../selection/primitives';
14
+ import type { SelectionState } from '../selection/selection-state.svelte';
15
+ export interface ModeFlipDeps {
16
+ /** Getters, never values: the root binds after construction and the mode moves. */
17
+ get editorEl(): HTMLElement | undefined;
18
+ get mode(): PresentationMode;
19
+ selection: Pick<SelectionState, 'isCrossBlock' | 'gapCaret' | 'clearGapCaret'>;
20
+ /** The public snapshot, which also answers for a native caret no block reports. */
21
+ getSelection(): EditorSelection | null;
22
+ getBlockElByPath: BlockElLookup;
23
+ isHostChrome(node: Node | null): boolean;
24
+ edgeAffinity: Pick<EdgeAffinityState, 'reset'>;
25
+ heightOracle: Pick<HeightOracle, 'dropMeasured'>;
26
+ events: EditorEvents;
27
+ /** The bare-mount restore road: a flip is a view operation and writes no scrollport. */
28
+ restoreCaret(path: number[], offset: number): Promise<unknown>;
29
+ }
30
+ export interface ModeFlip {
31
+ /** The `$effect.pre` half, run untracked: capture and blur while the outgoing mode owns the DOM. */
32
+ beforeFlip(to: PresentationMode): void;
33
+ /** The `$effect` half: reset mode-bound geometry, then restore the caret after the flush. */
34
+ afterFlip(to: PresentationMode): void;
35
+ }
36
+ export declare function createModeFlip(deps: ModeFlipDeps): ModeFlip;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Editor-root presentation-mode flip: carry the caret across a mode change. The pre phase is
3
+ * the last moment the outgoing mode owns the DOM (past it the mode's render key has rebuilt
4
+ * every block from its own CST bytes), so the caret is captured and the surface blurred there;
5
+ * the post phase drops the geometry the old markers set and re-seats the caret through the
6
+ * reveal road. The two `$effect`s stay in `Editor.svelte` as the mode read plus one call each.
7
+ */
8
+ import { tick } from 'svelte';
9
+ import { isTextEntrySurface } from '../active-editor';
10
+ import { ambientLengthOf } from '../ambient/ambient-dom';
11
+ import { toClampedRawOffset } from '../cursor/coordinate-spaces';
12
+ import { domTextOffsetAtNode } from '../cursor/widget-offset';
13
+ import { findSurfacePathForElement } from '../selection/path-lookup';
14
+ export function createModeFlip(deps) {
15
+ let flipCaret = null;
16
+ let preFlipSeenMode = deps.mode;
17
+ let lastEffectiveMode = deps.mode;
18
+ /** The focused leaf's caret as (path, raw offset): the per-backend road while a leaf holds
19
+ * focus, else the native range a toggle click leaves behind while chrome takes focus. */
20
+ function captureCaret() {
21
+ if (deps.selection.isCrossBlock || deps.selection.gapCaret)
22
+ return null;
23
+ const focused = deps.getSelection()?.focus;
24
+ if (focused)
25
+ return { path: focused.path, offset: focused.offset };
26
+ const sel = window.getSelection();
27
+ const node = sel?.focusNode;
28
+ if (!node || !deps.editorEl?.contains(node) || deps.isHostChrome(node))
29
+ return null;
30
+ const el = node instanceof Element ? node : node.parentElement;
31
+ const path = findSurfacePathForElement(el);
32
+ if (!path)
33
+ return null;
34
+ const contentEl = deps.getBlockElByPath(path);
35
+ if (!contentEl?.contains(node))
36
+ return null;
37
+ const offset = toClampedRawOffset(domTextOffsetAtNode(contentEl, node, sel.focusOffset), ambientLengthOf(contentEl));
38
+ return { path, offset };
39
+ }
40
+ // A post-tick focus like every structural op's; the road clamps the saved offset into the
41
+ // destination mode's landable range. Yielding to a focused text-entry surface keeps the
42
+ // restore from stealing a host field mid-typing.
43
+ async function restoreAfterFlush(caret, to) {
44
+ await tick();
45
+ if (deps.mode !== to || isTextEntrySurface(document.activeElement))
46
+ return;
47
+ await deps.restoreCaret(caret.path, caret.offset);
48
+ }
49
+ return {
50
+ beforeFlip(to) {
51
+ if (to === preFlipSeenMode)
52
+ return;
53
+ const from = preFlipSeenMode;
54
+ preFlipSeenMode = to;
55
+ // Reading keeps its entry snapshot: it has no caret of its own to recapture on the way out.
56
+ if (from !== 'reading')
57
+ flipCaret = captureCaret();
58
+ // A flip is a blur-class event: a live reveal or composition folds through the existing
59
+ // blur choke point. Host chrome is exempt, or a mode toggle blurs a title field mid-edit.
60
+ const active = document.activeElement;
61
+ if (active instanceof HTMLElement &&
62
+ deps.editorEl?.contains(active) &&
63
+ !deps.isHostChrome(active)) {
64
+ active.blur();
65
+ // A blur the editor performs announces the selection it drops: the document listener
66
+ // only reports a range the browser still anchors in the root, and this one is gone.
67
+ deps.events.emit('selectionChange', deps.getSelection());
68
+ }
69
+ },
70
+ afterFlip(to) {
71
+ if (to === lastEffectiveMode)
72
+ return;
73
+ lastEffectiveMode = to;
74
+ // Which markers paint just changed: a side recorded against the old geometry no longer
75
+ // names the offset the user meant, and every measured height is the other mode's. Not
76
+ // paired with a width bump, since the flip has blurred and a rebuild would lose the pin.
77
+ deps.edgeAffinity.reset();
78
+ deps.heightOracle.dropMeasured();
79
+ if (to === 'reading') {
80
+ // The gap is an editor-owned caret no DOM blur can reach, so the flip clears it
81
+ // rather than each arrival path.
82
+ deps.selection.clearGapCaret();
83
+ }
84
+ else if (flipCaret) {
85
+ const caret = flipCaret;
86
+ flipCaret = null;
87
+ void restoreAfterFlush(caret, to);
88
+ }
89
+ deps.events.emit('presentationModeChange', to);
90
+ }
91
+ };
92
+ }
@@ -41,6 +41,8 @@
41
41
  } = $props();
42
42
 
43
43
  let imageOverlayEl: HTMLDivElement | undefined = $state();
44
+ // The crop session owns the pointer over the image; the resize grip steps aside meanwhile.
45
+ let cropping = $state(false);
44
46
 
45
47
  const linkRef = getContext<EditorDoc | undefined>(EDITOR_DOC_KEY)?.linkRef;
46
48
 
@@ -95,19 +97,25 @@
95
97
  {@const ctx = imageEdit.getSelectedImageFields()}
96
98
  {#if ctx?.widgetEl}
97
99
  <div bind:this={imageOverlayEl} class="md-image-overlay" data-image-overlay>
98
- <ImageResizeHandles
99
- getWidgetEl={() => imageEdit.getSelectedImageFields()?.widgetEl ?? null}
100
- editorContentWidth={imageEdit.getEditorContentWidth()}
101
- editorEvents={events}
102
- onCommit={imageEdit.commitImageResize}
103
- />
100
+ {#if !cropping}
101
+ <ImageResizeHandles
102
+ getWidgetEl={() => imageEdit.getSelectedImageFields()?.widgetEl ?? null}
103
+ editorContentWidth={imageEdit.getEditorContentWidth()}
104
+ editorEvents={events}
105
+ onCommit={imageEdit.commitImageResize}
106
+ />
107
+ {/if}
104
108
  {#key `${sel.paragraphPath.join(',')}@${sel.sourceStart}`}
105
109
  <ImageProperties
106
110
  target={sel}
107
111
  fields={imageFieldsFromInline(ctx.image)}
112
+ getWidgetEl={() => imageEdit.getSelectedImageFields()?.widgetEl ?? null}
108
113
  buildBytes={imageEdit.buildEditBytes}
109
114
  onCommit={imageEdit.commitImageEdit}
115
+ onRemove={imageEdit.removeImage}
110
116
  onDismiss={imageEdit.dismissImagePopover}
117
+ maxFrameWidth={imageEdit.getEditorContentWidth}
118
+ bind:cropping
111
119
  />
112
120
  {/key}
113
121
  </div>