@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
@@ -18,7 +18,7 @@ import { createMarkerCompletion } from './marker-completion';
18
18
  import { resolveEdgeDeletion } from './construct-edge-delete';
19
19
  import { hidesStructuralSuffix } from './hidden-suffix';
20
20
  import { resolveEdgeSeat } from './edge-seat';
21
- import { deleteRangeRaw } from './live-selection-edit';
21
+ import { replaceRangeRaw } from './live-selection-edit';
22
22
  import { resolveMarkedInsertion } from './pending-mark-insert';
23
23
  import { widgetAtCursor } from './widget-adjacency';
24
24
  import { resolveDelimiterAutoPair } from './delimiter-autopair';
@@ -112,6 +112,12 @@ export function createEdgePolicyDispatch(deps) {
112
112
  const d = display();
113
113
  writeDisplay(d.slice(0, start) + insert + d.slice(end), start + insert.length, source, caretBefore);
114
114
  }
115
+ /** The range this surface holds, or null at a caret. A key aimed at one edits the RANGE: the
116
+ * caret the arms read is only its start. */
117
+ function heldRange() {
118
+ const selection = deps.getRawSelection();
119
+ return selection && selection.start < selection.end ? selection : null;
120
+ }
115
121
  // ── The seams the arms below defer to ────────────────────────────────────
116
122
  /**
117
123
  * The rung a range rewrite can be asked about. Past the reading cut a surface revealing no
@@ -150,6 +156,20 @@ export function createEdgePolicyDispatch(deps) {
150
156
  return null;
151
157
  return resolveEdgeSeat(caret, inlinesOf(deps.node), deps.getEdgeAffinity(), deps.node.raw, screenVisibilityOf(el), typed);
152
158
  }
159
+ /** A printable byte an arm claimed because the engine drops it at an element-level caret. Over
160
+ * a held range it REPLACES through the join seam, the road a mid-block range takes; at a caret
161
+ * the seat answers which side of an unpainted run it lands on. */
162
+ function writeTypedByte(el, caretOffset, typed, source) {
163
+ const range = heldRange();
164
+ if (range) {
165
+ const edit = replaceRangeRaw(deps.node, range, typed, joinSeamMode(el), deps.linkRef, deps.getAmbientPrefix?.() ?? '');
166
+ void deps.blockEdit.updateBlockContent(deps.index, edit.raw, range.start, edit.caret);
167
+ deps.setPendingCursor(edit.caret, source, edit.raw);
168
+ return;
169
+ }
170
+ const seatedAt = typingSeatAt(el, caretOffset, typed)?.offset ?? caretOffset;
171
+ editDisplay(seatedAt, seatedAt, typed, source, caretOffset);
172
+ }
153
173
  // ── CST inline widget ────────────────────────────────────────────────────
