@quillmark/svelte 0.18.0 → 0.20.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/NOTICE +4 -2
  2. package/dist/core/codec/blocks.d.ts +11 -2
  3. package/dist/core/codec/blocks.js +45 -14
  4. package/dist/core/codec/breaks.d.ts +3 -2
  5. package/dist/core/codec/breaks.js +3 -2
  6. package/dist/core/codec/decode.d.ts +7 -3
  7. package/dist/core/codec/decode.js +34 -8
  8. package/dist/core/codec/field.d.ts +11 -5
  9. package/dist/core/codec/field.js +11 -8
  10. package/dist/core/codec/index.d.ts +1 -1
  11. package/dist/core/codec/index.js +1 -1
  12. package/dist/core/codec/islands.d.ts +3 -1
  13. package/dist/core/codec/islands.js +8 -5
  14. package/dist/core/codec/markdown-paste.d.ts +9 -0
  15. package/dist/core/codec/markdown-paste.js +49 -0
  16. package/dist/core/codec/prose.css +109 -1
  17. package/dist/core/codec/schema.d.ts +8 -2
  18. package/dist/core/codec/schema.js +16 -5
  19. package/dist/core/codec/slash.d.ts +9 -6
  20. package/dist/core/codec/slash.js +0 -0
  21. package/dist/core/codec/table-paste.d.ts +7 -0
  22. package/dist/core/codec/table-paste.js +132 -0
  23. package/dist/core/codec/table-view.d.ts +12 -0
  24. package/dist/core/codec/table-view.js +401 -57
  25. package/dist/core/codec/table.d.ts +38 -16
  26. package/dist/core/codec/table.js +106 -35
  27. package/dist/core/theme.css +1 -1
  28. package/dist/visual/ArrayField.svelte +17 -6
  29. package/dist/visual/ArrayField.svelte.d.ts +1 -1
  30. package/dist/visual/Field.svelte +9 -3
  31. package/dist/visual/Field.svelte.d.ts +1 -1
  32. package/dist/visual/FieldHint.svelte +62 -10
  33. package/dist/visual/FormatPopover.svelte +27 -31
  34. package/dist/visual/FormatPopover.svelte.d.ts +2 -2
  35. package/dist/visual/MatrixAdd.svelte +13 -132
  36. package/dist/visual/MatrixAdd.svelte.d.ts +2 -5
  37. package/dist/visual/MatrixField.svelte +5 -63
  38. package/dist/visual/MatrixField.svelte.d.ts +0 -3
  39. package/dist/visual/ObjectField.svelte +8 -4
  40. package/dist/visual/ObjectField.svelte.d.ts +4 -2
  41. package/dist/visual/ProseField.svelte +2 -1
  42. package/dist/visual/ProseValue.svelte +113 -40
  43. package/dist/visual/ProseValue.svelte.d.ts +8 -4
  44. package/dist/visual/VisualEditorInner.svelte +22 -22
  45. package/dist/visual/VisualEditorInner.svelte.d.ts +2 -2
  46. package/dist/visual/checklists.d.ts +0 -79
  47. package/dist/visual/checklists.js +0 -164
  48. package/dist/visual/guidance.d.ts +15 -0
  49. package/dist/visual/guidance.js +37 -0
  50. package/dist/visual/leaves.d.ts +27 -1
  51. package/dist/visual/leaves.js +46 -0
  52. package/dist/visual/strings.d.ts +3 -8
  53. package/dist/visual/strings.js +0 -3
  54. package/dist/visual/structure.js +1 -1
  55. package/dist/visual/tips.js +1 -1
  56. package/package.json +2 -2
@@ -2,16 +2,18 @@
2
2
  The formatting selection popover (VISUAL_EDITOR §Chrome). One popover,
3
3
  shell-owned, observing the active leaf through
4
4
  `getActiveLeaf`: the VisualEditor's accessor over its `leaves` registry, and
