@quillmark/svelte 0.16.0 → 0.17.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.
@@ -30,7 +30,9 @@
30
30
  member's id. So a cell reads at whatever depth it sits, which nothing here knows.
31
31
 
32
32
  `bare` is the table's row: the same cells with no labels and no figure of their own,
33
- laid onto the tracks the table's header names ({@link ArrayField}).
33
+ laid onto the tracks the table's header names ({@link ArrayField}). `flat` is
34
+ `ui.layout: flat`: the cells keep their labels and lose the figure, standing at the
35
+ depth of whatever holds the subform.
34
36
  -->
35
37
  <script lang="ts">
36
38
  import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
@@ -39,6 +41,7 @@
39
41
  baseType,
40
42
  controlKind,
41
43
  declaredContent,
44
+ flatObject,
42
45
  humanize,
43
46
  isContainer,
44
47
  mountKey,
@@ -93,6 +96,9 @@
93
96
  /** A table's row: the cells alone, on the tracks the table's header names, with
94
97
  * no labels and no vertical of their own. */
95
98
  bare?: boolean;
99
+ /** `ui.layout: flat`: no vertical and no rung, the properties at the depth of the
100
+ * list the subform stands in. */
101
+ flat?: boolean;
96
102
  /** Raw keydown off a text or prose cell, for a row whose own keys run through its
97
103
  * cells: the table's Enter and Backspace ({@link ArrayField}). */
98
104
  onCellKey?: (e: KeyboardEvent, key: string) => void;
@@ -111,6 +117,7 @@
111
117
  contentAt,
112
118
  onCommit,
113
119
  bare = false,
120
+ flat = false,
114
121
  onCellKey,
115
122
  diagnostics
116
123
  }: Props = $props();
@@ -250,6 +257,7 @@
250
257
  bind:this={rootEl}
251
258
  class="qm-object"
252
259
  class:qm-object-bare={bare}
260
+ class:qm-object-flat={flat}
253
261
  role="group"
254
262
  aria-labelledby={labelledBy}
255
263
  aria-describedby={describedBy}
@@ -392,6 +400,7 @@
392
400
  bind:this={nestedEls[key]}
393
401
  value={obj[key] as Record<string, unknown> | undefined}
394
402
  properties={sub.properties}
403
+ flat={flatObject(sub)}
395
404
  label={ids ? undefined : fallbackName(key, sub)}
396
405
  idBase={ids?.control}
397
406
  labelledBy={ids?.label}
@@ -471,6 +480,8 @@
471
480
  }
472
481
  .qm-object:not(.qm-object-bare) {
473
482
  container-type: inline-size;
483
+ }
484
+ .qm-object:not(.qm-object-bare, .qm-object-flat) {
474
485
  border-inline-start: var(--_qm-vertical-width) solid var(--_qm-border);
475
486
  padding-inline-start: var(--_qm-nest);
476
487
  padding-block: var(--_qm-space);
@@ -481,7 +492,14 @@
481
492
  .qm-object-grid {
482
493
  display: grid;
483
494
  grid-template-columns: repeat(var(--cols), 1fr);
484
- gap: var(--_qm-space-2);
495
+ column-gap: var(--_qm-space-2);
496
+ --row-gap: var(--_qm-space-2);
497
+ row-gap: var(--row-gap);
498
+ }
499
+ /* Flat, the rows keep the rhythm of the list the subform stands in: a section's
500
+ (`Card.svelte`) or an enclosing subform's, inherited rather than restated. */
501
+ .qm-object-flat > .qm-object-grid {
502
+ --row-gap: inherit;
485
503
  }
486
504
  /* A property measures like a field: two tracks over the subform's own rows, so a
487
505
  label wrapping in one column leaves its control on the row's baseline rather than a
@@ -546,10 +564,9 @@
546
564
  grid-row: auto;
547
565
  grid-template-rows: none;
548
566
  row-gap: var(--_qm-space-half);
549
- /* A row is one line of controls, so a cell shorter than the box beside it stands
550
- on that line: a switch held at the top of a row a text box set the height of
551
- reads as a cell that slipped its track. */
552
- align-items: center;
567
+ /* A row reads along its first line: a prose cell that wraps grows downward, and
568
+ the cells beside it stay on the line it started on. */
569
+ align-items: start;
553
570
  }
