@quillmark/svelte 0.20.1 → 0.22.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 (85) hide show
  1. package/NOTICE +6 -7
  2. package/README.md +1 -1
  3. package/THEMING.md +1 -1
  4. package/dist/core/address.d.ts +6 -1
  5. package/dist/core/address.js +12 -3
  6. package/dist/core/bloom.d.ts +9 -4
  7. package/dist/core/bloom.js +18 -49
  8. package/dist/core/codec/atoms.d.ts +1 -1
  9. package/dist/core/codec/atoms.js +26 -11
  10. package/dist/core/codec/blocks.d.ts +5 -5
  11. package/dist/core/codec/blocks.js +27 -8
  12. package/dist/core/codec/code.d.ts +4 -4
  13. package/dist/core/codec/code.js +52 -22
  14. package/dist/core/codec/decode.d.ts +5 -5
  15. package/dist/core/codec/decode.js +18 -12
  16. package/dist/core/codec/encode.js +6 -1
  17. package/dist/core/codec/field.d.ts +22 -15
  18. package/dist/core/codec/field.js +66 -26
  19. package/dist/core/codec/index.d.ts +1 -1
  20. package/dist/core/codec/index.js +2 -2
  21. package/dist/core/codec/inputrules.d.ts +4 -0
  22. package/dist/core/codec/inputrules.js +44 -6
  23. package/dist/core/codec/link-paste.d.ts +11 -0
  24. package/dist/core/codec/link-paste.js +32 -0
  25. package/dist/core/codec/lists.d.ts +5 -4
  26. package/dist/core/codec/lists.js +87 -31
  27. package/dist/core/codec/nesting.d.ts +20 -0
  28. package/dist/core/codec/nesting.js +62 -0
  29. package/dist/core/codec/prose.css +145 -63
  30. package/dist/core/codec/schema.d.ts +2 -2
  31. package/dist/core/codec/schema.js +71 -18
  32. package/dist/core/codec/slash.js +0 -0
  33. package/dist/core/codec/table-copy.d.ts +17 -0
  34. package/dist/core/codec/table-copy.js +84 -0
  35. package/dist/core/codec/table-view.d.ts +6 -7
  36. package/dist/core/codec/table-view.js +281 -201
  37. package/dist/core/codec/table.d.ts +2 -9
  38. package/dist/core/codec/table.js +10 -19
  39. package/dist/core/codec/tasks.d.ts +19 -0
  40. package/dist/core/codec/tasks.js +140 -0
  41. package/dist/core/errors.d.ts +2 -2
  42. package/dist/core/rungs.d.ts +6 -0
  43. package/dist/core/rungs.js +11 -0
  44. package/dist/core/theme.css +24 -12
  45. package/dist/visual/ArrayField.svelte +236 -78
  46. package/dist/visual/ArrayField.svelte.d.ts +0 -2
  47. package/dist/visual/BooleanField.svelte +12 -1
  48. package/dist/visual/Card.svelte +41 -23
  49. package/dist/visual/Card.svelte.d.ts +3 -2
  50. package/dist/visual/CardControls.svelte +15 -15
  51. package/dist/visual/DateField.svelte +93 -64
  52. package/dist/visual/EnumField.svelte +9 -4
  53. package/dist/visual/Field.svelte +20 -16
  54. package/dist/visual/Field.svelte.d.ts +4 -2
  55. package/dist/visual/FieldHint.svelte +22 -5
  56. package/dist/visual/FieldLabel.svelte +5 -33
  57. package/dist/visual/FieldLabel.svelte.d.ts +0 -2
  58. package/dist/visual/FormatPopover.svelte +78 -81
  59. package/dist/visual/MatrixField.svelte +28 -19
  60. package/dist/visual/NumberField.svelte +32 -3
  61. package/dist/visual/ObjectField.svelte +40 -24
  62. package/dist/visual/ObjectField.svelte.d.ts +6 -0
  63. package/dist/visual/ProseField.svelte +10 -13
  64. package/dist/visual/ProseField.svelte.d.ts +5 -2
  65. package/dist/visual/ProseValue.svelte +25 -19
  66. package/dist/visual/ProseValue.svelte.d.ts +4 -4
  67. package/dist/visual/TextField.svelte +3 -2
  68. package/dist/visual/TipsCard.svelte +7 -7
  69. package/dist/visual/VariantField.svelte +1 -1
  70. package/dist/visual/VariantField.svelte.d.ts +1 -1
  71. package/dist/visual/VisualEditorInner.svelte +121 -39
  72. package/dist/visual/controls.css +21 -2
  73. package/dist/visual/icons/Icon.svelte +3 -2
  74. package/dist/visual/icons/Icon.svelte.d.ts +3 -2
  75. package/dist/visual/icons/nodes.d.ts +25 -21
  76. package/dist/visual/icons/nodes.js +9 -7
  77. package/dist/visual/leaves.d.ts +12 -9
  78. package/dist/visual/leaves.js +19 -0
  79. package/dist/visual/motion.d.ts +25 -3
  80. package/dist/visual/motion.js +58 -9
  81. package/dist/visual/strings.d.ts +15 -9
  82. package/dist/visual/strings.js +19 -12
  83. package/dist/visual/structure.d.ts +21 -31
  84. package/dist/visual/structure.js +54 -51
  85. package/package.json +3 -3
