@quillmark/svelte 0.10.1 → 0.11.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 (58) hide show
  1. package/README.md +1 -1
  2. package/THEMING.md +2 -2
  3. package/dist/core/codec/blocks.js +21 -1
  4. package/dist/core/codec/breaks.d.ts +10 -2
  5. package/dist/core/codec/breaks.js +65 -9
  6. package/dist/core/codec/code.d.ts +6 -4
  7. package/dist/core/codec/code.js +7 -4
  8. package/dist/core/codec/decode.js +91 -62
  9. package/dist/core/codec/encode.d.ts +5 -8
  10. package/dist/core/codec/encode.js +55 -85
  11. package/dist/core/codec/field.js +43 -24
  12. package/dist/core/codec/index.d.ts +2 -1
  13. package/dist/core/codec/index.js +6 -3
  14. package/dist/core/codec/islands.d.ts +13 -9
  15. package/dist/core/codec/islands.js +75 -49
  16. package/dist/core/codec/keymap.js +2 -1
  17. package/dist/core/codec/marks.d.ts +17 -13
  18. package/dist/core/codec/marks.js +39 -42
  19. package/dist/core/codec/positions.d.ts +4 -1
  20. package/dist/core/codec/positions.js +5 -1
  21. package/dist/core/codec/reconcile.d.ts +6 -0
  22. package/dist/core/codec/reconcile.js +19 -0
  23. package/dist/core/codec/schema.d.ts +5 -8
  24. package/dist/core/codec/schema.js +30 -156
  25. package/dist/core/codec/table-view.js +77 -26
  26. package/dist/core/codec/table.d.ts +6 -5
  27. package/dist/core/codec/table.js +14 -31
  28. package/dist/core/codec/urls.d.ts +20 -0
  29. package/dist/core/codec/urls.js +45 -0
  30. package/dist/preset/recipes.css +1 -1
  31. package/dist/preset/scale.css +7 -4
  32. package/dist/preview/bridge.d.ts +8 -1
  33. package/dist/preview/bridge.js +31 -25
  34. package/dist/preview/controller.js +13 -13
  35. package/dist/preview/geometry.d.ts +12 -1
  36. package/dist/preview/geometry.js +14 -1
  37. package/dist/preview/paint.js +6 -4
  38. package/dist/preview/strings.d.ts +0 -2
  39. package/dist/preview/strings.js +5 -6
  40. package/dist/visual/ArrayField.svelte +11 -13
  41. package/dist/visual/Card.svelte +5 -8
  42. package/dist/visual/DateField.svelte +2 -2
  43. package/dist/visual/Field.svelte +7 -3
  44. package/dist/visual/FieldHint.svelte +1 -1
  45. package/dist/visual/ObjectField.svelte +6 -1
  46. package/dist/visual/VisualEditor.svelte +1 -1
  47. package/dist/visual/VisualEditorInner.svelte +65 -18
  48. package/dist/visual/controls.css +8 -9
  49. package/dist/visual/ext.d.ts +12 -13
  50. package/dist/visual/ext.js +22 -19
  51. package/dist/visual/index.js +2 -3
  52. package/dist/visual/links.d.ts +4 -0
  53. package/dist/visual/links.js +15 -4
  54. package/dist/visual/strings.d.ts +7 -4
  55. package/dist/visual/strings.js +1 -0
  56. package/dist/visual/structure.d.ts +9 -1
  57. package/dist/visual/structure.js +26 -4
  58. package/package.json +2 -2
@@ -32,7 +32,14 @@
32
32
  import { errorMessage, reportError } from '../core/errors.js';
33
33
  import { bloomInside } from '../core/bloom.js';
34
34
  import { createLifespan } from '../core/teardown.js';
35
- import type { Addr, CardAddr, Diagnostic, Resolved, ResolvedField } from '@quillmark/wasm';
35
+ import type {
36
+ Addr,
37
+ CardAddr,
38
+ Diagnostic,
39
+ PayloadItem,
40
+ Resolved,
41
+ ResolvedField
42
+ } from '@quillmark/wasm';
36
43
  import type { VisualEditorProps } from './props.js';
37
44
  import { mergeStrings, setWording } from './strings.js';
38
45
  import type { CardId, ChangeSource } from './signals.js';
@@ -45,6 +52,8 @@
45
52
  groupSections,
46
53
  groupLabel,