554
571
  /* What the row could not hand down stands across it: a diagnostic in the first
555
572
  column alone would read as that column's. */
@@ -31,6 +31,9 @@ interface Props {
31
31
  /** A table's row: the cells alone, on the tracks the table's header names, with
32
32
  * no labels and no vertical of their own. */
33
33
  bare?: boolean;
34
+ /** `ui.layout: flat`: no vertical and no rung, the properties at the depth of the
35
+ * list the subform stands in. */
36
+ flat?: boolean;
34
37
  /** Raw keydown off a text or prose cell, for a row whose own keys run through its
35
38
  * cells: the table's Enter and Backspace ({@link ArrayField}). */
36
39
  onCellKey?: (e: KeyboardEvent, key: string) => void;
@@ -81,6 +81,7 @@
81
81
  import { createLeafRegistry, type FieldControl } from './leaves.js';
82
82
  import { reorder, reorderArm, reorderTrips } from './motion.js';
83
83
  import { tipsChannel } from './tips.js';
84
+ import { cardChecklists, setChecklists } from './checklists.js';
84
85
  import { patchEditorExt } from './ext.js';
85
86
  import Card from './Card.svelte';
86
87
  import TipsCard from './TipsCard.svelte';
@@ -661,6 +662,20 @@
661
662
  };
662
663
  });
663
664
 
665
+ // The document's checklists, which an open matrix's add box searches whole
666
+ // (`checklists.ts`): off the model, so a list in a closed group or another card is
667
+ // reached without being mounted.
668
+ const lists = $derived(
669
+ cardChecklists(
670
+ [model.main, ...model.cards],
671
+ (c) => c.titleOverride || c.titlePlaceholder,
672
+ (c, field) => pathFor(makeAddr(c.id, c.isMain, field)),
673
+ (c, field) => fieldKeyToString({ card: c.isMain ? undefined : c.id, field }),
674
+ (c, field, next) => commitScalar(c.id, c.isMain, field, next)
675
+ )
676
+ );
677
+ setChecklists({ list: () => lists, land: (path) => void focusField(path) });
678
+
664
679
  // ── Public entry points ─────────────────────────────────────────────────────