5
- the whole of what this surface knows about the editor. A non-empty selection in
6
- the active leaf raises it over
7
- that leaf's selection rect; each button dispatches a PM `toggleMark` command
8
- straight at the leaf's `EditorView`. The codec's own `dispatchTransaction`
9
- (field.ts, read-only from here) lowers the resulting transaction to
10
- `markOps` and commits via `applyChange`: this component never touches the
11
- content. The keymap mirror (Mod-b/i/u) already lives in the codec's keymap
12
- (field.ts); this is the pointer affordance for all six marks.
5
+ the whole of what this surface knows about the editor. The active leaf is a
6
+ field's or a by-value one (an array's element, a subform's cell): its views either
7
+ way, its controller only for a field's. A non-empty selection in the active leaf
8
+ raises it over that leaf's selection rect; each button dispatches a PM `toggleMark`
9
+ command straight at the leaf's `EditorView`, whose own `dispatchTransaction` commits
10
+ the result as it commits a keystroke: a field's lowers it to `markOps` through
11
+ `applyChange` (field.ts), a by-value leaf's hands the re-encoded value to its parent
12
+ (`ProseValue`). This component never touches the content. The keymap mirror
13
+ (Mod-b/i/u) already lives in the codec's keymap (field.ts); this is the pointer
14
+ affordance for all six marks.
13
15
 
14
- Selection observation. The view's `dispatchTransaction` is the codec's
16
+ Selection observation. The view's `dispatchTransaction` is the leaf's
15
17
  (read-only), so there is no hook to observe transactions from outside.
16
18
  Fallback: a document-level `selectionchange` listener, coalesced to one
