@voithos-labs/aragonite 0.10.2 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/README.md +6 -21
  2. package/THIRD-PARTY-NOTICES.md +25 -0
  3. package/dist/a11y-strings.d.ts +18 -0
  4. package/dist/a11y-strings.js +18 -0
  5. package/dist/action-contracts.d.ts +7 -1
  6. package/dist/ambient/ambient-dom.js +5 -1
  7. package/dist/block-component.d.ts +14 -0
  8. package/dist/components/BlockDragHandle.svelte +35 -24
  9. package/dist/components/BlockHost.svelte +16 -9
  10. package/dist/components/Editor.svelte +381 -198
  11. package/dist/components/Editor.svelte.d.ts +1 -1
  12. package/dist/components/SelectionOverlay.svelte +17 -3
  13. package/dist/components/TailInsert.svelte +107 -0
  14. package/dist/components/TailInsert.svelte.d.ts +17 -0
  15. package/dist/components/block-content-selector.d.ts +6 -2
  16. package/dist/components/block-content-selector.js +6 -2
  17. package/dist/components/blocks/ThematicBreakBlock.svelte +19 -6
  18. package/dist/components/blocks/ThematicBreakBlock.svelte.d.ts +1 -0
  19. package/dist/components/blocks/code/CodeBlock.svelte +211 -30
  20. package/dist/components/blocks/code/CodeBlockRail.svelte +686 -0
  21. package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
  22. package/dist/components/blocks/code/code-bootstrap.js +4 -0
  23. package/dist/components/blocks/code/code-context-actions.d.ts +1 -0
  24. package/dist/components/blocks/code/code-context-actions.js +24 -0
  25. package/dist/components/blocks/code/code-fence-exit.d.ts +15 -0
  26. package/dist/components/blocks/code/code-fence-exit.js +26 -0
  27. package/dist/components/blocks/code/code-languages.d.ts +6 -0
  28. package/dist/components/blocks/code/code-languages.js +11 -0
  29. package/dist/components/blocks/code/code-renderer.js +11 -0
  30. package/dist/components/blocks/directive/DirectiveContainerBlock.svelte +1 -1
  31. package/dist/components/blocks/editable-leaf.d.ts +37 -6
  32. package/dist/components/blocks/editable-leaf.js +243 -29
  33. package/dist/components/blocks/editable-surface.d.ts +9 -0
  34. package/dist/components/blocks/editable-surface.js +22 -3
  35. package/dist/components/blocks/list/ListItemBlock.svelte +3 -3
  36. package/dist/components/blocks/list/task-checkbox.d.ts +2 -0
  37. package/dist/components/blocks/list/task-checkbox.js +11 -2
  38. package/dist/components/blocks/surface-wiring.svelte.d.ts +4 -0
  39. package/dist/components/blocks/surface-wiring.svelte.js +8 -1
  40. package/dist/components/blocks/table/TableActionMenu.svelte +210 -86
  41. package/dist/components/blocks/table/TableActionMenu.svelte.d.ts +5 -0
  42. package/dist/components/blocks/table/TableBlock.svelte +205 -186
  43. package/dist/components/blocks/table/TableBlock.svelte.d.ts +1 -0
  44. package/dist/components/blocks/table/TableCellBlock.svelte +87 -22
  45. package/dist/components/blocks/table/TableRowBlock.svelte +4 -18
  46. package/dist/components/blocks/table/TableRowBlock.svelte.d.ts +0 -5
  47. package/dist/components/blocks/table/cell-clipboard.d.ts +10 -0
  48. package/dist/components/blocks/table/cell-clipboard.js +34 -1
  49. package/dist/components/blocks/table/cell-keydown-plan.d.ts +1 -1
  50. package/dist/components/blocks/table/cell-keydown-plan.js +3 -1
  51. package/dist/components/blocks/table/table-cell-paste.js +2 -1
  52. package/dist/components/blocks/table/table-menu-model.d.ts +35 -8
  53. package/dist/components/blocks/table/table-menu-model.js +36 -17
  54. package/dist/components/blocks/text/TextEditableBlock.svelte +58 -11
  55. package/dist/components/blocks/text/delimiter-autopair.d.ts +71 -0
  56. package/dist/components/blocks/text/delimiter-autopair.js +216 -0
  57. package/dist/components/blocks/text/edge-policy-dispatch.d.ts +4 -0
  58. package/dist/components/blocks/text/edge-policy-dispatch.js +56 -2
  59. package/dist/components/blocks/text/live-selection-edit.js +27 -0
  60. package/dist/components/blocks/text/text-keydown.d.ts +7 -1
  61. package/dist/components/blocks/text/text-keydown.js +11 -1
  62. package/dist/components/blocks/text/text-render.d.ts +1 -1
  63. package/dist/components/blocks/text/text-render.js +3 -1
  64. package/dist/components/blocks/text/widget-interaction.d.ts +3 -0
  65. package/dist/components/blocks/text/widget-interaction.js +144 -22
  66. package/dist/components/drag-handle.d.ts +35 -0
  67. package/dist/components/drag-handle.js +126 -0
  68. package/dist/components/editor-root-focus.d.ts +19 -0
  69. package/dist/components/editor-root-focus.js +67 -0
  70. package/dist/components/editor-root-geometry.d.ts +39 -0
  71. package/dist/components/editor-root-geometry.js +91 -0
  72. package/dist/components/editor-root-keydown.d.ts +1 -1
  73. package/dist/components/editor-root-keydown.js +12 -3
  74. package/dist/components/editor-root-listeners.d.ts +10 -6
  75. package/dist/components/editor-root-listeners.js +23 -22
  76. package/dist/components/editor-root-mode-flip.d.ts +36 -0
  77. package/dist/components/editor-root-mode-flip.js +92 -0
  78. package/dist/components/image/ImageOverlayHost.svelte +14 -6
  79. package/dist/components/image/ImageProperties.svelte +512 -70
  80. package/dist/components/image/ImageProperties.svelte.d.ts +6 -1
  81. package/dist/components/image/ImageResizeHandles.svelte +59 -34
  82. package/dist/components/image/image-crop.d.ts +39 -0
  83. package/dist/components/image/image-crop.js +74 -0
  84. package/dist/components/image/image-edit-commit.d.ts +1 -0
  85. package/dist/components/image/image-edit-commit.js +21 -5
  86. package/dist/components/image/image-source-bytes.js +10 -3
  87. package/dist/components/image/image-widget-editing.js +1 -0
  88. package/dist/components/image/widget-dom.js +5 -1
  89. package/dist/components/link-card/link-card-commit.js +1 -1
  90. package/dist/components/lrd-map-gate.js +1 -1
  91. package/dist/components/menu/BlockMenu.svelte +315 -0
  92. package/dist/components/menu/BlockMenu.svelte.d.ts +34 -0
  93. package/dist/components/menu/MenuIcon.svelte +153 -0
  94. package/dist/components/menu/MenuIcon.svelte.d.ts +51 -0
  95. package/dist/components/menu/clipboard-actions.d.ts +12 -0
  96. package/dist/components/menu/clipboard-actions.js +42 -0
  97. package/dist/components/menu/default-context-actions.d.ts +15 -0
  98. package/dist/components/menu/default-context-actions.js +76 -0
  99. package/dist/components/menu/flyout-placement.d.ts +6 -0
  100. package/dist/components/menu/flyout-placement.js +25 -0
  101. package/dist/core/inline/format-toggle.d.ts +12 -4
  102. package/dist/core/inline/format-toggle.js +94 -40
  103. package/dist/core/inline/image-dimensions.d.ts +3 -0
  104. package/dist/core/inline/image-dimensions.js +46 -10
  105. package/dist/core/inline/inline-widgets.d.ts +11 -0
  106. package/dist/core/inline/scan/brackets.js +1 -0
  107. package/dist/core/inline/scan/plugin-syntax.d.ts +8 -0
  108. package/dist/core/inline/scan/plugin-syntax.js +15 -1
  109. package/dist/core/inline-render.d.ts +6 -0
  110. package/dist/core/inline-render.js +32 -0
  111. package/dist/core/nodes.d.ts +14 -0
  112. package/dist/cursor/edge-affinity.js +2 -1
  113. package/dist/cursor/overlay-remeasure.js +8 -0
  114. package/dist/cursor/reveal-source.js +7 -2
  115. package/dist/cursor/widget-offset.d.ts +6 -0
  116. package/dist/cursor/widget-offset.js +61 -4
  117. package/dist/debug/interaction-trace.d.ts +4 -0
  118. package/dist/debug/interaction-trace.js +15 -0
  119. package/dist/decorations/decoration-state.svelte.js +1 -1
  120. package/dist/editor-actions/ancestry-folds.d.ts +2 -2
  121. package/dist/editor-actions/ancestry-folds.js +1 -1
  122. package/dist/editor-actions/block-edit-scope.js +1 -1
  123. package/dist/editor-actions/commit/text-batch.d.ts +3 -2
  124. package/dist/editor-actions/commit/text-batch.js +1 -1
  125. package/dist/editor-actions/commit/undo-controller.js +4 -2
  126. package/dist/editor-actions/container-edit.js +2 -1
  127. package/dist/editor-actions/enter-completion.d.ts +2 -0
  128. package/dist/editor-actions/enter-completion.js +23 -2
  129. package/dist/editor-actions/inline-range-commit.js +1 -1
  130. package/dist/editor-actions/reorder-action.js +19 -10
  131. package/dist/editor-actions/reorder-drag.js +29 -1
  132. package/dist/editor-actions/replacement-focus.d.ts +1 -1
  133. package/dist/editor-actions/replacement-focus.js +1 -1
  134. package/dist/editor-actions/search-replace.js +1 -1
  135. package/dist/editor-actions/table-context.d.ts +4 -1
  136. package/dist/editor-actions/table-context.js +57 -1
  137. package/dist/editor-events.d.ts +3 -0
  138. package/dist/editor-keys.d.ts +33 -0
  139. package/dist/editor-props.d.ts +21 -12
  140. package/dist/index.d.ts +1 -1
  141. package/dist/plugin.d.ts +6 -0
  142. package/dist/plugin.js +11 -0
  143. package/dist/plugins/latex/BlockMath.svelte +264 -29
  144. package/dist/plugins/latex/BlockMath.svelte.d.ts +2 -0
  145. package/dist/plugins/latex/index.d.ts +2 -1
  146. package/dist/plugins/latex/latex-kind.js +36 -3
  147. package/dist/plugins/latex/math-completion.js +4 -1
  148. package/dist/plugins/latex/math-layout.d.ts +15 -0
  149. package/dist/plugins/latex/math-layout.js +13 -0
  150. package/dist/plugins/latex/math-source.d.ts +19 -0
  151. package/dist/plugins/latex/math-source.js +97 -0
  152. package/dist/plugins/latex/register.d.ts +11 -2
  153. package/dist/plugins/latex/register.js +3 -1
  154. package/dist/plugins/latex/renderer.d.ts +3 -3
  155. package/dist/plugins/latex/renderer.js +13 -6
  156. package/dist/reactivity/list-windowing.svelte.d.ts +8 -8
  157. package/dist/reactivity/list-windowing.svelte.js +49 -24
  158. package/dist/schema/block-completions.d.ts +8 -0
  159. package/dist/schema/block-completions.js +11 -0
  160. package/dist/schema/context-actions.d.ts +31 -0
  161. package/dist/schema/context-actions.js +23 -0
  162. package/dist/schema/fenced-code-raw.js +31 -1
  163. package/dist/schema/operations.d.ts +8 -1
  164. package/dist/schema/reserved-chords.js +25 -4
  165. package/dist/schema/table-cell-raw.d.ts +1 -1
  166. package/dist/schema/table-cell-raw.js +1 -1
  167. package/dist/selection/block-hit-test.js +3 -2
  168. package/dist/selection/char-endpoint-snap.js +1 -1
  169. package/dist/selection/clipboard-text.js +6 -1
  170. package/dist/selection/covered-block.d.ts +10 -0
  171. package/dist/selection/covered-block.js +24 -0
  172. package/dist/selection/cross-block/dispatch.d.ts +3 -0
  173. package/dist/selection/cross-block/dispatch.js +13 -1
  174. package/dist/selection/cross-block/format-range.d.ts +1 -1
  175. package/dist/selection/cross-block/format-range.js +4 -15
  176. package/dist/selection/cross-block/format-toggle.js +1 -1
  177. package/dist/selection/cross-block/keydown.js +1 -1
  178. package/dist/selection/cross-block/ops.js +1 -1
  179. package/dist/selection/cross-block/paste.js +32 -30
  180. package/dist/selection/cross-block/type-replace.d.ts +3 -2
  181. package/dist/selection/cross-block/type-replace.js +62 -13
  182. package/dist/selection/dead-space-caret.d.ts +10 -0
  183. package/dist/selection/dead-space-caret.js +47 -1
  184. package/dist/selection/double-click-trim.d.ts +17 -0
  185. package/dist/selection/double-click-trim.js +57 -0
  186. package/dist/selection/drag-pointer.d.ts +7 -2
  187. package/dist/selection/drag-pointer.js +61 -2
  188. package/dist/selection/gap-caret.js +1 -1
  189. package/dist/selection/keyboard-extend.js +1 -1
  190. package/dist/selection/path-lookup.js +1 -1
  191. package/dist/selection/range-delete-ceremony.js +4 -2
  192. package/dist/selection/range-delete-chrome.js +2 -1
  193. package/dist/selection/range-delete-table-coverage.js +2 -1
  194. package/dist/selection/range-delete-table.js +3 -2
  195. package/dist/selection/range-delete.js +30 -2
  196. package/dist/selection/selection-restore.js +1 -1
  197. package/dist/selection/selection-state.svelte.d.ts +6 -0
  198. package/dist/selection/selection-state.svelte.js +36 -1
  199. package/dist/selection/table-endpoint-snap.js +1 -1
  200. package/dist/selection/table-rect-extend.js +1 -1
  201. package/dist/styles/editor-theme.css +57 -28
  202. package/dist/styles/editor.css +217 -18
  203. package/dist/testing/container-conformance.js +2 -2
  204. package/dist/testing/inline-conformance.js +2 -1
  205. package/dist/tree-operations/blockquote.js +1 -1
  206. package/dist/tree-operations/chain-rebuild.d.ts +63 -0
  207. package/dist/tree-operations/chain-rebuild.js +142 -0
  208. package/dist/tree-operations/children.d.ts +1 -1
  209. package/dist/tree-operations/children.js +1 -1
  210. package/dist/tree-operations/cleanup.js +1 -1
  211. package/dist/tree-operations/content-write.d.ts +50 -0
  212. package/dist/tree-operations/content-write.js +263 -0
  213. package/dist/tree-operations/index.d.ts +8 -3
  214. package/dist/tree-operations/index.js +6 -2
  215. package/dist/tree-operations/list/exit-replacement.js +1 -1
  216. package/dist/tree-operations/list/unwrap-merge.js +3 -3
  217. package/dist/tree-operations/node-ops.d.ts +17 -234
  218. package/dist/tree-operations/node-ops.js +47 -1113
  219. package/dist/tree-operations/node-primitives.d.ts +75 -0
  220. package/dist/tree-operations/node-primitives.js +117 -0
  221. package/dist/tree-operations/paste/apply.js +1 -1
  222. package/dist/tree-operations/paste/body-write.d.ts +1 -1
  223. package/dist/tree-operations/paste/body-write.js +2 -2
  224. package/dist/tree-operations/paste/container-match.js +4 -2
  225. package/dist/tree-operations/paste/dispatch.js +2 -1
  226. package/dist/tree-operations/paste/find-enclosing-list.js +1 -1
  227. package/dist/tree-operations/paste/focus-target.d.ts +1 -1
  228. package/dist/tree-operations/paste/list-absorb.js +1 -1
  229. package/dist/tree-operations/paste/list-break-out.js +1 -1
  230. package/dist/tree-operations/paste/parent-scope.js +1 -1
  231. package/dist/tree-operations/paste/paste-replacement.js +1 -1
  232. package/dist/tree-operations/paste/replace-block-at-parent.js +1 -1
  233. package/dist/tree-operations/path-mutate.d.ts +1 -1
  234. package/dist/tree-operations/path-mutate.js +2 -1
  235. package/dist/tree-operations/reorder-unit.js +1 -1
  236. package/dist/tree-operations/reorder.d.ts +5 -2
  237. package/dist/tree-operations/reorder.js +55 -2
  238. package/dist/tree-operations/settle.d.ts +105 -0
  239. package/dist/tree-operations/settle.js +660 -0
  240. package/dist/tree-operations/table-grid-clipboard.d.ts +21 -0
  241. package/dist/tree-operations/table-grid-clipboard.js +90 -0
  242. package/dist/tree-operations/unshare.d.ts +15 -77
  243. package/dist/tree-operations/unshare.js +15 -166
  244. package/docs/guide/consumer-guide.md +126 -96
  245. package/docs/guide/plugin-api.md +31 -3
  246. package/docs/guide/plugin-guide.md +26 -3
  247. package/package.json +4 -2
  248. package/dist/components/blocks/code/CodeLanguageChip.svelte +0 -127
  249. package/dist/components/blocks/code/CodeLanguageChip.svelte.d.ts +0 -14
  250. package/dist/components/blocks/table/TableGrip.svelte +0 -91
  251. package/dist/components/blocks/table/TableGrip.svelte.d.ts +0 -8
  252. package/dist/components/blocks/table/table-drop-target.d.ts +0 -1
  253. package/dist/components/blocks/table/table-drop-target.js +0 -16
  254. package/dist/components/blocks/table/table-reorder-drag.d.ts +0 -78
  255. package/dist/components/blocks/table/table-reorder-drag.js +0 -97
