@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
@@ -5,16 +5,53 @@
5
5
  */
6
6
  import { tick } from 'svelte';
7
7
  import { resolvedInlineContent } from '../../../core/inline/inline-cache';
8
- import { isInlineWidget, flattenInlineWidgets, getInlineWidgetEditing, isWidgetActivationClick } from '../../../core/inline/inline-widgets';
8
+ import { flattenInlineWidgets, getInlineWidgetEditing, isCharacterLikeWidget, isWidgetActivationClick } from '../../../core/inline/inline-widgets';
9
9
  import { isVerticallyTransparentNode } from '../../../core/inline/transparency';
10
10
  import { trimTrailingLineEnding, trailingLineEnding } from '../../../core/lines';
11
11
  import { asRawOffset, toClampedRawOffset, toDomTextOffset } from '../../../cursor/coordinate-spaces';
12
12
  import { domTextOffsetAtNode, createRangeAtDomTextOffsets, selectionFocusWalkOffset } from '../../../cursor/widget-offset';
13
13
  import { createSourceReveal } from '../../../cursor/reveal-source';
14
+ import { nearestWidgetEdgeSeat } from '../../../cursor/widget-edge-snap';
14
15
  import { traceRevealOpen, traceRevealFold } from '../../../debug/interaction-trace';
15
16
  import { assertInvariant } from '../../../assert';
16
- import { caretIsInTextContent, hasModifier, isPlainTypingKey } from './click-snap-guard';
17
+ import { caretIsInTextContent, hasModifier, isPlainTypingKey, surfaceHoldsRange } from './click-snap-guard';
17
18
  import { findWidgetNodeByStart, findFirstEdgeWidget, findLastEdgeWidget, rawHasNoTextBefore, rawHasNoTextAfter, widgetElByStart } from './widget-adjacency';
