@quillmark/svelte 0.0.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 (143) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +58 -0
  3. package/README.md +241 -0
  4. package/THEMING.md +153 -0
  5. package/dist/core/address.d.ts +56 -0
  6. package/dist/core/address.js +95 -0
  7. package/dist/core/bloom.d.ts +37 -0
  8. package/dist/core/bloom.js +116 -0
  9. package/dist/core/codec/anchor.d.ts +19 -0
  10. package/dist/core/codec/anchor.js +36 -0
  11. package/dist/core/codec/code.d.ts +12 -0
  12. package/dist/core/codec/code.js +118 -0
  13. package/dist/core/codec/decode.d.ts +24 -0
  14. package/dist/core/codec/decode.js +270 -0
  15. package/dist/core/codec/encode.d.ts +71 -0
  16. package/dist/core/codec/encode.js +480 -0
  17. package/dist/core/codec/field.d.ts +171 -0
  18. package/dist/core/codec/field.js +493 -0
  19. package/dist/core/codec/index.d.ts +22 -0
  20. package/dist/core/codec/index.js +43 -0
  21. package/dist/core/codec/inputrules.d.ts +7 -0
  22. package/dist/core/codec/inputrules.js +168 -0
  23. package/dist/core/codec/islands.d.ts +36 -0
  24. package/dist/core/codec/islands.js +82 -0
  25. package/dist/core/codec/keymap.d.ts +13 -0
  26. package/dist/core/codec/keymap.js +29 -0
  27. package/dist/core/codec/lists.d.ts +22 -0
  28. package/dist/core/codec/lists.js +104 -0
  29. package/dist/core/codec/marks.d.ts +37 -0
  30. package/dist/core/codec/marks.js +74 -0
  31. package/dist/core/codec/positions.d.ts +17 -0
  32. package/dist/core/codec/positions.js +108 -0
  33. package/dist/core/codec/prose.css +458 -0
  34. package/dist/core/codec/reconcile.d.ts +26 -0
  35. package/dist/core/codec/reconcile.js +64 -0
  36. package/dist/core/codec/schema.d.ts +13 -0
  37. package/dist/core/codec/schema.js +155 -0
  38. package/dist/core/codec/slash.d.ts +79 -0
  39. package/dist/core/codec/slash.js +0 -0
  40. package/dist/core/codec/table-view.d.ts +50 -0
  41. package/dist/core/codec/table-view.js +928 -0
  42. package/dist/core/codec/table.d.ts +85 -0
  43. package/dist/core/codec/table.js +224 -0
  44. package/dist/core/errors.d.ts +73 -0
  45. package/dist/core/errors.js +24 -0
  46. package/dist/core/index.d.ts +5 -0
  47. package/dist/core/index.js +19 -0
  48. package/dist/core/lifecycle.d.ts +7 -0
  49. package/dist/core/lifecycle.js +21 -0
  50. package/dist/core/rebind.svelte.d.ts +18 -0
  51. package/dist/core/rebind.svelte.js +44 -0
  52. package/dist/core/teardown.d.ts +20 -0
  53. package/dist/core/teardown.js +37 -0
  54. package/dist/core/theme.css +276 -0
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.js +9 -0
  57. package/dist/preset/index.css +10 -0
  58. package/dist/preset/recipes.css +188 -0
  59. package/dist/preset/scale.css +97 -0
  60. package/dist/preview/Preview.svelte +118 -0
  61. package/dist/preview/Preview.svelte.d.ts +39 -0
  62. package/dist/preview/bridge.d.ts +13 -0
  63. package/dist/preview/bridge.js +88 -0
  64. package/dist/preview/controller.d.ts +40 -0
  65. package/dist/preview/controller.js +141 -0
  66. package/dist/preview/geometry.d.ts +37 -0
  67. package/dist/preview/geometry.js +45 -0
  68. package/dist/preview/index.d.ts +5 -0
  69. package/dist/preview/index.js +14 -0
  70. package/dist/preview/overlay.d.ts +10 -0
  71. package/dist/preview/overlay.js +82 -0
  72. package/dist/preview/paint.d.ts +17 -0
  73. package/dist/preview/paint.js +262 -0
  74. package/dist/preview/strings.d.ts +14 -0
  75. package/dist/preview/strings.js +22 -0
  76. package/dist/visual/ArrayField.svelte +362 -0
  77. package/dist/visual/ArrayField.svelte.d.ts +24 -0
  78. package/dist/visual/BooleanField.svelte +88 -0
  79. package/dist/visual/BooleanField.svelte.d.ts +20 -0
  80. package/dist/visual/Card.svelte +849 -0
  81. package/dist/visual/Card.svelte.d.ts +43 -0
  82. package/dist/visual/CardControls.svelte +92 -0
  83. package/dist/visual/CardControls.svelte.d.ts +11 -0
  84. package/dist/visual/DateField.svelte +203 -0
  85. package/dist/visual/DateField.svelte.d.ts +28 -0
  86. package/dist/visual/DiagnosticList.svelte +50 -0
  87. package/dist/visual/DiagnosticList.svelte.d.ts +7 -0
  88. package/dist/visual/EnumField.svelte +169 -0
  89. package/dist/visual/EnumField.svelte.d.ts +25 -0
  90. package/dist/visual/Field.svelte +270 -0
  91. package/dist/visual/Field.svelte.d.ts +37 -0
  92. package/dist/visual/FieldHint.svelte +185 -0
  93. package/dist/visual/FieldHint.svelte.d.ts +13 -0
  94. package/dist/visual/FieldLabel.svelte +141 -0
  95. package/dist/visual/FieldLabel.svelte.d.ts +19 -0
  96. package/dist/visual/FormatPopover.svelte +396 -0
  97. package/dist/visual/FormatPopover.svelte.d.ts +9 -0
  98. package/dist/visual/NumberField.svelte +72 -0
  99. package/dist/visual/NumberField.svelte.d.ts +19 -0
  100. package/dist/visual/ObjectField.svelte +131 -0
  101. package/dist/visual/ObjectField.svelte.d.ts +17 -0
  102. package/dist/visual/ProseArrayElement.svelte +98 -0
  103. package/dist/visual/ProseArrayElement.svelte.d.ts +19 -0
  104. package/dist/visual/ProseField.svelte +213 -0
  105. package/dist/visual/ProseField.svelte.d.ts +50 -0
  106. package/dist/visual/SlashMenu.svelte +126 -0
  107. package/dist/visual/SlashMenu.svelte.d.ts +14 -0
  108. package/dist/visual/TextField.svelte +63 -0
  109. package/dist/visual/TextField.svelte.d.ts +23 -0
  110. package/dist/visual/TipsCard.svelte +191 -0
  111. package/dist/visual/TipsCard.svelte.d.ts +13 -0
  112. package/dist/visual/VisualEditor.svelte +96 -0
  113. package/dist/visual/VisualEditor.svelte.d.ts +14 -0
  114. package/dist/visual/VisualEditorInner.svelte +1032 -0
  115. package/dist/visual/VisualEditorInner.svelte.d.ts +17 -0
  116. package/dist/visual/controls.css +272 -0
  117. package/dist/visual/diagnostics.d.ts +52 -0
  118. package/dist/visual/diagnostics.js +83 -0
  119. package/dist/visual/domid.d.ts +21 -0
  120. package/dist/visual/domid.js +36 -0
  121. package/dist/visual/ext.d.ts +19 -0
  122. package/dist/visual/ext.js +29 -0
  123. package/dist/visual/icons/Icon.svelte +39 -0
  124. package/dist/visual/icons/Icon.svelte.d.ts +12 -0
  125. package/dist/visual/icons/nodes.d.ts +103 -0
  126. package/dist/visual/icons/nodes.js +55 -0
  127. package/dist/visual/index.d.ts +9 -0
  128. package/dist/visual/index.js +24 -0
  129. package/dist/visual/motion.d.ts +18 -0
  130. package/dist/visual/motion.js +53 -0
  131. package/dist/visual/props.d.ts +77 -0
  132. package/dist/visual/props.js +1 -0
  133. package/dist/visual/signals.d.ts +59 -0
  134. package/dist/visual/signals.js +1 -0
  135. package/dist/visual/strings.d.ts +123 -0
  136. package/dist/visual/strings.js +80 -0
  137. package/dist/visual/structure.d.ts +224 -0
  138. package/dist/visual/structure.js +270 -0
  139. package/dist/visual/synced.svelte.d.ts +15 -0
  140. package/dist/visual/synced.svelte.js +34 -0
  141. package/dist/visual/tips.d.ts +24 -0
  142. package/dist/visual/tips.js +50 -0
  143. package/package.json +102 -0