@@ -32,7 +32,8 @@
32
32
  import { pathsEqual } from '../../../selection/path-math';
33
33
  import { placeCaret } from '../../../selection/caret-doors';
34
34
  import { columnNearestX } from './cell-x-mapping';
35
- import { cellAtPoint, mountedRowEls, rowCellEls } from './cell-pointer';
35
+ import { cellAtPoint, installCellDragListener, mountedRowEls, rowCellEls } from './cell-pointer';
36
+ import { tableCaretAtPoint } from './table-caret-at-point';
36
37
  import { intraTableRect } from './cell-clipboard';
37
38
  import { selectedCells } from './selected-cells';
38
39
  import { createBlockListState } from '../../../reactivity/block-list-state.svelte';
@@ -45,15 +46,10 @@
45
46
  type NodeScope
46
47
  } from '../../../editor-actions/nested/nested-actions';
47
48
  import { createTableMutationsContext } from '../../../editor-actions/table-context';
48
- import {
49
- startRowReorderDrag,
50
- startColumnReorderDrag,
51
- type RowReorderLine,
52
- type ColumnReorderLine
53
- } from './table-reorder-drag';
54
49
  import TableRowBlock from './TableRowBlock.svelte';
55
- import TableGrip from './TableGrip.svelte';
56
50
  import TableActionMenu from './TableActionMenu.svelte';