47
54
  cardTitle,
55
+ titleFields,
56
+ fieldValues,
48
57
  bodyEnabled,
49
58
  humanize,
50
59
  provenanceMap,
@@ -144,6 +153,7 @@
144
153
  */
145
154
  function bump(source: ChangeSource, cardId?: CardId, at?: Addr | number): void {
146
155
  revision++;
156
+ liveTitles = {};
147
157
  const path =
148
158
  at == null ? undefined : typeof at === 'number' ? cardPath(at, liveKinds()) : pathFor(at);
149
159
  onChange?.({ source, cardId, path });
@@ -154,8 +164,11 @@
154
164
  return cardIds.indexOf(id);
155
165
  }
156
166
  /** The inverse, for the two signals that arrive as an address and nothing else (a
157
- * focus, a prose commit), off the same array the mutation boundary reads. */
158
- function cardIdOf(addr: Addr): CardId {
167
+ * focus, a prose commit), off the same array the mutation boundary reads.
168
+ * `undefined` for an address outside the live card array, as {@link pathFor}: the
169
+ * key is a host's only handle on a card, so a stale one is dropped rather than
170
+ * reported. */
171
+ function cardIdOf(addr: Addr): CardId | undefined {
159
172
  return addr.card == null ? 'main' : cardIds[addr.card];
160
173
  }
161
174
 
@@ -214,7 +227,9 @@
214
227
  lastCaretField = undefined;
215
228
  lastCaretPos = undefined;
216
229
  const field = pathFor(plain);
217
- if (field != null) onActiveLeafChange?.({ field, cardId: activeCardId });
230
+ // Both halves or neither: a stale addr names no path and no live card.
231
+ if (field != null && activeCardId != null)
232
+ onActiveLeafChange?.({ field, cardId: activeCardId });
218
233
  }
219
234
  /** No card kinds are read for a main address; the shared empty stands in so the
220
235
  * common case allocates nothing. */
@@ -261,7 +276,35 @@
261
276
  * bump `revision` (see {@link bump}). */
262
277
  function proseChanged(addr: Addr): void {
263
278
  const plain = normalize(addr);
264
- onChange?.({ source: 'prose', cardId: cardIdOf(plain), path: pathFor(plain) });
279
+ const cardId = cardIdOf(plain);
280
+ if (cardId == null) return;
281
+ refreshTitle(plain, cardId);
282
+ onChange?.({ source: 'prose', cardId, path: pathFor(plain) });
283
+ }
284
+
285
+ /** A card's header after a prose commit to a field its `{field}` title names: the
286
+ * one piece of the model that lane changes, held beside the model and dropped on
287
+ * the next bump, which rebuilds it from the document. The kind is the derived
288
+ * tree's ({@link liveKinds}), so the document is read only for a title that
289
+ * names the field. */
290
+ let liveTitles = $state<Record<CardId, string>>({});
291
+ function refreshTitle(addr: Addr, cardId: CardId): void {
292
+ const { card: at, field } = addr;
293
+ if (field == null) return;
294
+ const shown = at == null ? model.main : model.cards[at];
295
+ if (!shown) return;
296
+ const cardSchema = at == null ? quill.schema.main : quill.schema.card_kinds?.[shown.kind];
297
+ if (!titleFields(cardSchema?.ui?.title).includes(field)) return;
298
+ const live = at == null ? doc.main : doc.cards[at];
299
+ if (!live) return;
300
+ liveTitles = {
301
+ ...liveTitles,
302
+ [cardId]: cardTitle(cardSchema, shown.kind, fieldValues(live.payloadItems), undefined)
303
+ };
304
+ }
305
+ function titled(c: CardModel): CardModel {
306
+ const t = liveTitles[c.id];
307
+ return t == null ? c : { ...c, titlePlaceholder: t };
265
308
  }
266
309
 
267
310
  // ── Commit routing ──────────────────────────────────────────────────────────
@@ -423,10 +466,11 @@
423
466
  // Ids are never reused, so an entry keyed to the gone card is unreachable rather
424
467
  // than mis-attributed to whichever card takes this position: it needs dropping
425
468
  // because nothing else will ever clear it.
