@quillmark/svelte 0.21.0 → 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 (67) hide show
  1. package/NOTICE +6 -7
  2. package/README.md +1 -1
  3. package/dist/core/address.d.ts +6 -1
  4. package/dist/core/address.js +12 -3
  5. package/dist/core/bloom.d.ts +9 -4
  6. package/dist/core/bloom.js +18 -49
  7. package/dist/core/codec/atoms.d.ts +1 -1
  8. package/dist/core/codec/atoms.js +1 -1
  9. package/dist/core/codec/blocks.js +4 -2
  10. package/dist/core/codec/code.d.ts +1 -1
  11. package/dist/core/codec/code.js +1 -1
  12. package/dist/core/codec/decode.d.ts +5 -5
  13. package/dist/core/codec/decode.js +18 -12
  14. package/dist/core/codec/encode.js +6 -1
  15. package/dist/core/codec/field.d.ts +15 -10
  16. package/dist/core/codec/field.js +46 -17
  17. package/dist/core/codec/index.d.ts +1 -1
  18. package/dist/core/codec/index.js +2 -2
  19. package/dist/core/codec/inputrules.js +1 -1
  20. package/dist/core/codec/lists.d.ts +3 -2
  21. package/dist/core/codec/lists.js +21 -5
  22. package/dist/core/codec/nesting.d.ts +20 -0
  23. package/dist/core/codec/nesting.js +62 -0
  24. package/dist/core/codec/prose.css +74 -20
  25. package/dist/core/codec/schema.d.ts +2 -2
  26. package/dist/core/codec/schema.js +16 -7
  27. package/dist/core/codec/table-copy.d.ts +17 -0
  28. package/dist/core/codec/table-copy.js +84 -0
  29. package/dist/core/codec/table-view.d.ts +1 -7
  30. package/dist/core/codec/table-view.js +70 -119
  31. package/dist/core/codec/table.d.ts +2 -9
  32. package/dist/core/codec/table.js +10 -19
  33. package/dist/core/codec/tasks.d.ts +19 -0
  34. package/dist/core/codec/tasks.js +140 -0
  35. package/dist/core/errors.d.ts +2 -2
  36. package/dist/visual/ArrayField.svelte +101 -63
  37. package/dist/visual/ArrayField.svelte.d.ts +0 -2
  38. package/dist/visual/Card.svelte +9 -3
  39. package/dist/visual/Card.svelte.d.ts +2 -2
  40. package/dist/visual/Field.svelte +20 -16
  41. package/dist/visual/Field.svelte.d.ts +4 -2
  42. package/dist/visual/FieldLabel.svelte +5 -33
  43. package/dist/visual/FieldLabel.svelte.d.ts +0 -2
  44. package/dist/visual/FormatPopover.svelte +54 -73
  45. package/dist/visual/MatrixField.svelte +18 -12
  46. package/dist/visual/NumberField.svelte +32 -3
  47. package/dist/visual/ObjectField.svelte +38 -20
  48. package/dist/visual/ObjectField.svelte.d.ts +6 -0
  49. package/dist/visual/ProseField.svelte +10 -13
  50. package/dist/visual/ProseField.svelte.d.ts +5 -2
  51. package/dist/visual/ProseValue.svelte +25 -19
  52. package/dist/visual/ProseValue.svelte.d.ts +4 -4
  53. package/dist/visual/TextField.svelte +3 -2
  54. package/dist/visual/TipsCard.svelte +5 -2
  55. package/dist/visual/VariantField.svelte +1 -1
  56. package/dist/visual/VariantField.svelte.d.ts +1 -1
  57. package/dist/visual/VisualEditorInner.svelte +13 -13
  58. package/dist/visual/controls.css +19 -1
  59. package/dist/visual/icons/nodes.d.ts +0 -21
  60. package/dist/visual/icons/nodes.js +1 -7
  61. package/dist/visual/leaves.d.ts +12 -9
  62. package/dist/visual/leaves.js +19 -0
  63. package/dist/visual/strings.d.ts +10 -5
  64. package/dist/visual/strings.js +16 -9
  65. package/dist/visual/structure.d.ts +21 -31
  66. package/dist/visual/structure.js +54 -51
  67. package/package.json +3 -3
@@ -1,6 +1,6 @@
1
1
  <!--