51
+ import MenuIcon from '../../menu/MenuIcon.svelte';
52
+ import { ADD_COLUMN_RIGHT, ADD_ROW_BELOW } from '../../../a11y-strings';
57
53
  import { tableMenuItems, type ClipboardAction } from './table-menu-model';
58
54
 
59
55
  let {
@@ -78,19 +74,14 @@
78
74
  } = getContext<EditorServices>(EDITOR_SERVICES_KEY);
79
75
  const {
80
76
  editorRoot: getEditorRoot,
81
- scrollHost: getScrollHost,
82
77
  widthVersion: getWidthVersion,
83
78
  lifetime: editorLifetime,
84
79
  linkRef
85
80
  } = getContext<EditorDoc>(EDITOR_DOC_KEY);
86
- const { presentationMode: getPresentationMode, blockDragHandles: getDragHandles } =
87
- getContext<EditorPolicies>(EDITOR_POLICIES_KEY);
81
+ const { presentationMode: getPresentationMode } = getContext<EditorPolicies>(EDITOR_POLICIES_KEY);
88
82
  // Every menu item mutates the table, so reading mode declines to open it and the
89
83
  // native context menu (with Copy) shows instead.
90
84
  const readOnly = $derived(getPresentationMode() === 'reading');
91
- // The block handle's switch covers the grips: one mouse-only affordance policy, and the
92
- // getter already folds reading mode in.
93
- const showGrips = $derived(getDragHandles());
94
85
 