426
- if ([...commitErrors.keys()].some((k) => k.startsWith(`${id}:`)))
427
- editCommitErrors((m) => {
428
- for (const k of [...m.keys()]) if (k.startsWith(`${id}:`)) m.delete(k);
429
- });
469
+ const prefix = `${id}:`;
470
+ const stale = [...commitErrors.keys()].filter((k) => k.startsWith(prefix));
471
+ // Guarded: `editCommitErrors` clones the map, and a removal matching nothing has
472
+ // nothing to re-derive.
473
+ if (stale.length) editCommitErrors((m) => stale.forEach((k) => m.delete(k)));
430
474
  // No addr: the removed card has none left and every survivor's shifted. The
431
475
  // stack changed, not a leaf, and the id is the only handle the removal leaves.
432
476
  bump('structure', id);
@@ -435,7 +479,12 @@
435
479
  const i = cardIndexOf(id);
436
480
  if (i < 0) return;
437
481
  try {
438
- doc.setCardKind(i, kind);
482
+ // Insert the retyped card ahead of the original and drop the original after.
483
+ // `insertCard` validates the whole card before it mutates and `removeCard`
484
+ // cannot throw, so a refusal leaves the document as it was; the reverse order
485
+ // would leave it a card short.
486
+ doc.insertCard({ ...doc.card(i), kind }, i);
487
+ doc.removeCard(i + 1);
439
488
  bump('structure', id, i);
440
489
  } catch (e) {
441
490
  reportError(onError, {
@@ -536,15 +585,13 @@
536
585
  isMain: boolean,
537
586
  kind: string,
538
587
  card: {
539
- payloadItems: { type: string; key?: string; value?: unknown }[];
588
+ payloadItems: readonly PayloadItem[];
540
589
  ext?: Record<string, unknown>;
541
590
  },
542
591
  cardSchema: Parameters<typeof fieldModels>[0] | undefined,
543
592
  rows: ResolvedCardRows
544
593
  ): CardModel {
545
- const values: Record<string, unknown> = {};
546
- for (const p of card.payloadItems)
547
- if (p.type === 'field' && p.key != null) values[p.key] = p.value;
594
+ const values = fieldValues(card.payloadItems);
548
595
  const fields = cardSchema ? fieldModels(cardSchema) : [];
549
596
  const sections = cardSchema
550
597
  ? groupSections(fields, groupOrder(cardSchema), (g) => groupLabel(cardSchema, g))
@@ -588,12 +635,12 @@
588
635
  // chrome, so a resolve failure degrades to no ghosts, never a blank form.
589
636
  let resolved: Resolved | undefined;
590
637
  try {
591
- resolved = quill.resolve(doc);
638
+ resolved = quill.reader(doc).resolve();
592
639
  } catch (e) {
593
640
  reportError(onError, {
594
641
  code: 'resolve-failed',
595
642
  severity: 'error',
596
- message: `quill.resolve threw; ghosted defaults fall back to none: ${errorMessage(e)}`,
643
+ message: `reader.resolve threw; ghosted defaults fall back to none: ${errorMessage(e)}`,
597
644
  cause: e
598
645
  });
599
646
  }
@@ -807,7 +854,7 @@
807
854
  <div class="qm-primary">
808
855
  <Card
809
856
  bind:this={mainCard}
810
- card={model.main}
857
+ card={titled(model.main)}
811
858
  {doc}
812
859
  {quill}
813
860
  isFirst={true}
@@ -839,7 +886,7 @@
839
886
  <div class="qm-card-slot" bind:this={slotEls[i]} animate:reorder={isReordering}>
840
887
  <Card
841
888
  bind:this={cardRefs[i]}
842
- card={c}
889
+ card={titled(c)}
843
890
  {doc}
844
891
  {quill}
845
892
  isFirst={i === 0}
@@ -12,9 +12,9 @@
12
12
  Every selector here is `:where()`-wrapped, so a recipe computes at specificity zero.
13
13
  That is the point twice over: a consumer's stylesheet beats the chrome without
14
14
  `!important`, and a component's own scoped block overrides its shared base —
15
- `.qm-card-delete` recolours a `.qm-chip` under the pointer, `.qm-json` adds the monospace face over
16
- `.qm-input` — with no specificity fight. Unlayered with it, so a host's own layered
17
- reset never reaches these rules whatever order the sheets load in (`core/theme.css`). */
15
+ `.qm-card-delete` recolours a `.qm-chip` under the pointer — with no specificity
16
+ fight. Unlayered with it, so a host's own layered reset never reaches these rules
17
+ whatever order the sheets load in (`core/theme.css`). */
18
18
 
19
19
  /* The themed focus ring in place of the raw UA outline, as a
20
20
  rule rather than a pair of rungs each control assembles. `--_qm-ring`
@@ -63,11 +63,10 @@
63
63
  }
64
64
 
65
65
  /* A typed value's box (every scalar control and every prose leaf): the text and
66
- numeric inputs, the array's JSON textarea, the enum trigger, the date field, and
67
- the inline richtext leaves. One rule, because a
68
- leaf that agrees with the input beside it "by construction" has to draw the same
69
- declarations, not a copy of them: five hand-kept copies agree until one is
70
- edited, and nothing raises it (no literal is minted, so `check:style` stays
66
+ numeric inputs, the enum trigger, the date field, and the inline richtext leaves.
67
+ One rule, because a leaf that agrees with the input beside it "by construction" has
68
+ to draw the same declarations, not a copy of them: five hand-kept copies agree
69
+ until one is edited, and nothing raises it (no literal is minted, so `check:style` stays
71
70
  green while a control renders subtly the wrong height).
72
71
 
73
72
  Leading sits on the wide list rather than on the box's own, because a box's
@@ -91,7 +90,7 @@
91
90
  color: var(--_qm-ink);
92
91
  caret-color: currentColor;
93
92
  }
94
- /* The box is A fill and not an edge (ARCHITECTURE §Styling), and the fill is the
93
+ /* The box is a fill and not an edge (ARCHITECTURE §Styling), and the fill is the
95
94
  well rung: a field is a hole cut into the card rather than a plane of its own,
96
95
  and with no edge to carry it the step is the whole of what says so. */
97
96
  :where(.qm-control-box),
@@ -4,23 +4,22 @@ import type { Document, CardAddr } from '@quillmark/wasm';
4
4
  * main). A key whose patch value is `undefined` is dropped; every other key in the
5
5
  * namespace is carried through untouched.
6
6
  *
7
- * **The namespace is the write unit.** `storeExtNamespace` replaces the namespace
8
- * it targets (preserving sibling namespaces, but not sibling keys) so a writer
9
- * that stores only its own key silently destroys the others, and
10
- * `removeExtNamespace` destroys all of them. `tips` and `title` are siblings here,
11
- * so a namespace-replacing dismissal would wipe every renamed card's title. Routing
12
- * every writer through this function is what makes that unexpressible rather than
13
- * merely documented: key N+1 inherits the merge instead of re-deriving it.
7
+ * **The whole map is the write unit.** `storeExt` replaces `$ext` entire, so a writer
8
+ * that stores only its own key silently destroys every sibling namespace, and a
9
+ * namespace-replacing write destroys the sibling keys inside it. `tips` and `title`
10
+ * are siblings here, so either would wipe every renamed card's title. Routing every
11
+ * writer through this function is what makes that unexpressible rather than merely
12
+ * documented: key N+1 inherits the merge instead of re-deriving it.
14
13
  *
15
14
  * The drop is an explicit `delete`, not a stored `undefined`: whether a JS
16
15
  * `undefined` survives the wasm-bindgen crossing is not a property worth depending
17
16
  * on.
18
17
  *
19
- * **A namespace emptied of keys is removed, not stored empty.** That is
20
- * `removeExtNamespace`'s one correct use: it takes the whole namespace, and there is
21
- * nothing left in this one to lose. `$ext` goes with it when `editor` was the last
22
- * namespace, so a document nothing has renamed or hinted carries no editor slot
23
- * rather than an empty one. A patch dropping keys the namespace does not have writes
24
- * nothing, so dismissing tips on a document that carries none is not a mutation.
18
+ * **A namespace emptied of keys is removed, not stored empty**, and `$ext` goes with
19
+ * it when `editor` was the last namespace — `removeExt` being the one call that drops
20
+ * the map, where `storeExt({})` records an explicit empty one. So a document nothing
21
+ * has renamed or hinted carries no editor slot rather than an empty one. A patch
22
+ * dropping keys the namespace does not have writes nothing, so dismissing tips on a
23
+ * document that carries none is not a mutation.
25
24
  */
26
25
  export declare function patchEditorExt(doc: Document, addr: CardAddr, patch: Record<string, unknown>): void;
@@ -3,29 +3,27 @@
3
3
  * main). A key whose patch value is `undefined` is dropped; every other key in the
4
4
  * namespace is carried through untouched.
5
5
  *
6
- * **The namespace is the write unit.** `storeExtNamespace` replaces the namespace
7
- * it targets (preserving sibling namespaces, but not sibling keys) so a writer
8
- * that stores only its own key silently destroys the others, and
9
- * `removeExtNamespace` destroys all of them. `tips` and `title` are siblings here,
10
- * so a namespace-replacing dismissal would wipe every renamed card's title. Routing
11
- * every writer through this function is what makes that unexpressible rather than
12
- * merely documented: key N+1 inherits the merge instead of re-deriving it.
6
+ * **The whole map is the write unit.** `storeExt` replaces `$ext` entire, so a writer
7
+ * that stores only its own key silently destroys every sibling namespace, and a
8
+ * namespace-replacing write destroys the sibling keys inside it. `tips` and `title`
9
+ * are siblings here, so either would wipe every renamed card's title. Routing every
10
+ * writer through this function is what makes that unexpressible rather than merely
11
+ * documented: key N+1 inherits the merge instead of re-deriving it.
13
12
  *
14
13
  * The drop is an explicit `delete`, not a stored `undefined`: whether a JS
15
14
  * `undefined` survives the wasm-bindgen crossing is not a property worth depending
16
15
  * on.
17
16
  *
18
- * **A namespace emptied of keys is removed, not stored empty.** That is
19
- * `removeExtNamespace`'s one correct use: it takes the whole namespace, and there is
20
- * nothing left in this one to lose. `$ext` goes with it when `editor` was the last
21
- * namespace, so a document nothing has renamed or hinted carries no editor slot
22
- * rather than an empty one. A patch dropping keys the namespace does not have writes
23
- * nothing, so dismissing tips on a document that carries none is not a mutation.
17
+ * **A namespace emptied of keys is removed, not stored empty**, and `$ext` goes with
18
+ * it when `editor` was the last namespace — `removeExt` being the one call that drops
19
+ * the map, where `storeExt({})` records an explicit empty one. So a document nothing
20
+ * has renamed or hinted carries no editor slot rather than an empty one. A patch
21
+ * dropping keys the namespace does not have writes nothing, so dismissing tips on a
22
+ * document that carries none is not a mutation.
24
23
  */
25
24
  export function patchEditorExt(doc, addr, patch) {
26
- // `getExtNamespace` reads just this namespace rather than serializing the whole
27
- // card to fish out one `$ext` slot.
28
- const current = doc.getExtNamespace(addr, 'editor');
25
+ const ext = doc.getExt(addr) ?? {};
26
+ const current = ext.editor;
29
27
  const next = { ...(current ?? {}) };
30
28
  for (const [key, value] of Object.entries(patch)) {
31
29
  if (value === undefined)
@@ -34,7 +32,12 @@ export function patchEditorExt(doc, addr, patch) {
34
32
  next[key] = value;
35
33
  }
36
34
  if (Object.keys(next).length > 0)
37
- doc.storeExtNamespace(addr, 'editor', next);
38
- else if (current)
39
- doc.removeExtNamespace(addr, 'editor');
35
+ doc.storeExt(addr, { ...ext, editor: next });
36
+ else if (current) {
37
+ const { editor: _editor, ...rest } = ext;
38
+ if (Object.keys(rest).length > 0)
39
+ doc.storeExt(addr, rest);
40
+ else
41
+ doc.removeExt(addr);
42
+ }
40
43
  }
@@ -25,7 +25,6 @@ export { createField } from '../core/codec/index.js';
25
25
  // it. The empty body's per-card hook is the `bodyPlaceholder` key inside it.
26
26
  export { DEFAULT_VISUAL_STRINGS } from './strings.js';
27
27
  // The `$ext.editor` write unit. A consumer seeding editor-side chrome
28
- // state (a card title, the tips channel) goes through this rather than
29
- // `storeExtNamespace`, which replaces the namespace and takes the sibling keys with
30
- // it.
28
+ // state (a card title, the tips channel) goes through this rather than `storeExt`,
29
+ // which replaces the whole map and takes every sibling with it.
31
30
  export { patchEditorExt } from './ext.js';
@@ -9,6 +9,10 @@ import type { Command, EditorState } from 'prosemirror-state';
9
9
  * A rooted value is left alone, being the spelling that asks for the embedding
10
10
  * page, and so is anything already carrying a scheme the link mark renders.
11
11
  *
12
+ * `localhost:5173` names a host too, and `SCHEME` reads its host as the scheme: a
13
+ * colon followed by a port is a host with a port, so it takes the host prefix rather
14
+ * than the refusal a scheme nothing renders gets.
15
+ *
12
16
  * `''` for a blank value and for a scheme the mark renders inert, both being nothing
13
17
  * to apply: `setLink` declines an empty href, so the refusal lands at the prompt's
14
18
  * own exit rather than storing a link that draws as plain text.
@@ -1,4 +1,4 @@
1
- import { rendersHref } from '../core/codec/index.js';
1
+ import { rendersHref, storableUrl } from '../core/codec/index.js';
2
2
  /** A scheme: a letter, then letters, digits, `+`, `-` or `.`, then `:` (RFC 3986). */
3
3
  const SCHEME = /^[a-z][a-z0-9+.-]*:/i;
4
4
  /** Rooted at the page rather than at a host: an absolute path, a query, a fragment,
@@ -6,6 +6,9 @@ const SCHEME = /^[a-z][a-z0-9+.-]*:/i;
6
6
  const ROOTED = /^[/#?]/;
7
7
  /** A bare mail address: one `@`, no scheme and no path. */
8
8
  const ADDRESS = /^[^\s/@]+@[^\s/@]+$/;
9
+ /** A host carrying a port, which spells a scheme to `SCHEME` and is not one: what
10
+ * follows the colon is digits, and no scheme the mark renders takes a port. */
11
+ const HOST_PORT = /^[a-z0-9.-]+:\d+([/?#]|$)/i;
9
12
  /**
10
13
  * The href a typed value stands for. A value with no scheme names a host, and
11
14
  * stored verbatim it is a relative href resolving against whatever page the editor
@@ -16,15 +19,23 @@ const ADDRESS = /^[^\s/@]+@[^\s/@]+$/;
16
19
  * A rooted value is left alone, being the spelling that asks for the embedding
17
20
  * page, and so is anything already carrying a scheme the link mark renders.
18
21
  *
22
+ * `localhost:5173` names a host too, and `SCHEME` reads its host as the scheme: a
23
+ * colon followed by a port is a host with a port, so it takes the host prefix rather
24
+ * than the refusal a scheme nothing renders gets.
25
+ *
19
26
  * `''` for a blank value and for a scheme the mark renders inert, both being nothing
20
27
  * to apply: `setLink` declines an empty href, so the refusal lands at the prompt's
21
28
  * own exit rather than storing a link that draws as plain text.
22
29
  */
23
30
  export function normalizeHref(raw) {
24
- const value = raw.trim();
25
- if (!value || !rendersHref(value))
31
+ const value = storableUrl(raw.trim());
32
+ if (!value)
33
+ return '';
34
+ if (!rendersHref(value) && !HOST_PORT.test(value))
26
35
  return '';
27
- if (SCHEME.test(value) || ROOTED.test(value))
36
+ if (SCHEME.test(value) && rendersHref(value))
37
+ return value;
38
+ if (ROOTED.test(value))
28
39
  return value;
29
40
  return ADDRESS.test(value) ? `mailto:${value}` : `https://${value}`;
30
41
  }
@@ -16,7 +16,7 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
16
16
  cardTitle: string;
17
17
  /** The trailing strip's visible words, which are also every strip's accessible
18
18
  * name; the plus rides the wording. One string over
19
- * one kind and many: the trigger seeds A card, and which kind is the menu's word
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;
@@ -64,6 +64,9 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
64
64
  retypeLabel: string;
65
65
  retypePlaceholder: string;
66
66
  noCardKinds: string;
67
+ /** The body leaf's accessible name: no label names it, the body being the one
68
+ * surface in a card that is paper ({@link Card}). */
69
+ bodyLabel: string;
67
70
  /**
68
71
  * The flat built-in invitation: the package's own words, kept to the one thing
69
72
  * true of every body so it claims nothing about the card it sits in.
@@ -91,9 +94,9 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
91
94
  * - **`edit::field_coercion_failed`**: re-words from the app's own control state, the
92
95
  * refused value being in neither document (unchanged on throw) nor schema.
93
96
  * - **`parse::yaml_error_with_location`, `invalid_structure`**: does not re-word. No
94
- * `path`, and `args` carries the location (`blockIndex`, `line`) and nothing else,
95
- * engine prose riding under no key: the parser's own text, its column and its caret
96
- * snippet exist only inside `message`.
97
+ * `path`; `args` names the block (`blockIndex`) and `location` places the failure in
98
+ * the document's own coordinates, engine prose riding under no key: the parser's own
99
+ * text, its column and its caret snippet exist only inside `message`.
97
100
  * - **`LiveSession.warnings`**: does not re-word. Backend text, an external feed.
98
101
  *
99
102
  * The last two are the boundary's shape, not a gap in it, so the fallback arm is
@@ -55,6 +55,7 @@ export const DEFAULT_VISUAL_STRINGS = {
55
55
  retypeLabel: 'Change to',
56
56
  retypePlaceholder: 'Choose a type…',
57
57
  noCardKinds: 'This document declares no card types — delete this card to remove it.',
58
+ bodyLabel: 'Body',
58
59
  bodyGhost: 'Write…',
59
60
  bodyPlaceholder: () => undefined
60
61
  };
@@ -1,4 +1,4 @@
1
- import type { QuillCardSchema, QuillFieldSchema, ResolvedField, Resolved } from '@quillmark/wasm';
1
+ import type { PayloadItem, QuillCardSchema, QuillFieldSchema, ResolvedField, 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';
4
4
  /** One field, projected: its schema, the control it renders as, and its layout hints. */
@@ -233,8 +233,16 @@ export interface PlacedField {
233
233
  * Pure, and stays pure: no width, no measurement, nothing to re-derive on resize.
234
234
  */
235
235
  export declare function placeFields(fields: FieldModel[]): PlacedField[];
236
+ /** A card's field values by key, off its payload items. */
237
+ export declare function fieldValues(items: readonly PayloadItem[]): Record<string, unknown>;
236
238
  /** Interpolate a `{field}` card-title template against live field values. */
237
239
  export declare function interpolateTitle(template: string, values: Record<string, unknown>): string;
240
+ /** A field value as title text. A parsed field rests as the authored string and a
241
+ * committed one as `Content`, whose `text` is the same words; a scalar reads as
242
+ * itself and any other container as nothing. */
243
+ export declare function titleText(v: unknown): string;
244
+ /** The field names a `{field}` title reads. */
245
+ export declare function titleFields(template: string | undefined): string[];
238
246
  /**
239
247
  * Resolve a card instance's header title: the per-instance `$ext.editor.title`
240
248
  * override wins; else the schema `ui.title` (literal or `{field}` template
@@ -268,12 +268,34 @@ export function placeFields(fields) {
268
268
  flush();
269
269
  return out;
270
270
  }
271
+ /** A card's field values by key, off its payload items. */
272
+ export function fieldValues(items) {
273
+ const values = {};
274
+ for (const p of items)
275
+ if (p.type === 'field')
276
+ values[p.key] = p.value;
277
+ return values;
278
+ }
271
279
  /** Interpolate a `{field}` card-title template against live field values. */
272
280
  export function interpolateTitle(template, values) {
273
- return template.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, name) => {
274
- const v = values[name];
275
- return v == null ? '' : String(v);
276
- });
281
+ return template.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, name) => titleText(values[name]));
282
+ }
283
+ /** A field value as title text. A parsed field rests as the authored string and a
284
+ * committed one as `Content`, whose `text` is the same words; a scalar reads as
285
+ * itself and any other container as nothing. */
286
+ export function titleText(v) {
287
+ if (v == null)
288
+ return '';
289
+ if (typeof v !== 'object')
290
+ return String(v);
291
+ const text = v.text;
292
+ return typeof text === 'string' ? text : '';
293
+ }
294
+ /** The field names a `{field}` title reads. */
295
+ export function titleFields(template) {
296
+ if (!template)
297
+ return [];
298
+ return [...template.matchAll(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g)].map((m) => m[1]);
277
299
  }
278
300
  /**
279
301
  * Resolve a card instance's header title: the per-instance `$ext.editor.title`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/svelte",
3
- "version": "0.10.1",
3
+ "version": "0.11.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.110.0-0",
69
+ "@quillmark/wasm": ">=0.113.0-0",
70
70
  "svelte": "^5.0.0"
71
71
  },
72
72
  "dependencies": {