@voithos-labs/aragonite 0.10.3 → 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 (101) hide show
  1. package/dist/a11y-strings.d.ts +1 -0
  2. package/dist/a11y-strings.js +1 -0
  3. package/dist/components/BlockDragHandle.svelte +2 -0
  4. package/dist/components/BlockHost.svelte +2 -2
  5. package/dist/components/Editor.svelte +56 -3
  6. package/dist/components/SelectionOverlay.svelte +17 -23
  7. package/dist/components/SelectionOverlay.svelte.d.ts +1 -1
  8. package/dist/components/blocks/code/CodeBlockRail.svelte +42 -24
  9. package/dist/components/blocks/code/code-bootstrap.js +12 -0
  10. package/dist/components/blocks/code/code-languages.d.ts +5 -5
  11. package/dist/components/blocks/code/code-languages.js +20 -12
  12. package/dist/components/blocks/editable-leaf.d.ts +3 -3
  13. package/dist/components/blocks/editable-leaf.js +6 -2
  14. package/dist/components/blocks/list/ListBlock.svelte +1 -0
  15. package/dist/components/blocks/list/ListItemBlock.svelte +12 -3
  16. package/dist/components/blocks/list/ListItemBlock.svelte.d.ts +2 -0
  17. package/dist/components/blocks/text/click-snap-guard.d.ts +3 -0
  18. package/dist/components/blocks/text/click-snap-guard.js +8 -0
  19. package/dist/components/blocks/text/edge-policy-dispatch.js +33 -18
  20. package/dist/components/blocks/text/live-selection-edit.d.ts +5 -5
  21. package/dist/components/blocks/text/live-selection-edit.js +10 -7
  22. package/dist/components/blocks/text/text-clipboard.js +2 -2
  23. package/dist/components/blocks/text/widget-interaction.js +45 -23
  24. package/dist/components/drag-handle.d.ts +6 -0
  25. package/dist/components/drag-handle.js +8 -0
  26. package/dist/components/editor-root-geometry.d.ts +1 -1
  27. package/dist/components/editor-root-geometry.js +1 -1
  28. package/dist/components/editor-root-listeners.d.ts +0 -7
  29. package/dist/components/editor-root-listeners.js +0 -22
  30. package/dist/components/image/ImageResizeHandles.svelte +18 -10
  31. package/dist/components/menu/SelectionToolbar.svelte +385 -0
  32. package/dist/components/menu/SelectionToolbar.svelte.d.ts +16 -0
  33. package/dist/core/inline/inline-widgets.d.ts +12 -4
  34. package/dist/core/inline/inline-widgets.js +5 -0
  35. package/dist/core/inline/transparency.js +3 -3
  36. package/dist/cursor/height-oracle.d.ts +2 -3
  37. package/dist/cursor/height-oracle.js +0 -1
  38. package/dist/cursor/scroll-hold.d.ts +10 -0
  39. package/dist/cursor/scroll-hold.js +22 -0
  40. package/dist/cursor/scrollport.d.ts +9 -0
  41. package/dist/cursor/scrollport.js +30 -1
  42. package/dist/cursor/visual-lines.d.ts +5 -4
  43. package/dist/cursor/visual-lines.js +56 -11
  44. package/dist/cursor/widget-edge-snap.d.ts +28 -0
  45. package/dist/cursor/widget-edge-snap.js +38 -0
  46. package/dist/cursor/widget-offset.d.ts +3 -0
  47. package/dist/cursor/widget-offset.js +10 -0
  48. package/dist/decorations/reserved-attrs.js +1 -0
  49. package/dist/editor-actions/focus/focus-dispatch.js +7 -4
  50. package/dist/editor-actions/focus/focus-landing.d.ts +10 -1
  51. package/dist/editor-actions/focus/focus-landing.js +20 -6
  52. package/dist/editor-actions/plugin/container.d.ts +7 -0
  53. package/dist/editor-actions/plugin/container.js +3 -1
  54. package/dist/editor-props.d.ts +3 -0
  55. package/dist/plugin.d.ts +2 -1
  56. package/dist/plugin.js +6 -1
  57. package/dist/plugins/latex/latex-kind.js +48 -31
  58. package/dist/plugins/latex/math-source.d.ts +3 -3
  59. package/dist/plugins/latex/math-source.js +32 -26
  60. package/dist/plugins/mermaid/MermaidBlock.svelte +19 -9
  61. package/dist/plugins/parrot/ParrotBlock.svelte +3 -1
  62. package/dist/reactivity/list-windowing.svelte.js +26 -6
  63. package/dist/schema/commands.d.ts +9 -5
  64. package/dist/schema/commands.js +12 -7
  65. package/dist/schema/operations.d.ts +1 -1
  66. package/dist/schema/reserved-chords.js +12 -0
  67. package/dist/selection/cross-block/keydown.js +2 -28
  68. package/dist/selection/cross-block/paste.js +7 -13
  69. package/dist/selection/cross-block/type-replace.js +5 -15
  70. package/dist/selection/dead-space-caret.d.ts +3 -0
  71. package/dist/selection/dead-space-caret.js +12 -2
  72. package/dist/selection/drag-pointer.d.ts +19 -0
  73. package/dist/selection/drag-pointer.js +42 -0
  74. package/dist/selection/keyboard-extend.d.ts +3 -2
  75. package/dist/selection/keyboard-extend.js +3 -2
  76. package/dist/selection/multi-click.d.ts +42 -0
  77. package/dist/selection/multi-click.js +203 -0
  78. package/dist/selection/native-bridge.d.ts +3 -0
  79. package/dist/selection/native-bridge.js +31 -2
  80. package/dist/selection/path-lookup.d.ts +10 -2
  81. package/dist/selection/path-lookup.js +10 -3
  82. package/dist/selection/pointer-gesture.d.ts +9 -0
  83. package/dist/selection/pointer-gesture.js +11 -0
  84. package/dist/selection/primitives.d.ts +7 -0
  85. package/dist/selection/primitives.js +19 -1
  86. package/dist/selection/range-delete.d.ts +4 -0
  87. package/dist/selection/range-delete.js +1 -1
  88. package/dist/selection/selection-drop.d.ts +34 -0
  89. package/dist/selection/selection-drop.js +253 -0
  90. package/dist/styles/editor-theme.css +18 -3
  91. package/dist/styles/editor.css +64 -13
  92. package/dist/tree-operations/paste/replace-block-at-parent.d.ts +3 -1
  93. package/dist/tree-operations/paste/replace-block-at-parent.js +4 -1
  94. package/dist/tree-operations/paste/replacement-parse.d.ts +17 -0
  95. package/dist/tree-operations/paste/replacement-parse.js +22 -0
  96. package/docs/guide/consumer-guide.md +10 -8
  97. package/docs/guide/plugin-api.md +54 -38
  98. package/docs/guide/plugin-guide.md +31 -22
  99. package/package.json +7 -7
  100. package/dist/selection/double-click-trim.d.ts +0 -17
  101. package/dist/selection/double-click-trim.js +0 -57