95
86
  const meta = $derived(metadataOf(node, 'table'));
96
87
  const rowCount = $derived(node.children?.length ?? 0);
@@ -110,6 +101,14 @@
110
101
  // the focusout handler, which Svelte 5 traps as state_unsafe_mutation.
111
102
  let internalStickyColumn: number | null = null;
112
103
  let focusedCell: { rowIdx: number; colIdx: number } | null = null;
104
+ // The reactive mirror the edge affordances read, written a microtask after the plain one so
105
+ // a focusout fired mid-reconcile never mutates state inside the render.
106
+ let caretCell = $state<{ rowIdx: number; colIdx: number } | null>(null);
107
+ function mirrorCaretCell(): void {
108
+ void tick().then(() => {
109
+ caretCell = focusedCell;
110
+ });
111
+ }
113
112
  let tableEl: HTMLDivElement | undefined = $state();
114
113
 
115
114
  const rowsState = createBlockListState(() => node);
@@ -164,16 +163,11 @@
164
163
  // wide cell scrolls out of the mounted set (F6). The floor only ever grows.
165
164
  let columnMaxWidths = $state<number[]>([]);
166
165
 
167
- // The row-grip gutter leads, and only while the grips render: rows auto-place, so a track
168
- // with nothing in it would shift every cell left. Zero width, so the geometry is untouched.
169
166
  const trackTemplate = $derived(
170
- [
171
- ...(showGrips ? ['0'] : []),
172
- ...Array.from({ length: columnCount }, (_, c) => {
173
- const floor = Math.max(80, columnMaxWidths[c] ?? 0);
174
- return `minmax(${floor}px, max-content)`;
175
- })
176
- ].join(' ')
167
+ Array.from({ length: columnCount }, (_, c) => {
168
+ const floor = Math.max(80, columnMaxWidths[c] ?? 0);
169
+ return `minmax(${floor}px, max-content)`;
170
+ }).join(' ')
177
171
  );
178
172
 
179
173
  let measuredColumnEpoch = '';
@@ -224,9 +218,21 @@
224
218
 