665
680
  /**
666
681
  * Resolve a preview {@link Landing} to a mounted leaf and land in it.
@@ -0,0 +1,82 @@
1
+ import type { QuillFieldSchema } from '@quillmark/wasm';
2
+ import type { DocPath } from '../core/address.js';
3
+ import { type CardModel, type MatrixMember } from './structure.js';
4
+ /** One matrix of the document, as the add box reaches it. */
5
+ export interface Checklist {
6
+ /** The field's leaf key: how a mounted matrix tells itself from the rest. */
7
+ key: string;
8
+ /** The field's path, the prefix a member's landing address extends. */
9
+ path: DocPath;
10
+ /** What a result from this list reads under: the field's label, and the card's
11
+ * where the field is not the main card's. */
12
+ label: string;
13
+ schema: QuillFieldSchema;
14
+ /** The held set as it prints: the stored map, else the declared `default:`. */
15
+ value: Record<string, unknown> | undefined;
16
+ /** Commit the whole map, as the field's own control does. */
17
+ commit: (next: Record<string, unknown> | undefined) => void;
18
+ }
19
+ export interface ChecklistIndex {
20
+ list: () => readonly Checklist[];
21
+ /** Reveal and focus a member by its address. */
22
+ land: (path: DocPath) => void;
23
+ }
24
+ export declare function setChecklists(index: ChecklistIndex): void;
25
+ /** The index under a mounted editor; `undefined` off-tree. */
26
+ export declare function checklists(): ChecklistIndex | undefined;
27
+ /** The value a matrix prints: the stored map, else a mapping `default:`. */
28
+ export declare function matrixPrinted(value: unknown, schema: QuillFieldSchema): Record<string, unknown> | undefined;
29
+ /** Every card-level matrix of the cards, in document order. */
30
+ export declare function cardChecklists(cards: readonly CardModel[], cardLabel: (card: CardModel) => string, path: (card: CardModel, field: string) => DocPath | undefined, leafKey: (card: CardModel, field: string) => string, commit: (card: CardModel, field: string, next: Record<string, unknown> | undefined) => void): Checklist[];
31
+ /** Every item a matrix can show: the roster in declaration order, then the items its
32
+ * map adds, in id order. */
33
+ export declare function checklistItems(schema: QuillFieldSchema, value: Record<string, unknown> | undefined): MatrixMember[];
34
+ /** One option of an add box. */
35
+ export interface AddOption {
36
+ /** Stable across one query's options: the option element's id suffix. */
37
+ key: string;
38
+ title: string;
39
+ /** Words after the title: the list a result stands in, its held state. */
40
+ note?: string;
41
+ /** The option that adds what was typed, rather than a result. */
42
+ add?: boolean;
43
+ /** A result the typed words name outright, which Enter takes unarrowed: an add box
44
+ * whose best result is weaker takes its add option instead. */
45
+ strong?: boolean;
46
+ /** What the owner reads back off a chosen option. */
47
+ payload?: unknown;
48
+ }
49
+ /** One search result: an item of some checklist, `list` naming which. */
50
+ export interface ChecklistHit<L> {
51
+ list: L;
52
+ id: string;
53
+ title: string;
54
+ held: boolean;
55
+ score: number;
56
+ /** Every query word claimed a title word whole or as its opening. */
57
+ strong: boolean;
58
+ }
59
+ /**
60
+ * How well `query` names `title`, or `undefined` for not at all. Each query word claims
61
+ * a title word — whole, as its opening, or as an abbreviation of it (`flt cc` names
62
+ * `Flight CC`) — the stronger claims taken first, so `c cc` names `CC Candidate`; a
63
+ * word left over may claim a run of words whose openings it spells (`fcc`, `jqo`). The
64
+ * title the query spells outright ranks first, then one it opens.
65
+ */
66
+ export declare function matchTitle(query: string, title: string): {
67
+ score: number;
68
+ strong: boolean;
69
+ } | undefined;
70
+ /** {@link matchTitle}'s score, `0` for no match. */
71
+ export declare function matchScore(query: string, title: string): number;
72
+ /** What a search reads of one list: its items and their ticks. */
73
+ export interface Searchable<L> {
74
+ list: L;
75
+ schema: QuillFieldSchema;
76
+ value: Record<string, unknown> | undefined;
77
+ }
78
+ /**
79
+ * The items across `lists` that `query` names, best first; a tie keeps the order of
80
+ * `lists`, then of each list's items.
81
+ */
82
+ export declare function searchChecklists<L>(query: string, lists: readonly Searchable<L>[], limit?: number): ChecklistHit<L>[];
@@ -0,0 +1,171 @@
1
+ /**
2
+ * The document's checklists, and the search an open matrix's add box runs over them
3
+ * (VISUAL_EDITOR §"The matrix"). The editor root publishes every card-level matrix off
4
+ * the model it derives, so a list in a collapsed group or another card is searched
5
+ * as surely as the one the author is typing in. A matrix mounted off-tree, or nested
6
+ * in a container, is not indexed and searches its own items alone.
7
+ */
8
+ import { getContext, setContext } from 'svelte';
9
+ import { addedItems, matrixHeld, matrixMembers, memberValue } from './structure.js';
10
+ const KEY = Symbol('qm-checklists');
11
+ export function setChecklists(index) {
12
+ setContext(KEY, index);
13
+ }
14
+ /** The index under a mounted editor; `undefined` off-tree. */
15
+ export function checklists() {
16
+ return getContext(KEY);
17
+ }
18
+ /** The value a matrix prints: the stored map, else a mapping `default:`. */
19
+ export function matrixPrinted(value, schema) {
20
+ const map = (v) => typeof v === 'object' && v !== null && !Array.isArray(v)
21
+ ? v
22
+ : undefined;
23
+ return value == null ? map(schema.default) : map(value);
24
+ }
25
+ /** Every card-level matrix of the cards, in document order. */
26
+ export function cardChecklists(cards, cardLabel, path, leafKey, commit) {
27
+ const out = [];
28
+ for (const card of cards) {
29
+ if (card.unschemable)
30
+ continue;
31
+ for (const section of card.sections)
32
+ for (const f of section.fields) {
33
+ if (f.control !== 'matrix')
34
+ continue;
35
+ const at = path(card, f.name);
36
+ if (at == null)
37
+ continue;
38
+ out.push({
39
+ key: leafKey(card, f.name),
40
+ path: at,
41
+ label: card.isMain ? f.label : `${f.label} · ${cardLabel(card)}`,
42
+ schema: f.schema,
43
+ value: matrixPrinted(card.values[f.name], f.schema),
44
+ commit: (next) => commit(card, f.name, next)
45
+ });
46
+ }
47
+ }
48
+ return out;
49
+ }
50
+ /** Every item a matrix can show: the roster in declaration order, then the items its
51
+ * map adds, in id order. */
52
+ export function checklistItems(schema, value) {
53
+ return [...matrixMembers(schema.members), ...addedItems(schema, value)];
54
+ }
55
+ /** Lowercase, unaccented words, any script: `Exec / Aide / CAG` → `exec aide cag`. */
56
+ function words(s) {
57
+ return s
58
+ .normalize('NFKD')
59
+ .replace(/[\u0300-\u036f]/g, '')
60
+ .toLowerCase()
61
+ .split(/[^\p{L}\p{N}]+/u)
62
+ .filter(Boolean);
63
+ }
64
+ /** `flt` in `flight`: every letter in order, the first letters equal. */
65
+ function abbreviates(token, word) {
66
+ if (token[0] !== word[0])
67
+ return false;
68
+ let i = 0;
69
+ for (const c of word)
70
+ if (c === token[i])
71
+ i++;
72
+ return i === token.length;
73
+ }
74
+ /** How a query word claims one title word: whole (3), as its opening (2), as an
75
+ * abbreviation (1), or not (0). */
76
+ function claim(token, word) {
77
+ return word === token ? 3 : word.startsWith(token) ? 2 : abbreviates(token, word) ? 1 : 0;
78
+ }
79
+ /** The end of the run of free words from `i` whose openings spell `token` in order —
80
+ * `fcc` over `flight cc`, `jqo` over `joint qualified officer` — or `-1`. */
81
+ function runFrom(token, w, free, i) {
82
+ if (i >= w.length || !free[i])
83
+ return -1;
84
+ for (let k = Math.min(token.length, w[i].length); k > 0; k--) {
85
+ if (!w[i].startsWith(token.slice(0, k)))
86
+ continue;
87
+ if (k === token.length)
88
+ return i + 1;
89
+ const end = runFrom(token.slice(k), w, free, i + 1);
90
+ if (end >= 0)
91
+ return end;
92
+ }
93
+ return -1;
94
+ }
95
+ /**
96
+ * How well `query` names `title`, or `undefined` for not at all. Each query word claims
97
+ * a title word — whole, as its opening, or as an abbreviation of it (`flt cc` names
98
+ * `Flight CC`) — the stronger claims taken first, so `c cc` names `CC Candidate`; a
99
+ * word left over may claim a run of words whose openings it spells (`fcc`, `jqo`). The
100
+ * title the query spells outright ranks first, then one it opens.
101
+ */
102
+ export function matchTitle(query, title) {
103
+ const q = words(query);
104
+ const w = words(title);
105
+ if (!q.length || !w.length)
106
+ return undefined;
107
+ const free = w.map(() => true);
108
+ const left = q.map(() => true);
109
+ let score = 0;
110
+ let strong = true;
111
+ for (const level of [3, 2, 1])
112
+ q.forEach((token, t) => {
113
+ if (!left[t])
114
+ return;
115
+ const at = w.findIndex((word, i) => free[i] && claim(token, word) === level);
116
+ if (at < 0)
117
+ return;
118
+ free[at] = left[t] = false;
119
+ score += level;
120
+ if (level === 1)
121
+ strong = false;
122
+ });
123
+ for (let t = 0; t < q.length; t++) {
124
+ if (!left[t])
125
+ continue;
126
+ let found = false;
127
+ for (let i = 0; i < w.length && !found; i++) {
128
+ const end = runFrom(q[t], w, free, i);
129
+ if (end - i < 2)
130
+ continue;
131
+ for (let k = i; k < end; k++)
132
+ free[k] = false;
133
+ found = true;
134
+ }
135
+ if (!found)
136
+ return undefined;
137
+ score += 1;
138
+ strong = false;
139
+ }
140
+ const typed = q.join(' ');
141
+ const whole = w.join(' ');
142
+ return { score: score + (whole === typed ? 3 : whole.startsWith(typed) ? 1 : 0), strong };
143
+ }
144
+ /** {@link matchTitle}'s score, `0` for no match. */
145
+ export function matchScore(query, title) {
146
+ return matchTitle(query, title)?.score ?? 0;
147
+ }
148
+ /**
149
+ * The items across `lists` that `query` names, best first; a tie keeps the order of
150
+ * `lists`, then of each list's items.
151
+ */
152
+ export function searchChecklists(query, lists, limit = 8) {
153
+ const hits = [];
154
+ for (const { list, schema, value } of lists)
155
+ for (const item of checklistItems(schema, value)) {
156
+ const match = matchTitle(query, item.title);
157
+ if (match)
158
+ hits.push({
159
+ list,
160
+ id: item.id,
161
+ title: item.title,
162
+ held: matrixHeld(memberValue(value, item.id)),
163
+ ...match
164
+ });
165
+ }
166
+ return hits
167
+ .map((h, i) => [h, i])
168
+ .sort(([a, i], [b, j]) => b.score - a.score || i - j)
169
+ .slice(0, limit)
170
+ .map(([h]) => h);
171
+ }
@@ -45,8 +45,22 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
45
45
  /** The tag on the unset sentinel's row, which is what tells it from the member it
46
46
  * ghosts ({@link EnumField}). */