@@ -0,0 +1,203 @@
1
+ /**
2
+ * The click ladder past the caret: the second press of a run selects the word under it, the
3
+ * third the surface's content, and a drag from either extends at that granularity. Owned from
4
+ * the second press, so the browser paints nothing of its own: its word rule is per platform,
5
+ * its walk enters atomic islands, and a marker here never joins a word.
6
+ */
7
+ import { ambientLengthOf } from '../ambient/ambient-dom';
8
+ import { asDomTextOffset, asRawOffset, toClampedRawOffset, toDomTextOffset } from '../cursor/coordinate-spaces';
9
+ import { caretOffsetAtPoint } from '../cursor/point-offset';
10
+ import { containerDomTextLength, maskedWalkText } from '../cursor/widget-offset';
11
+ import { isWholeBlockInputProxy } from '../editor-actions/whole-block-focus-surface';
12
+ import { installDragListener } from './drag-pointer';
13
+ import { applySingleBlockRange, applySurfaceContentRange } from './native-bridge';
14
+ import { blockNearPoint } from './nearest-block';
15
+ // ── The word rule ────────────────────────────────────────────────────────────
16
+ let segmenter;
17
+ function wordSegmenter() {
18
+ if (segmenter === undefined) {
19
+ segmenter =
20
+ typeof Intl.Segmenter === 'function'
21
+ ? new Intl.Segmenter(undefined, { granularity: 'word' })
22
+ : null;
23
+ }
24
+ return segmenter;
25
+ }
26
+ /**
27
+ * [start, end) of the segment at `offset` in `text`: the word ending there when it is one, else
28
+ * the segment starting there (whitespace or punctuation, as the engine would take). Null on
29
+ * empty text, or on a platform with no segmenter.
30
+ */
31
+ export function wordSpanAt(text, offset) {
32
+ const seg = wordSegmenter();
33
+ if (!seg || text.length === 0)
34
+ return null;
35
+ const segments = seg.segment(text);
36
+ const at = Math.max(0, Math.min(offset, text.length));
37
+ const left = at > 0 ? segments.containing(at - 1) : undefined;
38
+ const right = at < text.length ? segments.containing(at) : undefined;
39
+ if (left && right && left.index === right.index)
40
+ return spanOf(left);
41
+ if (left?.isWordLike)
42
+ return spanOf(left);
43
+ const pick = right ?? left;
44
+ return pick ? spanOf(pick) : null;
45
+ }
46
+ function spanOf(segment) {
47
+ return { start: segment.index, end: segment.index + segment.segment.length };
48
+ }
49
+ /** The raw span `granularity` takes around a raw offset in `surface`; null where the word rule
50
+ * has nothing to read. */
51
+ export function spanAround(surface, granularity, rawOffset) {
52
+ const ambient = ambientLengthOf(surface);
53
+ if (granularity === 'block') {
54
+ return { start: 0, end: toClampedRawOffset(containerDomTextLength(surface), ambient) };
55
+ }
56
+ const walk = toDomTextOffset(asRawOffset(rawOffset), ambient);
57
+ const span = wordSpanAt(maskedWalkText(surface), walk);
58
+ if (!span)
59
+ return null;
60
+ return {
61
+ start: toClampedRawOffset(asDomTextOffset(span.start), ambient),
62
+ end: toClampedRawOffset(asDomTextOffset(span.end), ambient)
63
+ };
64
+ }
65
+ /** The rung a press's click count reaches; the run stays at the block rung past three. */
66
+ export function granularityForClickCount(clickCount) {
67
+ if (clickCount === 2)
68
+ return 'word';
69
+ return clickCount >= 3 ? 'block' : null;
70
+ }
71
+ /**
72
+ * Root listeners for the ladder. The count rides the mousedown (a pointer event carries none),
73
+ * and the drag rides the pointerdown of the same press, so both are heard. The mousedown is
74
+ * cancelled, never the pointerdown (that would silence the click), and so is the mouseup: the
75
+ * browser's release seats a caret at a press that landed off a glyph, over the range just painted.
76
+ */
77
+ export function installMultiClickSelect(deps) {
78
+ let lastPointerDown = null;
79
+ let claimed = false;
80
+ const onPointerDown = (e) => {
81
+ lastPointerDown = e;
82
+ };
83
+ const onMouseUp = (e) => {
84
+ if (claimed)
85
+ e.preventDefault();
86
+ };
87
+ const onMouseDown = (e) => {
88
+ claimed = false;
89
+ const granularity = granularityForClickCount(e.detail);
90
+ if (!granularity || e.button !== 0 || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey)
91
+ return;
92
+ if (pressesInlineWidget(e.target))
93
+ return;
94
+ const press = pressedSurface(deps, e);
95
+ if (!press)
96
+ return;
97
+ const anchorSpan = selectAtPoint(press.surface, granularity, e.clientX, e.clientY);
98
+ if (!anchorSpan)
99
+ return;
100
+ e.preventDefault();
101
+ claimed = true;
102
+ const anchor = press.anchor ?? blockNearPoint(deps.editorRoot, e.clientX, e.clientY)?.endpointHere();
103
+ if (!anchor || !lastPointerDown)
104
+ return;
105
+ installDragListener({
106
+ editorRoot: deps.editorRoot,
107
+ scrollContainer: deps.getScrollContainer(),
108
+ selection: deps.selection,
109
+ getBlockElByPath: deps.getBlockElByPath,
110
+ lifetimeSignal: deps.lifetimeSignal,
111
+ granularity: createGranularity(deps.getBlockElByPath, press.surface, granularity, anchorSpan)
112
+ }, anchor, lastPointerDown);
113
+ };
114
+ deps.editorRoot.addEventListener('pointerdown', onPointerDown);
115
+ deps.editorRoot.addEventListener('mousedown', onMouseDown);
116
+ deps.editorRoot.addEventListener('mouseup', onMouseUp);
117
+ return () => {
118
+ deps.editorRoot.removeEventListener('pointerdown', onPointerDown);
119
+ deps.editorRoot.removeEventListener('mousedown', onMouseDown);
120
+ deps.editorRoot.removeEventListener('mouseup', onMouseUp);
121
+ };
122
+ }
123
+ /** The surface a press selects in: the editable under it, else the nearest block's, the way a
124
+ * single click in the margin lands there. A block with no text surface is nobody's word. */
125
+ function pressedSurface(deps, e) {
126
+ // Up through an island inside the editable (a list's `- ` prefix) to the surface itself.
127
+ const own = surfaceOf(e.target);
128
+ if (own)
129
+ return isWholeBlockInputProxy(own) ? null : { surface: own, anchor: null };
130
+ const path = deps.marginBlockAt(e.target, e.clientX, e.clientY);
131
+ const block = path && deps.getBlockElByPath(path);
132
+ if (!block)
133
+ return null;
134
+ // A container's own box (a quote's gutter) selects in the leaf nearest the press, the way a
135
+ // click beside a line lands in that line.
136
+ const surface = block.contentEditable === 'true' ? block : nearestSurfaceIn(block, e.clientY);
137
+ const surfacePath = surface && pathOfSurface(surface);
138
+ const offset = surface && caretOffsetAtPoint(surface, e.clientX, e.clientY);
139
+ if (!surface)
140
+ return null;
141
+ return { surface, anchor: surfacePath && offset !== null ? { path: surfacePath, offset } : null };
142
+ }
143
+ function nearestSurfaceIn(block, clientY) {
144
+ let best = null;
145
+ let bestDistance = Infinity;
146
+ for (const el of block.querySelectorAll('[contenteditable="true"]')) {
147
+ const rect = el.getBoundingClientRect();
148
+ const distance = clientY < rect.top ? rect.top - clientY : Math.max(0, clientY - rect.bottom);
149
+ if (distance < bestDistance) {
150
+ best = el;
151
+ bestDistance = distance;
152
+ }
153
+ }
154
+ return best;
155
+ }
156
+ /** The path of the block a surface is the content of; null for a surface addressed some other
157
+ * way (a table's cell), whose drag is not a char-point drag. */
158
+ function pathOfSurface(surface) {
159
+ const host = surface.closest('[data-block-path]');
160
+ const raw = host?.getAttribute('data-block-path');
161
+ if (!raw || !host?.contains(surface))
162
+ return null;
163
+ const path = JSON.parse(raw);
164
+ return host.querySelector('[contenteditable="true"]') === surface ? path : null;
165
+ }
166
+ /** Paint the rung's span at the press and answer it, in raw offsets. */
167
+ function selectAtPoint(surface, granularity, clientX, clientY) {
168
+ if (granularity === 'block') {
169
+ applySurfaceContentRange(surface);
170
+ return spanAround(surface, 'block', 0);
171
+ }
172
+ const offset = caretOffsetAtPoint(surface, clientX, clientY);
173
+ const span = offset === null ? null : spanAround(surface, 'word', offset);
174
+ if (!span || span.start === span.end)
175
+ return null;
176
+ applySingleBlockRange(surface, span.start, span.end);
177
+ return span;
178
+ }
179
+ function createGranularity(getBlockElByPath, surface, granularity, anchorSpan) {
180
+ return {
181
+ surface,
182
+ anchorSpan,
183
+ spanAround: (offset) => spanAround(surface, granularity, offset) ?? { start: offset, end: offset },
184
+ expandFocus: (point, side) => {
185
+ const el = point.cellCoordinate ? null : getBlockElByPath(point.path);
186
+ const span = el && spanAround(el, granularity, point.offset);
187
+ if (!span)
188
+ return point;
189
+ return { path: point.path.slice(), offset: side === 'after' ? span.end : span.start };
190
+ }
191
+ };
192
+ }
193
+ /** A press on an inline widget is that widget's own gesture (a footnote's double-click takes
194
+ * its whole token), so the ladder leaves it alone. */
195
+ function pressesInlineWidget(target) {
196
+ return target instanceof Element && target.closest('[data-inline-widget]') !== null;
197
+ }
198
+ function surfaceOf(target) {
199
+ let el = target instanceof HTMLElement ? target : null;
200
+ while (el && el.contentEditable !== 'true')
201
+ el = el.parentElement;
202
+ return el;
203
+ }
@@ -25,6 +25,9 @@ export declare function applyCollapsedCaret(blockEl: HTMLElement, point: Selecti
25
25
  * Returns whether an element was found; a missing target is a no-op.
26
26
  */
27
27
  export declare function focusCollapsedCaret(getBlockElByPath: (path: number[]) => HTMLElement | null, point: SelectionPoint): boolean;
28
+ /** Select a surface's content whole, past its ambient prefix: the first-press Ctrl+A range and
29
+ * the triple-click one. */
30
+ export declare function applySurfaceContentRange(el: HTMLElement): void;
28
31
  export declare function applySingleBlockRange(blockEl: HTMLElement, startOffset: number, endOffset: number): void;
29
32
  export declare function clearNativeSelection(): void;
30
33
  /**
@@ -4,9 +4,10 @@
4
4
  * DOM→raw conversion subtracts the length of a leading ambient marker span, whose textContent
5
5
  * counts toward DOM offsets but not raw.
6
6
  */
7
- import { asRawOffset, toClampedRawOffset, toDomTextOffset } from '../cursor/coordinate-spaces';
7
+ import { asDomTextOffset, asRawOffset, toClampedRawOffset, toDomTextOffset } from '../cursor/coordinate-spaces';
8
+ import { createRangeFromOffsets } from '../cursor/content-offsets';
8
9
  import { clampToLandableRaw, createRangeAtDomTextOffsets, domTextOffsetAtNode } from '../cursor/widget-offset';
9
- import { ambientLengthOf, placeCaretAfterAmbientSpan } from '../ambient/ambient-dom';
10
+ import { ambientLengthOf, ambientSpanOf, placeCaretAfterAmbientSpan } from '../ambient/ambient-dom';
10
11
  // ── Read native → SelectionPoint ────────────────────────────────────────────
11
12
  /**
12
13
  * Read the collapsed caret inside `blockEl` into a raw-semantic SelectionPoint.
@@ -64,12 +65,40 @@ export function focusCollapsedCaret(getBlockElByPath, point) {
64
65
  blockEl.focus();
65
66
  return true;
66
67
  }
68
+ /** Select a surface's content whole, past its ambient prefix: the first-press Ctrl+A range and
69
+ * the triple-click one. */
70
+ export function applySurfaceContentRange(el) {
71
+ const ambient = ambientSpanOf(el);
72
+ const ambientLen = ambient?.textContent?.length ?? 0;
73
+ const textLen = el.textContent?.length ?? 0;
74
+ if (ambient && textLen > ambientLen) {
75
+ if (!placeCaretAfterAmbientSpan(el))
76
+ return;
77
+ // textLen counts the full textContent (marker included) — a DomTextOffset by construction.
78
+ const endRange = createRangeFromOffsets(el, asDomTextOffset(textLen), asDomTextOffset(textLen));
79
+ if (endRange) {
80
+ window.getSelection()?.extend(endRange.endContainer, endRange.endOffset);
81
+ }
82
+ return;
83
+ }
84
+ const range = document.createRange();
85
+ range.selectNodeContents(el);
86
+ const sel = window.getSelection();
87
+ sel?.removeAllRanges();
88
+ sel?.addRange(range);
89
+ }
67
90
  export function applySingleBlockRange(blockEl, startOffset, endOffset) {
68
91
  const ambient = ambientLengthOf(blockEl);
69
92
  const range = createRangeAtDomTextOffsets(blockEl, toDomTextOffset(asRawOffset(startOffset), ambient), toDomTextOffset(asRawOffset(endOffset), ambient));
70
93
  if (!range)
71
94
  return;
72
95
  const sel = window.getSelection();
96
+ // A start at raw 0 under an ambient marker goes AFTER the island, as a caret does: Chromium
97
+ // drops a range that opens inside its contenteditable="false".
98
+ if (ambient > 0 && startOffset <= 0 && placeCaretAfterAmbientSpan(blockEl)) {
99
+ sel?.extend(range.endContainer, range.endOffset);
100
+ return;
101
+ }
73
102
  sel?.removeAllRanges();
74
103
  sel?.addRange(range);
75
104
  }
@@ -27,6 +27,14 @@ export declare function findBlockPathForElement(el: Element | null): number[] |
27
27
  * cell grid; addresses the grid by selector contract (`data-table-row-idx`, `role="cell"`).
28
28
  */
29
29
  export declare function findCellPathForElement(el: Element | null): number[] | null;
30
- /** The path of the editing SURFACE `el` sits in — the enclosing cell where there is one, else
31
- * the enclosing block. The pairing {@link findCellPathForElement} says every producer owes. */
30
+ /** The editing SURFACE `el` sits in: the enclosing cell where there is one, else the enclosing
31
+ * block, and which of the two answered — a caller whose bytes live at a different scope for a
32
+ * cell reads the flag rather than re-deriving the pairing. */
33
+ export interface EditingSurface {
34
+ path: number[];
35
+ inCell: boolean;
36
+ }
37
+ /** The pairing {@link findCellPathForElement} says every producer owes, in one place. */
38
+ export declare function findSurfaceForElement(el: Element | null): EditingSurface | null;
39
+ /** {@link findSurfaceForElement} for a caller that needs only the path. */
32
40
  export declare function findSurfacePathForElement(el: Element | null): number[] | null;
@@ -125,8 +125,15 @@ export function findCellPathForElement(el) {
125
125
  return null;
126
126
  return [...tablePath, rowIdx, colIdx];
127
127
  }
128
- /** The path of the editing SURFACE `el` sits in — the enclosing cell where there is one, else
129
- * the enclosing block. The pairing {@link findCellPathForElement} says every producer owes. */
128
+ /** The pairing {@link findCellPathForElement} says every producer owes, in one place. */
129
+ export function findSurfaceForElement(el) {
130
+ const cellPath = findCellPathForElement(el);
131
+ if (cellPath)
132
+ return { path: cellPath, inCell: true };
133
+ const blockPath = findBlockPathForElement(el);
134
+ return blockPath ? { path: blockPath, inCell: false } : null;
135
+ }
136
+ /** {@link findSurfaceForElement} for a caller that needs only the path. */
130
137
  export function findSurfacePathForElement(el) {
131
- return findCellPathForElement(el) ?? findBlockPathForElement(el);
138
+ return findSurfaceForElement(el)?.path ?? null;
132
139
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The door a widget declares over an element whose pointer drags belong to its own gesture (a
3
+ * diagram's pan, a canvas's brush): the editor's pointer arms decline a press inside it, so the
4
+ * gesture never doubles as a block range. A declaration, not a `stopPropagation`: pointer events
5
+ * are delegated at the app root, so a component's own handler runs after the editor's.
6
+ */
7
+ /** Declared on the gesture surface itself; any descendant press counts. */
8
+ export declare const POINTER_GESTURE_ATTR = "data-pointer-gesture";
9
+ export declare function claimsPointerGesture(target: EventTarget | null): boolean;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The door a widget declares over an element whose pointer drags belong to its own gesture (a
3
+ * diagram's pan, a canvas's brush): the editor's pointer arms decline a press inside it, so the
4
+ * gesture never doubles as a block range. A declaration, not a `stopPropagation`: pointer events
5
+ * are delegated at the app root, so a component's own handler runs after the editor's.
6
+ */
7
+ /** Declared on the gesture surface itself; any descendant press counts. */
8
+ export const POINTER_GESTURE_ATTR = 'data-pointer-gesture';
9
+ export function claimsPointerGesture(target) {
10
+ return target instanceof Element && target.closest(`[${POINTER_GESTURE_ATTR}]`) !== null;
11
+ }
@@ -73,3 +73,10 @@ export type BlockSelectionClass = 'outside' | 'start' | 'middle' | 'end' | 'sing
73
73
  * tells the caller to delegate to the browser instead of painting.
74
74
  */
75
75
  export declare function classifyBlockForSelection(path: number[], selection: EditorSelection): BlockSelectionClass;
76
+ /**
77
+ * Whether this block paints the range over its whole box: the range holds its entire subtree and
78
+ * no ancestor's box already covers it, or it is the whole unit a single-block range holds. Chrome
79
+ * a container derives from its markers (a GitHub alert's badge) has no child host to paint it, so
80
+ * the covering block takes the box in one piece and its children paint nothing.
81
+ */
82
+ export declare function blockPaintsWholeBox(path: readonly number[], selection: EditorSelection, wholeUnitPath: readonly number[] | null): boolean;
@@ -3,7 +3,7 @@
3
3
  * classification, the delete-commit snapshot rule. No DOM, no state. Path-level predicates
4
4
  * live in `./path-math`.
5
5
  */
6
- import { comparePaths, isPathBetween, pathHasPrefix } from './path-math';
6
+ import { comparePaths, isPathBetween, isStrictAncestorOf, pathHasPrefix, pathsEqual } from './path-math';
7
7
  import { asCellIndex, asRawOffset, docPathFrom } from '../cursor/coordinate-spaces';
8
8
  import { devWarn } from '../dev-warn';
9
9
  export function isWholeBlockEndpoint(endpoint) {
@@ -89,3 +89,21 @@ export function classifyBlockForSelection(path, selection) {
89
89
  return 'middle';
90
90
  return 'outside';
91
91
  }
92
+ /**
93
+ * Whether this block paints the range over its whole box: the range holds its entire subtree and
94
+ * no ancestor's box already covers it, or it is the whole unit a single-block range holds. Chrome
95
+ * a container derives from its markers (a GitHub alert's badge) has no child host to paint it, so
96
+ * the covering block takes the box in one piece and its children paint nothing.
97
+ */
98
+ export function blockPaintsWholeBox(path, selection, wholeUnitPath) {
99
+ if (wholeUnitPath)
100
+ return pathsEqual(path, wholeUnitPath);
101
+ const { start, end } = normalize(selection);
102
+ return (holdsSubtree(path, start.path, end.path) &&
103
+ !holdsSubtree(path.slice(0, -1), start.path, end.path));
104
+ }
105
+ /** The range holds this block's whole subtree: inside it in document order, and not an ancestor
106
+ * of the end endpoint, whose own descendants the range cuts through. */
107
+ function holdsSubtree(path, start, end) {
108
+ return isPathBetween(path, start, end) && !isStrictAncestorOf(path, end);
109
+ }
@@ -31,3 +31,7 @@ export interface RangeDeleteResult {
31
31
  * post-unshare references. Caller pre-normalizes and keeps endpoints on focusable blocks.
32
32
  */
33
33
  export declare function rangeDelete(doc: Document, start: SelectionPoint, end: SelectionPoint, sharing: SharingState, grammar: GrammarView | undefined, presentationMode: PresentationMode | undefined, linkRef: InlineResolverRef | undefined): RangeDeleteResult;
34
+ /** The container prefix the survivor renders under, so the join seam can read its candidate back
35
+ * through it (live-mode.md § 4.5). An ambient marker rides the container's FIRST child only, the
36
+ * way `BlockList` forwards it, and a list item is the one built-in container that paints one. */
37
+ export declare function containerAmbientPrefix(doc: Document, path: readonly number[]): string;
@@ -138,7 +138,7 @@ export function rangeDelete(doc, start, end, sharing, grammar, presentationMode,
138
138
  /** The container prefix the survivor renders under, so the join seam can read its candidate back
139
139
  * through it (live-mode.md § 4.5). An ambient marker rides the container's FIRST child only, the
140
140
  * way `BlockList` forwards it, and a list item is the one built-in container that paints one. */
141
- function containerAmbientPrefix(doc, path) {
141
+ export function containerAmbientPrefix(doc, path) {
142
142
  if (path.length < 2 || path[path.length - 1] !== 0)
143
143
  return '';
144
144
  const parent = blockNodeAt(doc, path.slice(0, -1));
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Dragging a selection and dropping it. The browser's own version is two native edits committed
3
+ * apart — `deleteByDrag` on the source, `insertFromDrop` on the target — so undo takes two presses
4
+ * over a document that lost bytes in between, and inside one block the source commit rebuilds the
5
+ * surface under the drop. Owned here instead: one snapshot over two raw writes, which splice
6
+ * nothing, so no path moves under the second.
7
+ */
8
+ import type { Document } from '../core/nodes';
9
+ import type { GrammarView } from '../schema/block-openers';
10
+ import type { LinkReferenceResolverRef, PresentationModeGetter } from '../editor-keys';
11
+ import type { PluginActivation } from '../schema/plugin-activation';
12
+ import type { CommitController } from '../action-contracts';
13
+ import type { PasteCommitCoordinator } from '../tree-operations/paste/paste-deps';
14
+ import { type EditorEvents } from '../editor-events';
15
+ export interface SelectionDropDeps {
16
+ editorRoot: HTMLElement;
17
+ getDoc(): Document;
18
+ controller: CommitController;
19
+ coordinator: PasteCommitCoordinator;
20
+ getPresentationMode: PresentationModeGetter | undefined;
21
+ linkRef: LinkReferenceResolverRef | undefined;
22
+ grammar: GrammarView | undefined;
23
+ activePlugins: PluginActivation | undefined;
24
+ /** The instance event surface: a ceremony that throws mid-move has nowhere else to report. */
25
+ events: EditorEvents;
26
+ isReadOnly(): boolean;
27
+ }
28
+ export declare function installSelectionDrop(deps: SelectionDropDeps): () => void;
29
+ /** Where a splice of `delta` blocks at `at` inside `parent` leaves `target`: what the drop's
30
+ * second write addresses once the first one's reparse changed its slot's block count. */
31
+ export declare function shiftPathAfterSplice(target: number[], parent: number[], at: number, delta: number): number[];
32
+ /** Where a drop at `offset` lands once `[start, end)` left the same block, `shrunkBy` bytes
33
+ * shorter than the range was wide once the join seam cleaned up. Null inside the range. */
34
+ export declare function dropOffsetAfterCut(offset: number, start: number, end: number, shrunkBy: number): number | null;