@quillmark/svelte 0.5.0 → 0.6.0

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 (56) hide show
  1. package/README.md +8 -2
  2. package/THEMING.md +22 -27
  3. package/dist/core/address.d.ts +17 -3
  4. package/dist/core/address.js +20 -3
  5. package/dist/core/codec/blocks.d.ts +37 -0
  6. package/dist/core/codec/blocks.js +163 -0
  7. package/dist/core/codec/field.js +3 -5
  8. package/dist/core/codec/index.d.ts +1 -1
  9. package/dist/core/codec/index.js +3 -2
  10. package/dist/core/codec/inputrules.js +24 -144
  11. package/dist/core/codec/prose.css +14 -25
  12. package/dist/core/codec/schema.d.ts +8 -0
  13. package/dist/core/codec/schema.js +33 -1
  14. package/dist/core/codec/slash.d.ts +18 -16
  15. package/dist/core/codec/slash.js +0 -0
  16. package/dist/core/codec/table-view.js +127 -157
  17. package/dist/core/codec/table.d.ts +0 -10
  18. package/dist/core/codec/table.js +0 -15
  19. package/dist/core/theme.css +350 -347
  20. package/dist/preset/recipes.css +10 -21
  21. package/dist/preset/scale.css +6 -7
  22. package/dist/preview/Preview.svelte +4 -2
  23. package/dist/preview/geometry.d.ts +10 -9
  24. package/dist/preview/geometry.js +11 -13
  25. package/dist/visual/ArrayField.svelte +242 -63
  26. package/dist/visual/ArrayField.svelte.d.ts +5 -1
  27. package/dist/visual/Card.svelte +21 -2
  28. package/dist/visual/DiagnosticList.svelte +6 -11
  29. package/dist/visual/EnumField.svelte +33 -1
  30. package/dist/visual/Field.svelte +29 -5
  31. package/dist/visual/FieldLabel.svelte +2 -2
  32. package/dist/visual/FormatPopover.svelte +13 -1
  33. package/dist/visual/ObjectField.svelte +157 -35
  34. package/dist/visual/ObjectField.svelte.d.ts +13 -4
  35. package/dist/visual/ProseArrayElement.svelte +22 -2
  36. package/dist/visual/ProseArrayElement.svelte.d.ts +1 -0
  37. package/dist/visual/TipsCard.svelte +33 -13
  38. package/dist/visual/VariantField.svelte +98 -0
  39. package/dist/visual/VariantField.svelte.d.ts +21 -0
  40. package/dist/visual/VisualEditorInner.svelte +18 -15
  41. package/dist/visual/controls.css +282 -283
  42. package/dist/visual/diagnostics.d.ts +15 -24
  43. package/dist/visual/diagnostics.js +14 -30
  44. package/dist/visual/domid.d.ts +13 -0
  45. package/dist/visual/domid.js +16 -0
  46. package/dist/visual/leaves.d.ts +9 -7
  47. package/dist/visual/links.d.ts +5 -2
  48. package/dist/visual/links.js +9 -3
  49. package/dist/visual/props.d.ts +1 -1
  50. package/dist/visual/strings.d.ts +21 -6
  51. package/dist/visual/strings.js +3 -0
  52. package/dist/visual/structure.d.ts +32 -7
  53. package/dist/visual/structure.js +49 -3
  54. package/package.json +2 -2
  55. package/dist/core/codec/clipboard.d.ts +0 -59
  56. package/dist/core/codec/clipboard.js +0 -202
@@ -17,17 +17,16 @@
17
17
  // The chrome is A band and A selection (CODEC §"The table island"), and it raises
18
18
  // nothing. Every control is absolutely positioned out of the grid, so the band is in no
19
19
  // row and no column of it; `codec/prose.css` draws the band and the selection wash both.
20
- import { baseKeymap, toggleMark } from 'prosemirror-commands';
20
+ import { baseKeymap, chainCommands, toggleMark } from 'prosemirror-commands';
21
21
  import { redo, undo } from 'prosemirror-history';
22
22
  import { keymap } from 'prosemirror-keymap';
23
23
  import { EditorState, NodeSelection, Selection, TextSelection } from 'prosemirror-state';
24
24
  import { EditorView } from 'prosemirror-view';