@@ -0,0 +1,224 @@
1
+ import type { QuillCardSchema, QuillFieldSchema, ResolvedField, Resolved } from '@quillmark/wasm';
2
+ /** The control a field type maps to (VISUAL_EDITOR §"Structure mirrors the schema"). */
3
+ export type ControlKind = 'prose' | 'text' | 'enum' | 'number' | 'boolean' | 'date' | 'array' | 'object';
4
+ /** One field, projected: its schema, the control it renders as, and its layout hints. */
5
+ export interface FieldModel {
6
+ name: string;
7
+ schema: QuillFieldSchema;
8
+ control: ControlKind;
9
+ /** `ui.group` (undefined = ungrouped). */
10
+ group: string | undefined;
11
+ /** `ui.compact`: asks to share a row with adjacent compacts. A request, not a
12
+ * guarantee: `placeFields` declines it for the shapes that grow (see `packable`). */
13
+ compact: boolean;
14
+ /** Display label: `ui.title` when set, else the humanized field name. */
15
+ label: string;
16
+ /** Schema `description`: authoring help rendered beside the label,
17
+ * undefined when the field declares none. Chrome-only; never gates. */
18
+ description: string | undefined;
19
+ /**
20
+ * Required-ness: a field with NO `default:` is "Unendorsed": its
21
+ * seed carries a `!must_fill` marker (DOCUMENT_MODEL: there is no separate
22
+ * `required` axis). Drives a persistent label `*`, complementary to the ghosted
23
+ * `default:` (a required field has no default to ghost). Persistent (schema-
24
+ * derived, survives filling); the on-commit `must_fill` `validate` warning
25
+ * remains the unmet-ness signal. Label chrome only; never gates.
26
+ */
27
+ required: boolean;
28
+ /** Prose-leaf flags (only meaningful when `control === 'prose'` / array items). */
29
+ inline: boolean;
30
+ plaintext: boolean;
31
+ }
32
+ /** A `ui.group` section: its label and the fields (declaration order) inside it. */
33
+ export interface GroupSection {
34
+ group: string | undefined;
35
+ label: string;
36
+ fields: FieldModel[];
37
+ }
38
+ /**
39
+ * One card instance projected for rendering: the schema × payload join result
40
+ * the VisualEditor's `$derived` produces and hands to `<Card>`. Positional
41
+ * identity is carried by `id` (a session key), NOT by array index.
42
+ */
43
+ export interface CardModel {
44
+ /** Stable session id (`'main'` for the main card). */
45
+ id: string;
46
+ isMain: boolean;
47
+ kind: string;
48
+ /**
49
+ * The card's `kind` has no projectable schema: a foreign kind under
50
+ * a schema that declares others, or a card under a schema with no `card_kinds` at
51
+ * all. Such a card renders a RECOVERY SHELL (humanized title + retype + delete)
52
+ * instead of a field list, so its content is never dropped or trapped: retyping
53
+ * to a declared kind re-projects it, delete removes it. Always `false` for `main`.
54
+ */
55
+ unschemable: boolean;
56
+ /** Raw `$ext.editor.title` override (composable cards). */
57
+ titleOverride: string;
58
+ /** Schema-resolved title used as the rename placeholder (composable cards). */
59
+ titlePlaceholder: string;
60
+ /** Field name → current stored value (absent fields missing). */
61
+ values: Record<string, unknown>;
62
+ /**
63
+ * Field name → its resolved provenance row (`{ value, source }`), parallel to
64
+ * `values` (FIELD_PROVENANCE). The channel that feeds chrome (the ghosted
65
+ * `default:` and any authored/default/zero affordance), NEVER the control
66
+ * value. Empty when `quill.resolve` is unavailable.
67
+ */
68
+ provenance: Record<string, ResolvedField>;
69
+ sections: GroupSection[];
70
+ hasBody: boolean;
71
+ /**
72
+ * The empty-body ghost, or undefined when the card renders no body. Never
73
+ * empty for a card that does: the resolved body `default:` when there is one,
74
+ * else the consumer's wording, else the built-in invitation
75
+ * ({@link resolveBodyGhost}). A body leaf therefore always has something in it
76
+ * to write into, which the inline fields deliberately do not: their ghost IS
77
+ * the resolved default, and an invented one would read as a value.
78
+ */
79
+ bodyGhost?: string;
80
+ }
81
+ /**
82
+ * A card's resolved rows keyed by field name: the provenance channel parallel to
83
+ * {@link CardModel.values}. `resolve` returns rows in declaration order; the
84
+ * editor keys them the same way `values` keys its payload walk, so a field's value
85
+ * and its provenance resolve under one name.
86
+ */
87
+ export declare function provenanceMap(fields: ResolvedField[]): Record<string, ResolvedField>;
88
+ /** A card's resolved rows: its declared fields, and the `body` sibling `resolve`
89
+ * hangs off each card. */
90
+ export interface ResolvedCardRows {
91
+ fields: ResolvedField[];
92
+ body: ResolvedField | null;
93
+ }
94
+ /** The empty rows a card with no resolve entry reads: a shared constant so the
95
+ * per-card miss allocates nothing. */
96
+ export declare const NO_RESOLVED_ROWS: ResolvedCardRows;
97
+ /**
98
+ * Composable cards' resolved rows keyed by document index (`ResolvedCard.index`,
99
+ * not array position, so it holds whatever order the resolve view returns): built
100
+ * once per derive, so the per-card provenance join is O(1). Both channels ride one
101
+ * entry: a card reads its fields and its body from a single lookup. Empty map when
102
+ * `resolved` is absent (a `resolve` failure degrades to no ghosts, never a blank
103
+ * form).
104
+ */
105
+ export declare function resolvedByCardIndex(resolved: Resolved | undefined): Map<number, ResolvedCardRows>;
106
+ /** The ghost a field shows when unset: the resolved `default:` value the render
107
+ * would use (`source === 'default'`), else undefined: an `authored` field shows
108
+ * its value, a `zero` field has no default to ghost. */
109
+ export declare function ghostDefault(row: ResolvedField | undefined): unknown;
110
+ /** A ghost value's string form, or undefined for null/object (only text ghosts
111
+ * render a placeholder). The one text-ghost projection: a scalar field's
112
+ * placeholder and a body leaf's both read `stringifyGhost ∘
113
+ * ghostDefault`, since a richtext body resolves to a text render: the correct
114
+ * thing to display as a placeholder (FIELD_PROVENANCE). */
115
+ export declare function stringifyGhost(ghost: unknown): string | undefined;
116
+ /** What a {@link BodyPlaceholder} is told about the body it words. The card's
117
+ * IDENTITY, not its chrome: `kind` keys the consumer's own `quill.schema` for
118
+ * anything richer, and a renamed card must not shift its ghost. */
119
+ export interface BodyPlaceholderContext {
120
+ /** The card's kind; `'main'` for the main card. */
121
+ kind: string;
122
+ /** The main card: whose `kind` is not a `card_kinds` key. */
123
+ isMain: boolean;
124
+ }
125
+ /**
126
+ * Consumer wording for an empty body, per KIND, in place of the flat `bodyGhost`
127
+ * string; returning `undefined` takes it. Consulted ONCE PER KIND per session and
128
+ * cached by the editor, so a hook that samples a set at random still reads as one
129
+ * deliberate string: two empty cards of a kind ghost the same, and a remount does
130
+ * not re-roll. Impurity is expected, and the cache is what contains it.
131
+ */
132
+ export type BodyPlaceholder = (ctx: BodyPlaceholderContext) => string | undefined;
133
+ /**
134
+ * The empty body's ghost: the resolved body `default:`, else per-kind consumer
135
+ * wording, else the flat built-in. The `default:` WINS because it is the only one
136
+ * of the three that describes the render: it promises what prints if nothing is
137
+ * written, and wording placed over it would make that promise unreadable. The
138
+ * other two are invitations, and an invitation belongs only where there is no
139
+ * promise.
140
+ */
141
+ export declare function resolveBodyGhost(resolvedDefault: string | undefined, custom: string | undefined, builtIn: string): string;
142
+ /** Map a field schema to its control (precedence: prose › enum › text › …).
143
+ * An array's ELEMENT control is this over `items`: a missing `items` is a text
144
+ * element. */
145
+ export declare function controlKind(f: QuillFieldSchema): ControlKind;
146
+ /** The closed value domain of an enum/enum-string field, or undefined. */
147
+ export declare function enumValues(f: QuillFieldSchema): string[] | undefined;
148
+ /** `foo_bar` → `Foo bar`: the label fallback when a field declares no `ui.title`. */
149
+ export declare function humanize(name: string): string;
150
+ /** Project a card schema's `fields` map (declaration = key order) into models. */
151
+ export declare function fieldModels(cardSchema: QuillCardSchema): FieldModel[];
152
+ /**
153
+ * Group-section order (VISUAL_EDITOR §"Structure mirrors the schema"): the schema's `ui.groups` registry
154
+ * KEY ORDER when present: `QuillCardUi.groups` is a typed `Record` at the
155
+ * boundary, so this reads it uncast: else the first-appearance order of each
156
+ * field's `ui.group`.
157
+ */
158
+ export declare function groupOrder(cardSchema: QuillCardSchema): string[];
159
+ /**
160
+ * A group's display label: the `ui.groups[g].title` override when the registry
161
+ * declares one, else the humanized id (`memo_for` → "Memo for"): the same
162
+ * id-derives-the-label rule a field's key follows.
163
+ */
164
+ export declare function groupLabel(cardSchema: QuillCardSchema, group: string): string;
165
+ /**
166
+ * Sort field models into ordered group sections. Declared groups first (in
167
+ * `order`), each carrying its fields in declaration order; then any remaining
168
+ * groups (including the ungrouped bucket) in first-appearance order. `labelFor`
169
+ * resolves a group id to its display label: always {@link groupLabel} bound to
170
+ * the card schema, which falls back to {@link humanize} itself.
171
+ */
172
+ export declare function groupSections(fields: FieldModel[], order: string[], labelFor: (group: string) => string): GroupSection[];
173
+ /**
174
+ * The group section a card's accordion opens on first mount: the first in
175
+ * order, or `null` when the card declares none. Ungrouped fields render outside
176
+ * the accordion, so only `group != null` sections count.
177
+ *
178
+ * A card opens on fields. All-collapsed paints as a stack of chevrons that name
179
+ * their sections and disclose nothing of what is behind them, and a body leaf is
180
+ * no substitute: on a card carrying both, the fields are what the card is for.
181
+ * The first section is the one `ui.groups` already ranks highest, and it is the
182
+ * same section on every mount: a rule keyed on document state would move the
183
+ * opening under a user as they fill.
184
+ *
185
+ * State is ephemeral session state the Card owns; this only seeds it.
186
+ */
187
+ export declare function initialExpandedGroup(sections: GroupSection[]): string | null;
188
+ /** How wide a field sits in its section grid. */
189
+ export type FieldSpan = 'cell' | 'lone' | 'full';
190
+ export interface PlacedField {
191
+ field: FieldModel;
192
+ span: FieldSpan;
193
+ }
194
+ /**
195
+ * Assign each field its span in the section grid. Consecutive packable fields are
196
+ * `cell`s the grid auto-places: capacity and wrapping are the container query's
197
+ * business, so a trailing orphan keeps its column width rather than growing to fill.
198
+ * A packable run of ONE is `lone`: with no row above to align to, one column reads as
199
+ * truncated, so it takes half the capacity from column 1. Everything else is `full`.
200
+ *
201
+ * Pure, and stays pure: no width, no measurement, nothing to re-derive on resize.
202
+ */
203
+ export declare function placeFields(fields: FieldModel[]): PlacedField[];
204
+ /** Interpolate a `{field}` card-title template against live field values. */
205
+ export declare function interpolateTitle(template: string, values: Record<string, unknown>): string;
206
+ /**
207
+ * Resolve a card instance's header title: the per-instance `$ext.editor.title`
208
+ * override wins; else the schema `ui.title` (literal or `{field}` template
209
+ * interpolated); else the humanized kind. Empty overrides fall through so a
210
+ * cleared rename reverts to the schema title.
211
+ */
212
+ export declare function cardTitle(cardSchema: QuillCardSchema | undefined, kind: string, values: Record<string, unknown>, extTitle: string | undefined): string;
213
+ /** Whether a card kind renders a body leaf: gated by `body.enabled !== false`. */
214
+ export declare function bodyEnabled(cardSchema: QuillCardSchema | undefined): boolean;
215
+ /** A monotonic per-session id source. Ids are opaque strings, stable for the
216
+ * session; a fresh array for N existing instances is `seq.take(N)`, and an id
217
+ * resolves to its current index by `ids.indexOf(id)` (-1 once the instance is
218
+ * gone): read at the mutation boundary, never cached. */
219
+ export declare class IdSeq {
220
+ #private;
221
+ next(): string;
222
+ /** `count` fresh ids, in order. */
223
+ take(count: number): string[];
224
+ }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * A card's resolved rows keyed by field name: the provenance channel parallel to
3
+ * {@link CardModel.values}. `resolve` returns rows in declaration order; the
4
+ * editor keys them the same way `values` keys its payload walk, so a field's value
5
+ * and its provenance resolve under one name.
6
+ */
7
+ export function provenanceMap(fields) {
8
+ return Object.fromEntries(fields.map((f) => [f.name, f]));
9
+ }
10
+ /** The empty rows a card with no resolve entry reads: a shared constant so the
11
+ * per-card miss allocates nothing. */
12
+ export const NO_RESOLVED_ROWS = { fields: [], body: null };
13
+ /**
14
+ * Composable cards' resolved rows keyed by document index (`ResolvedCard.index`,
15
+ * not array position, so it holds whatever order the resolve view returns): built
16
+ * once per derive, so the per-card provenance join is O(1). Both channels ride one
17
+ * entry: a card reads its fields and its body from a single lookup. Empty map when
18
+ * `resolved` is absent (a `resolve` failure degrades to no ghosts, never a blank
19
+ * form).
20
+ */
21
+ export function resolvedByCardIndex(resolved) {
22
+ return new Map((resolved?.cards ?? []).map((c) => [c.index, { fields: c.fields, body: c.body }]));
23
+ }
24
+ /** The ghost a field shows when unset: the resolved `default:` value the render
25
+ * would use (`source === 'default'`), else undefined: an `authored` field shows
26
+ * its value, a `zero` field has no default to ghost. */
27
+ export function ghostDefault(row) {
28
+ return row?.source === 'default' ? row.value : undefined;
29
+ }
30
+ /** A ghost value's string form, or undefined for null/object (only text ghosts
31
+ * render a placeholder). The one text-ghost projection: a scalar field's
32
+ * placeholder and a body leaf's both read `stringifyGhost ∘
33
+ * ghostDefault`, since a richtext body resolves to a text render: the correct
34
+ * thing to display as a placeholder (FIELD_PROVENANCE). */
35
+ export function stringifyGhost(ghost) {
36
+ return ghost != null && typeof ghost !== 'object' ? String(ghost) : undefined;
37
+ }
38
+ /**
39
+ * The empty body's ghost: the resolved body `default:`, else per-kind consumer
40
+ * wording, else the flat built-in. The `default:` WINS because it is the only one
41
+ * of the three that describes the render: it promises what prints if nothing is
42
+ * written, and wording placed over it would make that promise unreadable. The
43
+ * other two are invitations, and an invitation belongs only where there is no
44
+ * promise.
45
+ */
46
+ export function resolveBodyGhost(resolvedDefault, custom, builtIn) {
47
+ return resolvedDefault || custom || builtIn;
48
+ }
49
+ /** Map a field schema to its control (precedence: prose › enum › text › …).
50
+ * An array's ELEMENT control is this over `items`: a missing `items` is a text
51
+ * element. */
52
+ export function controlKind(f) {
53
+ switch (f.type) {
54
+ case 'richtext':
55
+ case 'plaintext':
56
+ return 'prose';
57
+ case 'enum':
58
+ return 'enum';
59
+ case 'string':
60
+ // Either spelling of a closed domain (the `enum` modifier or `values`)
61
+ // promotes a string to a select; a bare string is a text input.
62
+ return enumValues(f) ? 'enum' : 'text';
63
+ case 'number':
64
+ case 'integer':
65
+ return 'number';
66
+ case 'boolean':
67
+ return 'boolean';
68
+ case 'date':
69
+ case 'datetime':
70
+ return 'date';
71
+ case 'array':
72
+ return 'array';
73
+ case 'object':
74
+ return 'object';
75
+ default:
76
+ return 'text';
77
+ }
78
+ }
79
+ /** The closed value domain of an enum/enum-string field, or undefined. */
80
+ export function enumValues(f) {
81
+ return f.enum ?? f.values;
82
+ }
83
+ /** `foo_bar` → `Foo bar`: the label fallback when a field declares no `ui.title`. */
84
+ export function humanize(name) {
85
+ const spaced = name.replace(/_/g, ' ').trim();
86
+ return spaced ? spaced[0].toUpperCase() + spaced.slice(1) : spaced;
87
+ }
88
+ /** Project a card schema's `fields` map (declaration = key order) into models. */
89
+ export function fieldModels(cardSchema) {
90
+ return Object.entries(cardSchema.fields).map(([name, schema]) => ({
91
+ name,
92
+ schema,
93
+ control: controlKind(schema),
94
+ group: schema.ui?.group,
95
+ compact: !!schema.ui?.compact,
96
+ label: schema.ui?.title ?? humanize(name),
97
+ description: schema.description,
98
+ required: schema.default === undefined,
99
+ inline: !!schema.inline,
100
+ plaintext: schema.type === 'plaintext'
101
+ }));
102
+ }
103
+ /**
104
+ * Group-section order (VISUAL_EDITOR §"Structure mirrors the schema"): the schema's `ui.groups` registry
105
+ * KEY ORDER when present: `QuillCardUi.groups` is a typed `Record` at the
106
+ * boundary, so this reads it uncast: else the first-appearance order of each
107
+ * field's `ui.group`.
108
+ */
109
+ export function groupOrder(cardSchema) {
110
+ const groups = cardSchema.ui?.groups;
111
+ if (groups)
112
+ return Object.keys(groups);
113
+ const order = [];
114
+ for (const f of Object.values(cardSchema.fields)) {
115
+ const g = f.ui?.group;
116
+ if (g && !order.includes(g))
117
+ order.push(g);
118
+ }
119
+ return order;
120
+ }
121
+ /**
122
+ * A group's display label: the `ui.groups[g].title` override when the registry
123
+ * declares one, else the humanized id (`memo_for` → "Memo for"): the same
124
+ * id-derives-the-label rule a field's key follows.
125
+ */
126
+ export function groupLabel(cardSchema, group) {
127
+ return cardSchema.ui?.groups?.[group]?.title ?? humanize(group);
128
+ }
129
+ /**
130
+ * Sort field models into ordered group sections. Declared groups first (in
131
+ * `order`), each carrying its fields in declaration order; then any remaining
132
+ * groups (including the ungrouped bucket) in first-appearance order. `labelFor`
133
+ * resolves a group id to its display label: always {@link groupLabel} bound to
134
+ * the card schema, which falls back to {@link humanize} itself.
135
+ */
136
+ export function groupSections(fields, order, labelFor) {
137
+ const sections = [];
138
+ const emitted = new Set();
139
+ for (const g of order) {
140
+ const fs = fields.filter((f) => f.group === g);
141
+ if (fs.length) {
142
+ sections.push({ group: g, label: labelFor(g), fields: fs });
143
+ emitted.add(g);
144
+ }
145
+ }
146
+ for (const f of fields) {
147
+ if (emitted.has(f.group))
148
+ continue;
149
+ sections.push({
150
+ group: f.group,
151
+ label: f.group ? labelFor(f.group) : '',
152
+ fields: fields.filter((x) => x.group === f.group)
153
+ });
154
+ emitted.add(f.group);
155
+ }
156
+ return sections;
157
+ }
158
+ /**
159
+ * The group section a card's accordion opens on first mount: the first in
160
+ * order, or `null` when the card declares none. Ungrouped fields render outside
161
+ * the accordion, so only `group != null` sections count.
162
+ *
163
+ * A card opens on fields. All-collapsed paints as a stack of chevrons that name
164
+ * their sections and disclose nothing of what is behind them, and a body leaf is
165
+ * no substitute: on a card carrying both, the fields are what the card is for.
166
+ * The first section is the one `ui.groups` already ranks highest, and it is the
167
+ * same section on every mount: a rule keyed on document state would move the
168
+ * opening under a user as they fill.
169
+ *
170
+ * State is ephemeral session state the Card owns; this only seeds it.
171
+ */
172
+ export function initialExpandedGroup(sections) {
173
+ return sections.find((s) => s.group != null)?.group ?? null;
174
+ }
175
+ /**
176
+ * Whether a field can share a row. `ui.compact` asks; a shape declines when the
177
+ * DOCUMENT sets its height, because a row is as tall as its tallest cell and the cell
178
+ * beside it does not grow in step: an object nests a whole field set, block richtext
179
+ * (`inline` absent) holds paragraphs, and an array holds however many elements the
180
+ * document carries. Any of them stands its neighbour in a column of whitespace. An
181
+ * inline prose leaf is one line tall and packs like any scalar.
182
+ *
183
+ * An array declines WHATEVER ITS ITEMS ARE: one-line elements make a one-line step,
184
+ * but nothing holds two arrays to the same number of them, so the shorter of a packed
185
+ * pair pays a cell of whitespace for every element the taller one has past it, and
186
+ * pays more of it as the document is filled.
187
+ */
188
+ function packable(f) {
189
+ if (!f.compact)
190
+ return false;
191
+ if (f.control === 'object' || f.control === 'array')
192
+ return false;
193
+ return f.control !== 'prose' || f.inline;
194
+ }
195
+ /**
196
+ * Assign each field its span in the section grid. Consecutive packable fields are
197
+ * `cell`s the grid auto-places: capacity and wrapping are the container query's
198
+ * business, so a trailing orphan keeps its column width rather than growing to fill.
199
+ * A packable run of ONE is `lone`: with no row above to align to, one column reads as
200
+ * truncated, so it takes half the capacity from column 1. Everything else is `full`.
201
+ *
202
+ * Pure, and stays pure: no width, no measurement, nothing to re-derive on resize.
203
+ */
204
+ export function placeFields(fields) {
205
+ const out = [];
206
+ let run = 0;
207
+ // A run of one is only knowable at its end, so the span is patched back onto the
208
+ // field already placed.
209
+ const flush = () => {
210
+ if (run === 1)
211
+ out[out.length - 1].span = 'lone';
212
+ run = 0;
213
+ };
214
+ for (const f of fields) {
215
+ if (packable(f)) {
216
+ out.push({ field: f, span: 'cell' });
217
+ run++;
218
+ }
219
+ else {
220
+ flush();
221
+ out.push({ field: f, span: 'full' });
222
+ }
223
+ }
224
+ flush();
225
+ return out;
226
+ }
227
+ /** Interpolate a `{field}` card-title template against live field values. */
228
+ export function interpolateTitle(template, values) {
229
+ return template.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, name) => {
230
+ const v = values[name];
231
+ return v == null ? '' : String(v);
232
+ });
233
+ }
234
+ /**
235
+ * Resolve a card instance's header title: the per-instance `$ext.editor.title`
236
+ * override wins; else the schema `ui.title` (literal or `{field}` template
237
+ * interpolated); else the humanized kind. Empty overrides fall through so a
238
+ * cleared rename reverts to the schema title.
239
+ */
240
+ export function cardTitle(cardSchema, kind, values, extTitle) {
241
+ if (extTitle && extTitle.trim())
242
+ return extTitle;
243
+ const t = cardSchema?.ui?.title;
244
+ if (t && t.trim())
245
+ return interpolateTitle(t, values);
246
+ return humanize(kind);
247
+ }
248
+ /** Whether a card kind renders a body leaf: gated by `body.enabled !== false`. */
249
+ export function bodyEnabled(cardSchema) {
250
+ return cardSchema?.body?.enabled !== false;
251
+ }
252
+ // ── Session identity (VISUAL_EDITOR §"The address is the spine") ─────────────
253
+ // Cards are POSITIONAL in the content and `doc.cards` re-allocates on each read,
254
+ // so a stable card-instance key cannot be the card object (a fresh object every
255
+ // derive): it is a session id held in a parallel array, reordered in lockstep
256
+ // with the structure ops and resolved to an index only at the mutation boundary.
257
+ /** A monotonic per-session id source. Ids are opaque strings, stable for the
258
+ * session; a fresh array for N existing instances is `seq.take(N)`, and an id
259
+ * resolves to its current index by `ids.indexOf(id)` (-1 once the instance is
260
+ * gone): read at the mutation boundary, never cached. */
261
+ export class IdSeq {
262
+ #n = 0;
263
+ next() {
264
+ return `c${this.#n++}`;
265
+ }
266
+ /** `count` fresh ids, in order. */
267
+ take(count) {
268
+ return Array.from({ length: count }, () => this.next());
269
+ }
270
+ }
@@ -0,0 +1,15 @@
1
+ /** A settable local synced to an external projection. Bind `.value` in the
2
+ * template; assign `.value` on input. */
3
+ export interface SyncedLocal<T> {
4
+ value: T;
5
+ }
6
+ /**
7
+ * Seed a local from `project` and reconcile only an EXTERNAL change into it.
8
+ * `project` reads the reactive source (a prop) and maps it to the local's type:
9
+ * ` => value ?? ''`, ` => (value != null ? String(value) : '')`, etc. The
10
+ * seed is `untrack`ed (the ongoing sync is the `$effect`, not the initializer),
11
+ * and each reconcile is `untrack`ed so an own-edit write never re-triggers it.
12
+ * Reassignment through the `value` setter is a plain local write; only a changed
13
+ * `project` result flows the other way.
14
+ */
15
+ export declare function syncedLocal<T>(project: () => T): SyncedLocal<T>;
@@ -0,0 +1,34 @@
1
+ // The controlled-local reconcile shared by the scalar controls (TextField /
2
+ // NumberField / DateField / BooleanField / EnumField). Each seeds a local from a
3
+ // projection of its `value` prop and reconciles ONLY an external change back into
4
+ // the local: own edits stay `untrack`ed, so typing never re-runs the sync and
5
+ // resets the caret. The five controls carried a character-identical block; this
6
+ // is the one copy (VISUAL_EDITOR §Surface).
7
+ import { untrack } from 'svelte';
8
+ /**
9
+ * Seed a local from `project` and reconcile only an EXTERNAL change into it.
10
+ * `project` reads the reactive source (a prop) and maps it to the local's type:
11
+ * ` => value ?? ''`, ` => (value != null ? String(value) : '')`, etc. The
12
+ * seed is `untrack`ed (the ongoing sync is the `$effect`, not the initializer),
13
+ * and each reconcile is `untrack`ed so an own-edit write never re-triggers it.
14
+ * Reassignment through the `value` setter is a plain local write; only a changed
15
+ * `project` result flows the other way.
16
+ */
17
+ export function syncedLocal(project) {
18
+ let local = $state(untrack(project));
19
+ $effect(() => {
20
+ const incoming = project();
21
+ untrack(() => {
22
+ if (incoming !== local)
23
+ local = incoming;
24
+ });
25
+ });
26
+ return {
27
+ get value() {
28
+ return local;
29
+ },
30
+ set value(v) {
31
+ local = v;
32
+ }
33
+ };
34
+ }
@@ -0,0 +1,24 @@
1
+ import { type EditorErrorHandler } from '../core/errors.js';
2
+ /**
3
+ * Narrow a raw `$ext.editor.tips` value to the renderable channel. The channel is
4
+ * CONSUMER-authored and opaque to the schema (nothing validates it on the way in)
5
+ * so anything unusable drops here rather than reaching the card: a non-array, a
6
+ * non-string element, a blank string. `[]` reads as "no tips" everywhere downstream.
7
+ */
8
+ export declare function tipsChannel(raw: unknown): string[];
9
+ /**
10
+ * One tip's markdown as DOM: `importMarkdown` → `Content` → the codec's
11
+ * `renderContent`. The tip is written in the body's mark vocabulary
12
+ * (`strong`/`emph`/`code`/`link`) rather than by a second markdown renderer that
13
+ * would drift from it.
14
+ *
15
+ * The INLINE schema, not the block one: a tip is a one-line hint, and
16
+ * `decodeInline` folds stray lines to a space and drops island slots, so any string
17
+ * produces one paragraph: no tip can change the card's block structure as the
18
+ * cursor advances.
19
+ *
20
+ * Raw HTML does not survive the round-trip, so this is not the injection seam an
21
+ * `{@html}` of the same string would be. A throw degrades to the literal text:
22
+ * chrome never breaks the editor.
23
+ */
24
+ export declare function renderTip(markdown: string, onError?: EditorErrorHandler): Node;
@@ -0,0 +1,50 @@
1
+ // The tips channel: `$ext.editor.tips`, a list of authoring hints a
2
+ // quill or consumer seeds, which the editor only ever RENDERS and CLEARS.
3
+ // Editor-only chrome; never reaches the render backend, never gates, absent when
4
+ // the channel is empty (VISUAL_EDITOR §"Card operations").
5
+ //
6
+ // The channel narrowing the derive reads and the render the card paints. The write
7
+ // is `patchEditorExt` (ext.ts), the one door into the namespace.
8
+ import { importMarkdown } from '@quillmark/wasm';
9
+ import { reportError, errorMessage } from '../core/errors.js';
10
+ import { renderContent, inlineSchema } from '../core/codec/index.js';
11
+ /**
12
+ * Narrow a raw `$ext.editor.tips` value to the renderable channel. The channel is
13
+ * CONSUMER-authored and opaque to the schema (nothing validates it on the way in)
14
+ * so anything unusable drops here rather than reaching the card: a non-array, a
15
+ * non-string element, a blank string. `[]` reads as "no tips" everywhere downstream.
16
+ */
17
+ export function tipsChannel(raw) {
18
+ if (!Array.isArray(raw))
19
+ return [];
20
+ return raw.filter((t) => typeof t === 'string' && t.trim() !== '');
21
+ }
22
+ /**
23
+ * One tip's markdown as DOM: `importMarkdown` → `Content` → the codec's
24
+ * `renderContent`. The tip is written in the body's mark vocabulary
25
+ * (`strong`/`emph`/`code`/`link`) rather than by a second markdown renderer that
26
+ * would drift from it.
27
+ *
28
+ * The INLINE schema, not the block one: a tip is a one-line hint, and
29
+ * `decodeInline` folds stray lines to a space and drops island slots, so any string
30
+ * produces one paragraph: no tip can change the card's block structure as the
31
+ * cursor advances.
32
+ *
33
+ * Raw HTML does not survive the round-trip, so this is not the injection seam an
34
+ * `{@html}` of the same string would be. A throw degrades to the literal text:
35
+ * chrome never breaks the editor.
36
+ */
37
+ export function renderTip(markdown, onError) {
38
+ try {
39
+ return renderContent(importMarkdown(markdown), inlineSchema);
40
+ }
41
+ catch (e) {
42
+ reportError(onError, {
43
+ code: 'tip-render-failed',
44
+ severity: 'error',
45
+ message: `a tip's markdown did not render; showing it literally: ${errorMessage(e)}`,
46
+ cause: e
47
+ });
48
+ return document.createTextNode(markdown);
49
+ }
50
+ }