154
174
  function handleCstWidget(e, caretOffset) {
155
175
  if (deps.isRevealing())
@@ -163,9 +183,11 @@ export function createEdgePolicyDispatch(deps) {
163
183
  const widgetAt = widgetAtCursor(caretOffset, inlinesOf(node), node.raw, direction);
164
184
  if (!widgetAt)
165
185
  return false;
166
- // Unchorded only: a modifier makes the key a word-scoped platform command, and
167
- // entering here would swap that word-step for the modal widget-selected state.
168
- const plainEdgeKey = !e.shiftKey && !hasModifier(e);
186
+ // Below the bail: every keystroke in every prose block reaches the line above.
187
+ const range = heldRange();
188
+ // Unchorded, and at a CARET: a modifier makes the key a word-scoped platform command, and
189
+ // a held range is an edit of the range, not an entry into the construct beside its start.
190
+ const plainEdgeKey = !e.shiftKey && !hasModifier(e) && !range;
169
191
  const enterFromRight = plainEdgeKey && widgetAt.atRight && (e.key === 'ArrowLeft' || e.key === 'Backspace');
170
192
  const enterFromLeft = plainEdgeKey && !widgetAt.atRight && (e.key === 'ArrowRight' || e.key === 'Delete');
171
193
  if (enterFromRight || enterFromLeft) {
@@ -200,14 +222,7 @@ export function createEdgePolicyDispatch(deps) {
200
222
  !deps.isReading()) {
201
223
  e.preventDefault();
202
224
  deps.setSnapTarget(null);
203
- const typed = e.key;
204
- // This branch owns only the fact that the engine drops the key beside a widget; which
205
- // side of an unpainted run the byte belongs on is still the seat's answer.
206
- const seatedAt = typingSeatAt(el, caretOffset, typed)?.offset ?? caretOffset;
207
- const newRaw = node.raw.slice(0, seatedAt) + typed + node.raw.slice(seatedAt);
208
- const postEdit = seatedAt + typed.length;
209
- void deps.blockEdit.updateBlockContent(deps.index, newRaw, caretOffset, postEdit);
210
- deps.setPendingCursor(postEdit, 'widget', newRaw);
225
+ writeTypedByte(el, caretOffset, e.key, 'widget');
211
226
  return true;
212
227
  }
213
228
  return false;
@@ -254,8 +269,8 @@ export function createEdgePolicyDispatch(deps) {
254
269
  // Second press: the native selection already wraps a replace island, so delete its
255
270
  // whole hidden range through the CST as one undo entry.
256
271
  if (isDestructive) {
257
- const selection = deps.getRawSelection();
258
- if (selection && selection.start < selection.end) {
272
+ const selection = heldRange();
273
+ if (selection) {
259
274
  const selected = islands.find((i) => islandPolicy(i).onEdge === 'select' &&
260
275
  i.start === selection.start &&
261
276
  i.end === selection.end);
@@ -312,7 +327,7 @@ export function createEdgePolicyDispatch(deps) {
312
327
  // branch in e2e, so it is unit-pinned instead.
313
328
  if (isTyping && !caretIsInTextContent(el, window.getSelection())) {
314
329
  e.preventDefault();
315
- editDisplay(caretOffset, caretOffset, e.key);
330
+ writeTypedByte(el, caretOffset, e.key, 'island');
316
331
  return true;
317
332
  }
318
333
  return false;
@@ -337,11 +352,11 @@ export function createEdgePolicyDispatch(deps) {
337
352
  if (!touchesAmbient)
338
353
  return false;
339
354
  e.preventDefault();
340
- const range = deps.getRawSelection();
341
- if (range && range.start < range.end) {
355
+ const range = heldRange();
356
+ if (range) {
342
357
  // Consumed at KEYDOWN, so no `beforeinput` carries this range to the shared seam: the arm
343
358
  // asks it here, or a literal splice prints the runs the cut stranded (live-mode.md § 4.5).
344
- const edit = deleteRangeRaw(deps.node, range, joinSeamMode(el), deps.linkRef, deps.getAmbientPrefix?.() ?? '');
359
+ const edit = replaceRangeRaw(deps.node, range, '', joinSeamMode(el), deps.linkRef, deps.getAmbientPrefix?.() ?? '');
345
360
  void deps.blockEdit.updateBlockContent(deps.index, edit.raw, range.start, edit.caret);
346
361
  deps.setPendingCursor(edit.caret, 'ambient-delete');
347
362
  }
@@ -59,10 +59,10 @@ export declare function resolveSelectionEdit(node: NodeView, selection: {
59
59
  start: number;
60
60
  end: number;
61
61
  }, typed: string, presentationMode: PresentationMode | undefined, linkRef: InlineResolverRef | undefined, ambientPrefix?: string): SelectionEdit | null;
62
- /** The delete of `[start, end)` a destructive gesture installs: the seam's cleaned rewrite where
63
- * it has one, else the literal display splice with the caret at the cut — the fallback's one
64
- * home, so no arm carrying it can drift. */
65
- export declare function deleteRangeRaw(node: NodeView, range: {
62
+ /** The rewrite of `[start, end)` to `typed` a gesture the surface consumed installs, where a
63
+ * delete is the empty one. The seam's cleaned bytes where it has them, else the literal display
64
+ * splice with the caret past the insert: the fallback's one home, so no arm can drift. */
65
+ export declare function replaceRangeRaw(node: NodeView, range: {
66
66
  start: number;
67
67
  end: number;
68
- }, presentationMode: PresentationMode | undefined, linkRef: InlineResolverRef | undefined, ambientPrefix: string): SelectionEdit;
68
+ }, typed: string, presentationMode: PresentationMode | undefined, linkRef: InlineResolverRef | undefined, ambientPrefix: string): SelectionEdit;
@@ -102,17 +102,20 @@ export function resolveSelectionEdit(node, selection, typed, presentationMode, l
102
102
  caret: joined.seam + typed.length
103
103
  };
104
104
  }
105
- /** The delete of `[start, end)` a destructive gesture installs: the seam's cleaned rewrite where
106
- * it has one, else the literal display splice with the caret at the cut — the fallback's one
107
- * home, so no arm carrying it can drift. */
108
- export function deleteRangeRaw(node, range, presentationMode, linkRef, ambientPrefix) {
109
- const cleaned = resolveSelectionEdit(node, range, '', presentationMode, linkRef, ambientPrefix);
105
+ /** The rewrite of `[start, end)` to `typed` a gesture the surface consumed installs, where a
106
+ * delete is the empty one. The seam's cleaned bytes where it has them, else the literal display
107
+ * splice with the caret past the insert: the fallback's one home, so no arm can drift. */
108
+ export function replaceRangeRaw(node, range, typed, presentationMode, linkRef, ambientPrefix) {
109
+ const cleaned = resolveSelectionEdit(node, range, typed, presentationMode, linkRef, ambientPrefix);
110
110
  if (cleaned)
111
111
  return cleaned;
112
112
  const display = trimTrailingLineEnding(node.raw);
113
113
  return {
114
- raw: display.slice(0, range.start) + display.slice(range.end) + trailingLineEnding(node.raw),
115
- caret: range.start
114
+ raw: display.slice(0, range.start) +
115
+ typed +
116
+ display.slice(range.end) +
117
+ trailingLineEnding(node.raw),
118
+ caret: range.start + typed.length
116
119
  };
117
120
  }
118
121
  // ── Reading the event ────────────────────────────────────────────────────────
@@ -6,7 +6,7 @@ import { resolvedInlineContent } from '../../../core/inline/inline-cache';
6
6
  import { isInlineWidget } from '../../../core/inline/inline-widgets';
7
7
  import { createClipboardHandlers } from '../editable-surface';
8
8
  import { pasteDispatch } from '../../../tree-operations/paste/dispatch';
9
- import { deleteRangeRaw } from './live-selection-edit';
9
+ import { replaceRangeRaw } from './live-selection-edit';
10
10
  export function createTextClipboard(deps) {
11
11
  function getSelectedTextFromRaw() {
12
12
  const offsets = deps.cursor.getRawSelection();
@@ -77,7 +77,7 @@ export function createTextClipboard(deps) {
77
77
  return;
78
78
  // A cut is a delete, so it crosses the same join seam: in live the range can span
79
79
  // delimiter runs the reader never saw, and a raw splice would print them.
80
- const edit = deleteRangeRaw(deps.node, selOffsets, deps.getPresentationMode(), deps.linkRef, deps.getAmbientPrefix());
80
+ const edit = replaceRangeRaw(deps.node, selOffsets, '', deps.getPresentationMode(), deps.linkRef, deps.getAmbientPrefix());
81
81
  void deps.blockEdit.updateBlockContent(deps.index, edit.raw, selOffsets.start);
82
82
  deps.setPendingCursor(edit.caret);
83
83
  },
@@ -5,15 +5,16 @@
5
5
  */
6
6
  import { tick } from 'svelte';
7
7
  import { resolvedInlineContent } from '../../../core/inline/inline-cache';
8
- import { 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';
18
19
  /**
19
20
  * The inline-code wash over a revealed source, painted through the CSS Custom Highlight API
@@ -323,16 +324,27 @@ export function createWidgetInteraction(deps) {
323
324
  return inline ? { inline } : null;
324
325
  }
325
326
  /**
326
- * The caret a click on a rendered widget wants: the END of the content a kind names, INSIDE
327
- * its delimiters, so typing continues the construct rather than escaping it. A kind naming no
328
- * span keeps the leading edge: guessing an end from its parsed text put a footnote's caret
329
- * past its bracket.
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.
330
331
  */
331
332
  function insideEndOffset(inline) {
332
333
  const source = deps.node.raw.slice(inline.start, inline.end);
333
334
  const span = getInlineWidgetEditing(inline.kind)?.revealContentSpan?.(source);
334
335
  return span && span.end >= 0 && span.end <= source.length ? span.end : 0;
335
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
+ }
336
348
  function hitTestRevealWidget(el, x, y) {
337
349
  const painted = revealWidgetFromDom(el, x, y);
338
350
  if (painted)
@@ -370,6 +382,7 @@ export function createWidgetInteraction(deps) {
370
382
  const hit = hitTestRevealWidget(el, clickX, clickY);
371
383
  if (!hit)
372
384
  return;
385
+ const seat = seatFromPoint(el, hit.inline, clickX, clickY);
373
386
  let targetStart = hit.inline.start;
374
387
  if (revealState) {
375
388
  const active = revealState;
@@ -391,9 +404,7 @@ export function createWidgetInteraction(deps) {
391
404
  return;
392
405
  el.focus();
393
406
  revealOpenedByLastClick = true;
394
- // A click can't map to a source glyph, so it lands at the end of the content rather
395
- // than guessing: the caret sits inside the delimiters, ready to type.
396
- void startReveal(target, target.start, insideEndOffset(target));
407
+ void startReveal(target, target.start, seat ?? insideEndOffset(target));
397
408
  }
398
409
  /**
399
410
  * Take the whole revealed token; declines unless the double-click landed on it. Keyed off the
@@ -645,24 +656,35 @@ export function createWidgetInteraction(deps) {
645
656
  // text and the browser's word rule takes `[` or `$` as a word of its own.
646
657
  if (doubleClick && revealOpenedByLastClick && selectRevealedSource(clickX, clickY))
647
658
  return;
648
- // A click in a real text node keeps the native caret; a synthetic overlay would compete.
649
- 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))
650
663
  return;
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) {
651
678
  for (const inline of widgetsOf()) {
652
679
  const widget = widgetElByStart(el, inline.start);
653
- if (!widget)
654
- continue;
655
- const rect = widget.getBoundingClientRect();
656
- const snapTo = clickX > rect.right ? inline.end : clickX < rect.left ? inline.start : null;
657
- if (snapTo === null)
658
- continue;
659
- el.focus();
660
- deps.cursor.setRaw(asRawOffset(snapTo));
661
- // `setRaw`'s walker may have landed in a trailing text node, where native renders.
662
- if (!caretIsInTextContent(el, window.getSelection())) {
663
- 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
+ };
664
687
  }
665
- return;
666
688
  }
667
689
  }
668
690
  function widgetExtensionTarget(key) {
@@ -16,6 +16,12 @@ export declare function isImageOnlyParagraph(node: NodeView): boolean;
16
16
  * caller still gates reading mode, which shows no affordances at all.
17
17
  */
18
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;
19
25
  /**
20
26
  * Vertical centre of the grip. A one-line block centres on its row. A taller one takes the
21
27
  * first line-height of its OWN box — not its first line of text, since a card pads above that
@@ -41,6 +41,14 @@ export function showsDragHandle(node, handlesEnabled) {
41
41
  return true;
42
42
  return handlesEnabled && !GRIPLESS_KINDS.has(node.kind);
43
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
+ }
44
52
  /** A box within this many line-heights holds one row of content, margins included. */
45
53
  const SINGLE_LINE = 1.5;
46
54
  /**
@@ -26,7 +26,7 @@ export interface TypeScaleProbeDeps {
26
26
  export declare function installTypeScaleProbe(el: HTMLElement, deps: TypeScaleProbeDeps): () => void;
27
27
  export interface HeaderSlotCompensationDeps {
28
28
  el: HTMLElement;
29
- port: Pick<Scrollport, 'scrollTop' | 'setScrollTop'>;
29
+ port: Pick<Scrollport, 'scrollTop' | 'scrollBy'>;
30
30
  ownsScrollCorrection(): boolean;
31
31
  revealHoldsScroll(): boolean;
32
32
  }
@@ -84,7 +84,7 @@ export function installHeaderSlotCompensation(deps) {
84
84
  if (delta === 0 || !deps.ownsScrollCorrection() || port.scrollTop() === 0)
85
85
  return;
86
86
  if (!deps.revealHoldsScroll())
87
- port.setScrollTop(port.scrollTop() + delta);
87
+ port.scrollBy(delta);
88
88
  });
89
89
  observer.observe(el);
90
90
  return () => observer.disconnect();
@@ -39,10 +39,3 @@ export declare function installEditorBlurAnnouncer(deps: {
39
39
  root: HTMLElement;
40
40
  emit: () => void;
41
41
  }): () => 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;
@@ -5,8 +5,6 @@
5
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';
10
8
  // ── Listener plumbing ───────────────────────────────────────────────
11
9
  // Typed off `Event`, not the per-target event maps: those names are type-only,
12
10
  // and `target` ranges over every EventTarget the root effects listen on.
@@ -91,23 +89,3 @@ export function installEditorBlurAnnouncer(deps) {
91
89
  };
92
90
  return onRoot(deps.root, 'focusout', handler);
93
91
  }
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))
105
- return;
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;
113
- }
@@ -187,24 +187,32 @@
187
187
  {/if}
188
188
 
189
189
  <style>
190
- /* An accent pill straddling the edge with a white rim, so it reads on any picture. The hit
191
- strip is wider than the paint. */
190
+ /* A neutral grip sitting inside the picture's right edge: the selection ring owns the edge
191
+ itself, and the accent is the ring's, so a second accent shape beside it reads as decoration
192
+ rather than as a control. White over a hairline rim so it carries on a dark picture and a
193
+ pale one alike. The hit strip is wider than the paint, so on a picture too narrow to hold
194
+ both the strip and a clickable middle (an icon-sized image) the grip steps back outside
195
+ the edge, where it stood before. */
192
196
  .md-resize-handle {
193
197
  position: absolute;
194
- right: -3px;
198
+ right: clamp(-3px, calc(50% - 20px), 7px);
195
199
  top: 50%;
196
- width: 6px;
197
- height: 44px;
198
- max-height: 60%;
200
+ width: 4px;
201
+ height: 36px;
202
+ max-height: 40%;
199
203
  transform: translateY(-50%);
200
- border-radius: 3px;
201
- background: var(--color-accent, #567b67);
204
+ border-radius: 999px;
205
+ background: rgba(255, 255, 255, 0.92);
202
206
  box-shadow:
203
- 0 0 0 1.5px rgba(255, 255, 255, 0.9),
204
- 0 1px 3px rgba(0, 0, 0, 0.3);
207
+ 0 0 0 0.5px rgba(0, 0, 0, 0.35),
208
+ 0 1px 3px rgba(0, 0, 0, 0.35);
209
+ opacity: 0.85;
205
210
  z-index: 10;
206
211
  cursor: ew-resize;
207
212
  }
213
+ .md-resize-handle:hover {
214
+ opacity: 1;
215
+ }
208
216
  .md-resize-handle::before {
209
217
  content: '';
210
218
  position: absolute;