@quillmark/svelte 0.12.0 → 0.14.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 (46) hide show
  1. package/NOTICE +3 -2
  2. package/dist/core/codec/decode.d.ts +17 -0
  3. package/dist/core/codec/decode.js +57 -4
  4. package/dist/core/codec/field.d.ts +63 -15
  5. package/dist/core/codec/field.js +201 -65
  6. package/dist/core/codec/index.d.ts +3 -3
  7. package/dist/core/codec/index.js +3 -3
  8. package/dist/core/codec/keymap.d.ts +5 -4
  9. package/dist/core/codec/keymap.js +5 -4
  10. package/dist/core/codec/prose.css +42 -0
  11. package/dist/core/codec/schema.d.ts +24 -5
  12. package/dist/core/codec/schema.js +40 -9
  13. package/dist/core/errors.d.ts +3 -2
  14. package/dist/core/theme.css +9 -6
  15. package/dist/preview/paint.js +21 -17
  16. package/dist/visual/ArrayField.svelte +66 -22
  17. package/dist/visual/ArrayField.svelte.d.ts +2 -0
  18. package/dist/visual/BooleanField.svelte +28 -6
  19. package/dist/visual/BooleanField.svelte.d.ts +2 -0
  20. package/dist/visual/Card.svelte +4 -3
  21. package/dist/visual/DateField.svelte +158 -40
  22. package/dist/visual/DateField.svelte.d.ts +5 -2
  23. package/dist/visual/EnumField.svelte +27 -9
  24. package/dist/visual/EnumField.svelte.d.ts +5 -0
  25. package/dist/visual/Field.svelte +48 -11
  26. package/dist/visual/FormatPopover.svelte +4 -3
  27. package/dist/visual/MatrixField.svelte +3 -2
  28. package/dist/visual/NumberField.svelte +73 -29
  29. package/dist/visual/NumberField.svelte.d.ts +5 -1
  30. package/dist/visual/ObjectField.svelte +63 -15
  31. package/dist/visual/ProseField.svelte +53 -23
  32. package/dist/visual/ProseField.svelte.d.ts +15 -3
  33. package/dist/visual/ProseValue.svelte +102 -46
  34. package/dist/visual/ProseValue.svelte.d.ts +17 -5
  35. package/dist/visual/TextField.svelte +35 -18
  36. package/dist/visual/TextField.svelte.d.ts +7 -0
  37. package/dist/visual/VariantField.svelte +1 -0
  38. package/dist/visual/VisualEditorInner.svelte +24 -24
  39. package/dist/visual/controls.css +16 -5
  40. package/dist/visual/icons/nodes.d.ts +28 -0
  41. package/dist/visual/icons/nodes.js +14 -1
  42. package/dist/visual/strings.d.ts +11 -1
  43. package/dist/visual/strings.js +3 -0
  44. package/dist/visual/structure.d.ts +77 -43
  45. package/dist/visual/structure.js +138 -64
  46. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import { type PathStep, type PayloadItem, type QuillCardSchema, type QuillFieldSchema, type ResolvedField, type Resolved } from '@quillmark/wasm';
1
+ import { type Content, type PathStep, type PayloadItem, type QuillCardSchema, type QuillFieldSchema, type QuillFieldType, type ResolvedField, type Resolved } from '@quillmark/wasm';
2
2
  /** The control a field type maps to (VISUAL_EDITOR §"Structure mirrors the schema"). */
3
3
  export type ControlKind = 'prose' | 'text' | 'enum' | 'variant' | 'number' | 'boolean' | 'date' | 'array' | 'object' | 'matrix';
4
4
  /** The container controls: the ones a nested address walks into and a deep
@@ -14,7 +14,7 @@ export interface FieldModel {
14
14
  /** `ui.compact`: asks to share a row with adjacent compacts. A request, not a
15
15
  * guarantee: `placeFields` declines it for the shapes that grow (see `packable`). */
16
16
  compact: boolean;
17
- /** Display label: `ui.title` when set, else the humanized field name. */
17
+ /** Display label: `title` when set, else the humanized field name. */
18
18
  label: string;
