@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.
- package/LICENSE +201 -0
- package/NOTICE +58 -0
- package/README.md +241 -0
- package/THEMING.md +153 -0
- package/dist/core/address.d.ts +56 -0
- package/dist/core/address.js +95 -0
- package/dist/core/bloom.d.ts +37 -0
- package/dist/core/bloom.js +116 -0
- package/dist/core/codec/anchor.d.ts +19 -0
- package/dist/core/codec/anchor.js +36 -0
- package/dist/core/codec/code.d.ts +12 -0
- package/dist/core/codec/code.js +118 -0
- package/dist/core/codec/decode.d.ts +24 -0
- package/dist/core/codec/decode.js +270 -0
- package/dist/core/codec/encode.d.ts +71 -0
- package/dist/core/codec/encode.js +480 -0
- package/dist/core/codec/field.d.ts +171 -0
- package/dist/core/codec/field.js +493 -0
- package/dist/core/codec/index.d.ts +22 -0
- package/dist/core/codec/index.js +43 -0
- package/dist/core/codec/inputrules.d.ts +7 -0
- package/dist/core/codec/inputrules.js +168 -0
- package/dist/core/codec/islands.d.ts +36 -0
- package/dist/core/codec/islands.js +82 -0
- package/dist/core/codec/keymap.d.ts +13 -0
- package/dist/core/codec/keymap.js +29 -0
- package/dist/core/codec/lists.d.ts +22 -0
- package/dist/core/codec/lists.js +104 -0
- package/dist/core/codec/marks.d.ts +37 -0
- package/dist/core/codec/marks.js +74 -0
- package/dist/core/codec/positions.d.ts +17 -0
- package/dist/core/codec/positions.js +108 -0
- package/dist/core/codec/prose.css +458 -0
- package/dist/core/codec/reconcile.d.ts +26 -0
- package/dist/core/codec/reconcile.js +64 -0
- package/dist/core/codec/schema.d.ts +13 -0
- package/dist/core/codec/schema.js +155 -0
- package/dist/core/codec/slash.d.ts +79 -0
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-view.d.ts +50 -0
- package/dist/core/codec/table-view.js +928 -0
- package/dist/core/codec/table.d.ts +85 -0
- package/dist/core/codec/table.js +224 -0
- package/dist/core/errors.d.ts +73 -0
- package/dist/core/errors.js +24 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +19 -0
- package/dist/core/lifecycle.d.ts +7 -0
- package/dist/core/lifecycle.js +21 -0
- package/dist/core/rebind.svelte.d.ts +18 -0
- package/dist/core/rebind.svelte.js +44 -0
- package/dist/core/teardown.d.ts +20 -0
- package/dist/core/teardown.js +37 -0
- package/dist/core/theme.css +276 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +9 -0
- package/dist/preset/index.css +10 -0
- package/dist/preset/recipes.css +188 -0
- package/dist/preset/scale.css +97 -0
- package/dist/preview/Preview.svelte +118 -0
- package/dist/preview/Preview.svelte.d.ts +39 -0
- package/dist/preview/bridge.d.ts +13 -0
- package/dist/preview/bridge.js +88 -0
- package/dist/preview/controller.d.ts +40 -0
- package/dist/preview/controller.js +141 -0
- package/dist/preview/geometry.d.ts +37 -0
- package/dist/preview/geometry.js +45 -0
- package/dist/preview/index.d.ts +5 -0
- package/dist/preview/index.js +14 -0
- package/dist/preview/overlay.d.ts +10 -0
- package/dist/preview/overlay.js +82 -0
- package/dist/preview/paint.d.ts +17 -0
- package/dist/preview/paint.js +262 -0
- package/dist/preview/strings.d.ts +14 -0
- package/dist/preview/strings.js +22 -0
- package/dist/visual/ArrayField.svelte +362 -0
- package/dist/visual/ArrayField.svelte.d.ts +24 -0
- package/dist/visual/BooleanField.svelte +88 -0
- package/dist/visual/BooleanField.svelte.d.ts +20 -0
- package/dist/visual/Card.svelte +849 -0
- package/dist/visual/Card.svelte.d.ts +43 -0
- package/dist/visual/CardControls.svelte +92 -0
- package/dist/visual/CardControls.svelte.d.ts +11 -0
- package/dist/visual/DateField.svelte +203 -0
- package/dist/visual/DateField.svelte.d.ts +28 -0
- package/dist/visual/DiagnosticList.svelte +50 -0
- package/dist/visual/DiagnosticList.svelte.d.ts +7 -0
- package/dist/visual/EnumField.svelte +169 -0
- package/dist/visual/EnumField.svelte.d.ts +25 -0
- package/dist/visual/Field.svelte +270 -0
- package/dist/visual/Field.svelte.d.ts +37 -0
- package/dist/visual/FieldHint.svelte +185 -0
- package/dist/visual/FieldHint.svelte.d.ts +13 -0
- package/dist/visual/FieldLabel.svelte +141 -0
- package/dist/visual/FieldLabel.svelte.d.ts +19 -0
- package/dist/visual/FormatPopover.svelte +396 -0
- package/dist/visual/FormatPopover.svelte.d.ts +9 -0
- package/dist/visual/NumberField.svelte +72 -0
- package/dist/visual/NumberField.svelte.d.ts +19 -0
- package/dist/visual/ObjectField.svelte +131 -0
- package/dist/visual/ObjectField.svelte.d.ts +17 -0
- package/dist/visual/ProseArrayElement.svelte +98 -0
- package/dist/visual/ProseArrayElement.svelte.d.ts +19 -0
- package/dist/visual/ProseField.svelte +213 -0
- package/dist/visual/ProseField.svelte.d.ts +50 -0
- package/dist/visual/SlashMenu.svelte +126 -0
- package/dist/visual/SlashMenu.svelte.d.ts +14 -0
- package/dist/visual/TextField.svelte +63 -0
- package/dist/visual/TextField.svelte.d.ts +23 -0
- package/dist/visual/TipsCard.svelte +191 -0
- package/dist/visual/TipsCard.svelte.d.ts +13 -0
- package/dist/visual/VisualEditor.svelte +96 -0
- package/dist/visual/VisualEditor.svelte.d.ts +14 -0
- package/dist/visual/VisualEditorInner.svelte +1032 -0
- package/dist/visual/VisualEditorInner.svelte.d.ts +17 -0
- package/dist/visual/controls.css +272 -0
- package/dist/visual/diagnostics.d.ts +52 -0
- package/dist/visual/diagnostics.js +83 -0
- package/dist/visual/domid.d.ts +21 -0
- package/dist/visual/domid.js +36 -0
- package/dist/visual/ext.d.ts +19 -0
- package/dist/visual/ext.js +29 -0
- package/dist/visual/icons/Icon.svelte +39 -0
- package/dist/visual/icons/Icon.svelte.d.ts +12 -0
- package/dist/visual/icons/nodes.d.ts +103 -0
- package/dist/visual/icons/nodes.js +55 -0
- package/dist/visual/index.d.ts +9 -0
- package/dist/visual/index.js +24 -0
- package/dist/visual/motion.d.ts +18 -0
- package/dist/visual/motion.js +53 -0
- package/dist/visual/props.d.ts +77 -0
- package/dist/visual/props.js +1 -0
- package/dist/visual/signals.d.ts +59 -0
- package/dist/visual/signals.js +1 -0
- package/dist/visual/strings.d.ts +123 -0
- package/dist/visual/strings.js +80 -0
- package/dist/visual/structure.d.ts +224 -0
- package/dist/visual/structure.js +270 -0
- package/dist/visual/synced.svelte.d.ts +15 -0
- package/dist/visual/synced.svelte.js +34 -0
- package/dist/visual/tips.d.ts +24 -0
- package/dist/visual/tips.js +50 -0
- 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
|
+
}
|