225
219
  function focusCell(rowIdx: number, colIdx: number, position: CellPosition): void {
226
220
  rowRefAt(rowIdx)?.focusByPath?.([colIdx], offsetForPosition(position));
221
+ revealColumn(rowIdx, colIdx);
222
+ }
223
+
224
+ // The grid is its own horizontal scroller and a cell's focus never scrolls (a click must not
225
+ // jump the page), so a column reached by keyboard or by a move is brought into the box here.
226
+ function revealColumn(rowIdx: number, colIdx: number): void {
227
+ const cell = cellElementAt(rowIdx, colIdx)?.getBoundingClientRect();
228
+ if (!tableEl || !cell) return;
229
+ const grid = tableEl.getBoundingClientRect();
230
+ if (cell.right > grid.right) tableEl.scrollLeft += cell.right - grid.right;
231
+ else if (cell.left < grid.left) tableEl.scrollLeft -= grid.left - cell.left;
227
232
  }
228
233
 
229
234
  const mutations = createTableMutationsContext({
235
+ grammar: registryView.grammar,
230
236
  get node() {
231
237
  return node;
232
238
  },
@@ -272,29 +278,29 @@
272
278
  },
273
279
  notifyCellFocused(rowIdx, colIdx) {
274
280
  focusedCell = { rowIdx, colIdx };
281
+ mirrorCaretCell();
275
282
  },
276
283
  notifyCellBlurred() {
277
284
  focusedCell = null;
285
+ mirrorCaretCell();
278
286
  },
279
287
  ...mutations
280
288
  };
281
289
 
282
290
  setContext(TABLE_CONTEXT_KEY, ctx);
283
291
 
284
- // ── Affordance menu (row + column) ─────────────────────────────────────
292
+ // ── Cell menu ──────────────────────────────────────────────────────────
285
293
 
286
- const columnIndices = $derived(Array.from({ length: columnCount }, (_, c) => c));
287
-
288
- type MenuAxis = 'row' | 'column';
289
- type MenuTarget = { rowIdx?: number; colIdx?: number };
290
294
  // clipboardSel is the cell's selection captured at right-click, before the menu steals
291
- // focus null for grip menus and for an empty cell with no caret.
295
+ // focus; null for an empty cell with no caret.
292
296
  type CellSelection = { start: number; end: number };
293
297
  let menu = $state<{
294
- target: MenuTarget;
298
+ target: { rowIdx: number; colIdx: number };
295
299
  x: number;
296
300
  y: number;
297
301
  clipboardSel: CellSelection | null;
302
+ /** Re-reads the open point where its element is now (scroll, resize). */
303
+ anchor?: () => { x: number; y: number } | null;
298
304
  } | null>(null);
299
305
 
300
306
  // A live rectangle suppresses the cell-local selection, so the menu reads it
@@ -314,14 +320,47 @@
314
320
  : []
315
321
  );
316
322
 
317
- function openMenu(axis: MenuAxis, axisIdx: number, e: MouseEvent): void {
318
- const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
319
- const target: MenuTarget = axis === 'column' ? { colIdx: axisIdx } : { rowIdx: axisIdx };
320
- // A row grip opens beside itself rather than below, so the menu clears the left edge.
321
- menu =
322
- axis === 'column'
323
- ? { target, x: rect.left, y: rect.bottom, clipboardSel: null }
324
- : { target, x: rect.right, y: rect.top, clipboardSel: null };
323
+ // Notion's edge affordances: a strip past the table's right edge adds a column, one below
324
+ // it adds a row. Each shows on hover of its strip, and stays shown while the caret is in
325
+ // the last column / row. Geometry is the table's own box, measured after layout settles and
326
+ // again whenever the table resizes.
327
+ const addAffordance = $derived({
328
+ column: !readOnly && caretCell?.colIdx === columnCount - 1,
329
+ row: !readOnly && caretCell?.rowIdx === rowCount - 1
330
+ });
331
+ let addGeometry = $state<{ left: number; top: number; width: number; height: number } | null>(
332
+ null
333
+ );
334
+ $effect(() => {
335
+ const el = tableEl;
336
+ if (readOnly || !el) {
337
+ addGeometry = null;
338
+ return;
339
+ }
340
+ const measure = () => {
341
+ addGeometry = {
342
+ left: el.offsetLeft,
343
+ top: el.offsetTop,
344
+ width: el.offsetWidth,
345
+ height: el.offsetHeight
346
+ };
347
+ };
348
+ void tick().then(measure);
349
+ const observer = typeof ResizeObserver === 'function' ? new ResizeObserver(measure) : null;
350
+ observer?.observe(el);
351
+ return () => observer?.disconnect();
352
+ });
353
+
354
+ // The open point as an offset into an element's box, so a scroll re-reads it where the
355
+ // element is now rather than where the viewport left it.
356
+ function anchorOn(el: Element, point: { x: number; y: number }): () => { x: number; y: number } {
357
+ const rect = el.getBoundingClientRect();
358
+ const dx = point.x - rect.left;
359
+ const dy = point.y - rect.top;
360
+ return () => {
361
+ const now = el.getBoundingClientRect();
362
+ return { x: now.left + dx, y: now.top + dy };
363
+ };
325
364
  }
326
365
 
327
366
  function cellRefAt(rowIdx: number, colIdx: number): BlockComponent | null {
@@ -332,7 +371,9 @@
332
371
  // Cut/Copy have a range to act on.
333
372
  function openMenuAtCell(rowIdx: number, colIdx: number, x: number, y: number): void {
334
373
  const clipboardSel = cellRefAt(rowIdx, colIdx)?.getSelectionOffsets?.() ?? null;
335
- menu = { target: { rowIdx, colIdx }, x, y, clipboardSel };
374
+ const cellEl = cellElementAt(rowIdx, colIdx);
375
+ const anchor = cellEl ? anchorOn(cellEl, { x, y }) : undefined;
376
+ menu = { target: { rowIdx, colIdx }, x, y, clipboardSel, anchor };
336
377
  }
337
378
 
338
379
  // preventDefault only over a cell, so a right-click in the table's padding gaps keeps
@@ -356,19 +397,28 @@
356
397
  openMenuAtCell(rowIdx, colIdx, rect ? rect.left : 0, rect ? rect.bottom : 0);
357
398
  }
358
399
 
359
- // Cell menus only a grip menu has no caret to return to. The restore goes through
360
- // `focusCell`: a bare `el.focus()` on a contenteditable seats no typeable caret.
400
+ // The restore goes through `focusCell`: a bare `el.focus()` on a contenteditable seats no
401
+ // typeable caret.
361
402
  async function closeMenuRestoringFocus(): Promise<void> {
362
403
  const target = menu?.target;
363
404
  const offset = menu?.clipboardSel?.start ?? 'start';
364
405
  menu = null;
365
- if (target?.rowIdx == null || target?.colIdx == null) return;
406
+ if (!target) return;
366
407
  await tick();
367
408
  focusCell(target.rowIdx, target.colIdx, offset);
368
409
  }
369
410
 
411
+ // A right-click seats no caret, so without this an action's focus-follow reads no focused
412
+ // cell and lands in column 0 (or row 0) of what it moved.
413
+ function seatMenuCaret(): void {
414
+ if (!menu) return;
415
+ const { rowIdx, colIdx } = menu.target;
416
+ focusCell(rowIdx, colIdx, menu.clipboardSel?.start ?? 'start');
417
+ }
418
+
370
419
  async function runAction(action: TableAxisAction, axisIdx: number): Promise<void> {
371
420
  if (!menu) return;
421
+ seatMenuCaret();
372
422
  await ctx[action](axisIdx);
373
423
  menu = null;
374
424
  }
@@ -378,107 +428,17 @@
378
428
  const { rowIdx, colIdx } = menu.target;
379
429
  const sel = menu.clipboardSel ?? { start: 0, end: 0 };
380
430
  menu = null;
381
- if (rowIdx == null || colIdx == null) return;
382
431
  await cellRefAt(rowIdx, colIdx)?.applyMenuClipboard?.(action, sel);
383
432
  }