@@ -31,8 +31,9 @@
31
31
  type Landing
32
32
  } from '../core/address.js';
33
33
  import { errorMessage, reportError } from '../core/errors.js';
34
- import { bloomInside } from '../core/bloom.js';
34
+ import { bloom } from '../core/bloom.js';
35
35
  import { createLifespan } from '../core/teardown.js';
36
+ import { spacePx } from '../core/rungs.js';
36
37
  import type {
37
38
  Addr,
38
39
  CardAddr,
@@ -80,11 +81,12 @@
80
81
  } from './diagnostics.js';
81
82
  import { fieldDomIds, groupPanelId } from './domid.js';
82
83
  import { createLeafRegistry, type ActiveProse, type FieldControl } from './leaves.js';
83
- import { reorder, reorderArm, reorderTrips } from './motion.js';
84
+ import { reorder, reorderArm, reorderTrips, scrollSettled, slot } from './motion.js';
84
85
  import { tipsChannel } from './tips.js';
85
86
  import { patchEditorExt } from './ext.js';
86
87
  import { raiseGuidance } from './guidance.js';
87
88
  import Card from './Card.svelte';
89
+ import Icon from './icons/Icon.svelte';
88
90
  import TipsCard from './TipsCard.svelte';
89
91
  import FormatPopover from './FormatPopover.svelte';
90
92
  // The add trigger and the kind menu draw shared recipes; a component carrying a
@@ -194,6 +196,7 @@
194
196
  type CardHandle = {
195
197
  revealLeaf(key: string): void;
196
198
  scrollIntoViewCard(block: ScrollLogicalPosition): void;
199
+ focusHeader(): void;
197
200
  };
198
201
  let mainCard = $state<CardHandle | undefined>(undefined);
199
202
  let cardRefs = $state<(CardHandle | undefined)[]>([]);
@@ -217,7 +220,7 @@
217
220
  const card = addr.card;
218
221
  return card != null ? { card, field: addr.field } : { field: addr.field };
219
222
  }