17
19
  `requestAnimationFrame`-deferred check (see `deferredSync` below: a bare
@@ -65,21 +67,14 @@
65
67
  import { Popover } from 'bits-ui';
66
68
  import Icon from './icons/Icon.svelte';
67
69
  import type { IconName } from './icons/nodes.js';
68
- import {
69
- hasMarks,
70
- rangeAnchor,
71
- type FieldController,
72
- type LeafViews,
73
- type RangeAnchor
74
- } from '../core/codec/index.js';
70
+ import { hasMarks, rangeAnchor, type RangeAnchor } from '../core/codec/index.js';
71
+ import type { ActiveProse } from './leaves.js';
75
72
  import { clearLink, hrefInSelection, normalizeHref, setLink } from './links.js';
76
73
  import './controls.css';
77
74
 
78
- type LeafWithView = FieldController & Partial<LeafViews>;
79
-
80
75
  interface Props {
81
76
  /** The observation seam VisualEditor exposes over its `leaves` registry. */
82
- getActiveLeaf: () => FieldController | undefined;
77
+ getActiveLeaf: () => ActiveProse | undefined;
83
78
  }
84
79
  let { getActiveLeaf }: Props = $props();
85
80
 
@@ -99,8 +94,8 @@
99
94
  let open = $state(false);
100
95
  /** A pointer is down outside this surface: the selection is still being made. */
101
96
  let pressed = false;
102
- /** Whether the selection is in the field's coordinate space, which an anchor needs
103
- * and a table cell is not (see `sync`). */
97
+ /** Whether an anchor can be minted over the selection: in a field's own view, never
98
+ * in a table cell or a by-value leaf (see `sync`). */
104
99
  let anchorAvailable = $state(true);
105
100
  let linkPromptOpen = $state(false);
106
101
  /** Whether the selection carried a link when the prompt was raised. */
@@ -122,8 +117,7 @@
122
117
  * too, so a selection in a cell raises the same popover and toggles the same
123
118
  * commands; only `anchor` is withheld (below). */
124
119
  function activeLeafView(): EditorView | undefined {
125
- const leaf = getActiveLeaf() as LeafWithView | undefined;
126
- return leaf?.focusedView?.() ?? leaf?.view;
120
+ return getActiveLeaf()?.views.focusedView();
127
121
  }
128
122
 
129
123
  function sync(): void {
@@ -134,8 +128,8 @@
134
128
  // returns early, so a surface frozen here still tracks the selection it covers.
135
129
  if (insidePopover) return;
136
130
  if (pressed) return;
137
- const leaf = getActiveLeaf() as LeafWithView | undefined;
138
- const view = leaf?.focusedView?.() ?? leaf?.view;
131
+ const leaf = getActiveLeaf();
132
+ const view = leaf?.views.focusedView();
139
133
  // A non-empty text selection. A node selection is non-empty too and has nothing
140
134
  // to format: it covers a leaf block (an island, a rule), which is exactly where
141
135
  // Escape out of a table cell lands.
@@ -160,11 +154,13 @@
160
154
  portalTarget = view.dom.closest<HTMLElement>('[data-qm-root]') ?? undefined;
161
155
  const { from, to } = view.state.selection;
162
156
  anchor = rangeAnchor(view, from, to);
163
- // A selection inside a table cell is not in the field's coordinate space at all:
164
- // the field's position map holds one `atom` run for the whole island, so there is
165
- // no USV offset to mint an anchor at. The button is withheld rather than
166
- // disabled, because what it would toggle does not exist there (CODEC §Islands).
167
- anchorAvailable = !!leaf && view === leaf.view;
157
+ // An anchor is minted through a field's controller, which a by-value leaf has
158
+ // none of: its anchors are dropped on the parent's value write. A selection inside
159
+ // a table cell is not in the field's coordinate space at all: the field's position
160
+ // map holds one `atom` run for the whole island, so there is no USV offset to mint
161
+ // an anchor at. The button is withheld rather than disabled, because what it would
162
+ // toggle does not exist there (CODEC §Islands).
163
+ anchorAvailable = !!leaf?.controller && view === leaf.views.view;
168
164
  open = true;
169
165
  }
170
166
 
@@ -283,7 +279,7 @@
283
279
  * the content, its chrome awaiting comment-thread UX.
284
280
  */
285
281
  function toggleAnchor(): void {
286
- const leaf = getActiveLeaf();
282
+ const leaf = getActiveLeaf()?.controller;
287
283
  const view = activeLeafView();
288
284
  if (!leaf || !view || view.state.selection.empty) return;
289
285
  const { from, to } = leaf.selectionRange();
@@ -1,7 +1,7 @@
1
- import { type FieldController } from '../core/codec/index.js';
1
+ import type { ActiveProse } from './leaves.js';
2
2
  interface Props {
3
3
  /** The observation seam VisualEditor exposes over its `leaves` registry. */
4
- getActiveLeaf: () => FieldController | undefined;
4
+ getActiveLeaf: () => ActiveProse | undefined;
5
5
  }
6
6
  declare const FormatPopover: import("svelte").Component<Props, {}, "">;
7
7
  type FormatPopover = ReturnType<typeof FormatPopover>;
@@ -1,76 +1,38 @@
1
1
  <!--
2
- An open matrix's add box (VISUAL_EDITOR §"The matrix"): a combobox whose options are
3
- what the typed words name across the document's checklists, best first, and last the
4
- item they would add to this one. The list stands in flow under the input rather than
5
- floating, so a closing group panel clips nothing of it, and it closes while the focus
6
- is elsewhere. The input keeps the caret throughout and the active option is
7
- `aria-activedescendant`, the combobox pattern: ↑/↓ walk, Enter picks, Escape clears.
8
- Unwalked, the active option is the first result the words name outright, else the add
9
- option: a result named only by abbreviation, which may stand in another card, is
10
- taken by an arrow or a press and never by Enter alone.
2
+ An open matrix's add box (VISUAL_EDITOR §"The matrix"): a plain text input. Enter adds
3
+ what was typed, trimmed, and clears the box; Escape clears it.
11
4
  -->
12
5
  <script lang="ts">
13
6
  import Icon from './icons/Icon.svelte';
14
- import type { AddOption } from './checklists.js';
15
7
  import './controls.css';
16
8
 
17
9
  interface Props {
18
- /** The input's id, and the base its listbox and options derive theirs from. */
19
10
  id: string;
20
11
  /** The input's accessible name and its words at rest. */
21
12
  placeholder: string;
22
- /** The options `query` offers, best first; the add option, where there is one, last. */
23
- options: (query: string) => AddOption[];
24
- onChoose: (option: AddOption, query: string) => void;
13
+ /** Add an item titled `title`, never empty. */
14
+ onAdd: (title: string) => void;
25
15
  }
26
- let { id, placeholder, options, onChoose }: Props = $props();
16
+ let { id, placeholder, onAdd }: Props = $props();
27
17
 
28
- let query = $state('');
29
- /** The walked option, `undefined` until an arrow or the pointer moves it. */
30
- let walked = $state<number | undefined>();
31
- let focused = $state(false);
18
+ let typed = $state('');
32
19
  let inputEl = $state<HTMLInputElement | undefined>();
33
- const shown = $derived(query.trim() ? options(query.trim()) : []);
34
- const open = $derived(focused && shown.length > 0);
35
- const active = $derived.by(() => {
36
- if (walked !== undefined) return Math.min(walked, shown.length - 1);
37
- const strong = shown.findIndex((o) => o.strong);
38
- if (strong >= 0) return strong;
39
- const add = shown.findIndex((o) => o.add);
40
- return add >= 0 ? add : 0;
41
- });
42
- const listId = $derived(`${id}-list`);
43
- const optionId = (k: number): string => `${id}-opt-${k}`;
44
20
 
45
21
  export function focus(): void {
46
22
  inputEl?.focus();
47
23
  }
48
24
 
49
- function choose(k: number): void {
50
- const option = shown[k];
51
- if (!option) return;
52
- const typed = query.trim();
53
- query = '';
54
- walked = undefined;
55
- onChoose(option, typed);
56
- }
57
-
58
25
  function onkeydown(e: KeyboardEvent): void {
59
26
  if (e.isComposing) return;
60
- if (e.key === 'ArrowDown' && open) {
61
- e.preventDefault();
62
- walked = (active + 1) % shown.length;
63
- } else if (e.key === 'ArrowUp' && open) {
64
- e.preventDefault();
65
- walked = (active - 1 + shown.length) % shown.length;
66
- } else if (e.key === 'Enter' && open) {
27
+ if (e.key === 'Enter' && typed.trim()) {
67
28
  e.preventDefault();
68
- choose(active);
69
- } else if (e.key === 'Escape' && query) {
29
+ const title = typed.trim();
30
+ typed = '';
31
+ onAdd(title);
32
+ } else if (e.key === 'Escape' && typed) {
70
33
  e.preventDefault();
71
34
  e.stopPropagation();
72
- query = '';
73
- walked = undefined;
35
+ typed = '';
74
36
  }
75
37
  }
76
38
  </script>
@@ -80,67 +42,21 @@
80
42
  <Icon name="plus" class="qm-matrix-add-glyph" />
81
43
  <input
82
44
  bind:this={inputEl}
45
+ bind:value={typed}
83
46
  {id}
84
47
  class="qm-input qm-focus-ring"
85
48
  type="text"
86
- role="combobox"
87
49
  autocomplete="off"
88
50
  aria-label={placeholder}
89
- aria-autocomplete="list"
90
- aria-expanded={open}
91
- aria-controls={listId}
92
- aria-activedescendant={open ? optionId(active) : undefined}
93
51
  {placeholder}
94
- value={query}
95
- oninput={(e) => {
96
- query = e.currentTarget.value;
97
- walked = undefined;
98
- }}
99
- onfocus={() => (focused = true)}
100
- onblur={() => (focused = false)}
101
52
  {onkeydown}
102
53
  />
103
54
  </span>
104
- <!-- Always mounted, so `aria-controls` names an element at rest too; empty, it draws
105
- nothing. -->
106
- <div
107
- class="qm-menu-surface qm-matrix-add-list"
108
- role="listbox"
109
- id={listId}
110
- aria-label={placeholder}
111
- hidden={!open}
112
- >
113
- {#each shown as option, k (option.key)}
114
- <!-- The input keeps focus and owns the keys; a press picks without taking it. -->
115
- <!-- svelte-ignore a11y_click_events_have_key_events -->
116
- <div
117
- class={['qm-menu-item qm-matrix-add-option', { add: option.add }]}
118
- role="option"
119
- id={optionId(k)}
120
- tabindex="-1"
121
- aria-selected={k === active}
122
- data-highlighted={k === active ? '' : undefined}
123
- onpointerdown={(e) => e.preventDefault()}
124
- onpointermove={() => (walked = k)}
125
- onclick={() => choose(k)}
126
- >
127
- {#if option.add}<Icon name="plus" class="qm-matrix-add-glyph" />{/if}
128
- <span class="qm-matrix-add-title">{option.title}</span>
129
- {#if option.note}<span class="qm-matrix-add-note">{option.note}</span>{/if}
130
- </div>
131
- {/each}
132
- </div>
133
55
  </div>
134
56
 
135
57
  <style>
136
- /* The box's text, and every option's, starts past the glyph slot at its leading edge,
137
- so a result reads in the column the typed words stand in and the add option's `+`
138
- stands under the box's. */
139
58
  .qm-matrix-add {
140
59
  --_slot: calc(var(--_qm-space-2) * 2 + var(--_qm-glyph-control));
141
- display: flex;
142
- flex-direction: column;
143
- gap: var(--_qm-space-half);
144
60
  min-width: 0;
145
61
  }
146
62
  .qm-matrix-add-field {
@@ -172,39 +88,4 @@
172
88
  .qm-matrix-add-field .qm-input:placeholder-shown:not(:focus):hover {
173
89
  background: var(--_qm-surface-hover);
174
90
  }
175
- /* The list is a menu, raised over the card rather than cut into it, so its options
176
- read as choices and not as a second field. Its edge and inset come off each
177
- option's, which keeps the column. */
178
- .qm-matrix-add-list {
179
- display: flex;
180
- flex-direction: column;
181
- }
182
- .qm-matrix-add-list[hidden] {
183
- display: none;
184
- }
185
- .qm-matrix-add-option {
186
- position: relative;
187
- display: flex;
188
- align-items: baseline;
189
- justify-content: space-between;
190
- gap: var(--_qm-space-2);
191
- padding-inline-start: calc(var(--_slot) - var(--_qm-space-half) - var(--_qm-border-width));
192
- font-size: var(--_qm-text-body);
193
- color: var(--_qm-ink);
194
- }
195
- .qm-matrix-add-option :global(.qm-matrix-add-glyph) {
196
- inset-inline-start: calc(var(--_qm-space-2) - var(--_qm-space-half) - var(--_qm-border-width));
197
- inset-block-start: 50%;
198
- translate: 0 -50%;
199
- }
200
- /* The add option reads at the label ink: it offers a new item, where a result names
201
- one the document already knows. */
202
- .qm-matrix-add-option.add {
203
- color: var(--_qm-ink-label);
204
- }
205
- .qm-matrix-add-note {
206
- flex-shrink: 0;
207
- font-size: var(--_qm-text-label);
208
- color: var(--_qm-ink-label);
209
- }
210
91
  </style>
@@ -1,12 +1,9 @@
1
- import type { AddOption } from './checklists.js';
2
1
  interface Props {
3
- /** The input's id, and the base its listbox and options derive theirs from. */
4
2
  id: string;
5
3
  /** The input's accessible name and its words at rest. */
6
4
  placeholder: string;
7
- /** The options `query` offers, best first; the add option, where there is one, last. */
8
- options: (query: string) => AddOption[];
9
- onChoose: (option: AddOption, query: string) => void;
5
+ /** Add an item titled `title`, never empty. */
6
+ onAdd: (title: string) => void;
10
7
  }
11
8
  declare const MatrixAdd: import("svelte").Component<Props, {
12
9
  focus: () => void;
@@ -10,8 +10,9 @@
10
10
 
11
11
  An open matrix's added items follow the roster in id order, as the plate prints them:
12
12
  held by being present, so each draws a fixed tick, its `title` as an input, a remove,
13
- and the same columns. The add box closes the list ({@link MatrixAdd}): it searches
14
- every checklist of the document before it offers to add what was typed.
13
+ and the same columns. The add box closes the list ({@link MatrixAdd}): what is typed
14
+ there is added under an id minted from it past every id the list holds, so a title the
15
+ list already carries is a second item.
15
16
 
16
17
  The control owns its label row, as an array does: the count of held items sits
17
18
  where an array's add chip sits, so {@link Field} skips its own label for it.
@@ -48,15 +49,7 @@
48
49
  onRoster,
49
50
  schemaAt
50
51
  } from './structure.js';
51
- import {
52
- checklists,
53
- matrixPrinted,
54
- searchChecklists,
55
- type AddOption,
56
- type Checklist,
57
- type ChecklistHit,
58
- type Searchable
59
- } from './checklists.js';
52
+ import { matrixPrinted } from './checklists.js';
60
53
  import { splitDeep, unrouted, type DeepDiagnostic } from './diagnostics.js';
61
54
  import type { LandingBox } from './leaves.js';
62
55
  import { propertyDomIds } from './domid.js';
@@ -85,9 +78,6 @@
85
78
  /** The field's control id: the base a member's tick and columns derive their own
86
79
  * names from, one `-m-<member id>` segment down. */
87
80
  idBase?: string;
88
- /** The field's leaf key, where it is a card's own field: how the add box tells
89
- * this list from the rest of the document's (`checklists.ts`). */
90
- listKey?: string;
91
81
  /** The boundary's nested content read, rooted at this field: a member's content
92
82
  * column reads at `[id, column]` (`reader.getContentAt`). */
93
83
  contentAt: (path: PathStep[]) => Content | undefined;
@@ -104,16 +94,11 @@
104
94
  labelId,
105
95
  descriptionId,
106
96
  idBase,
107
- listKey,
108
97
  contentAt,
109
98
  onCommit,
110
99
  diagnostics
111
100
  }: Props = $props();
112
101
 
113
- // A card's own matrix searches the document's lists; one nested in a container is
114
- // not indexed, and searches its own items alone.
115
- const index = checklists();
116
- const others = (): readonly Checklist[] => (listKey != null ? (index?.list() ?? []) : []);
117
102
  const members = $derived(matrixMembers(schema.members));
118
103
  const compact = $derived(!!schema.ui?.compact);
119
104
  const hasColumns = $derived(Object.keys(schema.properties ?? {}).length > 0);
@@ -220,48 +205,6 @@
220
205
  }
221
206
 
222
207
  // ── The add box ────────────────────────────────────────────────────────────
223
- /** A result's list: this one, or another of the document's. */
224
- type Hit = ChecklistHit<Checklist | undefined>;
225
- function options(query: string): AddOption[] {
226
- const own: Searchable<Checklist | undefined> = { list: undefined, schema, value: map };
227
- const lists: Searchable<Checklist | undefined>[] = others()
228
- .filter((l) => l.key !== listKey)
229
- .map((l) => ({ list: l, schema: l.schema, value: l.value }));
230
- const hits = searchChecklists(query, [own, ...lists]);
231
- const out: AddOption[] = hits.map((h, k) => ({
232
- key: `hit-${k}`,
233
- title: h.title,
234
- note:
235
- [
236
- h.list ? t.strings.matrixIn(h.list.label) : undefined,
237
- h.held ? t.strings.matrixHeldTag : undefined
238
- ]
239
- .filter(Boolean)
240
- .join(' · ') || undefined,
241
- strong: h.strong,
242
- payload: h
243
- }));
244
- // Offered last, and not where an item of this list already carries the words.
245
- const fold = (s: string) => s.trim().toLowerCase();
246
- const dup = hits.some((h) => h.list === undefined && fold(h.title) === fold(query));
247
- if (!dup) out.push({ key: 'add', title: t.strings.matrixAddNew(query), add: true });
248
- return out;
249
- }
250
- async function choose(option: AddOption, query: string): Promise<void> {
251
- if (option.add) return add(query);
252
- const hit = option.payload as Hit;
253
- const other = hit.list;
254
- if (other) {
255
- if (!hit.held) other.commit(commitMember(other.value, hit.id, memberWrite({})));
256
- index?.land(`${other.path}.${hit.id}`);
257
- return;
258
- }
259
- if (!hit.held) commit(commitMember(map, hit.id, ticked(hit.id)));
260
- await flush();
261
- focusPath([hit.id]);
262
- const box = ref(memberEls, hit.id);
263
- if (box) bloomInside(box);
264
- }
265
208
  async function add(title: string): Promise<void> {
266
209
  const taken = (id: string) => onRoster(schema, id) || (map != null && Object.hasOwn(map, id));
267
210
  const id = addedId(title, taken);
@@ -397,8 +340,7 @@
397
340
  bind:this={addEl}
398
341
  id={`${base}-add`}
399
342
  placeholder={t.strings.matrixAdd(label ?? '')}
400
- {options}
401
- onChoose={choose}
343
+ onAdd={add}
402
344
  />
403
345
  {/if}
404
346
  <DiagnosticList diagnostics={foot} />
@@ -18,9 +18,6 @@ interface Props {
18
18
  /** The field's control id: the base a member's tick and columns derive their own
19
19
  * names from, one `-m-<member id>` segment down. */
20
20
  idBase?: string;
21
- /** The field's leaf key, where it is a card's own field: how the add box tells
22
- * this list from the rest of the document's (`checklists.ts`). */
23
- listKey?: string;
24
21
  /** The boundary's nested content read, rooted at this field: a member's content
25
22
  * column reads at `[id, column]` (`reader.getContentAt`). */
26
23
  contentAt: (path: PathStep[]) => Content | undefined;
@@ -81,8 +81,10 @@
81
81
  /**
82
82
  * The parent control's DOM id: the base each property's own three names derive
83
83
  * from ({@link propertyDomIds}). Absent — a subform mounted with no field around
84
- * it, or a table's row — the properties fall back to `aria-label`, which names
85
- * them without a `for` target to click.
84
+ * it — the properties fall back to `aria-label`, which names them without a
85
+ * `for` target to click. A table's row takes the table's own and names its
86
+ * cells the same fallback way, reading only the description off it: the
87
+ * header parks one per column, which every row's cell in it points at.
86
88
  */
87
89
  idBase?: string;
88
90
  /** Accessible-name prefix used only on the `aria-label` fallback above. */
@@ -268,7 +270,8 @@
268
270
  {@const kind = controlKind(sub)}
269
271
  {@const ids = idBase && !bare ? propertyDomIds(idBase, key) : undefined}
270
272
  {@const named = ids ? undefined : fallbackName(key, sub)}
271
- {@const describes = sub.description && ids ? ids.description : undefined}
273
+ {@const describes =
274
+ sub.description && idBase ? propertyDomIds(idBase, key).description : undefined}
272
275
  {@const deep = routed.below.get(key)}
273
276
  {@const own = isContainer(kind) ? undefined : deep?.map((d) => d.diagnostic)}
274
277
  <!-- `for` reaches the labelable controls, a variant through its discriminant; the
@@ -359,6 +362,7 @@
359
362
  <ProseValue
360
363
  bind:this={proseEls[key]}
361
364
  content={() => contentAt([key])}
365
+ value={obj[key]}
362
366
  fallback={contentDefault(sub)}
363
367
  plaintext={baseType(sub) === 'plaintext'}
364
368
  placeholder={noneOf(key, sub)}
@@ -376,7 +380,7 @@
376
380
  under an open row costs is two rungs of the ladder and one figure. -->
377
381
  <ArrayField
378
382
  bind:this={nestedEls[key]}
379
- value={obj[key] as unknown[] | undefined}
383
+ value={obj[key]}
380
384
  fallback={Array.isArray(sub.default) ? sub.default : undefined}
381
385
  items={sub.items}
382
386
  layout={arrayLayout(sub)}
@@ -14,8 +14,10 @@ interface Props {
14
14
  /**
15
15
  * The parent control's DOM id: the base each property's own three names derive
16
16
  * from ({@link propertyDomIds}). Absent — a subform mounted with no field around
17
- * it, or a table's row — the properties fall back to `aria-label`, which names
18
- * them without a `for` target to click.
17
+ * it — the properties fall back to `aria-label`, which names them without a
18
+ * `for` target to click. A table's row takes the table's own and names its
19
+ * cells the same fallback way, reading only the description off it: the
20
+ * header parks one per column, which every row's cell in it points at.
19
21
  */
20
22
  idBase?: string;
21
23
  /** Accessible-name prefix used only on the `aria-label` fallback above. */
@@ -123,6 +123,7 @@
123
123
  placeholder,
124
124
  placeholderUntilEdit,
125
125
  tableStrings: () => t.strings,
126
+ slashStrings: () => t.strings,
126
127
  onSlash: (next) => {
127
128
  slash = next;
128
129
  },
@@ -155,7 +156,7 @@
155
156
  controller?.setFallback(fallback);
156
157
  });
157
158
 
158
- // Only a narrowed or a plain leaf can hold, so only one of them asks.
159
+ // Only a narrowed or a plain leaf holds by what a re-validation says, so only one of them asks.
159
160
  $effect(() => {
160
161
  void diagnostics;
161
162
  if (inline || plaintext) controller?.applyExternal();