2
2
  A prose leaf the parent commits by value: an element of a `richtext` (fixture
3
- `keywords`) or `plaintext` (`errata`) array, and a content cell of a subform — an
3
+ `keywords`) or `text` (`authors`, `errata`) array, and a content cell of a subform — an
4
4
  `object`'s property, a variant's cell ({@link ObjectField}). None of them is
5
5
  `applyChange`-addressable: `Addr.field` is a flat name, so `keywords[0]` and
6
6
  `contact.note` have no op address. So this is not a `createField` leaf: it mounts a
@@ -19,7 +19,7 @@
19
19
 
20
20
  The content is read, not passed: `reader.getContentAt(addr, path)` decodes through
21
21
  the codec the leaf's own declared type names, so what it rests as — the content
22
- object, a `plaintext` literal, the authored string a transport door left — stops
22
+ object, a `text` literal, the authored string a transport door left — stops
23
23
  being the row's or the cell's business. It is read at mount and again wherever the
24
24
  parent's re-derive hands down a `value` that moved, and a read differing from the
25
25
  content the leaf last committed or took replaces the view, the gate `createField`
@@ -28,8 +28,8 @@
28
28
  it, so the echo replaces nothing and the caret and the history stay.
29
29
 
30
30
  The schema is the declared type's where the parent has room for it: a subform cell
31
- declaring no `inline` takes the full row ({@link ObjectField}), a `richtext` one on the
32
- block schema and a `plaintext` one on the plain schema, paragraphs and hard breaks.
31
+ declaring `multiline: true` takes the full row ({@link ObjectField}), a `richtext` one
32
+ on the block schema and a `text` one on the plain schema, paragraphs and hard breaks.
33
33
  An array element is one textblock whatever it declares, since Enter there is the
34
34
  repeater's. There is no slash menu at either width, and no island view.
35
35
 
@@ -80,8 +80,8 @@
80
80
  /** The default an unset leaf holds, where it prints: drawn at the default rung,
81
81
  * and handed up whole with the first edit in it, as `createField` takes one. */
82
82
  fallback?: Content;
83
- /** The mark-free schema (a `plaintext` leaf): literal text, no formatting,
84
- * exactly as the scalar field of that type mounts. */
83
+ /** The mark-free schema (a `text` leaf): literal text, no formatting, exactly
84
+ * as the field of that type mounts. */
85
85
  plaintext?: boolean;
86
86
  /** A multi-block schema, for a cell the parent draws at full width: the block
87
87
  * schema, or the plain one with `plaintext`. Absent, the leaf is one textblock. */
@@ -96,8 +96,8 @@
96
96
  labelledBy?: string;
97
97
  /** The parked `description` (FieldLabel) → `aria-describedby`. */
98
98
  describedBy?: string;
99
- /** The empty leaf's ghost, a subform cell's, until the leaf's first edit: the
100
- * `none` an optional cell prints unset. Read per decoration pass. */
99
+ /** The empty leaf's ghost, a subform cell's: the `none` an optional cell prints
100
+ * unset. Read per decoration pass. */
101
101
  placeholder?: string;
102
102
  onChange: (rt: Content) => void;
103
103
  /** Raw keydown, for a container whose own keys run through this leaf: the
@@ -130,9 +130,8 @@
130
130
  /** Take the caret: what a parent placing focus on this leaf calls. The view's
131
131
  * focus, not the element's: a PM view restores its selection, where a bare DOM
132
132
  * focus on a contenteditable leaves the caret unplaced. And the reveal beside it,
133
- * since PM's focus prevents the scroll a `string` element's input takes by
134
- * default — an array whose two item types revealed differently is one row of it
135
- * landing off screen. */
133
+ * since PM's focus prevents the scroll a form control takes by default — an array
134
+ * whose two item types revealed differently is one row of it landing off screen. */
136
135
  export function focus(): void {
137
136
  if (!view) return;
138
137
  view.focus();
@@ -157,11 +156,18 @@
157
156
  view.dispatch(view.state.tr.setSelection(sel).scrollIntoView());
158
157
  }
159
158
 