19
19
  /** Schema `description`: authoring help rendered beside the label,
20
20
  * undefined when the field declares none. Chrome-only; never gates. */
@@ -56,7 +56,8 @@ export interface CardModel {
56
56
  unschemable: boolean;
57
57
  /** Raw `$ext.editor.title` override (composable cards). */
58
58
  titleOverride: string;
59
- /** Schema-resolved title used as the rename placeholder (composable cards). */
59
+ /** The header an unrenamed card shows ({@link cardTitle} below its rename rung),
60
+ * which the rename input ghosts (composable cards). */
60
61
  titlePlaceholder: string;
61
62
  /** Field name → current stored value (absent fields missing). */
62
63
  values: Record<string, unknown>;
@@ -70,12 +71,11 @@ export interface CardModel {
70
71
  sections: GroupSection[];
71
72
  hasBody: boolean;
72
73
  /**
73
- * The empty-body ghost, or undefined when the card renders no body. Never
74
- * empty for a card that does: the resolved body `default:` when there is one,
75
- * else the consumer's wording, else the built-in invitation
76
- * ({@link resolveBodyGhost}). A body leaf therefore always has something in it
77
- * to write into, which the inline fields deliberately do not: their ghost is
78
- * the resolved default, and an invented one would read as a value.
74
+ * The empty-body ghost, or undefined when the card renders no body. Never empty
75
+ * for a card that does ({@link resolveBodyGhost}), so a body leaf always has
76
+ * something in it to write into, which a field at rest deliberately does not: it
77
+ * draws what prints or what its quill declares, and an invented ghost would read as
78
+ * a value.
79
79
  */
80
80
  bodyGhost?: string;
81
81
  }
@@ -104,15 +104,14 @@ export declare const NO_RESOLVED_ROWS: ResolvedCardRows;
104
104
  * form).
105
105
  */
106
106
  export declare function resolvedByCardIndex(resolved: Resolved | undefined): Map<number, ResolvedCardRows>;
107
- /** The ghost a field shows when unset: the resolved `default:` value the render
108
- * would use (`source === 'default'`), else undefined: an `authored` field shows
109
- * its value, a `zero` field has no default to ghost. */
107
+ /** What an unset field shows of its `default:`: the resolved value the render would
108
+ * use (`source === 'default'`), else undefined: an `authored` field shows its value,
109
+ * a `blank` field has no default to show. */
110
110
  export declare function ghostDefault(row: ResolvedField | undefined): unknown;
111
111
  /** A ghost value's string form, or undefined for null/object (only text ghosts
112
- * render a placeholder). The one text-ghost projection: a scalar field's
113
- * placeholder and a body leaf's both read `stringifyGhost ∘
114
- * ghostDefault`, since a richtext body resolves to a text render: the correct
115
- * thing to display as a placeholder (FIELD_PROVENANCE). */
112
+ * render a placeholder). A scalar's resolved default reads through it; a content
113
+ * field's resolves as `Content`, which a prose leaf reads by its text instead
114
+ * ({@link titleText}). */
116
115
  export declare function stringifyGhost(ghost: unknown): string | undefined;
117
116
  /** What a {@link BodyPlaceholder} is told about the body it words. The card's
118
117
  * identity, not its chrome: `cardId` is what per-card wording keys off, `kind` keys
@@ -139,14 +138,53 @@ export interface BodyPlaceholderContext {
139
138
  */
140
139
  export type BodyPlaceholder = (ctx: BodyPlaceholderContext) => string | undefined;
141
140
  /**
142
- * The empty body's ghost: the resolved body `default:`, else the consumer's
143
- * wording, else the flat built-in. The `default:` wins because it is the only one
144
- * of the three that describes the render: it promises what prints if nothing is
145
- * written, and wording placed over it would make that promise unreadable. The
146
- * other two are invitations, and an invitation belongs only where there is no
147
- * promise.
141
+ * The empty body's ghost: the resolved body `default:`, else the kind's
142
+ * `body.example`, else the consumer's wording, else the flat built-in. The
143
+ * `default:` wins because it is the only one that describes the render: it promises
144
+ * what prints if nothing is written, and wording placed over it would make that
145
+ * promise unreadable. The rest are invitations, and an invitation belongs only where
146
+ * there is no promise; the quill's own comes first, being the one an agent working
147
+ * from the blueprint sees too.
148
148
  */