25
- import { tableFromClipboard, writeRectangle } from './clipboard.js';
26
25
  import { decode } from './decode.js';
27
26
  import { inputRulesPlugin } from './inputrules.js';
28
27
  import { tablePropsOfNode } from './islands.js';
29
28
  import { inlineSchema } from './schema.js';
30
- import { allRows, cellAt, cellContent, cellEqual, cellFromDoc, clearCells, columnCount, deleteColumn, deleteRow, insertColumn, insertRow, moveColumn, moveRow, normalizeTable, pasteCells, rowCells, rowCount, rowEmpty, shapeEqual, withCell } from './table.js';
29
+ import { cellAt, cellContent, cellEqual, cellFromDoc, clearCells, columnCount, deleteColumn, deleteRow, insertColumn, insertRow, moveColumn, moveRow, normalizeTable, rowCells, rowCount, rowEmpty, shapeEqual, withCell } from './table.js';
31
30
  /**
32
31
  * The package's English for the island chrome. It lives here, beside the surface
33
32
  * that draws it, rather than in the visual tier's table: the codec mounts this
@@ -83,8 +82,9 @@ function el(tag, className) {
83
82
  }
84
83
  /** A band button. It swallows its own `mousedown` (prosemirror-menu's trick, and
85
84
  * the format popover's): without it the browser focuses the button, blurring the
86
- * cell whose caret the op is about to be measured against. A grip wants that focus
87
- * and takes it explicitly, which is why this does not hand it out. */
85
+ * cell whose caret the op is about to be measured against. No control on the band ever
86
+ * holds the focus: a cell is the island's one host, and what a press acts on is where
87
+ * it lands the caret. */
88
88
  function chromeButton(className, label, run) {
89
89
  const btn = el('button', className);
90
90
  btn.type = 'button';
@@ -161,13 +161,6 @@ class TableIslandView {
161
161
  this.dom.setAttribute('data-island', node.attrs.islandType);
162
162
  this.dom.setAttribute('data-island-id', node.attrs.id);
163
163
  this.dom.addEventListener('mousedown', this.onPointerDown);
164
- // On the document rather than on the island: a `copy` is delivered to the DOM
165
- // selection's own node, and a held rectangle has cleared that selection (a sweep
166
- // removes the ranges itself, a grip press moves the focus to a button), so the
167
- // event never crosses this subtree at all. What makes one this island's is
168
- // {@link TableIslandView.onCopy}'s guard.
169
- this.dom.ownerDocument.addEventListener('copy', this.onCopy);
170
- this.dom.ownerDocument.addEventListener('cut', this.onCopy);
171
164
  this.render();
172
165
  }
173
166
  // ── PM's NodeView contract ────────────────────────────────────────────────
@@ -220,8 +213,6 @@ class TableIslandView {
220
213
  }
221
214
  destroy() {
222
215
  this.dom.removeEventListener('mousedown', this.onPointerDown);
223
- this.dom.ownerDocument.removeEventListener('copy', this.onCopy);
224
- this.dom.ownerDocument.removeEventListener('cut', this.onCopy);
225
216
  this.endDrag();
226
217
  this.endSweep();
227
218
  this.teardownCells();
@@ -317,10 +308,10 @@ class TableIslandView {
317
308
  this.outer.focus();
318
309
  this.outer.dispatch(this.outer.state.tr.setSelection(selection));
319
310
  }
320
- /** The island as the selection: Escape out of a cell or a line, which is the whole of
321
- * what selects it. Backspace there deletes the table, as it does over every other
322
- * island; the cell selection covering every rank reaches the same delete without
323
- * leaving the grid (CODEC §"The table island"). */
311
+ /** The island as the selection: the Escape that climbs out of a cell or a held
312
+ * rectangle, which is the whole of what selects it. Backspace there deletes the
313
+ * table, as it does over every other island; the cell selection covering every rank
314
+ * reaches the same delete without leaving the grid (CODEC §"The table island"). */
324
315
  selectIsland() {
325
316
  const pos = this.getPos();
326
317
  if (pos == null)
@@ -396,10 +387,6 @@ class TableIslandView {
396
387
  return best;
397
388
  }
398
389
  // ── The selection ─────────────────────────────────────────────────────────
399
- /** Take the line's grip: the focus a selection's keys land on. */
400
- gripFor(line) {
401
- return this.grips.get(lineKey(line));
402
- }
403
390
  /** The rectangle a line covers, which is what selecting one means: a grip draws no
404
391
  * second kind of selection, it draws this one. */
405
392
  lineCells(line) {
@@ -416,11 +403,43 @@ class TableIslandView {
416
403
  ? { floor: 0, limit: rowCount(props) - 1 }
417
404
  : { floor: 0, limit: columnCount(props) - 1 };
418
405
  }
419
- /** Select a line. The grip takes the focus, because the selection's keys bind
420
- * there and need somewhere to land. */
406
+ /** Select a line, and land the caret on it. The line verbs bind in the cell
407
+ * (CODEC §"The table island"), so the rectangle has to run through the caret for the
408
+ * next key to read a line off it. The focus moves first and the paint follows: a cell
409
+ * taking the focus retires whatever was held. */
421
410
  selectLine(line) {
411
+ this.carryCaret(line);
422
412
  this.select(this.lineCells(line));
423
- this.gripFor(line)?.focus();
413
+ }
414
+ /** Put the caret on the line, keeping its other coordinate, and leave it exactly
415
+ * where it is on a line it is already in: a walk down the rows stays in its column,
416
+ * and a grip press on the caret's own row does not shunt it to a cell's end. */
417
+ carryCaret(line) {
418
+ const held = this.cells.find((m) => m.view.hasFocus());
419
+ if (line.axis === 'row') {
420
+ if (held?.r !== line.index)
421
+ this.focusCell(line.index, held?.c ?? 0);
422
+ }
423
+ else if (held?.c !== line.index)
424
+ this.focusCell(held?.r ?? 0, line.index);
425
+ }
426
+ /** Which line the held rectangle is, on one axis: a row spanning every column, a
427
+ * column spanning every row, and nothing at all for a block that spans neither. The
428
+ * extent rule reaching the line verbs ({@link TableIslandView.deleteSelection}) — a
429
+ * rectangle answers by what it covers, never by the gesture that drew it, so a row
430
+ * swept cell by cell moves exactly as the row a grip named does. */
431
+ lineOn(axis) {
432
+ const held = this.selected;
433
+ if (!held)
434
+ return undefined;
435
+ const props = this.props();
436
+ if (axis === 'row')
437
+ return held.r0 === held.r1 && held.c0 === 0 && held.c1 === columnCount(props) - 1
438
+ ? held.r0
439
+ : undefined;
440
+ return held.c0 === held.c1 && held.r0 === 0 && held.r1 === rowCount(props) - 1
441
+ ? held.c0
442
+ : undefined;
424
443
  }
425
444
  select(cells) {
426
445
  // One subject at a time: a cell selection retires an island one, or the surface
@@ -457,94 +476,6 @@ class TableIslandView {
457
476
  for (const cell of this.cells)
458
477
  cell.box.toggleAttribute('data-selected', !!held && cell.r >= held.r0 && cell.r <= held.r1 && cell.c >= held.c0 && cell.c <= held.c1);
459
478
  }
460
- /**
461
- * A selected line's keys, bound to the line its own grip names rather than read off
462
- * the selection: focus and selection are separate, and a Tab between grips would
463
- * otherwise aim the next key at whichever line was last pressed.
464
- *
465
- * Undo is forwarded for the reason a cell forwards it: one undo stack per leaf, and
466
- * a `button` has no history of its own.
467
- */
468
- lineKeys(line) {
469
- return (event) => {
470
- const key = event.key;
471
- const mod = event.ctrlKey || event.metaKey;
472
- if (mod && (key === 'z' || key === 'Z' || key === 'y')) {
473
- event.preventDefault();
474
- const redoing = key === 'y' || (key === 'Z' && event.shiftKey);
475
- (redoing ? redo : undo)(this.outer.state, this.outer.dispatch);
476
- return;
477
- }
478
- const along = line.axis === 'row' ? ['ArrowUp', 'ArrowDown'] : ['ArrowLeft', 'ArrowRight'];
479
- const acts = along.includes(key) || ['Backspace', 'Delete', 'Enter', 'Escape'].includes(key);
480
- if (!acts)
481
- return;
482
- event.preventDefault();
483
- event.stopPropagation();
484
- if (key === 'Escape')
485
- return this.selectIsland();
486
- if (key === 'Enter') {
487
- this.clearSelection();
488
- return line.axis === 'row' ? this.focusCell(line.index, 0) : this.focusCell(0, line.index);
489
- }
490
- if (key === 'Backspace' || key === 'Delete')
491
- return this.deleteSelection();
492
- const by = key === 'ArrowUp' || key === 'ArrowLeft' ? -1 : 1;
493
- if (event.altKey)
494
- return this.moveLine(line, by);
495
- const { floor, limit } = this.bounds(line.axis, this.props());
496
- this.selectLine({
497
- axis: line.axis,
498
- index: Math.max(floor, Math.min(line.index + by, limit))
499
- });
500
- };
501
- }
502
- // ── The clipboard ─────────────────────────────────────────────────────────
503
- /** The held rectangle's cells, read off the stored props: the unit that crosses the
504
- * clipboard (`clipboard.ts`), so the unit the verbs here read. */
505
- heldBlock(held) {
506
- const props = this.props();
507
- return Array.from({ length: held.r1 - held.r0 + 1 }, (_, i) => Array.from({ length: held.c1 - held.c0 + 1 }, (_, j) => cellAt(props, held.r0 + i, held.c0 + j)));
508
- }
509
- /**
510
- * Copy or cut the held rectangle (`clipboard.ts` writes both wire formats). A cut is
511
- * a copy plus the delete Backspace already means over that rectangle, so what it
512
- * takes out is read by the same extent rule: a covered rank goes, a sub-rectangle
513
- * clears.
514
- *
515
- * The guard is the whole of what makes a document-level listener this island's. With
516
- * no rectangle the event belongs to whatever holds the caret. With the focus outside,
517
- * it is another surface's copy over a rectangle this island still happens to hold.
518
- */
519
- onCopy = (event) => {
520
- const held = this.selected;
521
- const data = event.clipboardData;
522
- const owner = this.dom.ownerDocument;
523
- if (!held || !data || !this.dom.contains(owner.activeElement))
524
- return;
525
- writeRectangle(data, this.heldBlock(held), this.props().aligns.slice(held.c0, held.c1 + 1), owner);
526
- event.preventDefault();
527
- if (event.type === 'cut')
528
- this.deleteSelection();
529
- };
530
- /**
531
- * A table pasted into a cell is written in at that cell, growing the table to hold
532
- * it (`table.ts` §{@link pasteCells}); anything else is the cell's own paste and
533
- * this declines. It lands at the caret's cell, which over a swept block is the cell
534
- * the press started in — the block's anchor rather than its first corner, a sweep
535
- * running up or left putting those at opposite ends.
536
- *
537
- * Without this arm the cell's inline schema reads the same clipboard as text and
538
- * flattens the table into one cell, which is the destructive case one level down.
539
- */
540
- pasteIntoCell(r, c, event) {
541
- const props = tableFromClipboard(event.clipboardData);
542
- if (!props)
543
- return false;
544
- this.clearSelection();
545
- this.write(pasteCells(this.props(), r, c, allRows(props)));
546
- return true;
547
- }
548
479
  /**
549
480
  * What Backspace means over the selection, decided by its extent rather than by the
550
481
  * gesture that drew it (CODEC §"The table island").
@@ -826,8 +757,9 @@ class TableIslandView {
826
757
  this.reseat(seat);
827
758
  }
828
759
  /**
829
- * Where the focus sits, in terms a rebuild can restore it by: a cell's position or a
830
- * grip's line, neither of which is an element that survives one.
760
+ * Where the focus sits, in terms a rebuild can restore it by: the caret's cell by
761
+ * position, that being the one seat the island has and not an element that survives
762
+ * one.
831
763
  *
832
764
  * A rebuild is what a changed rectangle costs, and the op that changed it usually
833
765
  * says where the caret lands ({@link TableIslandView.write}'s `focus`). An undo says
@@ -837,28 +769,15 @@ class TableIslandView {
837
769
  */
838
770
  focusedSeat() {
839
771
  const held = this.cells.find((m) => m.view.hasFocus());
840
- if (held)
841
- return { cell: { r: held.r, c: held.c } };
842
- const active = this.dom.ownerDocument.activeElement;
843
- for (const [key, grip] of this.grips) {
844
- if (grip !== active)
845
- continue;
846
- const [axis, index] = key.split(':');
847
- return { line: { axis: axis, index: Number(index) } };
848
- }
849
- return undefined;
772
+ return held && { r: held.r, c: held.c };
850
773
  }
851
- /** Put the focus back where {@link TableIslandView.focusedSeat} found it, clamped into
852
- * the rectangle that is there now: the seat's line may be the one the rebuild
853
- * removed. A caller that has a landing of its own overrides this by running after. */
774
+ /** Put the caret back where {@link TableIslandView.focusedSeat} found it, clamped by
775
+ * `focusCell` into the rectangle that is there now: the seat may be the cell the
776
+ * rebuild removed. A caller that has a landing of its own overrides this by running
777
+ * after. */
854
778
  reseat(seat) {
855
- if (seat?.cell)
856
- return this.focusCell(seat.cell.r, seat.cell.c);
857
- if (!seat?.line)
858
- return;
859
- const { floor, limit } = this.bounds(seat.line.axis, this.props());
860
- const index = Math.max(floor, Math.min(seat.line.index, limit));
861
- this.gripFor({ axis: seat.line.axis, index })?.focus();
779
+ if (seat)
780
+ this.focusCell(seat.r, seat.c);
862
781
  }
863
782
  /** One table row: its cells, each carrying whatever chrome hangs off it. */
864
783
  row(props, r, s) {
@@ -894,12 +813,12 @@ class TableIslandView {
894
813
  * press that travels drags it. Both are "this line", asked once with the pointer,
895
814
  * and the dead zone is what tells them apart.
896
815
  *
897
- * Out of the tab order, at no cost in reach: a grip follows the cell it hangs in, so
898
- * a forward Tab is the cell traversal's before the browser gets there and a backward
899
- * one leaves the grid off cell (0,0) without passing a grip
900
- * (§{@link TableIslandView.step}). A line's chrome answers to the pointer. Focus is
901
- * still taken here, by the routes this view owns: a selection, a drag, a rebuild's
902
- * reseat. */
816
+ * Pointer chrome and nothing else: no key binds here and no route focuses it, the
817
+ * line verbs binding in the cell the selection runs through
818
+ * (§{@link TableIslandView.cellKeys}). Out of the tab order at no cost in reach,
819
+ * since a grip follows the cell it hangs in: a forward Tab is the cell traversal's
820
+ * before the browser gets there, and a backward one leaves the grid off cell (0,0)
821
+ * without passing a grip (§{@link TableIslandView.step}). */
903
822
  grip(line, label) {
904
823
  const btn = chromeButton('qm-table-grip', label, () => {
905
824
  if (this.suppressClick)
@@ -914,7 +833,6 @@ class TableIslandView {
914
833
  bar.appendChild(svg(GRIP[line.axis]));
915
834
  btn.appendChild(bar);
916
835
  btn.addEventListener('pointerdown', (e) => this.onGripDown(line, btn, e));
917
- btn.addEventListener('keydown', this.lineKeys(line));
918
836
  this.grips.set(lineKey(line), btn);
919
837
  return btn;
920
838
  }
@@ -924,11 +842,12 @@ class TableIslandView {
924
842
  * the pointer out past the last line is the claim — so its name carries the verb for
925
843
  * everything that does not read position.
926
844
  *
927
- * In the tab order, where a grip is not: the column bar is the keyboard's only route
928
- * to a column at all, and the row bar is that control on the other axis, where Enter
929
- * at the last row and Tab past the last cell reach the verb from inside a cell. They
930
- * sit after the grid, so the Tab that declines off the last cell lands on them, and
931
- * the focus rung draws the one it lands on (`codec/prose.css`). */
845
+ * In the tab order, where a grip is not: growth is what the line verbs do not carry,
846
+ * so the column bar is the keyboard's only route to a new column, and the row bar is
847
+ * that control on the other axis, where Enter at the last row and Tab past the last
848
+ * cell reach the verb from inside a cell. They sit after the grid, so the Tab that
849
+ * declines off the last cell lands on them, and the focus rung draws the one it lands
850
+ * on (`codec/prose.css`). */
932
851
  addBar(axis, label) {
933
852
  const btn = chromeButton('qm-table-add', label, () => {
934
853
  const props = this.props();
@@ -955,10 +874,14 @@ class TableIslandView {
955
874
  view.updateState(next);
956
875
  if (!tr.docChanged)
957
876
  return;
877
+ // A cell edit is the caret's, so it retires the rectangle: a wash left up
878
+ // over text being typed says the next Backspace takes a rank, and it does
879
+ // not. The clear op is not this path — it writes through the leaf, and the
880
+ // reseed it comes back as changes no cell's own doc.
881
+ this.clearSelection();
958
882
  const now = this.props();
959
883
  this.write(withCell(now, r, c, cellFromDoc(next.doc, cellAt(now, r, c))));
960
884
  },
961
- handlePaste: (_view, event) => this.pasteIntoCell(r, c, event),
962
885
  handleDOMEvents: {
963
886
  focus: () => {
964
887
  this.clearSelection();
@@ -1026,10 +949,10 @@ class TableIslandView {
1026
949
  if (inlineSchema.marks.underline)
1027
950
  marks['Mod-u'] = toggleMark(inlineSchema.marks.underline);
1028
951
  // Up and down are the grid's own walk: nothing else moves the caret vertically,
1029
- // and `focusCell` clamps, so neither can grow the table. Left and right are
1030
- // deliberately absent: at a text edge they would call what Tab and Shift-Tab
1031
- // already call, and inherit the append-past-the-last-cell that makes Tab a
1032
- // growth affordance and would make a caret key one.
952
+ // and `focusCell` clamps, so neither can grow the table. Left and right do not
953
+ // traverse at all: at a text edge they would call what Tab and Shift-Tab already
954
+ // call, and inherit the append-past-the-last-cell that makes Tab a growth
955
+ // affordance and would make a caret key one.
1033
956
  const walk = (dir) => {
1034
957
  return (_state, _dispatch, view) => {
1035
958
  const { selection } = view?.state ?? {};
@@ -1050,6 +973,34 @@ class TableIslandView {
1050
973
  this.deleteSelection();
1051
974
  return true;
1052
975
  };
976
+ // The line verbs, over the held rectangle and nothing else: with none held every
977
+ // one of these declines and the key is the caret's, which is what lets them sit on
978
+ // keys a cell is already typing under. An arrow names the line on its own axis
979
+ // through the caret — the one already held steps to its neighbour and carries the
980
+ // caret, and any other rectangle turns into it, which is how a row becomes the
981
+ // column the caret is in. Alt moves the line instead, and over a rectangle that is
982
+ // not one it does nothing but keep the key: while a rectangle is held every arrow
983
+ // is its own, and an Alt+arrow handed back is the browser's Back on two platforms,
984
+ // mid-gesture.
985
+ const line = (axis, by, move) => {
986
+ return () => {
987
+ if (!this.selected)
988
+ return false;
989
+ const on = this.lineOn(axis);
990
+ if (move) {
991
+ if (on !== undefined)
992
+ this.moveLine({ axis, index: on }, by);
993
+ return true;
994
+ }
995
+ if (on === undefined) {
996
+ this.selectLine({ axis, index: axis === 'row' ? r : c });
997
+ return true;
998
+ }
999
+ const { floor, limit } = this.bounds(axis, this.props());
1000
+ this.selectLine({ axis, index: Math.max(floor, Math.min(on + by, limit)) });
1001
+ return true;
1002
+ };
1003
+ };
1053
1004
  return {
1054
1005
  ...marks,
1055
1006
  // One undo stack per leaf: a cell carries no history of its own, so Mod-z
@@ -1059,11 +1010,23 @@ class TableIslandView {
1059
1010
  'Shift-Mod-z': () => redo(this.outer.state, this.outer.dispatch),
1060
1011
  Backspace: erase,
1061
1012
  Delete: erase,
1062
- ArrowUp: walk('up'),
1063
- ArrowDown: walk('down'),
1013
+ ArrowUp: chainCommands(line('row', -1, false), walk('up')),
1014
+ ArrowDown: chainCommands(line('row', 1, false), walk('down')),
1015
+ ArrowLeft: line('column', -1, false),
1016
+ ArrowRight: line('column', 1, false),
1017
+ 'Alt-ArrowUp': line('row', -1, true),
1018
+ 'Alt-ArrowDown': line('row', 1, true),
1019
+ 'Alt-ArrowLeft': line('column', -1, true),
1020
+ 'Alt-ArrowRight': line('column', 1, true),
1064
1021
  Tab: () => this.step(r, c, 1),
1065
1022
  'Shift-Tab': () => this.step(r, c, -1),
1066
1023
  Enter: () => {
1024
+ // Over a rectangle it hands the caret back, which is the line verbs' own
1025
+ // exit: the caret never left the cell, so there is nowhere else to put it.
1026
+ if (this.selected) {
1027
+ this.clearSelection();
1028
+ return true;
1029
+ }
1067
1030
  const props = this.props();
1068
1031
  if (r === rowCount(props) - 1)
1069
1032
  this.write(insertRow(props, r), { r: r + 1, c });
@@ -1071,10 +1034,17 @@ class TableIslandView {
1071
1034
  this.focusCell(r + 1, c);
1072
1035
  return true;
1073
1036
  },
1074
- // The innermost Escape: out of the cell, onto the island. What the next one
1075
- // means is the shell's (VISUAL_EDITOR §"Settled and open").
1037
+ // Escape climbs a rung a press: the caret's own row, then the island. The row
1038
+ // is the entry the band has no other route to — every line verb reads a held
1039
+ // rectangle, and this is the gesture that draws one from the keyboard — and it
1040
+ // is the row rather than the column because the column is one arrow further
1041
+ // on. What the press past the island means is the shell's
1042
+ // (VISUAL_EDITOR §"Settled and open").
1076
1043
  Escape: () => {
1077
- this.selectIsland();
1044
+ if (this.selected)
1045
+ this.selectIsland();
1046
+ else
1047
+ this.selectLine({ axis: 'row', index: r });
1078
1048
  return true;
1079
1049
  }
1080
1050
  };
@@ -71,16 +71,6 @@ export declare function moveColumn(props: TableProps, c: number, by: number): Ta
71
71
  * Alignment is the column's rather than the cells', so it survives a clear that spans one.
72
72
  */
73
73
  export declare function clearCells(props: TableProps, r0: number, c0: number, r1: number, c1: number): TableProps;
74
- /**
75
- * A rectangle written in at `(r, c)`, the table growing to hold whatever runs past its
76
- * edges: what a paste into a cell means (`clipboard.ts`). The alternative is clipping
77
- * to the rectangle that is there, which silently drops the tail of what the author
78
- * copied; growth drops nothing and every added rank is one the ops already remove.
79
- *
80
- * `aligns` is the table's own throughout: a column keeps its alignment under a paste,
81
- * and a column the paste added arrives `none` like any other fresh one.
82
- */
83
- export declare function pasteCells(props: TableProps, r: number, c: number, block: TableCell[][]): TableProps;
84
74
  /** Set column `c`'s alignment: the one table capability the content round-trips
85
75
  * today and nothing in the editor could reach. */
86
76
  export declare function setAlign(props: TableProps, c: number, align: TableAlign): TableProps;
@@ -189,21 +189,6 @@ export function clearCells(props, r0, c0, r1, c1) {
189
189
  aligns: props.aligns
190
190
  });
191
191
  }
192
- /**
193
- * A rectangle written in at `(r, c)`, the table growing to hold whatever runs past its
194
- * edges: what a paste into a cell means (`clipboard.ts`). The alternative is clipping
195
- * to the rectangle that is there, which silently drops the tail of what the author
196
- * copied; growth drops nothing and every added rank is one the ops already remove.
197
- *
198
- * `aligns` is the table's own throughout: a column keeps its alignment under a paste,
199
- * and a column the paste added arrives `none` like any other fresh one.
200
- */
201
- export function pasteCells(props, r, c, block) {
202
- const rows = allRows(props);
203
- const height = Math.max(rows.length, r + block.length);
204
- const width = Math.max(columnCount(props), ...block.map((row) => c + row.length));
205
- return fromRows(Array.from({ length: height }, (_, i) => Array.from({ length: width }, (_, j) => block[i - r]?.[j - c] ?? rows[i]?.[j] ?? emptyCell())), props.aligns);
206
- }
207
192
  /** Set column `c`'s alignment: the one table capability the content round-trips
208
193
  * today and nothing in the editor could reach. */
209
194
  export function setAlign(props, c, align) {