220
- function handleFocus(addr: Addr): void {
223
+ function handleFocus(addr: Addr, steps: readonly PathStep[] = []): void {
221
224
  const plain = normalize(addr);
222
225
  activeAddr = plain;
223
226
  activeCardId = cardIdOf(plain);
@@ -225,7 +228,7 @@
225
228
  // to at the offset it still names has to report again.
226
229
  lastCaretField = undefined;
227
230
  lastCaretPos = undefined;
228
- const field = pathFor(plain);
231
+ const field = pathFor(plain, steps);
229
232
  // Both halves or neither: a stale addr names no path and no live card.
230
233
  if (field != null && activeCardId != null)
231
234
  onActiveLeafChange?.({ field, cardId: activeCardId });
@@ -242,11 +245,11 @@
242
245
  return model.cards.map((c) => c.kind);
243
246
  }
244
247
  /**
245
- * A leaf's canonical `DocPath`. `undefined` for an address outside the live card
246
- * array (a stale addr: drop rather than mis-target).
248
+ * A leaf's canonical `DocPath`, `steps` down from the field. `undefined` for an
249
+ * address outside the live card array (a stale addr: drop rather than mis-target).
247
250
  */
248
- function pathFor(addr: Addr): DocPath | undefined {
249
- return fieldPathForAddr(addr, addr.card == null ? NO_KINDS : liveKinds());
251
+ function pathFor(addr: Addr, steps: readonly PathStep[] = []): DocPath | undefined {
252
+ return fieldPathForAddr(addr, addr.card == null ? NO_KINDS : liveKinds(), steps);
250
253
  }
251
254
  // The last place reported, so a caret that did not move is not news. A leaf fires
252
255
  // one caret signal per transaction, and a transaction need not have moved the
@@ -377,10 +380,10 @@
377
380
  * naming one id, on different terms — `center` against `nearest`.
378
381
  */
379
382
  let scrollTicket = 0;
380
- async function scrollCardIntoView(id: string, block: ScrollLogicalPosition): Promise<void> {
383
+ async function scrollCardIntoView(id: string, block: ScrollLogicalPosition): Promise<boolean> {
381
384
  const ticket = ++scrollTicket;
382
- if (!(await span.resumes(tick()))) return;
383
- if (ticket !== scrollTicket) return;
385
+ if (!(await span.resumes(tick()))) return false;
386
+ if (ticket !== scrollTicket) return false;
384
387
  // A reorder is running by this flush and holds the slot at its pre-move position
385
388
  // for the length of the trip (`motion.ts`), so asking now measures the box the
386
389
  // card is leaving — where a `nearest` would take the stack back to for the card
@@ -390,26 +393,33 @@
390
393
  const slot = slotEls[cardIndexOf(id)];
391
394
  const trips = slot ? reorderTrips(slot) : [];
392
395
  if (trips.length) {
393
- if (!(await span.resumes(Promise.allSettled(trips.map((trip) => trip.finished))))) return;
394
- if (ticket !== scrollTicket) return;
396
+ if (!(await span.resumes(Promise.allSettled(trips.map((trip) => trip.finished)))))
397
+ return false;
398
+ if (ticket !== scrollTicket) return false;
395
399
  }
396
400
  const i = cardIndexOf(id);
397
- if (i >= 0) cardRefs[i]?.scrollIntoViewCard(block);
401
+ if (i < 0) return false;
402
+ cardRefs[i]?.scrollIntoViewCard(block);
403
+ return true;
398
404
  }
399
405
  /** Returns the new card's session key, or `undefined` when the kind seeds nothing
400
- * or the insert threw: what `insertCard` hands a host that wants to track it. */
401
- function addCard(atIndex: number, kind: string): CardId | undefined {
406
+ * or the insert threw: what `insertCard` hands a host that wants to track it. The
407
+ * strip's own add `land`s focus in the new card; a host's leaves focus where it is. */
408
+ function addCard(atIndex: number, kind: string, land = false): CardId | undefined {
402
409
  try {
403
410
  const overlay = doc.seedOverlay(kind);
404
411
  const card = quill.seedCard(kind, overlay);
405
412
  if (!card) return undefined;
413
+ const from = document.activeElement;
406
414
  doc.insertCard(card, atIndex);
407
415
  const id = seq.next();
416
+ arm.arm();
408
417
  cardIds = [...cardIds.slice(0, atIndex), id, ...cardIds.slice(atIndex)];
409
418
  bump('structure', id, atIndex);
410
419
  // Centred: the new card is the subject, and the neighbours it landed between
411
420
  // are what say where it went.
412
- void scrollCardIntoView(id, 'center');
421
+ const scrolled = scrollCardIntoView(id, 'center');
422
+ if (land) void landInCard(id, scrolled, from);
413
423
  return id;
414
424
  } catch (e) {
415
425
  reportError(onError, {
@@ -421,8 +431,39 @@
421
431
  return undefined;
422
432
  }
423
433
  }
424
- // The reorder gesture's arming window (`motion.ts`): the reconcile that moves a slot
425
- // is the trip, and every other reconcile that happens to move one is not.
434
+ /**
435
+ * Focus the card the strip just added: its first field, else its body, else its title.
436
+ *
437
+ * After the centring scroll has run rather than beside it: a control's `focus()`
438
+ * takes the browser's own scroll, which cuts a smooth one short wherever it had
439
+ * reached. A focus that went elsewhere while the scroll ran was the user's, and stays.
440
+ */
441
+ async function landInCard(
442
+ id: CardId,
443
+ scrolled: Promise<boolean>,
444
+ from: Element | null
445
+ ): Promise<void> {
446
+ if (!(await scrolled) || !(await span.resumes(scrollSettled()))) return;
447
+ const now = document.activeElement;
448
+ if (now !== from && now !== document.body) return;
449
+ const at = cardIndexOf(id);
450
+ const shown = model.cards[at];
451
+ if (!shown) return;
452
+ const first = (shown.sections.find((s) => s.group == null) ?? shown.sections[0])?.fields[0];
453
+ const path = first || shown.hasBody ? pathFor({ card: at, field: first?.name }) : undefined;
454
+ const found = path && (await revealLeaf(path));
455
+ if (found) await land(found, undefined);
456
+ else cardRefs[cardIndexOf(id)]?.focusHeader();
457
+ }
458
+ // A kind picked off the menu lands focus in the card it added, so the menu's close
459
+ // hands none back to its trigger; an Escape out of the menu still does.
460
+ let picked = false;
461
+ function addPicked(atIndex: number, kind: string): void {
462
+ picked = addCard(atIndex, kind, true) != null;
463
+ }
464
+
465
+ // The list gesture's arming window (`motion.ts`): the reconcile that moves a slot is
466
+ // the trip, and every other reconcile that happens to move one is not.
426
467
  const arm = reorderArm();
427
468
  span.onEnd(arm.cancel);
428
469
 
@@ -445,7 +486,12 @@
445
486
  function removeCardById(id: string): void {
446
487
  const i = cardIndexOf(id);
447
488
  if (i < 0) return;
489
+ // Held visibly, which a pointer's press on delete is not: some engines focus a
490
+ // pressed button, and the hand-on is the keyboard's.
491
+ const focused = document.activeElement;
492
+ const held = !!focused && !!slotEls[i]?.contains(focused) && focused.matches(':focus-visible');
448
493
  doc.removeCard(i);
494
+ arm.arm();
449
495
  cardIds = cardIds.filter((_, k) => k !== i);
450
496
  if (activeCardId === id) {
451
497
  activeCardId = undefined;
@@ -460,6 +506,15 @@
460
506
  // No addr: the removed card has none left and every survivor's shifted. The
461
507
  // stack changed, not a leaf, and the id is the only handle the removal leaves.
462
508
  bump('structure', id);
509
+ if (held) void focusAfterRemove(i);
510
+ }
511
+ /** Hand the focus a removed card held to the title of the card before it, else the
512
+ * one that took its place, else the strip that adds one: a focused node the flush
513
+ * drops leaves the focus on the page's body, and the keyboard's place with it. */
514
+ async function focusAfterRemove(at: number): Promise<void> {
515
+ if (!(await span.resumes(tick()))) return;
516
+ if (cardIds.length) cardRefs[Math.max(at - 1, 0)]?.focusHeader();
517
+ else rootEl?.querySelector<HTMLElement>('.qm-add-btn')?.focus();
463
518
  }
464
519
  function retypeCardById(id: string, kind: string): void {
465
520
  const i = cardIndexOf(id);
@@ -481,7 +536,10 @@
481
536
  });
482
537
  }
483
538
  }
539
+ /** A title's blur can fire as the surface is torn down, against a document that may be
540
+ * freed. */
484
541
  function renameCardById(id: string, title: string): void {
542
+ if (!span.alive) return;
485
543
  const i = cardIndexOf(id);
486
544
  if (i < 0) return;
487
545
  patchEditorExt(doc, { card: i }, { title });
@@ -678,10 +736,10 @@
678
736
  // (where landing a caret changes nothing on screen) or one off-screen, where the
679
737
  // browser's focus-scroll moves the page and leaves the caret to be hunted for.
680
738
  const box = await land(found, at.granularity === 'segment' ? undefined : at.pos);
681
- if (span.alive) bloomInside(box);
739
+ if (span.alive) bloom(box);
682
740
  }
683
741
  /** The active prose leaf: the formatting popover's observation seam. The active
684
- * field's controller where it registered one, else the by-value leaf focused last
742
+ * field's own leaf where it registered one, else the by-value leaf focused last
685
743
  * under it (`leaves.ts`); `undefined` for a form control, which holds no marks to
686
744
  * toggle. */
687
745
  export function getActiveLeaf(): ActiveProse | undefined {
@@ -689,7 +747,7 @@
689
747
  const card = activeAddr.card != null ? activeCardId : undefined;
690
748
  const key = fieldKeyToString({ card, field: activeAddr.field });
691
749
  const controller = leaves.prose(key);
692
- if (controller) return { views: controller as FieldController & LeafViews, controller };
750
+ if (controller) return { views: controller as FieldController & LeafViews };
693
751
  const views = leaves.value(key);
694
752
  return views && { views };
695
753
  }
@@ -768,7 +826,7 @@
768
826
  const found = await revealLeaf(field);
769
827
  if (!found) return missed(`no mounted field at ${field}`, field);
770
828
  const box = await land(found, undefined);
771
- if (span.alive) bloomInside(box);
829
+ if (span.alive) bloom(box);
772
830
  }
773
831
  /** Seed a card of `kind` and insert it at `at` (default: the end). Returns the new
774
832
  * card's session key, or `undefined` when the quill seeds no card of that kind. */
@@ -892,7 +950,7 @@
892
950
  rather than being slid across. It is also the shape `animate:` asks for, being the
893
951
  keyed block's only child. -->
894
952
  {#each model.cards as c, i (c.id)}
895
- <div class="qm-card-slot" bind:this={slotEls[i]} animate:reorder={arm.armed}>
953
+ <div class="qm-card-slot" bind:this={slotEls[i]} animate:reorder={arm.armed} in:slot out:slot>
896
954
  <Card
897
955
  bind:this={cardRefs[i]}
898
956
  card={titled(c)}
@@ -925,9 +983,10 @@
925
983
  {#snippet addAffordance(atIndex: number)}
926
984
  {#if kinds.length}
927
985
  <!-- The strip past the last card is the append point, the one a reader looks for
928
- rather than finds by position, so it alone states itself in words at rest. The
929
- gaps between cards stay bare: the strip is the gap, and the pill each fills on
930
- hover draws what a label would state, the space the new card takes. -->
986
+ rather than finds by position, so it alone states itself in words at rest. A gap
987
+ between cards carries a plus where the words would be: the strip is the gap, the
988
+ pill it fills on hover is the space the new card takes, and the plus says what a
989
+ press there does. -->
931
990
  {@const marked = atIndex === model.cards.length}
932
991
  <div class={['qm-add-card', { 'qm-add-card-marked': marked }]}>
933
992
  <!-- Marked, the words are the accessible name and no `aria-label` doubles
@@ -936,9 +995,10 @@
936
995
  {#if kinds.length === 1}
937
996
  <button
938
997
  type="button"
939
- class="qm-add-btn qm-add-affordance qm-tap-floor"
998
+ class="qm-add-btn qm-add-affordance qm-tap-floor qm-focus-ring"
940
999
  aria-label={marked ? undefined : merged.addCard}
941
- onclick={() => addCard(atIndex, kinds[0])}>{marked ? merged.addCard : ''}</button
1000
+ onclick={() => addCard(atIndex, kinds[0], true)}
1001
+ >{#if marked}{merged.addCard}{:else}<Icon name="plus" class="qm-add-glyph" />{/if}</button
942
1002
  >
943
1003
  {:else}
944
1004
  <!-- Multi-kind add: pick the kind, then seed + insert. A menu rather than a
@@ -948,12 +1008,22 @@
948
1008
  rules below reach it through `:global`. -->
949
1009
  <DropdownMenu.Root>
950
1010
  <DropdownMenu.Trigger
951
- class="qm-add-btn qm-add-affordance qm-tap-floor"
1011
+ class="qm-add-btn qm-add-affordance qm-tap-floor qm-focus-ring"
952
1012
  aria-label={marked ? undefined : merged.addCard}
953
- >{marked ? merged.addCard : ''}</DropdownMenu.Trigger
1013
+ >{#if marked}{merged.addCard}{:else}<Icon
1014
+ name="plus"
1015
+ class="qm-add-glyph"
1016
+ />{/if}</DropdownMenu.Trigger
954
1017
  >
955
1018
  <DropdownMenu.Portal to={rootEl}>
956
- <DropdownMenu.Content collisionBoundary={rootEl ?? []} sideOffset={4}>
1019
+ <DropdownMenu.Content
1020
+ collisionBoundary={rootEl ?? []}
1021
+ sideOffset={spacePx(rootEl)}
1022
+ onCloseAutoFocus={(e) => {
1023
+ if (picked) e.preventDefault();
1024
+ picked = false;
1025
+ }}
1026
+ >
957
1027
  <!-- Portalled out of the row but into the stack's root, and carrying the
958
1028
  marker itself: floating is still a detached subtree to the
959
1029
  derivation, like FormatPopover and the enum listbox. The root is the
@@ -961,7 +1031,7 @@
961
1031
  the surface where the consumer scrolls one. -->
962
1032
  <div class="qm-menu-surface" data-qm-root>
963
1033
  {#each kinds as k (k)}
964
- <DropdownMenu.Item class="qm-menu-item" onSelect={() => addCard(atIndex, k)}
1034
+ <DropdownMenu.Item class="qm-menu-item" onSelect={() => addPicked(atIndex, k)}
965
1035
  >{kindTitle(quill.schema.card_kinds?.[k], k)}</DropdownMenu.Item
966
1036
  >
967
1037
  {/each}
@@ -1072,9 +1142,10 @@
1072
1142
  /* Unboxed, like every button, and not dashed: a dashed edge is the placeholder idiom
1073
1143
  ("nothing is here yet"), which on a button reads as disabled or as a drop target. It
1074
1144
  stays honest in one place, the un-schemable card (`Card.svelte`), which is a state
1075
- rather than a control. A bare trigger rests invisible, so the fill is the whole of
1076
- what it shows: the pill fills the strip because the strip is what was pressed, and
1077
- the strip is the gap, so the fill is the card-to-be drawn where it will land.
1145
+ rather than a control. A gap's trigger rests invisible, so the fill and the plus are
1146
+ the whole of what it shows: the pill fills the strip because the strip is what was
1147
+ pressed, and the strip is the gap, so the fill is the card-to-be drawn where it will
1148
+ land.
1078
1149
 
1079
1150
  No inset of its own: the pill and the gap are the same rectangle, and a bare strip is
1080
1151
  a band of gutter, so its height is a rhythm rung. The press floor the box gives up is
@@ -1097,8 +1168,15 @@
1097
1168
  color: var(--_qm-ink-label);
1098
1169
  opacity: 0;
1099
1170
  transition:
1100
- opacity var(--_qm-reveal),
1101
- background-color var(--_qm-reveal);
1171
+ opacity var(--_qm-duration-fast) var(--_qm-ease-reverse),
1172
+ background-color var(--_qm-duration-fast) var(--_qm-ease-reverse);
1173
+ }
1174
+ /* The pill's own height, so a turned-down `--qm-space` shrinks the glyph with the
1175
+ strip rather than spilling it past the pill. */
1176
+ .qm-add-card :global(.qm-add-glyph) {
1177
+ height: 100%;
1178
+ width: auto;
1179
+ aspect-ratio: 1;
1102
1180
  }
1103
1181
  /* The strip that holds words at rest takes the height that centres them, and rests dim
1104
1182
  rather than absent, having something to be seen. */
@@ -1111,11 +1189,15 @@
1111
1189
  /* One arrival, three ways in. Keyboard focus, because the fill is the only thing that
1112
1190
  reads as the trigger on a bare strip; and an open menu, because it portals out of
1113
1191
  the strip, so a pointer moving onto an item has left the row whose hover was drawing
1114
- the pill the menu hangs from. */
1192
+ the pill the menu hangs from. The arrival takes the long trip and the leave the fast
1193
+ rung, a transition being the state's it ends in. */
1115
1194
  .qm-add-card :global(.qm-add-btn:hover),
1116
1195
  .qm-add-card :global(.qm-add-btn:focus-visible),
1117
1196
  .qm-add-card :global(.qm-add-btn[data-state='open']) {
1118
1197
  opacity: 1;
1119
1198
  background: var(--_qm-add-fill);
1199
+ transition:
1200
+ opacity var(--_qm-reveal),
1201
+ background-color var(--_qm-reveal);
1120
1202
  }
1121
1203
  </style>
@@ -233,7 +233,8 @@
233
233
  :where(.qm-chip:hover) {
234
234
  color: var(--_qm-ink);
235
235
  }
236
- :where(.qm-chip svg) {
236
+ :where(.qm-chip svg),
237
+ :where(.qm-icon-btn svg) {
237
238
  flex-shrink: 0;
238
239
  width: var(--_qm-glyph-control);
239
240
  height: var(--_qm-glyph-control);
@@ -266,6 +267,22 @@
266
267
  transform: translate(-50%, -50%);
267
268
  }
268
269
 
270
+ /* The arrival wash: a plane over the host's box at rest, which a landing plays
271
+ (`core/bloom.ts`). On the host from mount, because WebKit resizes a laid-out subgrid's
272
+ tracks when an out-of-flow box is added to it. */
273
+ :where(.qm-wash) {
274
+ position: relative;
275
+ }
276
+ :where(.qm-wash)::after {
277
+ content: '';
278
+ position: absolute;
279
+ inset: 0;
280
+ border-radius: inherit;
281
+ background: var(--_qm-accent-wash);
282
+ opacity: 0;
283
+ pointer-events: none;
284
+ }
285
+
269
286
  /* The capacity ladder: how many tracks a grid of fields steps to, off the width of
270
287
  the query container it stands in. One recipe, because three grids read it — a section's
271
288
  fields (`Card.svelte`), a subform's properties (`ObjectField`), each over its own
@@ -310,7 +327,8 @@
310
327
  highlight the keyboard cannot reach. It fills with the accent held rather than
311
328
  the hover rung: the mark is a cursor a keyboard drives, so it has to read at a
312
329
  glance from across the list, where a hover only has to confirm the pointer
313
- already under it.
330
+ already under it. So the mark is the item's whole focus indicator, and the
331
+ outline a focused item would draw beside it is taken off.
314
332
 
315
333
  The resting fill is the recipe's too: an item drawn on a `<button>` carries a UA
316
334
  fill to take back, and a scoped block outranks this file, so a caller taking it
@@ -337,6 +355,7 @@
337
355
  background: transparent;
338
356
  cursor: pointer;
339
357
  user-select: none;
358
+ outline: none;
340
359
  }
341
360
  :where(.qm-menu-item[data-highlighted]) {
342
361
  background: var(--_qm-accent-tint);
@@ -9,8 +9,9 @@
9
9
 
10
10
  interface Props extends Omit<SVGAttributes<SVGSVGElement>, 'name'> {
11
11
  name: IconName;
12
- /** The frame's edge, in px. A chip sizes the glyph from `--_qm-glyph-control`
13
- * in CSS, which beats this attribute; callers that are not chips pass a size. */
12
+ /** The frame's edge, in px. A chip or a glyph button sizes the glyph from
13
+ * `--_qm-glyph-control` in CSS, which beats this attribute; other callers pass
14
+ * a size. */
14
15
  size?: number;
15
16
  }
16
17
  let { name, size = 24, ...rest }: Props = $props();
@@ -2,8 +2,9 @@ import { type IconName } from './nodes.js';
2
2
  import type { SVGAttributes } from 'svelte/elements';
3
3
  interface Props extends Omit<SVGAttributes<SVGSVGElement>, 'name'> {
4
4
  name: IconName;
5
- /** The frame's edge, in px. A chip sizes the glyph from `--_qm-glyph-control`
6
- * in CSS, which beats this attribute; callers that are not chips pass a size. */
5
+ /** The frame's edge, in px. A chip or a glyph button sizes the glyph from
6
+ * `--_qm-glyph-control` in CSS, which beats this attribute; other callers pass
7
+ * a size. */
7
8
  size?: number;
8
9
  }
9
10
  declare const Icon: import("svelte").Component<Props, {}, "">;
@@ -15,6 +15,31 @@ export declare const ICONS: {
15
15
  readonly 'chevron-up': readonly [readonly ["path", {
16
16
  readonly d: "m18 15-6-6-6 6";
17
17
  }]];
18
+ readonly 'grip-vertical': readonly [readonly ["circle", {
19
+ readonly cx: "9";
20
+ readonly cy: "12";
21
+ readonly r: "1";
22
+ }], readonly ["circle", {
23
+ readonly cx: "9";
24
+ readonly cy: "5";
25
+ readonly r: "1";
26
+ }], readonly ["circle", {
27
+ readonly cx: "9";
28
+ readonly cy: "19";
29
+ readonly r: "1";
30
+ }], readonly ["circle", {
31
+ readonly cx: "15";
32
+ readonly cy: "12";
33
+ readonly r: "1";
34
+ }], readonly ["circle", {
35
+ readonly cx: "15";
36
+ readonly cy: "5";
37
+ readonly r: "1";
38
+ }], readonly ["circle", {
39
+ readonly cx: "15";
40
+ readonly cy: "19";
41
+ readonly r: "1";
42
+ }]];
18
43
  readonly bold: readonly [readonly ["path", {
19
44
  readonly d: "M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8";
20
45
  }]];
@@ -62,27 +87,6 @@ export declare const ICONS: {
62
87
  }], readonly ["path", {
63
88
  readonly d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71";
64
89
  }]];
65
- readonly hash: readonly [readonly ["line", {
66
- readonly x1: "4";
67
- readonly x2: "20";
68
- readonly y1: "9";
69
- readonly y2: "9";
70
- }], readonly ["line", {
71
- readonly x1: "4";
72
- readonly x2: "20";
73
- readonly y1: "15";
74
- readonly y2: "15";
75
- }], readonly ["line", {
76
- readonly x1: "10";
77
- readonly x2: "8";
78
- readonly y1: "3";
79
- readonly y2: "21";
80
- }], readonly ["line", {
81
- readonly x1: "16";
82
- readonly x2: "14";
83
- readonly y1: "3";
84
- readonly y2: "21";
85
- }]];
86
90
  readonly info: readonly [readonly ["circle", {
87
91
  readonly cx: "12";
88
92
  readonly cy: "12";
@@ -5,7 +5,7 @@
5
5
  //
6
6
  // Verbatim from lucide 1.28.0 — the version is stated because a glyph that drifts
7
7
  // from upstream is otherwise invisible, and because the notices in `NOTICE` are the
8
- // ones that release carries (ISC, and MIT for the Feather-derived ten).
8
+ // ones that release carries (ISC, and MIT for the Feather-derived glyphs).
9
9
  export const ICONS = {
10
10
  'chevron-right': [['path', { d: 'm9 18 6-6-6-6' }]],
11
11
  'arrow-right': [
@@ -14,6 +14,14 @@ export const ICONS = {
14
14
  ],
15
15
  'chevron-down': [['path', { d: 'm6 9 6 6 6-6' }]],
16
16
  'chevron-up': [['path', { d: 'm18 15-6-6-6 6' }]],
17
+ 'grip-vertical': [
18
+ ['circle', { cx: '9', cy: '12', r: '1' }],
19
+ ['circle', { cx: '9', cy: '5', r: '1' }],
20
+ ['circle', { cx: '9', cy: '19', r: '1' }],
21
+ ['circle', { cx: '15', cy: '12', r: '1' }],
22
+ ['circle', { cx: '15', cy: '5', r: '1' }],
23
+ ['circle', { cx: '15', cy: '19', r: '1' }]
24
+ ],
17
25
  bold: [['path', { d: 'M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8' }]],
18
26
  italic: [
19
27
  ['line', { x1: '19', x2: '10', y1: '4', y2: '4' }],
@@ -37,12 +45,6 @@ export const ICONS = {
37
45
  ['path', { d: 'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71' }],
38
46
  ['path', { d: 'M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71' }]
39
47
  ],
40
- hash: [
41
- ['line', { x1: '4', x2: '20', y1: '9', y2: '9' }],
42
- ['line', { x1: '4', x2: '20', y1: '15', y2: '15' }],
43
- ['line', { x1: '10', x2: '8', y1: '3', y2: '21' }],
44
- ['line', { x1: '16', x2: '14', y1: '3', y2: '21' }]
45
- ],
46
48
  info: [
47
49
  ['circle', { cx: '12', cy: '12', r: '10' }],
48
50
  ['path', { d: 'M12 16v-4' }],
@@ -8,8 +8,8 @@ import type { FieldController, LeafViews } from '../core/codec/index.js';
8
8
  */
9
9
  export type LandingBox = HTMLElement | undefined | Promise<HTMLElement | undefined>;
10
10
  /**
11
- * A mounted field's landing handle: focus it, and the element the arrival wash blooms
12
- * inside ({@link import('../core/bloom.js').bloomInside}).
11
+ * A mounted field's landing handle: focus it, and the `.qm-wash` box the arrival wash
12
+ * plays over ({@link import('../core/bloom.js').bloom}).
13
13
  *
14
14
  * A `FieldController` is one, which is what lets the two lanes share a lookup; `Field`
15
15
  * builds the same shape over a form control off the handoff a label click already
@@ -71,16 +71,19 @@ export interface LeafRegistry {
71
71
  clear(): void;
72
72
  }
73
73
  export declare function createLeafRegistry(): LeafRegistry;
74
- /**
75
- * The active prose leaf as the formatting popover reaches it: the views a selection is
76
- * read from and a mark toggled at, and the controller where the leaf is a field's. A
77
- * by-value leaf has none, its anchors being dropped on the parent's value write, so the
78
- * anchor verbs the controller carries have nothing to act on there.
79
- */
74
+ /** The active prose leaf as the formatting popover reaches it: the views a selection is
75
+ * read from and a mark toggled at. */
80
76
  export interface ActiveProse {
81
77
  views: LeafViews;
82
- controller?: FieldController;
83
78
  }
79
+ /**
80
+ * The steps from a field's control box `root` down to the cell holding `target`, the
81
+ * reverse of {@link FieldControl.focusPath}: read off the marker each container rung
82
+ * stamps on the cell it draws, `data-qm-index` on an array's row and `data-qm-prop` on a
83
+ * key step (an object's property, a variant's cell, a matrix's member and an added
84
+ * item's title). `[]` for a target on the field's own control.
85
+ */
86
+ export declare function stepsTo(target: EventTarget | null, root: Element): PathStep[];
84
87
  /** The by-value lane with a field's key bound: what a leaf under that field announces
85
88
  * itself through, knowing no key of its own. */
86
89
  export interface ValueLeafSeat {
@@ -66,6 +66,25 @@ export function createLeafRegistry() {
66
66
  }
67
67
  };
68
68
  }
69
+ /**
70
+ * The steps from a field's control box `root` down to the cell holding `target`, the
71
+ * reverse of {@link FieldControl.focusPath}: read off the marker each container rung
72
+ * stamps on the cell it draws, `data-qm-index` on an array's row and `data-qm-prop` on a
73
+ * key step (an object's property, a variant's cell, a matrix's member and an added
74
+ * item's title). `[]` for a target on the field's own control.
75
+ */
76
+ export function stepsTo(target, root) {
77
+ const steps = [];
78
+ for (let el = target instanceof Element ? target : null; el && el !== root; el = el.parentElement) {
79
+ const index = el.getAttribute('data-qm-index');
80
+ const key = el.getAttribute('data-qm-prop');
81
+ if (index != null)
82
+ steps.push(Number(index));
83
+ else if (key != null)
84
+ steps.push(key);
85
+ }
86
+ return steps.reverse();
87
+ }
69
88
  const SEAT = Symbol('qm-value-leaf');
70
89
  /** Seat every by-value leaf under the calling field, at any depth of its containers. */
71
90
  export function seatValueLeaves(seat) {
@@ -1,4 +1,5 @@
1
1
  import type { AnimationConfig } from 'svelte/animate';
2
+ import type { TransitionConfig } from 'svelte/transition';
2
3
  /** The trips this module is running on `node`. A run holds the slot at its pre-move
3
4
  * position for its whole length, so anything that measures the node's box waits on
4
5
  * these rather than reading the one a transform is holding. */
@@ -23,9 +24,11 @@ export declare function reorderArm(): ReorderArm;
23
24
  * `armed` is why this is the reorder and not every reconcile: `animate:` fires wherever
24
25
  * a slot's rect moved, and a card growing under the caret moves every slot below it,
25
26
  * which is a layout change with no trip in it and one that would draw those slots over
26
- * the content that displaced them for the length of the run. So the reorder command
27
- * arms the gesture and the frame it lands in disarms it, which is the admission test
28
- * read off the endpoints: two positions in the list, not two positions on the page.
27
+ * the content that displaced them for the length of the run. So a command that moves
28
+ * slots in the list arms the gesture and the frame it lands in disarms it, which is the
29
+ * admission test read off the endpoints: two positions in the list, not two positions on
30
+ * the page. A move is one; so, in the card stack, are an insert and a removal, which move
31
+ * every slot past them by one.
29
32
  *
30
33
  * Both endpoints are rest states, so the curve is `--_qm-ease-reverse` (a second click
31
34
  * lands mid-slide) and the rung is `slow` (the trip moves the stack around it).
@@ -34,3 +37,22 @@ export declare function reorder(node: HTMLElement, { from, to }: {
34
37
  from: DOMRect;
35
38
  to: DOMRect;
36
39
  }, armed: () => boolean): AnimationConfig;
40
+ /**
41
+ * A slot entering or leaving the list fades where it stands, while the slots it displaces
42
+ * take the reorder trip: the stack is what slides, and a card is what appears or goes, under
43
+ * the slots crossing it. Opacity alone, because a leaving slot is held at its place by a
44
+ * transform `animate:` writes, which a keyframed transform would override.
45
+ *
46
+ * The returned duration is what keeps a leaving slot mounted until its fade has run, and
47
+ * the fill what keeps it from painting whole in the frame between the two.
48
+ */
49
+ export declare function slot(node: HTMLElement, _params: unknown, { direction }: {
50
+ direction: 'in' | 'out' | 'both';
51
+ }): TransitionConfig;
52
+ /**
53
+ * Resolves once nothing in the document has scrolled for {@link STILL_FRAMES} frames
54
+ * running. A smooth scroll moves its scroller on every frame until it lands, so stillness
55
+ * is its end on every engine, whether or not it reports one; a scroll with nowhere to go
56
+ * is still from the first frame.
57
+ */
58
+ export declare function scrollSettled(): Promise<void>;