384
433
 
385
434
  async function runAlign(alignment: 'left' | 'center' | 'right'): Promise<void> {
386
- const colIdx = menu?.target.colIdx;
387
- if (colIdx == null) return;
435
+ if (!menu) return;
436
+ const { colIdx } = menu.target;
437
+ seatMenuCaret();
388
438
  await ctx.setColumnAlignment(colIdx, alignment);
389
439
  menu = null;
390
440
  }
391
441
 
392
- // ── Row drag reorder ───────────────────────────────────────────────────
393
-
394
- let dragLine = $state<RowReorderLine | null>(null);
395
- // A drag that ends off the grip fires no click, so this resets on every grip
396
- // pointerdown rather than being consumed on click; a stale `true` would eat a menu.
397
- let suppressRowGripClick = false;
398
-
399
- // Rows are `display: contents` (no box), so measure each mounted row's first cell.
400
- // Carries the ABSOLUTE row index so a drop maps a mounted edge to a body position
401
- // under windowing; re-read live each move, so an autoscroll re-slice is reflected.
402
- function rowReorderGeometry() {
403
- if (!tableEl || rowCount === 0) return null;
404
- const rowEdges: number[] = [];
405
- const gapIndices: number[] = [];
406
- let lastBottom = 0;
407
- let lastIdx = -1;
408
- for (const rowEl of mountedRowEls(tableEl)) {
409
- const cell = rowCellEls(rowEl)[0];
410
- if (!cell) continue;
411
- const rect = cell.getBoundingClientRect();
412
- const idx = Number(rowEl.getAttribute('data-table-row-idx'));
413
- rowEdges.push(rect.top);
414
- gapIndices.push(idx);
415
- lastBottom = rect.bottom;
416
- lastIdx = idx;
417
- }
418
- if (rowEdges.length === 0) return null;
419
- rowEdges.push(lastBottom);
420
- gapIndices.push(lastIdx + 1);
421
- const tableRect = tableEl.getBoundingClientRect();
422
- return { rowEdges, gapIndices, left: tableRect.left, width: tableRect.width };
423
- }
424
-
425
- function onRowGripPointerDown(rowIdx: number, e: PointerEvent): void {
426
- // Before any bail: a prior drag released off the grip left no click to consume it.
427
- suppressRowGripClick = false;
428
- // The header row is positionally fixed, so its grip stays click-only.
429
- if (rowIdx === 0) return;
430
- startRowReorderDrag(e, {
431
- fromRowIdx: rowIdx,
432
- getRowCount: () => rowCount,
433
- getGeometry: rowReorderGeometry,
434
- // Autoscroll must move whatever scrolls this editor — the root in self mode, the
435
- // host's scroller in host mode — or off-window rows never mount.
436
- getScrollContainer: getScrollHost,
437
- setLine: (line) => (dragLine = line),
438
- onDragRecognized: () => (suppressRowGripClick = true),
439
- commit: (from, to) => void mutations.reorderRowTo(from, to),
440
- lifetimeSignal: editorLifetime
441
- });
442
- }
443
-
444
- // ── Column drag reorder ─────────────────────────────────────────────────
445
-
446
- let columnDragLine = $state<ColumnReorderLine | null>(null);
447
- // Plain let, reset on every grip pointerdown — see suppressRowGripClick.
448
- let suppressColumnGripClick = false;
449
-
450
- // Columns aren't windowed, so any mounted row carries the shared track geometry.
451
- // Client coords match the position:fixed insertion line.
452
- function columnReorderGeometry() {
453
- if (!tableEl || rowCount === 0 || columnCount === 0) return null;
454
- const firstRowEl = mountedRowEls(tableEl)[0];
455
- if (!firstRowEl) return null;
456
- const cells = rowCellEls(firstRowEl);
457
- if (cells.length === 0) return null;
458
- const colEdges: number[] = [];
459
- for (const cell of cells) colEdges.push(cell.getBoundingClientRect().left);
460
- const lastCell = cells[cells.length - 1];
461
- colEdges.push(lastCell.getBoundingClientRect().right);
462
- const tableRect = tableEl.getBoundingClientRect();
463
- return { colEdges, top: tableRect.top, height: tableRect.height };
464
- }
465
-
466
- function onColumnGripPointerDown(colIdx: number, e: PointerEvent): void {
467
- // Before any bail: a prior drag released off the grip left no click to consume it.
468
- suppressColumnGripClick = false;
469
- startColumnReorderDrag(e, {
470
- fromColIdx: colIdx,
471
- getColCount: () => columnCount,
472
- getGeometry: columnReorderGeometry,
473
- // `.table-block` is itself the overflow-x container.
474
- getScrollContainer: () => tableEl ?? null,
475
- setLine: (line) => (columnDragLine = line),
476
- onDragRecognized: () => (suppressColumnGripClick = true),
477
- commit: (from, to) => void mutations.reorderColumnTo(from, to),
478
- lifetimeSignal: editorLifetime
479
- });
480
- }
481
-
482
442
  // ── focusout: reset internal sticky when focus leaves the table ────────