159
+ $effect(() => {
160
+ void [label, labelledBy, describedBy, placeholder];
161
+ view?.setProps({});
162
+ });
163
+
160
164
  function mountView(mount: HTMLDivElement): () => void {
161
165
  return untrack(() => {
162
166
  const inline = !block;
163
167
  const leaf = { inline, plaintext, islands: false };
164
- const names = { label, labelledBy, describedBy };
168
+ // Read per attribute pass, not captured: an element's name carries its index,
169
+ // which a remove above it moves.
170
+ const names = () => ({ label, labelledBy, describedBy });
165
171
  const shown = (stored: Content | undefined): Content => stored ?? fallback ?? emptyContent();
166
172
  const stored = content();
167
173
  let seen = value;
@@ -171,8 +177,8 @@
171
177
  // Which of `aria-label` / `aria-labelledby` wins is the codec's one answer
172
178
  // (`proseAttributes`), so a cell carrying a label element and a row carrying
173
179
  // none cannot name their regions by different rules.
174
- const plainNamed = proseAttributes(names) ?? {};
175
- const heldNamed = heldAttributes(names, heldId);
180
+ const plainNamed = () => proseAttributes(names()) ?? {};
181
+ const heldNamed = () => heldAttributes(names(), heldId);
176
182
  const build = (rt: Content): EditorState => {
177
183
  // The same keymap and plugin stack a `createField` leaf mounts (shared
178
184
  // `proseLeafPlugins`), minus the anchor-position plugin: anchors are dropped on
@@ -184,8 +190,7 @@
184
190
  ? []
185
191
  : proseLeafPlugins(schema, {
186
192
  inline,
187
- placeholder: () => placeholder,
188
- placeholderUntilEdit: true
193
+ placeholder: () => placeholder
189
194
  })
190
195
  });
191
196
  };
@@ -195,11 +200,12 @@
195
200
  state: build(reconciler.last),
196
201
  editable: () => !held,
197
202
  attributes: () => {
198
- const named = held ? heldNamed : plainNamed;
203
+ const named = held ? heldNamed() : plainNamed();
199
204
  return defaulted ? { ...named, 'data-default': '' } : named;
200
205
  },
201
206
  dispatchTransaction(tr) {
202
207
  const next = mounted.state.apply(tr);
208
+ if (next === mounted.state) return;
203
209
  const edit = tr.docChanged && !held;
204
210
  if (edit) defaulted = false;
205
211
  mounted.updateState(next);
@@ -265,8 +271,8 @@
265
271
  </script>
266
272
 
267
273
  <!-- `.qm-control-box` (controls.css) is the whole box, so an array of `richtext` and
268
- an array of `string` render rows of equal height, and a subform's prose cell measures
269
- like the text cell beside it. No floor: the reset in `core/codec/prose.css` makes one
274
+ an array of `text` render rows of equal height, and a subform's prose cell measures
275
+ like the form control beside it. No floor: the reset in `core/codec/prose.css` makes one
270
276
  line of prose measure one line. Width is the row's or the cell's to give: the leaf
271
277
  fills the track it is placed in. -->
272
278
  <div class="qm-prose-value qm-control-box qm-focus-ring-within">
@@ -12,8 +12,8 @@ interface Props {
12
12
  /** The default an unset leaf holds, where it prints: drawn at the default rung,
13
13
  * and handed up whole with the first edit in it, as `createField` takes one. */
14
14
  fallback?: Content;
15
- /** The mark-free schema (a `plaintext` leaf): literal text, no formatting,
16
- * exactly as the scalar field of that type mounts. */
15
+ /** The mark-free schema (a `text` leaf): literal text, no formatting, exactly
16
+ * as the field of that type mounts. */
17
17
  plaintext?: boolean;
18
18
  /** A multi-block schema, for a cell the parent draws at full width: the block
19
19
  * schema, or the plain one with `plaintext`. Absent, the leaf is one textblock. */
@@ -28,8 +28,8 @@ interface Props {
28
28
  labelledBy?: string;
29
29
  /** The parked `description` (FieldLabel) → `aria-describedby`. */
30
30
  describedBy?: string;
31
- /** The empty leaf's ghost, a subform cell's, until the leaf's first edit: the
32
- * `none` an optional cell prints unset. Read per decoration pass. */
31
+ /** The empty leaf's ghost, a subform cell's: the `none` an optional cell prints
32
+ * unset. Read per decoration pass. */
33
33
  placeholder?: string;
34
34
  onChange: (rt: Content) => void;
35
35
  /** Raw keydown, for a container whose own keys run through this leaf: the
@@ -1,6 +1,7 @@
1
1
  <!--
2
- A `string` field → text input. Commits every edit live (on input) via the parent's
3
- typed `writer.set`, so the preview tracks typing.
2
+ A text input: the control a field of a type this build does not know falls to.
3
+ Commits every edit live (on input) via the parent's typed `writer.set`, so the
4
+ preview tracks typing.
4
5
 
5
6
  An unset field whose `default:` prints holds that default as its text, at the
6
7
  default rung (theme.css): the value it prints, drawn as one and a step off a written
@@ -69,7 +69,7 @@
69
69
  {#each tips as _, i (i)}
70
70
  <button
71
71
  type="button"
72
- class="qm-tips-dot"
72
+ class="qm-tips-dot qm-focus-ring"
73
73
  aria-label={t.strings.tipsDot(i + 1, tips.length)}
74
74
  aria-current={i === index}
75
75
  onclick={() => (cursor = i)}
@@ -77,7 +77,7 @@
77
77
  {/each}
78
78
  </div>
79
79
  {/if}
80
- <button type="button" class="qm-icon-btn qm-tips-action" onclick={advance}>
80
+ <button type="button" class="qm-icon-btn qm-tips-action qm-focus-ring" onclick={advance}>
81
81
  {#if isLast}
82
82
  {t.strings.tipsDismiss} <Icon name="trash-2" />
83
83
  {:else}
@@ -193,6 +193,9 @@
193
193
  .qm-tips-dot:hover {
194
194
  opacity: var(--_qm-opacity-muted);
195
195
  }
196
+ .qm-tips-dot:focus-visible {
197
+ opacity: 1;
198
+ }
196
199
  .qm-tips-dot[aria-current='true'] {
197
200
  opacity: 1;
198
201
  }
@@ -42,7 +42,7 @@
42
42
  describedBy?: string;
43
43
  /** The boundary's nested content read for this field, which the cells take
44
44
  * unprefixed: a variant's cell is one key in from the container it rests as, so
45
- * a `plaintext` cell's path is its own name ({@link ObjectField}). */
45
+ * a `text` cell's path is its own name ({@link ObjectField}). */
46
46
  contentAt: (path: PathStep[]) => Content | undefined;
47
47
  onCommit: (v: Record<string, unknown> | undefined) => void;
48
48
  optionAllowed?: (value: string) => boolean;
@@ -16,7 +16,7 @@ interface Props {
16
16
  describedBy?: string;
17
17
  /** The boundary's nested content read for this field, which the cells take
18
18
  * unprefixed: a variant's cell is one key in from the container it rests as, so
19
- * a `plaintext` cell's path is its own name ({@link ObjectField}). */
19
+ * a `text` cell's path is its own name ({@link ObjectField}). */
20
20
  contentAt: (path: PathStep[]) => Content | undefined;
21
21
  onCommit: (v: Record<string, unknown> | undefined) => void;
22
22
  optionAllowed?: (value: string) => boolean;
@@ -31,7 +31,7 @@
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
36
  import { spacePx } from '../core/rungs.js';
37
37
  import type {
@@ -220,7 +220,7 @@
220
220
  const card = addr.card;
221
221
  return card != null ? { card, field: addr.field } : { field: addr.field };
222
222
  }
223
- function handleFocus(addr: Addr): void {
223
+ function handleFocus(addr: Addr, steps: readonly PathStep[] = []): void {
224
224
  const plain = normalize(addr);
225
225
  activeAddr = plain;
226
226
  activeCardId = cardIdOf(plain);
@@ -228,7 +228,7 @@
228
228
  // to at the offset it still names has to report again.
229
229
  lastCaretField = undefined;
230
230
  lastCaretPos = undefined;
231
- const field = pathFor(plain);
231
+ const field = pathFor(plain, steps);
232
232
  // Both halves or neither: a stale addr names no path and no live card.
233
233
  if (field != null && activeCardId != null)
234
234
  onActiveLeafChange?.({ field, cardId: activeCardId });
@@ -245,11 +245,11 @@
245
245
  return model.cards.map((c) => c.kind);
246
246
  }
247
247
  /**
248
- * A leaf's canonical `DocPath`. `undefined` for an address outside the live card
249
- * 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).
250
250
  */
251
- function pathFor(addr: Addr): DocPath | undefined {
252
- 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);
253
253
  }
254
254
  // The last place reported, so a caret that did not move is not news. A leaf fires
255
255
  // one caret signal per transaction, and a transaction need not have moved the
@@ -736,10 +736,10 @@
736
736
  // (where landing a caret changes nothing on screen) or one off-screen, where the
737
737
  // browser's focus-scroll moves the page and leaves the caret to be hunted for.
738
738
  const box = await land(found, at.granularity === 'segment' ? undefined : at.pos);
739
- if (span.alive) bloomInside(box);
739
+ if (span.alive) bloom(box);
740
740
  }
741
741
  /** The active prose leaf: the formatting popover's observation seam. The active
742
- * 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
743
743
  * under it (`leaves.ts`); `undefined` for a form control, which holds no marks to
744
744
  * toggle. */
745
745
  export function getActiveLeaf(): ActiveProse | undefined {
@@ -747,7 +747,7 @@
747
747
  const card = activeAddr.card != null ? activeCardId : undefined;
748
748
  const key = fieldKeyToString({ card, field: activeAddr.field });
749
749
  const controller = leaves.prose(key);
750
- if (controller) return { views: controller as FieldController & LeafViews, controller };
750
+ if (controller) return { views: controller as FieldController & LeafViews };
751
751
  const views = leaves.value(key);
752
752
  return views && { views };
753
753
  }
@@ -826,7 +826,7 @@
826
826
  const found = await revealLeaf(field);
827
827
  if (!found) return missed(`no mounted field at ${field}`, field);
828
828
  const box = await land(found, undefined);
829
- if (span.alive) bloomInside(box);
829
+ if (span.alive) bloom(box);
830
830
  }
831
831
  /** Seed a card of `kind` and insert it at `at` (default: the end). Returns the new
832
832
  * card's session key, or `undefined` when the quill seeds no card of that kind. */
@@ -995,7 +995,7 @@
995
995
  {#if kinds.length === 1}
996
996
  <button
997
997
  type="button"
998
- class="qm-add-btn qm-add-affordance qm-tap-floor"
998
+ class="qm-add-btn qm-add-affordance qm-tap-floor qm-focus-ring"
999
999
  aria-label={marked ? undefined : merged.addCard}
1000
1000
  onclick={() => addCard(atIndex, kinds[0], true)}
1001
1001
  >{#if marked}{merged.addCard}{:else}<Icon name="plus" class="qm-add-glyph" />{/if}</button
@@ -1008,7 +1008,7 @@
1008
1008
  rules below reach it through `:global`. -->
1009
1009
  <DropdownMenu.Root>
1010
1010
  <DropdownMenu.Trigger
1011
- class="qm-add-btn qm-add-affordance qm-tap-floor"
1011
+ class="qm-add-btn qm-add-affordance qm-tap-floor qm-focus-ring"
1012
1012
  aria-label={marked ? undefined : merged.addCard}
1013
1013
  >{#if marked}{merged.addCard}{:else}<Icon
1014
1014
  name="plus"
@@ -267,6 +267,22 @@
267
267
  transform: translate(-50%, -50%);
268
268
  }
269
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
+
270
286
  /* The capacity ladder: how many tracks a grid of fields steps to, off the width of
271
287
  the query container it stands in. One recipe, because three grids read it — a section's
272
288
  fields (`Card.svelte`), a subform's properties (`ObjectField`), each over its own
@@ -311,7 +327,8 @@
311
327
  highlight the keyboard cannot reach. It fills with the accent held rather than
312
328
  the hover rung: the mark is a cursor a keyboard drives, so it has to read at a
313
329
  glance from across the list, where a hover only has to confirm the pointer
314
- 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.
315
332
 
316
333
  The resting fill is the recipe's too: an item drawn on a `<button>` carries a UA
317
334
  fill to take back, and a scoped block outranks this file, so a caller taking it
@@ -338,6 +355,7 @@
338
355
  background: transparent;
339
356
  cursor: pointer;
340
357
  user-select: none;
358
+ outline: none;
341
359
  }
342
360
  :where(.qm-menu-item[data-highlighted]) {
343
361
  background: var(--_qm-accent-tint);
@@ -87,27 +87,6 @@ export declare const ICONS: {
87
87
  }], readonly ["path", {
88
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";
89
89
  }]];
90
- readonly hash: readonly [readonly ["line", {
91
- readonly x1: "4";
92
- readonly x2: "20";
93
- readonly y1: "9";
94
- readonly y2: "9";
95
- }], readonly ["line", {
96
- readonly x1: "4";
97
- readonly x2: "20";
98
- readonly y1: "15";
99
- readonly y2: "15";
100
- }], readonly ["line", {
101
- readonly x1: "10";
102
- readonly x2: "8";
103
- readonly y1: "3";
104
- readonly y2: "21";
105
- }], readonly ["line", {
106
- readonly x1: "16";
107
- readonly x2: "14";
108
- readonly y1: "3";
109
- readonly y2: "21";
110
- }]];
111
90
  readonly info: readonly [readonly ["circle", {
112
91
  readonly cx: "12";
113
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': [
@@ -45,12 +45,6 @@ export const ICONS = {
45
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' }],
46
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' }]
47
47
  ],
48
- hash: [
49
- ['line', { x1: '4', x2: '20', y1: '9', y2: '9' }],
50
- ['line', { x1: '4', x2: '20', y1: '15', y2: '15' }],
51
- ['line', { x1: '10', x2: '8', y1: '3', y2: '21' }],
52
- ['line', { x1: '16', x2: '14', y1: '3', y2: '21' }]
53
- ],
54
48
  info: [
55
49
  ['circle', { cx: '12', cy: '12', r: '10' }],
56
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) {
@@ -15,13 +15,13 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
15
15
  /** The card-title input's accessible name. */
16
16
  cardTitle: string;
17
17
  /** The trailing strip's visible words, which are also every strip's accessible
18
- * name; the plus rides the wording. One string over
18
+ * name. One string over
19
19
  * one kind and many: the trigger seeds a card, and which kind is the menu's word
20
20
  * to say or the schema's to have already settled. */
21
21
  addCard: string;
22
22
  arrayAdd: string;
23
23
  arrayRemove: string;
24
- /** The drag handle on an `object` row: its tooltip and, with the row, its name. */
24
+ /** The drag handle on an `object` row: its tooltip. */
25
25
  arrayDrag: string;
26
26
  /**
27
27
  * A row control's accessible name, the action and the row it acts on: `Remove
@@ -59,8 +59,6 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
59
59
  /** The date field's toggle, pressed while the field holds `today`: the render
60
60
  * date rather than a written one. */
61
61
  dateToday: string;
62
- /** The required marker's accessible name; the glyph itself is a `*`. */
63
- fieldRequired: string;
64
62
  /** What an unset optional cell (`t?`) ghosts at rest: the `none` it prints
65
63
  * unanswered, in the product's words. */
66
64
  optionalGhost: string;
@@ -75,7 +73,8 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
75
73
  formatStrikethrough: string;
76
74
  formatCode: string;
77
75
  formatLink: string;
78
- /** The anchor button's accessible name. */
76
+ /** The anchor button's accessible name. The popover withholds the button, so
77
+ * nothing draws this or the title below. */
79
78
  formatAnchor: string;
80
79
  /** Its hover title, which says what an anchor is; the name above only says what. */
81
80
  formatAnchorTitle: string;
@@ -138,6 +137,12 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
138
137
  * diagnostic.
139
138
  */
140
139
  export type FormatDiagnostic = (d: Diagnostic) => string | undefined;
140
+ /**
141
+ * A `Mod` chord as the writer's platform prints it: `⌘B` on Apple's, `Ctrl+B`
142
+ * elsewhere. The platform test is the one `prosemirror-keymap` resolves `Mod` by, so a
143
+ * tooltip names the chord the keymap binds.
144
+ */
145
+ export declare function chord(key: string, platform?: string): string;
141
146
  /** The package's English. Every key, so the merge below is total. */
142
147
  export declare const DEFAULT_VISUAL_STRINGS: VisualStrings;
143
148
  /** A consumer's wording: any subset, the rest the package's. */
@@ -4,7 +4,7 @@
4
4
  // makes wording an override rather than a fork.
5
5
  //
6
6
  // Several of these are accessible names rather than decoration — the card
7
- // controls, the add trigger, the required marker — so an untranslated surface is
7
+ // controls, the add trigger — so an untranslated surface is
8
8
  // not merely inconsistent, it reads the wrong language to a screen reader. That is
9
9
  // why this exists as a seam and not as a list of literals to grep for.
10
10
  //
@@ -14,11 +14,19 @@
14
14
  //
15
15
  // The strings reach the tree through context, not props. They are ambient,
16
16
  // read-only, and wanted eight components deep (a card's controls, a field's
17
- // required marker, the formatting popover, the tips card); threading them by hand
17
+ // optional ghost, the formatting popover, the tips card); threading them by hand
18
18
  // would put a `strings` prop on every component between the root and each leaf,
19
19
  // which is a prop that means nothing to any of them.
20
20
  import { getContext, setContext } from 'svelte';
21
21
  import { DEFAULT_SLASH_STRINGS, DEFAULT_TABLE_STRINGS } from '../core/codec/index.js';
22
+ /**
23
+ * A `Mod` chord as the writer's platform prints it: `⌘B` on Apple's, `Ctrl+B`
24
+ * elsewhere. The platform test is the one `prosemirror-keymap` resolves `Mod` by, so a
25
+ * tooltip names the chord the keymap binds.
26
+ */
27
+ export function chord(key, platform = typeof navigator === 'undefined' ? '' : navigator.platform) {
28
+ return /Mac|iP(hone|[oa]d)/.test(platform) ? `⌘${key}` : `Ctrl+${key}`;
29
+ }
22
30
  /** The package's English. Every key, so the merge below is total. */
23
31
  export const DEFAULT_VISUAL_STRINGS = {
24
32
  ...DEFAULT_TABLE_STRINGS,
@@ -27,7 +35,7 @@ export const DEFAULT_VISUAL_STRINGS = {
27
35
  cardMoveDown: 'Move down',
28
36
  cardDelete: 'Delete card',
29
37
  cardTitle: 'Card title',
30
- addCard: '+ Add Card',
38
+ addCard: 'Add card',
31
39
  arrayAdd: '+ Add',
32
40
  arrayRemove: 'Remove',
33
41
  arrayDrag: 'Drag to move',
@@ -41,16 +49,15 @@ export const DEFAULT_VISUAL_STRINGS = {
41
49
  matrixItemTitle: (list, index) => `${list ? `${list} item` : 'Item'} ${index}`,
42
50
  matrixRemove: (title) => `Remove ${title}`,
43
51
  dateToday: 'Today',
44
- fieldRequired: 'required',
45
52
  optionalGhost: 'None',
46
53
  proseHeld: 'Holds more than this field can edit here.',
47
54
  formatGroup: 'Formatting',
48
- formatBold: 'Bold (Mod-B)',
49
- formatEmphasis: 'Emphasis (Mod-I)',
50
- formatUnderline: 'Underline (Mod-U)',
55
+ formatBold: `Bold (${chord('B')})`,
56
+ formatEmphasis: `Italic (${chord('I')})`,
57
+ formatUnderline: `Underline (${chord('U')})`,
51
58
  formatStrikethrough: 'Strikethrough',
52
59
  formatCode: 'Code',
53
- formatLink: 'Link',
60
+ formatLink: `Link (${chord('K')})`,
54
61
  formatAnchor: 'Anchor',
55
62
  formatAnchorTitle: 'Anchor — an identity handle over the selection',
56
63
  linkPlaceholder: 'https://…',
@@ -61,7 +68,7 @@ export const DEFAULT_VISUAL_STRINGS = {
61
68
  tipsDismiss: 'Dismiss',
62
69
  tipsNext: 'Next',
63
70
  tipsDot: (index, count) => `Tip ${index} of ${count}`,
64
- unknownKind: (kind) => `Unrecognized card type ${kind}. Its content is preserved.`,
71
+ unknownKind: (kind) => `Unknown card type ${kind}. Its content is kept.`,
65
72
  retypeLabel: 'Change to',
66
73
  retypePlaceholder: 'Choose a type…',
67
74
  noCardKinds: 'This document declares no card types — delete this card to remove it.',