47
47
  enumUnsetTag: string;
48
- /** The count in the matrix's label row: how many of the roster are held. */
48
+ /** The count in the matrix's label row: how many of its items are held. */
49
49
  matrixHeld: (held: number, total: number) => string;
50
+ /** An open matrix's add box at rest, naming the list it adds to: `Add to Staff…`. The box
51
+ * draws its own `+`. `list` is empty for a matrix mounted with no label. */
52
+ matrixAdd: (list: string) => string;
53
+ /** The add box's last option, which adds what was typed as an item of this list. */
54
+ matrixAddNew: (title: string) => string;
55
+ /** Beside a result from another list: the list it would be ticked in. */
56
+ matrixIn: (list: string) => string;
57
+ /** Beside a result already held: picking it lands on it and ticks nothing. */
58
+ matrixHeldTag: string;
59
+ /** An added item's title input, named for the list it stands in and its 1-based
60
+ * place among the added items; also what an untitled item is called. */
61
+ matrixItemTitle: (list: string, index: number) => string;
62
+ /** An added item's remove control, named for the item. */
63
+ matrixRemove: (title: string) => string;
50
64
  /** The date field's toggle, pressed while the field holds `today`: the render
51
65
  * date rather than a written one. */
52
66
  dateToday: string;
@@ -37,6 +37,12 @@ export const DEFAULT_VISUAL_STRINGS = {
37
37
  elementUntitled: (label, index) => `${label} ${index}`,
38
38
  enumUnsetTag: 'default',
39
39
  matrixHeld: (held, total) => `${held} of ${total} held`,
40
+ matrixAdd: (list) => (list ? `Add to ${list}…` : 'Add…'),
41
+ matrixAddNew: (title) => `Add “${title}”`,
42
+ matrixIn: (list) => `in ${list}`,
43
+ matrixHeldTag: 'held',
44
+ matrixItemTitle: (list, index) => `${list ? `${list} item` : 'Item'} ${index}`,
45
+ matrixRemove: (title) => `Remove ${title}`,
40
46
  dateToday: 'Today',
41
47
  fieldRequired: 'required',
42
48
  optionalGhost: 'None',
@@ -35,7 +35,13 @@ export interface GroupSection {
35
35
  group: string | undefined;
36
36
  label: string;
37
37
  fields: FieldModel[];
38
+ /** The field the section's header names in place of a label of its own: the one
39
+ * field of a group section, where that field is {@link flatObject}. */
40
+ head?: FieldModel;
38
41
  }
42
+ /** `ui.layout: flat` on a typed dictionary: no frame of its own, its properties
43
+ * standing at the depth of the fields around it rather than a rung in. */
44
+ export declare function flatObject(schema: QuillFieldSchema): boolean;
39
45
  /**
40
46
  * One card instance projected for rendering: the schema × payload join result
41
47
  * the VisualEditor's `$derived` produces and hands to `<Card>`. Positional
@@ -177,11 +183,11 @@ export declare function controlKind(f: QuillFieldSchema): ControlKind;
177
183
  * crosses inside untyped container data with no type to read it off, so a literal here
178
184
  * would be a second spelling of a reserved key. No variant may declare it. */
179
185
  export declare const VARIANT_DISCRIMINANT: "value";
180
- /** The tick cell of a matrix member, the second reserved key beside the discriminant.
181
- * The boundary synthesizes it on every member and exports no constant for it, so this
182
- * is where the package spells it once (canon `SCHEMAS.md` §Matrix). */
183
- export declare const MATRIX_HELD = "held";
184
- /** One member of the roster: its id and its title. */
186
+ /** The label key of an open matrix's added item, the one cell a roster member never
187
+ * stores. The boundary exports no constant for it, so the package spells it once
188
+ * (canon `SCHEMAS.md` §Matrix). */
189
+ export declare const MATRIX_TITLE = "title";
190
+ /** One item of a matrix: its id and its title. */
185
191
  export interface MatrixMember {
186
192
  id: string;
187
193
  title: string;
@@ -193,39 +199,60 @@ export declare function matrixMembers(roster: Record<string, string> | undefined
193
199
  * spelled `constructor`. */
194
200
  export declare function memberValue(map: Record<string, unknown> | undefined, id: string): unknown;
195
201
  /**
196
- * Whether a stored member is held. A bare scalar is the tick and a mapping is the member
197
- * object, whose `held` cell resolves as every absent cell does, to its `default:` of
198
- * `false` (canon `SCHEMAS.md` §Matrix): an absent key is unheld, a bare `true` is held,
199
- * and a member object is held only where its `held` cell says so. The spellings the
200
- * engine coerces to false — `false`, `0`, `"false"`, `null` — read false here; every other
201
- * present scalar reads held, so a document the engine would refuse still draws the tick
202
- * its spelling claims.
202
+ * Whether a stored member is held: by being present. A mapping is held whatever it
203
+ * carries, `{}` included, and a bare scalar is the tick itself, read as the engine's
204
+ * coercion reads it: `false`, `0`, `"false"` and `null` unheld, every other present
205
+ * scalar held, so a spelling the engine would refuse still draws the tick it claims.
203
206
  */
204
207
  export declare function matrixHeld(stored: unknown): boolean;
205
- /** A member's columns: the member object with its `held` cell taken out, `{}` for the
206
- * bare and absent spellings. What the columns subform takes as its value. */
208
+ /** A member's columns: its mapping without the reserved keys, `{}` for a bare tick or an
209
+ * absent member. What the columns subform takes as its value, so an edit writes back
210
+ * no `held` a document stored (`validation::held_stored`). */
207
211
  export declare function matrixColumns(stored: unknown): Record<string, unknown>;
212
+ /** What a matrix holding nothing commits: the unset field where unset prints nothing
213
+ * held, and the authored `{}` where it would print a `default:` or `none` instead. */
214
+ export declare function matrixEmpty(matrix: QuillFieldSchema): Record<string, unknown> | undefined;
208
215
  /**
209
216
  * The map after one member moves: `next` written under `id`, or the key dropped where
210
217
  * `next` is `undefined`; a map left holding nothing is an unset field. Every other
211
- * member rides through in the spelling the document had, so a stored `cyber_200: true`
212
- * stays `true` while its neighbour is edited.
218
+ * member rides through in the spelling the document had.
213
219
  */
214
- export declare function commitMember(map: Record<string, unknown> | undefined, id: string, next: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
220
+ export declare function commitMember(map: Record<string, unknown> | undefined, id: string, next: unknown): Record<string, unknown> | undefined;
215
221
  /**
216
- * The member a tick or an edit writes. Held with columns is the member object with an
217
- * explicit `held: true`; unheld keeps the columns under `held: false`, so tick, type,
218
- * untick, retick loses nothing; unheld with no columns is `undefined`, the key dropped.
219
- * Which columns are blank is the engine's rule and not restated here: a column control
220
- * that clears drops its key (`ObjectField`), so a member emptied by hand arrives with no
221
- * columns and only then leaves the map.
222
+ * The held member a tick or an edit writes: its columns, the bare tick `true` where
223
+ * it answers none, and an added item's `title` beside them. Unticking is removal
224
+ * ({@link commitMember} with `undefined`), never a write.
222
225
  */
223
- export declare function memberWrite(held: boolean, columns: Record<string, unknown>): Record<string, unknown> | undefined;
224
- /** The `object` a matrix member desugars to, for the schema walk: `held` beside the
225
- * declared columns. Composed here because the boundary serializes the roster alone. */
226
- export declare function matrixMemberSchema(matrix: QuillFieldSchema): QuillFieldSchema;
226
+ export declare function memberWrite(columns: Record<string, unknown>, title?: string): unknown;
227
+ /** Whether `id` is spelled as a matrix id: snake_case, the rule the roster's ids and
228
+ * an added item's key share. */
229
+ export declare function matrixId(id: string): boolean;
227
230
  /** Whether `id` is on the roster. */
231
+ export declare function onRoster(matrix: QuillFieldSchema, id: string): boolean;
232
+ /**
233
+ * Whether a step under the matrix names a member, judged on the schema alone as an
234
+ * index step is: a roster id, or on an open matrix any key spelled as an id, the added
235
+ * item it would be. So an unheld member's address resolves.
236
+ */
228
237
  export declare function matrixDeclares(matrix: QuillFieldSchema, id: string): boolean;
238
+ /**
239
+ * The added items a stored map holds, sorted by id as the plate orders them: on an
240
+ * open matrix, each key off the roster spelled as an id whose mapping carries a
241
+ * `title`. Any other key off the roster is the engine's refusal, drawn nowhere and
242
+ * ridden through.
243
+ */
244
+ export declare function addedItems(matrix: QuillFieldSchema, map: Record<string, unknown> | undefined): MatrixMember[];
245
+ /**
246
+ * The id an item added under `title` takes: the title folded to snake_case, prefixed
247
+ * where it opens on no letter, and suffixed past every id `taken` names, the roster's
248
+ * and the document's alike. The id is the wire's and never drawn, so a later retitle
249
+ * leaves it where it is.
250
+ */
251
+ export declare function addedId(title: string, taken: (id: string) => boolean): string;
252
+ /** The `object` a matrix member is, for the schema walk: the declared columns, and an
253
+ * added item's `title` beside them. Composed here because the boundary serializes the
254
+ * roster and the columns alone. */
255
+ export declare function matrixMemberSchema(matrix: QuillFieldSchema, id: string): QuillFieldSchema;
229
256
  /**
230
257
  * The schema at `steps` under `field`, or `undefined` where the schema declares no such
231
258
  * place: an index under an `array`, a key under an `object`'s properties, a member id
@@ -248,7 +275,7 @@ export type ArrayLayout = 'list' | 'table';
248
275
  * The layout an array takes: `'table'` where the array's own `ui.layout` asks for it
249
276
  * and every cell of the row is short (a block prose cell declines it, at load width and
250
277
  * every width after), else `'list'`. That every column is a leaf is the loader's
251
- * contract (`quill::table_column_not_flat`); whether a leaf fits a cell is the surface's
278
+ * contract (`quill::table_column_not_leaf`); whether a leaf fits a cell is the surface's
252
279
  * answer (canon `SCHEMAS.md`): a table composes by position, so a row that would stack
253
280
  * inside a cell is not one.
254
281
  */
@@ -273,9 +300,10 @@ export declare function commitDiscriminant(value: Record<string, unknown> | unde
273
300
  /**
274
301
  * Whether the schema obliges a cell: `default:`'s absence, which is the whole of the
275
302
  * obligation (DOCUMENT_MODEL). Exempt are the cells whose absence of one says nothing:
276
- * a typed dictionary and a matrix, since a namespace declares no `default:` at all and
277
- * `validate` anchors obligation on the leaves under it; and an optional cell, which can
278
- * declare none, its unanswered render being `none`.
303
+ * a typed dictionary, since a namespace declares no `default:` at all and `validate`
304
+ * anchors obligation on the leaves under it; a matrix, whose blank is nothing held, a
305
+ * whole answer; and an optional cell, which can declare none, its unanswered render
306
+ * being `none`.
279
307
  */
280
308
  export declare function obliged(schema: QuillFieldSchema): boolean;
281
309
  /** `foo_bar` → `Foo bar`: the label fallback when a field declares no `title`. */