483
443
 
484
444
  $effect(() => {
@@ -605,6 +565,24 @@
605
565
  return { start: win.start, end: win.end };
606
566
  }
607
567
 
568
+ // A press beside the table runs the same cell drag a press IN a cell runs, anchored at the
569
+ // nearest cell — so a sweep that starts in the margin grows the same rectangle it would from
570
+ // that cell, and leaves the table as a cross-block range the same way.
571
+ export function startDragAtPoint(clientX: number, clientY: number, e: PointerEvent): boolean {
572
+ if (!tableEl || readOnly) return false;
573
+ const host = tableEl.parentElement;
574
+ const target = host ? tableCaretAtPoint(host, clientX, clientY) : null;
575
+ const editorRoot = getEditorRoot();
576
+ if (!target || !editorRoot) return false;
577
+ const [rowIdx, colIdx] = target.path;
578
+ installCellDragListener(
579
+ { editorRoot, selection, lifetimeSignal: editorLifetime },
580
+ { tableEl, tablePath: myPath.slice(), rowIdx, colIdx, columnCount },
581
+ e
582
+ );
583
+ return true;
584
+ }
585
+
608
586
  function cellElementAt(rowIdx: number, colIdx: number): HTMLElement | null {
609
587
  if (!tableEl) return null;
610
588
  if (rowIdx < 0 || rowIdx >= rowCount || colIdx < 0 || colIdx >= columnCount) return null;
@@ -654,20 +632,9 @@
654
632
  oncontextmenu={openCellMenu}
655
633
  onkeydown={onTableKeyDown}
656
634
  >
657
- <!-- The corner occupies the zero-width gutter so the column grips align to their
658
- columns. The block boundaries below stay whitespace-adjacent: a stray text node
659
- joins the raw-offset walk and shifts a parked caret (cursor/widget-offset.ts). -->
660
- {#if showGrips}<span class="table-grip-corner" aria-hidden="true"
661
- ></span>{#each columnIndices as colIdx (colIdx)}
662
- <TableGrip
663
- axis="column"
664
- onActivate={(e) => {
665
- if (suppressColumnGripClick) return;
666
- openMenu('column', colIdx, e);
667
- }}
668
- onpointerdown={(e) => onColumnGripPointerDown(colIdx, e)}
669
- />
670
- {/each}{/if}{#if win.active}
635
+ <!-- The block boundaries stay whitespace-adjacent: a stray text node joins the raw-offset
636
+ walk and shifts a parked caret (cursor/widget-offset.ts). -->
637
+ {#if win.active}
671
638
  <div class="vr-spacer" style="height: {win.topSpacerPx}px"></div>
672
639
  {/if}{#each (node.children ?? []).slice(bounds.start, bounds.end) as rowNode, localIndex (rowsState.innerBlockIds[bounds.start + localIndex])}
673
640
  <!-- ABSOLUTE-INDEX INVARIANT: index/myPath/key carry the absolute row index
@@ -682,12 +649,6 @@
682
649
  alignments={meta.alignments ?? []}
683
650
  myPath={[...myPath, rowIdx]}
684
651
  slots={rowsState.refSlots}
685
- onOpenRowMenu={(r, e) => {
686
- if (suppressRowGripClick) return;
687
- openMenu('row', r, e);
688
- }}
689
- {onRowGripPointerDown}
690
- {showGrips}
691
652
  />
692
653
  {/each}{#if win.active}
693
654
  <div class="vr-spacer" style="height: {win.bottomSpacerPx}px"></div>
@@ -699,21 +660,44 @@
699
660
  onaction={runAction}
700
661
  onclipboard={runClipboard}
701
662
  onalign={runAlign}
663
+ anchor={menu.anchor}
702
664
  onclose={() => (menu = null)}
703
665
  onescape={closeMenuRestoringFocus}
704
666
  />
705
- {/if}{#if dragLine}
706
- <div
707
- class="table-reorder-line"
708
- style="left:{dragLine.left}px;top:{dragLine.top}px;width:{dragLine.width}px"
709
- ></div>
710
- {/if}{#if columnDragLine}
711
- <div
712
- class="table-reorder-line-vertical"
713
- style="left:{columnDragLine.left}px;top:{columnDragLine.top}px;height:{columnDragLine.height}px"
714
- ></div>
715
667
  {/if}
716
668
  </div>
669
+ {#if addGeometry}<div
670
+ class="table-add-zone table-add-zone-column"
671
+ class:table-add-pinned={addAffordance.column}
672
+ style="left:{addGeometry.left +
673
+ addGeometry.width}px;top:{addGeometry.top}px;height:{addGeometry.height}px"
674
+ >
675
+ <button
676
+ type="button"
677
+ class="table-add table-add-column"
678
+ aria-label={ADD_COLUMN_RIGHT}
679
+ title={ADD_COLUMN_RIGHT}
680
+ onmousedown={(e) => e.preventDefault()}
681
+ onclick={() => void ctx.insertColumnRight(columnCount - 1)}
682
+ ><MenuIcon name="plus" size={12} /></button
683
+ >
684
+ </div>
685
+ <div
686
+ class="table-add-zone table-add-zone-row"
687
+ class:table-add-pinned={addAffordance.row}
688
+ style="left:{addGeometry.left}px;top:{addGeometry.top +
689
+ addGeometry.height}px;width:{addGeometry.width}px"
690
+ >
691
+ <button
692
+ type="button"
693
+ class="table-add table-add-row"
694
+ aria-label={ADD_ROW_BELOW}
695
+ title={ADD_ROW_BELOW}
696
+ onmousedown={(e) => e.preventDefault()}
697
+ onclick={() => void ctx.insertRowBelow(rowCount - 1)}
698
+ ><MenuIcon name="plus" size={12} /></button
699
+ >
700
+ </div>{/if}
717
701
 
718
702
  <style>
719
703
  .table-block {
@@ -722,31 +706,66 @@
722
706
  max-width: 100%;
723
707
  overflow-x: auto;
724
708
  scrollbar-width: thin;
725
- scrollbar-color: var(--color-ui-muted, #a4a4a4) transparent;
709
+ scrollbar-color: var(--color-border, #3e3e3b) transparent;
710
+ /* The two sides the cells do not draw — see `.table-cell`. */
711
+ border-top: 1px solid var(--color-border, #3e3e3b);
712
+ border-left: 1px solid var(--color-border, #3e3e3b);
713
+ }
714
+ /* The edge strips sit in the host's box beside and below the grid, out of the grid's own
715
+ scroller, and start exactly at the table's edge so they never cover a cell. Pure-CSS
716
+ reveal on hover; `.table-add-pinned` is the caret's claim. The mousedown is swallowed so
717
+ the cell keeps the caret that pinned them. */
718
+ .table-add-zone {
719
+ position: absolute;
720
+ z-index: 3;
721
+ display: flex;
722
+ }
723
+ .table-add-zone-column {
724
+ width: 28px;
725
+ padding-left: 4px;
726
+ align-items: stretch;
727
+ }
728
+ .table-add-zone-row {
729
+ height: 22px;
730
+ padding-top: 4px;
731
+ flex-direction: column;
732
+ align-items: stretch;
733
+ }
734
+ .table-add {
735
+ display: flex;
736
+ align-items: center;
737
+ justify-content: center;
738
+ padding: 0;
739
+ border: 1px solid var(--color-border, #3e3e3b);
740
+ border-radius: 3px;
741
+ background: var(--color-bg-secondary, rgba(128, 128, 128, 0.12));
742
+ color: var(--color-ui-muted, #8f8f89);
743
+ cursor: pointer;
744
+ opacity: 0;
745
+ transition: opacity 120ms ease-out;
726
746
  }
727
- /* Spacers are direct grid children; span all columns to reserve a full row band. */
728
- .vr-spacer {
729
- grid-column: 1 / -1;
747
+ .table-add-pinned .table-add {
748
+ opacity: 0.7;
730
749
  }
731
- /* Zero-height so the corner + column-grip band (grid row 1) adds no visible row. */
732
- .table-grip-corner {
733
- height: 0;
750
+ .table-add-zone:hover .table-add,
751
+ .table-add:focus-visible {
752
+ opacity: 1;
753
+ color: var(--color-text-primary, #e8e8e5);
734
754
  }
735
- /* Viewport-fixed, matching the client coords the drag measures in; pointer-events:none
736
- so the overlay never intercepts the drag's own pointer stream. */
737
- .table-reorder-line,
738
- .table-reorder-line-vertical {
739
- position: fixed;
740
- background: var(--md-reorder-indicator);
741
- border-radius: 2px;
742
- pointer-events: none;
743
- z-index: 20;
755
+ .table-add-column {
756
+ width: 18px;
744
757
  }
745
- .table-reorder-line {
746
- height: 2px;
758
+ .table-add-row {
759
+ height: 16px;
747
760
  }
748
- .table-reorder-line-vertical {
749
- width: 2px;
761
+ @media (prefers-reduced-motion: reduce) {
762
+ .table-add {
763
+ transition: none;
764
+ }
765
+ }
766
+ /* Spacers are direct grid children; span all columns to reserve a full row band. */
767
+ .vr-spacer {
768
+ grid-column: 1 / -1;
750
769
  }
751
770
  /* Fallback for Chromium versions that don't honor `scrollbar-width`. */
752
771
  .table-block::-webkit-scrollbar {
@@ -756,7 +775,7 @@
756
775
  background: transparent;
757
776
  }
758
777
  .table-block::-webkit-scrollbar-thumb {
759
- background: var(--color-ui-muted, #a4a4a4);
778
+ background: var(--color-border, #3e3e3b);
760
779
  border-radius: 3px;
761
780
  }
762
781
  .table-block::-webkit-scrollbar-thumb:hover {
@@ -25,6 +25,7 @@ declare const TableBlock: import("svelte").Component<$$ComponentProps, {
25
25
  start: number;
26
26
  end: number;
27
27
  };
28
+ startDragAtPoint: (clientX: number, clientY: number, e: PointerEvent) => boolean;
28
29
  }, "">;
29
30
  type TableBlock = ReturnType<typeof TableBlock>;
30
31
  export default TableBlock;