149
- export declare function resolveBodyGhost(resolvedDefault: string | undefined, custom: string | undefined, builtIn: string): string;
149
+ export declare function resolveBodyGhost(resolvedDefault: string | undefined, example: string | undefined, custom: string | undefined, builtIn: string): string;
150
+ /** Whether a cell is optional: a `t?` type, which renders `none` unanswered and so
151
+ * never declares a `default:` (canon `SCHEMAS.md` §"Optional cells"). */
152
+ export declare function optionalCell(f: QuillFieldSchema): boolean;
153
+ /** The type a cell's control reads: the declared one, an optional cell's `?` stripped.
154
+ * The `?` moves the render floor and nothing else, so every read of a type is this. */
155
+ export declare function baseType(f: QuillFieldSchema): QuillFieldType;
156
+ /**
157
+ * A value the schema declares, as ghost text: a `default:`, an `example:` or a
158
+ * `body.example`. Markdown — a `richtext` value — ghosts as the text it renders, one
159
+ * block to a line, as a resolved default does ({@link titleText}); anything else as
160
+ * itself. `undefined` for a blank or a non-scalar.
161
+ */
162
+ export declare function declaredGhost(v: unknown, markdown: boolean): string | undefined;
163
+ /**
164
+ * A scalar `default:` as the text an unset control holds, where it prints: as
165
+ * declared, `undefined` for none, a blank or a non-scalar. A default of spaces prints
166
+ * nothing a reader sees, so a control holds none and draws its example instead.
167
+ */
168
+ export declare function printedText(v: unknown): string | undefined;
169
+ /**
170
+ * A declared content literal as the `Content` it holds: a `richtext` one's markdown
171
+ * imported, a `plaintext` one's text taken literally, one paragraph whose later lines
172
+ * continue it, as the literal codec reads a stored one. What a cell's static
173
+ * `default:` stands in its leaf as, where no resolved row reaches it. `undefined` for
174
+ * a blank or a non-scalar.
175
+ */
176
+ export declare function declaredContent(v: unknown, markdown: boolean): Content | undefined;
177
+ /**
178
+ * The `example:` an unset free-text cell ghosts: a `string`, `plaintext` or `richtext`
179
+ * whose `default:` prints nothing. Every other type takes none. A default that prints
180
+ * dominates, standing in the control as the value it is; a type-empty one, the
181
+ * skippable marker, prints nothing and leaves the example the only thing to draw.
182
+ */
183
+ export declare function exampleGhost(f: QuillFieldSchema): string | undefined;
184
+ /** A field's `{#each}` key: its name and what a mounted leaf reads once, its type and
185
+ * `inline` and its elements'. A retype or a quill swap that changes any of them under a
186
+ * name remounts the field, where a kept leaf would commit on the old schema. */
187
+ export declare function mountKey(name: string, f: QuillFieldSchema): string;
150
188
  /** Map a field schema to its control (precedence: prose › enum › text › …).
151
189
  * An array's element control is this over `items`: a missing `items` is a text
152
190
  * element. */
@@ -214,15 +252,11 @@ export declare function matrixDeclares(matrix: QuillFieldSchema, id: string): bo
214
252
  */
215
253
  export declare function schemaAt(field: QuillFieldSchema, steps: readonly PathStep[]): QuillFieldSchema | undefined;
216
254
  /** Whether a cell is one line tall by declaration: the shape a table column and a row
217
- * summary can hold. A `plaintext` leaf is inline at the codec whatever it declares. */
255
+ * summary can hold. A prose cell is one where it declares `inline`, `richtext` or
256
+ * `plaintext` alike. */
218
257
  export declare function shortCell(sub: QuillFieldSchema): boolean;