19
+ /**
20
+ * The inline-code wash over a revealed source, painted through the CSS Custom Highlight API
21
+ * (`::highlight(md-inline-reveal)` in editor.css) rather than a wrapper span: the reveal's
22
+ * contract, and every offset read on it, is that the source is a bare text node in the block.
23
+ * The range selects the NODE, so it keeps covering the text as typing grows it. Absent the API
24
+ * (jsdom, an old engine) the source simply shows unwashed.
25
+ */
26
+ const REVEAL_HIGHLIGHT = 'md-inline-reveal';
27
+ const washRanges = new WeakMap();
28
+ function revealHighlight() {
29
+ if (typeof CSS === 'undefined' || !('highlights' in CSS) || typeof Highlight !== 'function') {
30
+ return null;
31
+ }
32
+ let highlight = CSS.highlights.get(REVEAL_HIGHLIGHT);
33
+ if (!highlight) {
34
+ highlight = new Highlight();
35
+ CSS.highlights.set(REVEAL_HIGHLIGHT, highlight);
36
+ }
37
+ return highlight;
38
+ }
39
+ function washRevealedSource(node) {
40
+ const highlight = revealHighlight();
41
+ if (!highlight)
42
+ return;
43
+ const range = document.createRange();
44
+ range.selectNode(node);
45
+ washRanges.set(node, range);
46
+ highlight.add(range);
47
+ }
48
+ function unwashRevealedSource(node) {
49
+ const range = washRanges.get(node);
50
+ if (!range)
51
+ return;
52
+ washRanges.delete(node);
53
+ revealHighlight()?.delete(range);
54
+ }
18
55
  export function createWidgetInteraction(deps) {
19
56
  const isReading = () => deps.getPresentationMode?.() === 'reading';
20
57
  // Resolver-aware so widget detection matches the render path's view; a mismatch
@@ -22,12 +59,26 @@ export function createWidgetInteraction(deps) {
22
59
  function inlinesOf(node) {
23
60
  return resolvedInlineContent(node, deps.linkRef);
24
61
  }
62
+ /**
63
+ * Every widget in this block, DESCENDING into non-widget parents. The flat inline list is
64
+ * the wrong shape to scan for widgets: a construct wrapping one — emphasis around a math
65
+ * span, a link around an image — hides it, and every pointer and arrow-key path that
66
+ * scanned the flat list simply did not see it. `> *… $L/9 \times 10^{20}$ …*` is the case
67
+ * that surfaced it: the quote's whole text is one emphasis node, so nothing inside it was
68
+ * ever clickable.
69
+ */
70
+ function widgetsOf() {
71
+ return flattenInlineWidgets(inlinesOf(deps.node), deps.node.raw);
72
+ }
25
73
  let revealState = null;
26
74
  // Outside the record because they must survive `resetReveal` for the kernel's async
27
75
  // restore. Identity is load-bearing: two byte-identical widgets share a reuse-pool key,
28
76
  // so a lookup can return the OTHER instance and `replaceWith` would MOVE it.
29
77
  let activeSourceNode = null;
30
78
  let revealedWidget = null;
79
+ // The offset the last fold parked, which lies inside the construct it just folded: the
80
+ // restore must not read it as a formula closing around the caret and reopen the reveal.
81
+ let foldParkedCaret = null;
31
82
  // Every fold entry is module-private and pre-guarded by all of its callers, so a fold
32
83
  // with no active reveal means a new caller skipped the guard or a flag leaked (G1.26).
33
84
  function assertFoldTargetsActiveReveal(entry) {
@@ -38,6 +89,7 @@ export function createWidgetInteraction(deps) {
38
89
  function restoreRenderedWidget() {
39
90
  if (activeSourceNode === null || revealedWidget === null)
40
91
  return;
92
+ unwashRevealedSource(activeSourceNode);
41
93
  activeSourceNode.replaceWith(revealedWidget);
42
94
  activeSourceNode = null;
43
95
  revealedWidget = null;
@@ -93,6 +145,7 @@ export function createWidgetInteraction(deps) {
93
145
  revealedWidget = widget;
94
146
  activeSourceNode = document.createTextNode(source);
95
147
  widget.replaceWith(activeSourceNode);
148
+ washRevealedSource(activeSourceNode);
96
149
  },
97
150
  // Re-inserts the exact element the swap detached, still current because the edit
98
151
  // was discarded. The persist path re-renders reactively instead.
@@ -138,9 +191,12 @@ export function createWidgetInteraction(deps) {
138
191
  const { caretBefore, originalDisplay } = active;
139
192
  // The reactive re-render rebuilds the island, so drop the swap handles without a
140
193
  // DOM restore, then run the canonical teardown.
194
+ if (sourceNode)
195
+ unwashRevealedSource(sourceNode);
141
196
  activeSourceNode = null;
142
197
  revealedWidget = null;
143
198
  resetReveal();
199
+ foldParkedCaret = caretAfter;
144
200
  // No edit: fold without touching the CST. A zero-diff write still pushes a dead
145
201
  // undo entry, so the user's next Ctrl+Z would revert nothing instead of their
146
202
  // prior action. The pending-cursor set alone re-renders from the untouched CST.
@@ -243,10 +299,60 @@ export function createWidgetInteraction(deps) {
243
299
  }
244
300
  // The ONE hit test the pointerdown probe, the click dispatch, and the post-fold
245
301
  // re-resolve share.
302
+ /**
303
+ * What the pointer is actually over, asked of the DOM rather than of a rectangle. An
304
+ * island's border box is not what the reader sees: a KaTeX render (a superscript, a
305
+ * fraction) paints ABOVE and BELOW its own box, so a click on the visible glyphs missed
306
+ * the rect by a pixel or two and the widget looked inert to the mouse while the keyboard
307
+ * reached it fine. `elementFromPoint` has no such gap — it answers with whatever is
308
+ * painted at the point, overflow included.
309
+ */
310
+ function revealWidgetFromDom(el, x, y) {
311
+ // Feature-detected: jsdom implements no hit testing, and the rect scan below is the
312
+ // answer there — the same shape as `getTargetRanges`' detection on the code surface.
313
+ if (typeof document.elementFromPoint !== 'function')
314
+ return null;
315
+ const at = document.elementFromPoint(x, y);
316
+ const island = at instanceof Element ? at.closest('[data-inline-widget][data-source-start]') : null;
317
+ // Containment guard: the point may sit over another block's widget entirely.
318
+ if (!island || !el.contains(island))
319
+ return null;
320
+ const start = Number(island.getAttribute('data-source-start'));
321
+ if (!Number.isInteger(start))
322
+ return null;
323
+ const inline = widgetsOf().find((n) => n.start === start && getInlineWidgetEditing(n.kind)?.revealSource);
324
+ return inline ? { inline } : null;
325
+ }
326
+ /**
327
+ * Where a click on a rendered widget seats the caret when its kind maps no point: the END of
328
+ * the content a kind names, INSIDE its delimiters, so typing continues the construct rather
329
+ * than escaping it. A kind naming no span keeps the leading edge — guessing an end from its
330
+ * parsed text put a footnote's caret past its bracket.
331
+ */
332
+ function insideEndOffset(inline) {
333
+ const source = deps.node.raw.slice(inline.start, inline.end);
334
+ const span = getInlineWidgetEditing(inline.kind)?.revealContentSpan?.(source);
335
+ return span && span.end >= 0 && span.end <= source.length ? span.end : 0;
336
+ }
337
+ /** The offset a press names inside a widget's source. Read against the PRE-fold geometry:
338
+ * a fold reflows the line and the point stops meaning anything the moment it runs. */
339
+ function seatFromPoint(el, inline, x, y) {
340
+ const widget = widgetElByStart(el, inline.start);
341
+ const atPoint = getInlineWidgetEditing(inline.kind)?.revealOffsetAtPoint;
342
+ if (!widget || !atPoint)
343
+ return null;
344
+ const source = deps.node.raw.slice(inline.start, inline.end);
345
+ const seat = atPoint(widget, source, x, y);
346
+ return seat === null ? null : Math.max(0, Math.min(seat, source.length));
347
+ }
246
348
  function hitTestRevealWidget(el, x, y) {
247
- for (const inline of inlinesOf(deps.node)) {
248
- if (!isInlineWidget(inline, deps.node.raw))
249
- continue;
349
+ const painted = revealWidgetFromDom(el, x, y);
350
+ if (painted)
351
+ return painted;
352
+ // Fallback for a point the DOM cannot answer for (a synthetic call with no element
353
+ // under it): the original box test, which still covers a widget that paints inside
354
+ // its own bounds.
355
+ for (const inline of widgetsOf()) {
250
356
  if (!getInlineWidgetEditing(inline.kind)?.revealSource)
251
357
  continue;
252
358
  const widget = widgetElByStart(el, inline.start);
@@ -276,6 +382,7 @@ export function createWidgetInteraction(deps) {
276
382
  const hit = hitTestRevealWidget(el, clickX, clickY);
277
383
  if (!hit)
278
384
  return;
385
+ const seat = seatFromPoint(el, hit.inline, clickX, clickY);
279
386
  let targetStart = hit.inline.start;
280
387
  if (revealState) {
281
388
  const active = revealState;
@@ -292,26 +399,28 @@ export function createWidgetInteraction(deps) {
292
399
  targetStart += deps.node.raw.length - rawBefore;
293
400
  }
294
401
  }
295
- const target = inlinesOf(deps.node).find((n) => n.start === targetStart &&
296
- isInlineWidget(n, deps.node.raw) &&
297
- getInlineWidgetEditing(n.kind)?.revealSource);
402
+ const target = widgetsOf().find((n) => n.start === targetStart && getInlineWidgetEditing(n.kind)?.revealSource);
298
403
  if (!target)
299
404
  return;
300
405
  el.focus();
301
406
  revealOpenedByLastClick = true;
302
- // A click can't map to a source glyph — land at the leading edge (offset 0).
303
- void startReveal(target, target.start, 0);
407
+ void startReveal(target, target.start, seat ?? insideEndOffset(target));
304
408
  }
305
- /** Take the whole revealed token; declines unless the native selection sits inside it. */
306
- function selectRevealedSource() {
409
+ /**
410
+ * Take the whole revealed token; declines unless the double-click landed on it. Keyed off the
411
+ * point, not the selection: the root's second-press word select runs before this click and
412
+ * may have moved the selection off the source.
413
+ */
414
+ function selectRevealedSource(x, y) {
307
415
  const source = activeSourceNode;
308
- if (!revealState || !source)
309
- return false;
310
- const selection = window.getSelection();
311
- if (!selection || !selection.anchorNode || !source.contains(selection.anchorNode))
416
+ if (!revealState || !source || y === null)
312
417
  return false;
313
418
  const range = document.createRange();
314
419
  range.selectNodeContents(source);
420
+ const onSource = Array.from(range.getClientRects()).some((r) => x >= r.left && x <= r.right && y >= r.top && y <= r.bottom);
421
+ const selection = window.getSelection();
422
+ if (!onSource || !selection)
423
+ return false;
315
424
  selection.removeAllRanges();
316
425
  selection.addRange(range);
317
426
  return true;
@@ -411,6 +520,13 @@ export function createWidgetInteraction(deps) {
411
520
  }
412
521
  return true;
413
522
  }
523
+ // A vertical arrow is the shared pipeline's line walk, which needs a real caret to read:
524
+ // seat one at the edge the arrow leaves from and decline, so the walk runs from there.
525
+ if (!e.shiftKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
526
+ deps.cursor.setRaw(asRawOffset(e.key === 'ArrowUp' ? widget.start : widget.end));
527
+ deps.widgetSelection.clear();
528
+ return false;
529
+ }
414
530
  // Reading mode still swallows — a selected widget owns its keys — but commits nothing.
415
531
  // Undo anchored at the pre-select caret, so Ctrl+Z restores the caret where the user
416
532
  // actually was when selection took over.
@@ -477,6 +593,26 @@ export function createWidgetInteraction(deps) {
477
593
  });
478
594
  }
479
595
  }
596
+ // Only where the kind names a content span and the caret is inside it: a hard break is two
597
+ // bytes with a caret position between them and nothing to edit there.
598
+ function revealInterior(offset) {
599
+ const parkedByFold = foldParkedCaret === offset;
600
+ foldParkedCaret = null;
601
+ if (parkedByFold || revealState)
602
+ return false;
603
+ const widget = widgetsOf().find((w) => w.start < offset && offset < w.end);
604
+ if (!widget)
605
+ return false;
606
+ const editing = getInlineWidgetEditing(widget.kind);
607
+ if (!editing?.revealSource || !editing.revealContentSpan)
608
+ return false;
609
+ const at = offset - widget.start;
610
+ const span = editing.revealContentSpan(deps.node.raw.slice(widget.start, widget.end));
611
+ if (!span || at < span.start || at > span.end)
612
+ return false;
613
+ void startReveal(widget, offset, at);
614
+ return true;
615
+ }
480
616
  function enterEdgeWidget(side) {
481
617
  const inlines = inlinesOf(deps.node);
482
618
  if (inlines.length === 0)
@@ -518,28 +654,37 @@ export function createWidgetInteraction(deps) {
518
654
  }
519
655
  // The first click of a double-click already revealed, so the second lands in the source
520
656
  // text and the browser's word rule takes `[` or `$` as a word of its own.
521
- if (doubleClick && revealOpenedByLastClick && selectRevealedSource())
657
+ if (doubleClick && revealOpenedByLastClick && selectRevealedSource(clickX, clickY))
522
658
  return;
523
- // A click in a real text node keeps the native caret; a synthetic overlay would compete.
524
- if (caretIsInTextContent(el, window.getSelection()))
659
+ // The snap below seats a CARET, so it stands down for a range this surface paints, which it
660
+ // would collapse; the rule `clampOutOfAmbient` already carries.
661
+ const live = window.getSelection();
662
+ if (surfaceHoldsRange(el, live))
525
663
  return;
526
- for (const inline of inlinesOf(deps.node)) {
527
- if (!isInlineWidget(inline, deps.node.raw))
528
- continue;
664
+ const seat = nearestWidgetEdgeSeat(measuredWidgets(el), clickX, clickY);
665
+ if (seat === null)
666
+ return;
667
+ // A press BESIDE an island stands down for a visible caret; a press ON one cannot, since the
668
+ // engine answers that hit test with a position in the neighbouring text.
669
+ if (!seat.inside && caretIsInTextContent(el, live))
670
+ return;
671
+ el.focus();
672
+ deps.cursor.setRaw(asRawOffset(seat.offset));
673
+ // `setRaw`'s walker may have landed in a trailing text node, where native renders.
674
+ if (!caretIsInTextContent(el, window.getSelection()))
675
+ deps.setSnapTarget(seat.offset);
676
+ }
677
+ function* measuredWidgets(el) {
678
+ for (const inline of widgetsOf()) {
529
679
  const widget = widgetElByStart(el, inline.start);
530
- if (!widget)
531
- continue;
532
- const rect = widget.getBoundingClientRect();
533
- const snapTo = clickX > rect.right ? inline.end : clickX < rect.left ? inline.start : null;
534
- if (snapTo === null)
535
- continue;
536
- el.focus();
537
- deps.cursor.setRaw(asRawOffset(snapTo));
538
- // `setRaw`'s walker may have landed in a trailing text node, where native renders.
539
- if (!caretIsInTextContent(el, window.getSelection())) {
540
- deps.setSnapTarget(snapTo);
680
+ if (widget) {
681
+ yield {
682
+ start: inline.start,
683
+ end: inline.end,
684
+ rect: widget.getBoundingClientRect(),
685
+ seatsInside: isCharacterLikeWidget(inline.kind)
686
+ };
541
687
  }
542
- return;
543
688
  }
544
689
  }
545
690
  function widgetExtensionTarget(key) {
@@ -549,9 +694,7 @@ export function createWidgetInteraction(deps) {
549
694
  const focus = selectionFocusWalkOffset(el, deps.getAmbientLength());
550
695
  if (focus === null)
551
696
  return null;
552
- for (const inline of inlinesOf(deps.node)) {
553
- if (!isInlineWidget(inline, deps.node.raw))
554
- continue;
697
+ for (const inline of widgetsOf()) {
555
698
  if (key === 'ArrowRight' && focus >= inline.start && focus < inline.end) {
556
699
  return inline.end;
557
700
  }
@@ -579,6 +722,7 @@ export function createWidgetInteraction(deps) {
579
722
  handleSelectedWidgetKeydown,
580
723
  handleShiftArrowIntoWidget,
581
724
  enterWidget,
725
+ revealInterior,
582
726
  enterEdgeWidget,
583
727
  snapClickToWidgetEdge,
584
728
  isRevealing,
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The block drag handle's two policies, kept out of the components that render it: which
3
+ * blocks carry one, and where its grip sits.
4
+ *
5
+ * Presence: prose carries none. A paragraph, a heading, a quote and the note cards read as
6
+ * text on the page, and a grip beside every one of them is noise; the blocks that get one are
7
+ * the objects a reader picks up whole — pictures, code, tables, equations, diagrams, list
8
+ * items, dividers. A gripless block is still a reorder unit: keyboard reorder moves it and a
9
+ * dragged block still drops beside it.
10
+ */
11
+ import type { NodeView } from '../core/node-views';
12
+ export declare function isImageOnlyParagraph(node: NodeView): boolean;
13
+ /**
14
+ * A picture's grip does NOT wait for `blockDragHandles`: dragging it is the only pointer road
15
+ * to move a picture, and unlike prose it is a discrete object a reader expects to pick up. The
16
+ * caller still gates reading mode, which shows no affordances at all.
17
+ */
18
+ export declare function showsDragHandle(node: NodeView, handlesEnabled: boolean): boolean;
19
+ /**
20
+ * A list item's grip reorders it among its SIBLINGS and nowhere else: the drag is scoped to
21
+ * the enclosing list, so a lone item has nothing to trade places with and its grip is a
22
+ * promise the drop can't keep. It reappears the moment a second item joins the list.
23
+ */
24
+ export declare function showsListItemDragHandle(itemCount: number, handlesEnabled: boolean): boolean;
25
+ /**
26
+ * Vertical centre of the grip. A one-line block centres on its row. A taller one takes the
27
+ * first line-height of its OWN box — not its first line of text, since a card pads above that
28
+ * and a grip level with the first code line hangs below the card's shoulder. A declared anchor
29
+ * SHORTER than a line is a marker to centre on instead (a wrapped task item's checkbox); a
30
+ * taller one is a card, and the band already covers it.
31
+ */
32
+ export declare function dragHandleAnchorY(host: HTMLElement): number | null;
33
+ /**
34
+ * Svelte attachment for the handle element: places the grip once the block has laid out, and
35
+ * again on every pointerover inside its host, so it follows edits made while hovered.
36
+ *
37
+ * The mount placement is what makes the grip HITTABLE where it appears: it is its own hit
38
+ * target before any hover, so a target parked at the stylesheet's default while the grip would
39
+ * settle elsewhere is a target the pointer misses. One rAF-batched read per handle.
40
+ */
41
+ export declare function alignDragHandle(handle: HTMLElement): (() => void) | void;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The block drag handle's two policies, kept out of the components that render it: which
3
+ * blocks carry one, and where its grip sits.
4
+ *
5
+ * Presence: prose carries none. A paragraph, a heading, a quote and the note cards read as
6
+ * text on the page, and a grip beside every one of them is noise; the blocks that get one are
7
+ * the objects a reader picks up whole — pictures, code, tables, equations, diagrams, list
8
+ * items, dividers. A gripless block is still a reorder unit: keyboard reorder moves it and a
9
+ * dragged block still drops beside it.
10
+ */
11
+ import { BLOCK_CONTENT_SELECTOR, DRAG_ANCHOR_ATTR } from './block-content-selector';
12
+ /**
13
+ * Prose — the page's background and its asides — plus the list SHELL, whose own grip would
14
+ * land in the gutter on top of its first item's and take the hit test with it. A list moves
15
+ * an item at a time; the shell is a wrapper, not a thing to pick up.
16
+ */
17
+ const GRIPLESS_KINDS = new Set([
18
+ 'paragraph',
19
+ 'heading',
20
+ 'blockquote',
21
+ 'admonition',
22
+ 'githubAlert',
23
+ 'list'
24
+ ]);
25
+ /**
26
+ * A paragraph holding nothing but images is a picture in the page, not prose: it reads as a
27
+ * block of its own and is exactly the thing a reader reaches to move. A false negative here
28
+ * (an alt with an escaped bracket, an image beside a word) is the plain no-grip paragraph.
29
+ */
30
+ const IMAGE_ONLY_PARAGRAPH = /^\s*(?:!\[[^\]]*\](?:\([^)]*\)|\[[^\]]*\])\s*)+$/;
31
+ export function isImageOnlyParagraph(node) {
32
+ return node.kind === 'paragraph' && IMAGE_ONLY_PARAGRAPH.test(node.raw);
33
+ }
34
+ /**
35
+ * A picture's grip does NOT wait for `blockDragHandles`: dragging it is the only pointer road
36
+ * to move a picture, and unlike prose it is a discrete object a reader expects to pick up. The
37
+ * caller still gates reading mode, which shows no affordances at all.
38
+ */
39
+ export function showsDragHandle(node, handlesEnabled) {
40
+ if (isImageOnlyParagraph(node))
41
+ return true;
42
+ return handlesEnabled && !GRIPLESS_KINDS.has(node.kind);
43
+ }
44
+ /**
45
+ * A list item's grip reorders it among its SIBLINGS and nowhere else: the drag is scoped to
46
+ * the enclosing list, so a lone item has nothing to trade places with and its grip is a
47
+ * promise the drop can't keep. It reappears the moment a second item joins the list.
48
+ */
49
+ export function showsListItemDragHandle(itemCount, handlesEnabled) {
50
+ return handlesEnabled && itemCount > 1;
51
+ }
52
+ /** A box within this many line-heights holds one row of content, margins included. */
53
+ const SINGLE_LINE = 1.5;
54
+ /**
55
+ * Vertical centre of the grip. A one-line block centres on its row. A taller one takes the
56
+ * first line-height of its OWN box — not its first line of text, since a card pads above that
57
+ * and a grip level with the first code line hangs below the card's shoulder. A declared anchor
58
+ * SHORTER than a line is a marker to centre on instead (a wrapped task item's checkbox); a
59
+ * taller one is a card, and the band already covers it.
60
+ */
61
+ export function dragHandleAnchorY(host) {
62
+ const content = host.querySelector(BLOCK_CONTENT_SELECTOR);
63
+ if (!content)
64
+ return null;
65
+ const hostRect = host.getBoundingClientRect();
66
+ const line = lineHeightOf(content);
67
+ const rect = paintedRect(content);
68
+ // A one-line block IS its own band, and the eye puts the grip level with the row rather
69
+ // than a hair above its middle — a list item, a task row, a rule.
70
+ const box = rect.height <= line * SINGLE_LINE ? rect : (markerRect(content, line) ?? bandRect(rect, line));
71
+ if (!box || box.height === 0)
72
+ return null;
73
+ const y = box.top + box.height / 2 - hostRect.top;
74
+ // A box painting outside the host (a scrolled or offscreen region) seats nothing.
75
+ return y >= 0 && y <= hostRect.height ? y : null;
76
+ }
77
+ function lineHeightOf(content) {
78
+ const line = parseFloat(getComputedStyle(content).lineHeight);
79
+ return Number.isFinite(line) && line > 0 ? line : 20;
80
+ }
81
+ /** The declared anchor, taken only while it is marker-sized and on the block's first line. */
82
+ function markerRect(content, line) {
83
+ const declared = content.matches(`[${DRAG_ANCHOR_ATTR}]`)
84
+ ? content
85
+ : content.querySelector(`[${DRAG_ANCHOR_ATTR}]`);
86
+ const rect = declared?.getBoundingClientRect();
87
+ if (!rect || rect.height === 0 || rect.height > line)
88
+ return null;
89
+ return rect.top - content.getBoundingClientRect().top > line ? null : rect;
90
+ }
91
+ /**
92
+ * The box the band measures from: the content's own, unless it leads with an atomic widget
93
+ * (a picture in a paragraph), whose margin would otherwise seat the grip above the picture and
94
+ * flush with its corner.
95
+ */
96
+ function paintedRect(content) {
97
+ const rect = content.getBoundingClientRect();
98
+ const widget = content.firstElementChild?.closest('[data-inline-widget]');
99
+ const widgetRect = widget?.getBoundingClientRect();
100
+ return widgetRect && widgetRect.height > 0 && widgetRect.top > rect.top ? widgetRect : rect;
101
+ }
102
+ /** The top line-height of a box; a box shorter than a line is its own band. */
103
+ function bandRect(rect, line) {
104
+ return new DOMRect(rect.x, rect.y, rect.width, Math.min(line, rect.height));
105
+ }
106
+ /**
107
+ * Svelte attachment for the handle element: places the grip once the block has laid out, and
108
+ * again on every pointerover inside its host, so it follows edits made while hovered.
109
+ *
110
+ * The mount placement is what makes the grip HITTABLE where it appears: it is its own hit
111
+ * target before any hover, so a target parked at the stylesheet's default while the grip would
112
+ * settle elsewhere is a target the pointer misses. One rAF-batched read per handle.
113
+ */
114
+ export function alignDragHandle(handle) {
115
+ const host = handle.parentElement;
116
+ if (!host)
117
+ return;
118
+ const grip = handle.firstElementChild;
119
+ if (!grip)
120
+ return;
121
+ // Inline `top` on the grip (px from the host's top to its centre; the stylesheet's
122
+ // translateY(-50%) does the centring) overrides the stylesheet's half-line default.
123
+ const place = () => {
124
+ const y = dragHandleAnchorY(host);
125
+ if (y !== null)
126
+ grip.style.top = `${y}px`;
127
+ };
128
+ const settled = requestAnimationFrame(place);
129
+ host.addEventListener('pointerover', place);
130
+ return () => {
131
+ cancelAnimationFrame(settled);
132
+ host.removeEventListener('pointerover', place);
133
+ };
134
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Editor-root focus attribution: which block host holds the caret. The path drives each
3
+ * windowing scope's per-level pin, so a scroll that pushes the caret off-screen never tears
4
+ * down native focus/IME; the `data-focused` attribute is what both preview modes' CSS key
5
+ * their focused-block reveal off. The installing `$effect`s stay in `Editor.svelte`.
6
+ */
7
+ import { type PresentationMode } from '../presentation-mode';
8
+ export interface FocusAttributionDeps {
9
+ /** A getter, never a value: every attribute write is gated on the mode in force. */
10
+ get mode(): PresentationMode;
11
+ }
12
+ export interface FocusAttribution {
13
+ /** `root` is the element the installing effect captured, not a live binding. */
14
+ install(root: HTMLElement): () => void;
15
+ /** Entering a preview mode marks the already-focused block, since no re-focus fires. */
16
+ applyForMode(): void;
17
+ getFocusedPath(): number[] | null;
18
+ }
19
+ export declare function createFocusAttribution(deps: FocusAttributionDeps): FocusAttribution;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Editor-root focus attribution: which block host holds the caret. The path drives each
3
+ * windowing scope's per-level pin, so a scroll that pushes the caret off-screen never tears
4
+ * down native focus/IME; the `data-focused` attribute is what both preview modes' CSS key
5
+ * their focused-block reveal off. The installing `$effect`s stay in `Editor.svelte`.
6
+ */
7
+ import { assertInvariant } from '../assert';
8
+ import { checkLandableCaret } from '../invariants/landable-caret';
9
+ import { isPreviewMode } from '../presentation-mode';
10
+ import { readBlockPath } from '../selection/path-lookup';
11
+ import { onRoot, removeAll } from './editor-root-listeners';
12
+ export function createFocusAttribution(deps) {
13
+ // Plain fields, never reactive: focusout fires mid-teardown during a structural commit,
14
+ // where a reactive write would trip state_unsafe_mutation.
15
+ let focusedPath = null;
16
+ let focusedHostEl = null;
17
+ // Mode-gated so source/reading DOM stays byte-identical.
18
+ function applyFocusedAttr() {
19
+ if (focusedHostEl && isPreviewMode(deps.mode)) {
20
+ focusedHostEl.setAttribute('data-focused', '');
21
+ }
22
+ else {
23
+ focusedHostEl?.removeAttribute('data-focused');
24
+ }
25
+ }
26
+ function setFocusedHost(host) {
27
+ if (focusedHostEl === host)
28
+ return;
29
+ focusedHostEl?.removeAttribute('data-focused');
30
+ focusedHostEl = host;
31
+ applyFocusedAttr();
32
+ }
33
+ function clear() {
34
+ focusedPath = null;
35
+ setFocusedHost(null);
36
+ }
37
+ function install(root) {
38
+ const onFocusIn = (e) => {
39
+ const host = e.target?.closest('[data-block-path]');
40
+ if (!host || !root.contains(host)) {
41
+ clear();
42
+ return;
43
+ }
44
+ setFocusedHost(host);
45
+ const path = readBlockPath(host);
46
+ focusedPath = path && path.length > 0 ? path : null;
47
+ // G1.33 at the seam every caret door crosses: a door seats a caret by focusing the
48
+ // surface, whoever minted it, so a consumer's own door inherits the guard here.
49
+ const landed = e.target;
50
+ if (landed instanceof HTMLElement) {
51
+ assertInvariant('landable-caret', () => checkLandableCaret(landed, deps.mode, path ?? []));
52
+ }
53
+ };
54
+ const onFocusOut = (e) => {
55
+ const next = e.relatedTarget;
56
+ if (next && root.contains(next))
57
+ return; // moving between blocks — keep the pin
58
+ clear();
59
+ };
60
+ return removeAll(onRoot(root, 'focusin', onFocusIn), onRoot(root, 'focusout', onFocusOut));
61
+ }
62
+ return {
63
+ install,
64
+ applyForMode: applyFocusedAttr,
65
+ getFocusedPath: () => focusedPath
66
+ };
67
+ }
@@ -0,0 +1,39 @@
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 type { UserScrollport } from '../cursor/scroll-ancestors';
9
+ import type { Scrollport } from '../cursor/scrollport';
10
+ /** Calls `onWidthChange` when `el`'s width moves. A height-only resize is ignored: only a
11
+ * re-wrap stales the measured heights. */
12
+ export declare function installWidthWatcher(el: HTMLElement, onWidthChange: () => void): () => void;
13
+ /** Calls `bump` on a height change of the resolved scrollport `target`. */
14
+ export declare function installViewportHeightWatcher(target: UserScrollport, bump: () => void): () => void;
15
+ export interface TypeScaleProbeDeps {
16
+ /** Read at each report, so the sub-percent guard compares against the scale in force. */
17
+ getScale(): number;
18
+ onScale(next: number): void;
19
+ }
20
+ /**
21
+ * The width watcher's sibling: a font-size move puts the height estimates off several-fold, so
22
+ * a document whose true height clears the activation watermark can fail to window at all. No
23
+ * other box in the root reports it, hence the `1em` probe; the scale is relative to the size
24
+ * `HEIGHT_ESTIMATES` were calibrated at.
25
+ */
26
+ export declare function installTypeScaleProbe(el: HTMLElement, deps: TypeScaleProbeDeps): () => void;
27
+ export interface HeaderSlotCompensationDeps {
28
+ el: HTMLElement;
29
+ port: Pick<Scrollport, 'scrollTop' | 'scrollBy'>;
30
+ ownsScrollCorrection(): boolean;
31
+ revealHoldsScroll(): boolean;
32
+ }
33
+ /**
34
+ * The header slot's height lives outside the height model, so while the editor owns the
35
+ * correction a growing header would slide the document under the reader. Compensating from
36
+ * the SLOT's own resize composes with `correctAnchor` instead of double-correcting; a reveal
37
+ * already holding the scroll outranks it.
38
+ */
39
+ export declare function installHeaderSlotCompensation(deps: HeaderSlotCompensationDeps): () => void;