219
- /**
220
- * A collapsed row's own words: `items.ui.title` interpolated with the row's values
221
- * where the schema declares one, else the first short text cell in declaration order
222
- * — a `string`, or an inline `richtext` / `plaintext` — read through {@link titleText};
223
- * `undefined` while the row has nothing to say for itself. `title` sits on `items.ui`,
224
- * describing a row, where `layout` sits on the array's own `ui`, describing the array.
225
- */
258
+ /** A collapsed row's own words ({@link firstShortText}); `undefined` while the row has
259
+ * nothing to say for itself. */
226
260
  export declare function rowSummary(items: QuillFieldSchema | undefined, row: unknown): string | undefined;
227
261
  /** How an `array` draws its elements. */
228
262
  export type ArrayLayout = 'list' | 'table';
@@ -254,12 +288,13 @@ export declare function variantCells(schema: QuillFieldSchema, member: string |
254
288
  export declare function commitDiscriminant(value: Record<string, unknown> | undefined, member: string | undefined): Record<string, unknown> | undefined;
255
289
  /**
256
290
  * Whether the schema obliges a cell: `default:`'s absence, which is the whole of the
257
- * obligation (DOCUMENT_MODEL). A typed dictionary and a matrix are exempt — a namespace
258
- * declares no `default:` at all, and `validate` anchors obligation on the leaves under
259
- * it, so reading one off the container would mark every subform required.
291
+ * obligation (DOCUMENT_MODEL). Exempt are the cells whose absence of one says nothing:
292
+ * a typed dictionary and a matrix, since a namespace declares no `default:` at all and
293
+ * `validate` anchors obligation on the leaves under it; and an optional cell, which can
294
+ * declare none, its unanswered render being `none`.
260
295
  */
261
296
  export declare function obliged(schema: QuillFieldSchema): boolean;
262
- /** `foo_bar` → `Foo bar`: the label fallback when a field declares no `ui.title`. */
297
+ /** `foo_bar` → `Foo bar`: the label fallback when a field declares no `title`. */
263
298
  export declare function humanize(name: string): string;
264
299
  /** Project a card schema's `fields` map (declaration = key order) into models. */
265
300
  export declare function fieldModels(cardSchema: QuillCardSchema): FieldModel[];
@@ -319,20 +354,19 @@ export interface PlacedField {
319
354
  export declare function placeFields(fields: FieldModel[]): PlacedField[];
320
355
  /** A card's field values by key, off its payload items. */
321
356
  export declare function fieldValues(items: readonly PayloadItem[]): Record<string, unknown>;
322
- /** Interpolate a `{field}` card-title template against live field values. */
323
- export declare function interpolateTitle(template: string, values: Record<string, unknown>): string;
324
357
  /** A field value as title text. A parsed field rests as the authored string and a
325
358
  * committed one as `Content`, whose `text` is the same words; a scalar reads as
326
359
  * itself; a variant container reads as its discriminant member, the one cell of it
327
360
  * that names the world; any other container as nothing. */
328
361
  export declare function titleText(v: unknown): string;
329
- /** The field names a `{field}` title reads. */
330
- export declare function titleFields(template: string | undefined): string[];
362
+ /** A card kind's name: its schema `title`, else the humanized kind. What names a kind
363
+ * before there is an instance of it: the add menu, the retype select. */
364
+ export declare function kindTitle(cardSchema: QuillCardSchema | undefined, kind: string): string;
331
365
  /**
332
- * Resolve a card instance's header title: the per-instance `$ext.editor.title`
333
- * override wins; else the schema `ui.title` (literal or `{field}` template
334
- * interpolated); else the humanized kind. Empty overrides fall through so a
335
- * cleared rename reverts to the schema title.
366
+ * A card instance's header: the per-instance `$ext.editor.title` rename; else the
367
+ * kind's schema `title`, which names every instance alike; else the instance's own
368
+ * first short text cell ({@link firstShortText}); else the humanized kind. Empty
369
+ * renames fall through, so a cleared rename reverts to what the schema says.
336
370
  */
337
371
  export declare function cardTitle(cardSchema: QuillCardSchema | undefined, kind: string, values: Record<string, unknown>, extTitle: string | undefined): string;
338
372
  /** Whether a card kind renders a body leaf: gated by `body.enabled !== false`. */
@@ -1,10 +1,11 @@
1
1
  // The schema × payload join, done by the editor (VISUAL_EDITOR §Structure). Pure
2
2
  // functions only (no runes, no Document reads) so the ordering, control
3
- // dispatch, group layout, title interpolation, and session-identity bookkeeping
3
+ // dispatch, group layout, titles, ghosts, and session-identity bookkeeping
4
4
  // are unit-testable in isolation (tests/visual/structure.test.ts). The reactive
5
5
  // orchestration (revision counter, live doc reads) lives in VisualEditor.svelte;
6
6
  // this module is the projection math it feeds.
7
7
  import { VARIANT_DISCRIMINANT_KEY } from '@quillmark/wasm';
8
+ import { core } from '../core/lifecycle.js';
8
9
  /** The container controls: the ones a nested address walks into and a deep
9
10
  * diagnostic routes through. */
10
11
  export function isContainer(kind) {
@@ -33,36 +34,112 @@ export const NO_RESOLVED_ROWS = { fields: [], body: null };
33
34
  export function resolvedByCardIndex(resolved) {
34
35
  return new Map((resolved?.cards ?? []).map((c) => [c.index, { fields: c.fields, body: c.body }]));
35
36
  }
36
- /** The ghost a field shows when unset: the resolved `default:` value the render
37
- * would use (`source === 'default'`), else undefined: an `authored` field shows
38
- * its value, a `zero` field has no default to ghost. */
37
+ /** What an unset field shows of its `default:`: the resolved value the render would
38
+ * use (`source === 'default'`), else undefined: an `authored` field shows its value,
39
+ * a `blank` field has no default to show. */
39
40
  export function ghostDefault(row) {
40
41
  return row?.source === 'default' ? row.value : undefined;
41
42
  }
42
43
  /** A ghost value's string form, or undefined for null/object (only text ghosts
43
- * render a placeholder). The one text-ghost projection: a scalar field's
44
- * placeholder and a body leaf's both read `stringifyGhost ∘
45
- * ghostDefault`, since a richtext body resolves to a text render: the correct
46
- * thing to display as a placeholder (FIELD_PROVENANCE). */
44
+ * render a placeholder). A scalar's resolved default reads through it; a content
45
+ * field's resolves as `Content`, which a prose leaf reads by its text instead
46
+ * ({@link titleText}). */
47
47
  export function stringifyGhost(ghost) {
48
48
  return ghost != null && typeof ghost !== 'object' ? String(ghost) : undefined;
49
49
  }
50
50
  /**
51
- * The empty body's ghost: the resolved body `default:`, else the consumer's
52
- * wording, else the flat built-in. The `default:` wins because it is the only one
53
- * of the three that describes the render: it promises what prints if nothing is
54
- * written, and wording placed over it would make that promise unreadable. The
55
- * other two are invitations, and an invitation belongs only where there is no
56
- * promise.
51
+ * The empty body's ghost: the resolved body `default:`, else the kind's
52
+ * `body.example`, else the consumer's wording, else the flat built-in. The
53
+ * `default:` wins because it is the only one that describes the render: it promises
54
+ * what prints if nothing is written, and wording placed over it would make that
55
+ * promise unreadable. The rest are invitations, and an invitation belongs only where
56
+ * there is no promise; the quill's own comes first, being the one an agent working
57
+ * from the blueprint sees too.
57
58
  */
58
- export function resolveBodyGhost(resolvedDefault, custom, builtIn) {
59
- return resolvedDefault || custom || builtIn;
59
+ export function resolveBodyGhost(resolvedDefault, example, custom, builtIn) {
60
+ return resolvedDefault || example || custom || builtIn;
61
+ }
62
+ /** Whether a cell is optional: a `t?` type, which renders `none` unanswered and so
63
+ * never declares a `default:` (canon `SCHEMAS.md` §"Optional cells"). */
64
+ export function optionalCell(f) {
65
+ return f.type.endsWith('?');
66
+ }
67
+ /** The type a cell's control reads: the declared one, an optional cell's `?` stripped.
68
+ * The `?` moves the render floor and nothing else, so every read of a type is this. */
69
+ export function baseType(f) {
70
+ return (optionalCell(f) ? f.type.slice(0, -1) : f.type);
71
+ }
72
+ /**
73
+ * A value the schema declares, as ghost text: a `default:`, an `example:` or a
74
+ * `body.example`. Markdown — a `richtext` value — ghosts as the text it renders, one
75
+ * block to a line, as a resolved default does ({@link titleText}); anything else as
76
+ * itself. `undefined` for a blank or a non-scalar.
77
+ */
78
+ export function declaredGhost(v, markdown) {
79
+ const text = stringifyGhost(v)?.trim();
80
+ if (!text)
81
+ return undefined;
82
+ return (markdown ? core().importMarkdown(text).text.trim() : text) || undefined;
83
+ }
84
+ /**
85
+ * A scalar `default:` as the text an unset control holds, where it prints: as
86
+ * declared, `undefined` for none, a blank or a non-scalar. A default of spaces prints
87
+ * nothing a reader sees, so a control holds none and draws its example instead.
88
+ */
89
+ export function printedText(v) {
90
+ const text = stringifyGhost(v);
91
+ return text?.trim() ? text : undefined;
92
+ }
93
+ /**
94
+ * A declared content literal as the `Content` it holds: a `richtext` one's markdown
95
+ * imported, a `plaintext` one's text taken literally, one paragraph whose later lines
96
+ * continue it, as the literal codec reads a stored one. What a cell's static
97
+ * `default:` stands in its leaf as, where no resolved row reaches it. `undefined` for
98
+ * a blank or a non-scalar.
99
+ */
100
+ export function declaredContent(v, markdown) {
101
+ const text = printedText(v);
102
+ if (text === undefined)
103
+ return undefined;
104
+ if (markdown)
105
+ return core().importMarkdown(text);
106
+ return {
107
+ text,
108
+ lines: text
109
+ .split('\n')
110
+ .map((_, i) => i === 0
111
+ ? { containers: [], kind: 'para' }
112
+ : { containers: [], kind: 'para', continues: true }),
113
+ marks: [],
114
+ islands: []
115
+ };
116
+ }
117
+ /**
118
+ * The `example:` an unset free-text cell ghosts: a `string`, `plaintext` or `richtext`
119
+ * whose `default:` prints nothing. Every other type takes none. A default that prints
120
+ * dominates, standing in the control as the value it is; a type-empty one, the
121
+ * skippable marker, prints nothing and leaves the example the only thing to draw.
122
+ */
123
+ export function exampleGhost(f) {
124
+ const kind = controlKind(f);
125
+ if (kind !== 'text' && kind !== 'prose')
126
+ return undefined;
127
+ const markdown = baseType(f) === 'richtext';
128
+ if (declaredGhost(f.default, markdown) !== undefined)
129
+ return undefined;
130
+ return declaredGhost(f.example, markdown);
131
+ }
132
+ /** A field's `{#each}` key: its name and what a mounted leaf reads once, its type and
133
+ * `inline` and its elements'. A retype or a quill swap that changes any of them under a
134
+ * name remounts the field, where a kept leaf would commit on the old schema. */
135
+ export function mountKey(name, f) {
136
+ return `${name}\u0000${f.type}\u0000${f.inline === true}\u0000${f.items?.type ?? ''}\u0000${f.items?.inline === true}`;
60
137
  }
61
138
  /** Map a field schema to its control (precedence: prose › enum › text › …).
62
139
  * An array's element control is this over `items`: a missing `items` is a text
63
140
  * element. */
64
141
  export function controlKind(f) {
65
- switch (f.type) {
142
+ switch (baseType(f)) {
66
143
  case 'richtext':
67
144
  case 'plaintext':
68
145
  return 'prose';
@@ -237,37 +314,34 @@ function stepInto(schema, step) {
237
314
  }
238
315
  // ── Row summaries and layouts ────────────────────────────────────────────────
239
316
  /** Whether a cell is one line tall by declaration: the shape a table column and a row
240
- * summary can hold. A `plaintext` leaf is inline at the codec whatever it declares. */
317
+ * summary can hold. A prose cell is one where it declares `inline`, `richtext` or
318
+ * `plaintext` alike. */
241
319
  export function shortCell(sub) {
242
320
  const kind = controlKind(sub);
243
321
  if (kind === 'prose')
244
- return sub.type === 'plaintext' || !!sub.inline;
322
+ return !!sub.inline;
245
323
  return (kind === 'text' || kind === 'enum' || kind === 'number' || kind === 'boolean' || kind === 'date');
246
324
  }
247
325
  /**
248
- * A collapsed row's own words: `items.ui.title` interpolated with the row's values
249
- * where the schema declares one, else the first short text cell in declaration order
250
- * — a `string`, or an inline `richtext` / `plaintext` — read through {@link titleText};
251
- * `undefined` while the row has nothing to say for itself. `title` sits on `items.ui`,
252
- * describing a row, where `layout` sits on the array's own `ui`, describing the array.
326
+ * The first short text cell's words — the first `string`, or inline `richtext` /
327
+ * `plaintext`, in declaration order — read through {@link titleText}; `undefined` where
328
+ * there is no such cell or it is empty. The cell is picked by the schema alone, so an
329
+ * instance is named by one cell whatever the others hold. What names an instance from
330
+ * its own values: a collapsed row, and a card whose kind declares no `title`.
253
331
  */
254
- export function rowSummary(items, row) {
255
- const values = (row ?? {});
256
- const template = items?.ui?.title;
257
- if (template && template.trim()) {
258
- const shown = interpolateTitle(template, values).trim();
259
- if (shown)
260
- return shown;
261
- }
262
- for (const [k, sub] of Object.entries(items?.properties ?? {})) {
332
+ function firstShortText(cells, values) {
333
+ const named = Object.entries(cells ?? {}).find(([, sub]) => {
263
334
  const kind = controlKind(sub);
264
- if (kind !== 'text' && !(kind === 'prose' && shortCell(sub)))
265
- continue;
266
- const text = titleText(ownValue(values, k)).trim();
267
- if (text)
268
- return text;
269
- }
270
- return undefined;
335
+ return kind === 'text' || (kind === 'prose' && shortCell(sub));
336
+ });
337
+ if (!named)
338
+ return undefined;
339
+ return titleText(ownValue(values, named[0])).trim() || undefined;
340
+ }
341
+ /** A collapsed row's own words ({@link firstShortText}); `undefined` while the row has
342
+ * nothing to say for itself. */
343
+ export function rowSummary(items, row) {
344
+ return firstShortText(items?.properties, (row ?? {}));
271
345
  }
272
346
  /**
273
347
  * The layout an array takes: `'table'` where the array's own `ui.layout` asks for it
@@ -316,14 +390,18 @@ export function commitDiscriminant(value, member) {
316
390
  }
317
391
  /**
318
392
  * Whether the schema obliges a cell: `default:`'s absence, which is the whole of the
319
- * obligation (DOCUMENT_MODEL). A typed dictionary and a matrix are exempt — a namespace
320
- * declares no `default:` at all, and `validate` anchors obligation on the leaves under
321
- * it, so reading one off the container would mark every subform required.
393
+ * obligation (DOCUMENT_MODEL). Exempt are the cells whose absence of one says nothing:
394
+ * a typed dictionary and a matrix, since a namespace declares no `default:` at all and
395
+ * `validate` anchors obligation on the leaves under it; and an optional cell, which can
396
+ * declare none, its unanswered render being `none`.
322
397
  */
323
398
  export function obliged(schema) {
324
- return schema.type !== 'object' && schema.type !== 'matrix' && schema.default === undefined;
399
+ return (schema.type !== 'object' &&
400
+ schema.type !== 'matrix' &&
401
+ !optionalCell(schema) &&
402
+ schema.default === undefined);
325
403
  }
326
- /** `foo_bar` → `Foo bar`: the label fallback when a field declares no `ui.title`. */
404
+ /** `foo_bar` → `Foo bar`: the label fallback when a field declares no `title`. */
327
405
  export function humanize(name) {
328
406
  const spaced = name.replace(/_/g, ' ').trim();
329
407
  return spaced ? spaced[0].toUpperCase() + spaced.slice(1) : spaced;
@@ -336,11 +414,11 @@ export function fieldModels(cardSchema) {
336
414
  control: controlKind(schema),
337
415
  group: schema.ui?.group,
338
416
  compact: !!schema.ui?.compact,
339
- label: schema.ui?.title ?? humanize(name),
417
+ label: schema.title ?? humanize(name),
340
418
  description: schema.description,
341
419
  required: obliged(schema),
342
420
  inline: !!schema.inline,
343
- plaintext: schema.type === 'plaintext'
421
+ plaintext: baseType(schema) === 'plaintext'
344
422
  }));
345
423
  }
346
424
  /**
@@ -485,11 +563,7 @@ export function fieldValues(items) {
485
563
  values[p.key] = p.value;
486
564
  return values;
487
565
  }
488
- /** Interpolate a `{field}` card-title template against live field values. */
489
- export function interpolateTitle(template, values) {
490
- return template.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, name) => titleText(ownValue(values, name)));
491
- }
492
- /** A value by own key: a template or a declaration can name `constructor`. */
566
+ /** A value by own key: a declaration can name `constructor`. */
493
567
  function ownValue(values, name) {
494
568
  return Object.hasOwn(values, name) ? values[name] : undefined;
495
569
  }
@@ -508,25 +582,25 @@ export function titleText(v) {
508
582
  const member = v[VARIANT_DISCRIMINANT];
509
583
  return typeof member === 'string' ? member : '';
510
584
  }
511
- /** The field names a `{field}` title reads. */
512
- export function titleFields(template) {
513
- if (!template)
514
- return [];
515
- return [...template.matchAll(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g)].map((m) => m[1]);
585
+ /** A card kind's name: its schema `title`, else the humanized kind. What names a kind
586
+ * before there is an instance of it: the add menu, the retype select. */
587
+ export function kindTitle(cardSchema, kind) {
588
+ const t = cardSchema?.title;
589
+ return t && t.trim() ? t : humanize(kind);
516
590
  }
517
591
  /**
518
- * Resolve a card instance's header title: the per-instance `$ext.editor.title`
519
- * override wins; else the schema `ui.title` (literal or `{field}` template
520
- * interpolated); else the humanized kind. Empty overrides fall through so a
521
- * cleared rename reverts to the schema title.
592
+ * A card instance's header: the per-instance `$ext.editor.title` rename; else the
593
+ * kind's schema `title`, which names every instance alike; else the instance's own
594
+ * first short text cell ({@link firstShortText}); else the humanized kind. Empty
595
+ * renames fall through, so a cleared rename reverts to what the schema says.
522
596
  */
523
597
  export function cardTitle(cardSchema, kind, values, extTitle) {
524
598
  if (extTitle && extTitle.trim())
525
599
  return extTitle;
526
- const t = cardSchema?.ui?.title;
600
+ const t = cardSchema?.title;
527
601
  if (t && t.trim())
528
- return interpolateTitle(t, values);
529
- return humanize(kind);
602
+ return t;
603
+ return firstShortText(cardSchema?.fields, values) ?? humanize(kind);
530
604
  }
531
605
  /** Whether a card kind renders a body leaf: gated by `body.enabled !== false`. */
532
606
  export function bodyEnabled(cardSchema) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/svelte",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Editor, live-preview and source surfaces over a Quillmark WASM session.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -66,7 +66,7 @@
66
66
  "check": "svelte-check --tsconfig ./tsconfig.json"
67
67
  },
68
68
  "peerDependencies": {
69
- "@quillmark/wasm": ">=0.115.0-0",
69
+ "@quillmark/wasm": ">=0.116.0-0",
70
70
  "svelte": "^5.0.0"
71
71
  },
72
72
  "dependencies": {