@quillmark/svelte 0.2.0 → 0.3.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/README.md +2 -2
- package/THEMING.md +11 -7
- package/dist/core/address.d.ts +7 -7
- package/dist/core/address.js +6 -6
- package/dist/core/bloom.d.ts +5 -5
- package/dist/core/bloom.js +8 -8
- package/dist/core/codec/anchor.d.ts +1 -1
- package/dist/core/codec/anchor.js +1 -1
- package/dist/core/codec/atoms.d.ts +10 -0
- package/dist/core/codec/atoms.js +78 -0
- package/dist/core/codec/clipboard.d.ts +59 -0
- package/dist/core/codec/clipboard.js +202 -0
- package/dist/core/codec/code.d.ts +1 -1
- package/dist/core/codec/code.js +4 -4
- package/dist/core/codec/decode.d.ts +1 -1
- package/dist/core/codec/decode.js +8 -8
- package/dist/core/codec/encode.d.ts +4 -4
- package/dist/core/codec/encode.js +10 -10
- package/dist/core/codec/field.d.ts +21 -26
- package/dist/core/codec/field.js +33 -32
- package/dist/core/codec/index.js +5 -8
- package/dist/core/codec/inputrules.js +56 -18
- package/dist/core/codec/islands.d.ts +11 -7
- package/dist/core/codec/islands.js +16 -10
- package/dist/core/codec/keymap.d.ts +5 -6
- package/dist/core/codec/keymap.js +8 -7
- package/dist/core/codec/lists.d.ts +3 -3
- package/dist/core/codec/lists.js +32 -13
- package/dist/core/codec/marks.d.ts +1 -1
- package/dist/core/codec/marks.js +1 -1
- package/dist/core/codec/positions.js +1 -1
- package/dist/core/codec/prose.css +230 -215
- package/dist/core/codec/schema.js +4 -4
- package/dist/core/codec/slash.d.ts +39 -41
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-view.d.ts +12 -16
- package/dist/core/codec/table-view.js +488 -292
- package/dist/core/codec/table.d.ts +42 -15
- package/dist/core/codec/table.js +70 -26
- package/dist/core/errors.d.ts +5 -5
- package/dist/core/index.js +2 -2
- package/dist/core/lifecycle.js +1 -1
- package/dist/core/rebind.svelte.d.ts +2 -2
- package/dist/core/rebind.svelte.js +4 -4
- package/dist/core/teardown.d.ts +1 -1
- package/dist/core/teardown.js +1 -1
- package/dist/core/theme.css +144 -54
- package/dist/preset/index.css +1 -1
- package/dist/preset/recipes.css +31 -29
- package/dist/preset/scale.css +36 -25
- package/dist/preview/Preview.svelte +12 -10
- package/dist/preview/Preview.svelte.d.ts +3 -3
- package/dist/preview/bridge.js +4 -4
- package/dist/preview/controller.js +3 -3
- package/dist/preview/geometry.d.ts +2 -2
- package/dist/preview/geometry.js +2 -2
- package/dist/preview/index.js +3 -3
- package/dist/preview/overlay.d.ts +1 -1
- package/dist/preview/overlay.js +3 -3
- package/dist/preview/paint.js +15 -17
- package/dist/visual/ArrayField.svelte +80 -66
- package/dist/visual/ArrayField.svelte.d.ts +1 -1
- package/dist/visual/BooleanField.svelte +8 -6
- package/dist/visual/BooleanField.svelte.d.ts +2 -2
- package/dist/visual/Card.svelte +151 -325
- package/dist/visual/Card.svelte.d.ts +2 -6
- package/dist/visual/CardControls.svelte +1 -1
- package/dist/visual/DateField.svelte +19 -19
- package/dist/visual/DateField.svelte.d.ts +3 -3
- package/dist/visual/DiagnosticList.svelte +1 -1
- package/dist/visual/EnumField.svelte +16 -16
- package/dist/visual/EnumField.svelte.d.ts +2 -2
- package/dist/visual/Field.svelte +128 -115
- package/dist/visual/Field.svelte.d.ts +1 -1
- package/dist/visual/FieldHint.svelte +24 -42
- package/dist/visual/FieldHint.svelte.d.ts +1 -1
- package/dist/visual/FieldLabel.svelte +8 -8
- package/dist/visual/FormatPopover.svelte +80 -35
- package/dist/visual/NumberField.svelte +5 -5
- package/dist/visual/NumberField.svelte.d.ts +1 -1
- package/dist/visual/ObjectField.svelte +9 -9
- package/dist/visual/ObjectField.svelte.d.ts +1 -1
- package/dist/visual/ProseArrayElement.svelte +11 -13
- package/dist/visual/ProseArrayElement.svelte.d.ts +1 -1
- package/dist/visual/ProseField.svelte +53 -84
- package/dist/visual/ProseField.svelte.d.ts +12 -17
- package/dist/visual/SlashMenu.svelte +42 -16
- package/dist/visual/TextField.svelte +5 -5
- package/dist/visual/TextField.svelte.d.ts +1 -1
- package/dist/visual/TipsCard.svelte +43 -28
- package/dist/visual/VisualEditor.svelte +6 -6
- package/dist/visual/VisualEditorInner.svelte +122 -129
- package/dist/visual/controls.css +74 -30
- package/dist/visual/diagnostics.d.ts +5 -5
- package/dist/visual/diagnostics.js +3 -3
- package/dist/visual/domid.js +3 -3
- package/dist/visual/ext.d.ts +1 -1
- package/dist/visual/ext.js +1 -1
- package/dist/visual/icons/nodes.d.ts +3 -0
- package/dist/visual/icons/nodes.js +2 -1
- package/dist/visual/index.js +4 -4
- package/dist/visual/leaves.d.ts +3 -3
- package/dist/visual/leaves.js +1 -1
- package/dist/visual/links.d.ts +24 -0
- package/dist/visual/links.js +67 -0
- package/dist/visual/props.d.ts +8 -8
- package/dist/visual/signals.d.ts +6 -6
- package/dist/visual/strings.d.ts +14 -19
- package/dist/visual/strings.js +5 -7
- package/dist/visual/structure.d.ts +24 -17
- package/dist/visual/structure.js +8 -8
- package/dist/visual/synced.svelte.d.ts +1 -1
- package/dist/visual/synced.svelte.js +4 -4
- package/dist/visual/tips.d.ts +2 -2
- package/dist/visual/tips.js +3 -3
- package/package.json +1 -1
|
@@ -21,23 +21,19 @@ interface CardOps {
|
|
|
21
21
|
interface Props {
|
|
22
22
|
card: CardModel;
|
|
23
23
|
doc: Document;
|
|
24
|
-
/** The schema a prose leaf reads its content through (`ProseField`). */
|
|
25
24
|
quill: Quill;
|
|
26
25
|
index: number;
|
|
27
26
|
isFirst: boolean;
|
|
28
27
|
isLast: boolean;
|
|
29
28
|
kinds: string[];
|
|
30
29
|
ops: CardOps;
|
|
31
|
-
/** How an option `ops.enumAllowed` refuses draws. Nothing about it is per-card,
|
|
32
|
-
* so it sits beside the bundle rather than in it. */
|
|
33
30
|
enumDisallowed?: 'hide' | 'disable';
|
|
34
31
|
onFocus?: (addr: Addr) => void;
|
|
35
32
|
onCaretMove?: (addr: Addr, pos: number) => void;
|
|
36
33
|
onChange?: (addr: Addr) => void;
|
|
37
34
|
onError?: EditorErrorHandler;
|
|
38
|
-
/**
|
|
39
|
-
*
|
|
40
|
-
* resolve a `DocPath` to. */
|
|
35
|
+
/** Every field in this card registers its landing handle here, which is what the
|
|
36
|
+
* editor's `focusField`/`setCaret` resolve a `DocPath` to. */
|
|
41
37
|
leaves?: LeafRegistry;
|
|
42
38
|
}
|
|
43
39
|
declare const Card: import("svelte").Component<Props, {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Card controls, composable cards only.
|
|
3
3
|
Right-aligned in the header: a move-up/move-down chevron pair revealed while the
|
|
4
4
|
pointer or the caret is in the card (each disabled at its edge), then an
|
|
5
|
-
always-visible delete. Reorder is
|
|
5
|
+
always-visible delete. Reorder is buttons, not drag (V1). The card scopes that
|
|
6
6
|
reveal, so its condition lives in the parent's CSS; the pair keeps the narrower
|
|
7
7
|
guarantee here, that a focused chevron is a visible one. The card also hangs this
|
|
8
8
|
cluster to land these glyphs on its right gutter, and reads `--_qm-glyph-control`
|
|
@@ -11,26 +11,26 @@
|
|
|
11
11
|
calendar) rather than `DatePicker`: the segments are the entry affordance, and a
|
|
12
12
|
calendar is a second surface this field does not need.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
The boundary is A string, and the local is too. The primitive speaks
|
|
15
15
|
`CalendarDate`; the document speaks `YYYY-MM-DD`. `CalendarDate` carries no time
|
|
16
16
|
and no zone, so the round-trip is lossless and no local-midnight shift can occur:
|
|
17
17
|
the hazard that makes `new Date('2026-07-25')` the wrong tool here. Authored
|
|
18
|
-
values are data, not input: `parseDate`
|
|
18
|
+
values are data, not input: `parseDate` throws on anything malformed, so a bad
|
|
19
19
|
string degrades to an empty field rather than taking the editor down with it.
|
|
20
20
|
|
|
21
|
-
`syncedLocal` reconciles by
|
|
21
|
+
`syncedLocal` reconciles by identity, so the local must hold the string, not the
|
|
22
22
|
parsed value: a fresh `CalendarDate` is never `===` the last one, which would
|
|
23
23
|
make every reconcile fire and re-render all seven segments on each commit.
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
The ghost is the default'S digits, not A format hint. An unset field
|
|
26
26
|
carrying a `default:` prints the default's digits in the segments, ghost-toned,
|
|
27
27
|
instead of the primitive's `mm`/`dd`/`yyyy` hints, which say "empty" where the
|
|
28
|
-
rung says "will render 2026-01-01". The ghost is painted in the
|
|
28
|
+
rung says "will render 2026-01-01". The ghost is painted in the segment snippet,
|
|
29
29
|
over an unset primitive: substituting the default for `value` instead would make
|
|
30
30
|
the field indistinguishable from an authored one to every path that reads it
|
|
31
31
|
(`areAllSegmentsFilled`, Backspace, the hidden input), and the primitive shadows
|
|
32
32
|
a written-back `value` prop it was not `bind:`-ed to, so re-seating the ghost
|
|
33
|
-
after a clear never lands. `placeholder` (the `DateValue` the segments
|
|
33
|
+
after a clear never lands. `placeholder` (the `DateValue` the segments count
|
|
34
34
|
from, never one they display) carries the default too, so arrowing an empty
|
|
35
35
|
segment starts at the render's date rather than today's.
|
|
36
36
|
-->
|
|
@@ -45,15 +45,15 @@
|
|
|
45
45
|
/** The resolved `default:` in the boundary's currency (`YYYY-MM-DD`): parsed
|
|
46
46
|
* for display only, shown while unset, never written. */
|
|
47
47
|
fallback?: string;
|
|
48
|
-
/** Accessible name for a field
|
|
48
|
+
/** Accessible name for a field nothing else names: an object property, whose
|
|
49
49
|
* name is the field label plus the property's. A field's own date takes
|
|
50
50
|
* `labelledBy` instead. */
|
|
51
51
|
label?: string;
|
|
52
|
-
/** The field label's own id. `for`
|
|
52
|
+
/** The field label's own id. `for` cannot reach this control: the segment
|
|
53
53
|
* container is not a labelable element; so the association runs the other way,
|
|
54
54
|
* and the label's click comes back through {@link focus}. It lands on the
|
|
55
55
|
* container, which the primitive gives `role="group"`: the name belongs to the
|
|
56
|
-
*
|
|
56
|
+
* set of segments, and entering any of them announces it. (bits builds each
|
|
57
57
|
* segment's own `aria-labelledby` from a `DateField.Label` inside its root:
|
|
58
58
|
* unreachable from here, since the field's label is a grid child of the field,
|
|
59
59
|
* not of the control.) */
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
|
|
67
67
|
let wrapEl: HTMLElement | undefined = $state();
|
|
68
68
|
/** Take the caret: what the label click, and a parent placing focus here, calls.
|
|
69
|
-
* The
|
|
69
|
+
* The first segment, not the field: focus lives on a segment (which is why the
|
|
70
70
|
* ring is `.qm-focus-ring-within`), and the container holds none. `literal` is the
|
|
71
71
|
* separator between segments: present, never focusable. */
|
|
72
72
|
export function focus(): void {
|
|
@@ -85,14 +85,14 @@
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
// Local value synced to `value` as a
|
|
88
|
+
// Local value synced to `value` as a string (see the identity note above);
|
|
89
89
|
// own-edits stay local, only an external change reconciles back in. Driven
|
|
90
|
-
//
|
|
90
|
+
// controlled (`value` + `onValueChange`, never `bind:`) so reconciliation stays
|
|
91
91
|
// the package's.
|
|
92
92
|
const local = syncedLocal(() => value?.slice(0, 10) ?? '');
|
|
93
93
|
const parsed = $derived(toDateValue(local.value));
|
|
94
94
|
// The date the empty segments ghost, or undefined when there is nothing to ghost:
|
|
95
|
-
// the field is unset
|
|
95
|
+
// the field is unset and the default has a date form. A non-blank local that
|
|
96
96
|
// fails to parse is AUTHORED-but-malformed, which the empty field states
|
|
97
97
|
// honestly: the ghost would claim it unset. Held as the parsed value rather than
|
|
98
98
|
// a boolean so the substitution below narrows on the one fact it needs.
|
|
@@ -101,10 +101,10 @@
|
|
|
101
101
|
|
|
102
102
|
// What one segment prints, and whether that text is shown-never-written.
|
|
103
103
|
//
|
|
104
|
-
// An
|
|
104
|
+
// An unfilled segment is always shown-never-written, whether it prints the
|
|
105
105
|
// default's digits or the primitive's own `mm`/`dd`/`yyyy` hint: both state
|
|
106
106
|
// "nothing authored here". Only an unfilled segment ghosts: a half-entered date
|
|
107
|
-
// holds digits while the
|
|
107
|
+
// holds digits while the value is still undefined (one unfilled segment unsets
|
|
108
108
|
// the whole field), so ghosting unconditionally would paint over, and dim, the
|
|
109
109
|
// digits just typed. The segment's own text is the tell: its unfilled hint is
|
|
110
110
|
// alphabetic in every locale bits ships (`mm`/`yyyy`, `аа`, `年`), a filled one
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
>
|
|
143
143
|
<!-- `data-ghosted` states the rung the way the enum trigger does. The date
|
|
144
144
|
primitive emits no per-segment placeholder marker (only bits' `select`
|
|
145
|
-
does), so the
|
|
145
|
+
does), so the tone rides the same attribute, set per segment from the
|
|
146
146
|
substitution itself; which is also what keeps a half-typed date's own
|
|
147
147
|
digits at full ink while the segments around them ghost. -->
|
|
148
148
|
<BitsDateField.Input
|
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
data-ghosted={ghost ? '' : undefined}
|
|
154
154
|
>
|
|
155
155
|
{#snippet children({ segments })}
|
|
156
|
-
<!-- Keyed by
|
|
156
|
+
<!-- Keyed by index: `part` repeats (the `literal` separators between
|
|
157
157
|
segments all carry it) so a part-keyed block collides. -->
|
|
158
158
|
{#each segments as seg, i (i)}
|
|
159
159
|
{@const shown = segmentText(seg.part, seg.value)}
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
</span>
|
|
172
172
|
|
|
173
173
|
<style>
|
|
174
|
-
/* A primitive renders its
|
|
174
|
+
/* A primitive renders its own element, which a scoped selector cannot reach:
|
|
175
175
|
styled through the wrapper with `:global`. */
|
|
176
176
|
/* The box is `.qm-control-box` (controls.css), carried on the primitive's own
|
|
177
177
|
element beside `.qm-focus-ring-within`; the segments inherit its size and ink
|
|
@@ -183,7 +183,7 @@
|
|
|
183
183
|
box-sizing: border-box;
|
|
184
184
|
}
|
|
185
185
|
/* The ring rides `.qm-focus-ring-within` (controls.css) rather than the plain
|
|
186
|
-
marker: focus lives on the
|
|
186
|
+
marker: focus lives on the segment, so it rings the field, not the segment
|
|
187
187
|
the caret happens to be in. */
|
|
188
188
|
.qm-date-wrap :global(.qm-date-segment) {
|
|
189
189
|
padding: 0 var(--_qm-space-half);
|
|
@@ -4,15 +4,15 @@ interface Props {
|
|
|
4
4
|
/** The resolved `default:` in the boundary's currency (`YYYY-MM-DD`): parsed
|
|
5
5
|
* for display only, shown while unset, never written. */
|
|
6
6
|
fallback?: string;
|
|
7
|
-
/** Accessible name for a field
|
|
7
|
+
/** Accessible name for a field nothing else names: an object property, whose
|
|
8
8
|
* name is the field label plus the property's. A field's own date takes
|
|
9
9
|
* `labelledBy` instead. */
|
|
10
10
|
label?: string;
|
|
11
|
-
/** The field label's own id. `for`
|
|
11
|
+
/** The field label's own id. `for` cannot reach this control: the segment
|
|
12
12
|
* container is not a labelable element; so the association runs the other way,
|
|
13
13
|
* and the label's click comes back through {@link focus}. It lands on the
|
|
14
14
|
* container, which the primitive gives `role="group"`: the name belongs to the
|
|
15
|
-
*
|
|
15
|
+
* set of segments, and entering any of them announces it. (bits builds each
|
|
16
16
|
* segment's own `aria-labelledby` from a `DateField.Label` inside its root:
|
|
17
17
|
* unreachable from here, since the field's label is a grid child of the field,
|
|
18
18
|
* not of the control.) */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
Inline diagnostic display (VISUAL_EDITOR §Diagnostics), severity-styled via
|
|
3
|
-
CSS vars,
|
|
3
|
+
CSS vars, non-gating: purely additive, never disables the field it sits
|
|
4
4
|
under. Shared by `Field.svelte` (a scalar/prose field) and `Card.svelte` (the
|
|
5
5
|
body leaf, which has no `Field.svelte` wrapper), so a card's body diagnostics
|
|
6
6
|
render the same way a field's do.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
A `string`+`enum` (or `type: 'enum'`) field → a styled listbox over `enum ??
|
|
3
|
-
values`, on bits-ui. When nothing is authored the list shows a distinct
|
|
4
|
-
sentinel that
|
|
3
|
+
values`, on bits-ui. When nothing is authored the list shows a distinct unset
|
|
4
|
+
sentinel that ghosts the `default:` (muted, shown-never-written), distinguishable
|
|
5
5
|
from an authored pick and re-selectable; so re-picking the default fires a
|
|
6
|
-
change. The sentinel commits nothing; any real pick:
|
|
6
|
+
change. The sentinel commits nothing; any real pick: including the
|
|
7
7
|
value that equals the default: commits via the parent's typed `writer.set`.
|
|
8
8
|
Explicitly picking the default is the one place "commit the default" is genuine
|
|
9
9
|
intent, expressible. The sentinel stays in the list once a value is authored, as
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
value: string | undefined;
|
|
23
23
|
values: string[];
|
|
24
24
|
fallback?: string;
|
|
25
|
-
/** Accessible name for a trigger
|
|
25
|
+
/** Accessible name for a trigger nothing else names: an object property, whose
|
|
26
26
|
* name is the field label plus the property's. A field's own trigger takes `id`
|
|
27
27
|
* instead and is named by the `<label for>` beside it (the trigger is a
|
|
28
28
|
* `<button>`, so `for` reaches it and a label click opens the list: what the
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
describedBy?: string;
|
|
36
36
|
onCommit: (v: string | undefined) => void;
|
|
37
37
|
/** Consumer policy: `false` marks an option unavailable, so it can't be picked.
|
|
38
|
-
* The
|
|
38
|
+
* The unset sentinel is exempt: clear-to-default always works. */
|
|
39
39
|
optionAllowed?: (value: string) => boolean;
|
|
40
40
|
/** How a refused option draws: greyed and unpickable, or left out of the list. */
|
|
41
41
|
enumDisallowed?: 'hide' | 'disable';
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
|
|
60
60
|
// Local selection synced to `value` (sentinel when unauthored); own-picks stay
|
|
61
61
|
// local, only an external change reconciles back in (see `syncedLocal`). Driven
|
|
62
|
-
//
|
|
62
|
+
// controlled (`value` + `onValueChange`, never `bind:`) so reconciliation stays
|
|
63
63
|
// the package's rather than the primitive's.
|
|
64
64
|
const local = syncedLocal(() => value ?? UNSET);
|
|
65
65
|
|
|
@@ -71,16 +71,16 @@
|
|
|
71
71
|
/** What the closed trigger shows: the pick, or the ghosted default while unset. */
|
|
72
72
|
const shown = $derived(unset ? ghostText : dash(local.value));
|
|
73
73
|
|
|
74
|
-
/** The root to portal
|
|
74
|
+
/** The root to portal into: `document.body` would escape the consumer's dials
|
|
75
75
|
* along with the editor's subtree. `undefined` falls back to bits-ui's default. */
|
|
76
76
|
let wrapEl = $state<HTMLElement | undefined>(undefined);
|
|
77
77
|
const portalTarget = $derived(wrapEl?.closest<HTMLElement>('[data-qm-root]') ?? undefined);
|
|
78
78
|
</script>
|
|
79
79
|
|
|
80
80
|
<span class="qm-select-wrap" bind:this={wrapEl}>
|
|
81
|
-
<!-- `allowDeselect={false}`: the
|
|
81
|
+
<!-- `allowDeselect={false}`: the unset sentinel is the clear-to-default
|
|
82
82
|
affordance, so the primitive's own deselect must stay off. It reports a
|
|
83
|
-
deselect as `''`:
|
|
83
|
+
deselect as `''`: indistinguishable from picking the empty-string enum
|
|
84
84
|
member the reference quill actually declares. -->
|
|
85
85
|
<Select.Root
|
|
86
86
|
type="single"
|
|
@@ -106,18 +106,18 @@
|
|
|
106
106
|
</Select.Trigger>
|
|
107
107
|
<Select.Portal to={portalTarget}>
|
|
108
108
|
<Select.Content sideOffset={4}>
|
|
109
|
-
<!-- The list
|
|
109
|
+
<!-- The list portals out of the trigger's DOM but into the nearest
|
|
110
110
|
`[data-qm-root]`, and carries the marker itself, like FormatPopover.
|
|
111
111
|
The pill is this
|
|
112
112
|
element (not the primitive's) because scoped CSS keys off which
|
|
113
|
-
component
|
|
113
|
+
component owns the markup: a `class` passed to a primitive is a
|
|
114
114
|
plain string and never picks up the scoping hash. -->
|
|
115
115
|
<div class="qm-menu-surface qm-select-content" data-qm-root>
|
|
116
116
|
<Select.Viewport>
|
|
117
117
|
<Select.Item class="qm-menu-item qm-select-item" value={UNSET} label={ghostText}>
|
|
118
118
|
<span class="qm-select-ghost">{ghostText}</span>
|
|
119
119
|
</Select.Item>
|
|
120
|
-
<!-- A refused option still draws under `'disable'`, and under
|
|
120
|
+
<!-- A refused option still draws under `'disable'`, and under either
|
|
121
121
|
policy when it is the one selected: a listbox whose selected value
|
|
122
122
|
has no row shows nothing for what the document says, and offers none
|
|
123
123
|
for the primitive to mark, type-ahead to, or key onto
|
|
@@ -145,10 +145,10 @@
|
|
|
145
145
|
</span>
|
|
146
146
|
|
|
147
147
|
<style>
|
|
148
|
-
/* A primitive renders its
|
|
148
|
+
/* A primitive renders its own element, which a scoped selector cannot reach:
|
|
149
149
|
styled through the wrapper with `:global`. */
|
|
150
150
|
/* The box is `.qm-control-box` (controls.css), carried on the primitive's own
|
|
151
|
-
element the same way `.qm-focus-ring` is; only what a
|
|
151
|
+
element the same way `.qm-focus-ring` is; only what a trigger adds over a typed
|
|
152
152
|
value's box is here. */
|
|
153
153
|
.qm-select-wrap :global(.qm-select) {
|
|
154
154
|
display: flex;
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
color: var(--_qm-ink-ghost);
|
|
168
168
|
}
|
|
169
169
|
/* The open list is `.qm-menu-surface` and its rows `.qm-menu-item` (controls.css):
|
|
170
|
-
the lift, the inset and the highlight are the shared menu recipe. What a
|
|
170
|
+
the lift, the inset and the highlight are the shared menu recipe. What a listbox
|
|
171
171
|
adds over a menu is here: it spans its trigger rather than its own content, it
|
|
172
172
|
scrolls past a screenful, and it marks the value already stored. */
|
|
173
173
|
.qm-select-content {
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
overflow-y: auto;
|
|
177
177
|
}
|
|
178
178
|
.qm-select-content :global(.qm-select-item[data-selected]) {
|
|
179
|
-
font-weight: var(--_qm-weight-
|
|
179
|
+
font-weight: var(--_qm-weight-strong);
|
|
180
180
|
}
|
|
181
181
|
/* Consumer policy, for a refused option the list still draws: offered, unpickable. */
|
|
182
182
|
.qm-select-content :global(.qm-select-item[data-disabled]) {
|
|
@@ -3,7 +3,7 @@ interface Props {
|
|
|
3
3
|
value: string | undefined;
|
|
4
4
|
values: string[];
|
|
5
5
|
fallback?: string;
|
|
6
|
-
/** Accessible name for a trigger
|
|
6
|
+
/** Accessible name for a trigger nothing else names: an object property, whose
|
|
7
7
|
* name is the field label plus the property's. A field's own trigger takes `id`
|
|
8
8
|
* instead and is named by the `<label for>` beside it (the trigger is a
|
|
9
9
|
* `<button>`, so `for` reaches it and a label click opens the list: what the
|
|
@@ -16,7 +16,7 @@ interface Props {
|
|
|
16
16
|
describedBy?: string;
|
|
17
17
|
onCommit: (v: string | undefined) => void;
|
|
18
18
|
/** Consumer policy: `false` marks an option unavailable, so it can't be picked.
|
|
19
|
-
* The
|
|
19
|
+
* The unset sentinel is exempt: clear-to-default always works. */
|
|
20
20
|
optionAllowed?: (value: string) => boolean;
|
|
21
21
|
/** How a refused option draws: greyed and unpickable, or left out of the list. */
|
|
22
22
|
enumDisallowed?: 'hide' | 'disable';
|
package/dist/visual/Field.svelte
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
{@link FieldModel} and its live value, render the label + the control the type
|
|
4
4
|
maps to. Array controls own their label (paired with the add affordance in
|
|
5
5
|
{@link ArrayField}); other types render the label here. Prose leaves take a
|
|
6
|
-
parent-built
|
|
6
|
+
parent-built live `addr` (its `card` a getter over the stable-id→index map) so
|
|
7
7
|
a reorder re-targets without a remount; scalars, arrays, and objects commit
|
|
8
|
-
their value
|
|
8
|
+
their value up through `onCommitScalar`, which the parent lowers to the typed
|
|
9
9
|
writer.
|
|
10
10
|
|
|
11
11
|
`diagnostics` is the routed `Diagnostic[]` for this field (VisualEditor's
|
|
12
12
|
`diagByKey`, merging `quill.validate`, local commit errors, and the external
|
|
13
13
|
`diagnostics` prop (VISUAL_EDITOR §Diagnostics)) rendered via the shared
|
|
14
|
-
`DiagnosticList`, severity-styled,
|
|
14
|
+
`DiagnosticList`, severity-styled, non-gating.
|
|
15
15
|
-->
|
|
16
16
|
<script lang="ts">
|
|
17
17
|
import type { Document, Quill, Addr, Diagnostic, ResolvedField } from '@quillmark/wasm';
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
doc: Document;
|
|
43
43
|
/** The schema a prose leaf reads its content through (`ProseField`). */
|
|
44
44
|
quill: Quill;
|
|
45
|
-
/** This field's
|
|
45
|
+
/** This field's live address (getter-`card`, so a card reorder re-targets in
|
|
46
46
|
* place): the prose leaf commits to it, and a focus reports it. */
|
|
47
47
|
addr: Addr;
|
|
48
48
|
leafKey: string;
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
const ghost = $derived(ghostDefault(provenance));
|
|
93
93
|
const defaultStr = $derived(stringifyGhost(ghost));
|
|
94
94
|
|
|
95
|
-
// `for` reaches a
|
|
95
|
+
// `for` reaches a labelable control and the browser does the rest. The other four
|
|
96
96
|
// are not labelable (the prose leaf's `contenteditable`, the date field's segment
|
|
97
97
|
// container, the object subform, an array's N inputs) so `for` there would be
|
|
98
98
|
// both inert and invalid markup; they take `aria-labelledby` and a click handoff.
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
});
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
|
-
* A form control has no controller to report its focus through, so the
|
|
162
|
+
* A form control has no controller to report its focus through, so the wrapper
|
|
163
163
|
* reports: `focusin` bubbles, so one handler covers a plain input, an array's N
|
|
164
164
|
* elements and an object's properties alike, and the active leaf names a scalar
|
|
165
165
|
* field the way it names a prose one. A prose leaf reports through its own
|
|
@@ -181,137 +181,150 @@
|
|
|
181
181
|
description={field.description}
|
|
182
182
|
/>
|
|
183
183
|
{/if}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
184
|
+
<!-- The control and what hangs under it, in one grid cell: the diagnostics stack
|
|
185
|
+
inside the control's track rather than claiming a track of their own (the subgrid
|
|
186
|
+
rule below holds why). One markup for both spans; a field owning its row nests a
|
|
187
|
+
level and measures the same. -->
|
|
188
|
+
<div class="qm-field-stack">
|
|
189
|
+
<div class="qm-field-control" bind:this={controlEl} onfocusin={reportFocus}>
|
|
190
|
+
{#if field.control === 'prose'}
|
|
191
|
+
<ProseField
|
|
192
|
+
{quill}
|
|
193
|
+
bind:this={proseEl}
|
|
194
|
+
{doc}
|
|
195
|
+
{addr}
|
|
196
|
+
inline={field.inline}
|
|
197
|
+
plaintext={field.plaintext}
|
|
198
|
+
labelledBy={domIds.label}
|
|
199
|
+
{describedBy}
|
|
200
|
+
{leafKey}
|
|
201
|
+
{onFocus}
|
|
202
|
+
{onCaretMove}
|
|
203
|
+
{onChange}
|
|
204
|
+
{onError}
|
|
205
|
+
{leaves}
|
|
206
|
+
/>
|
|
207
|
+
{:else if field.control === 'enum'}
|
|
208
|
+
<EnumField
|
|
209
|
+
value={value as string | undefined}
|
|
210
|
+
values={enumValues(field.schema) ?? []}
|
|
211
|
+
fallback={ghost as string | undefined}
|
|
212
|
+
id={domIds.control}
|
|
213
|
+
{describedBy}
|
|
214
|
+
onCommit={onCommitScalar}
|
|
215
|
+
{optionAllowed}
|
|
216
|
+
{enumDisallowed}
|
|
217
|
+
/>
|
|
218
|
+
{:else if field.control === 'number'}
|
|
219
|
+
<NumberField
|
|
220
|
+
value={value as number | undefined}
|
|
221
|
+
integer={field.schema.type === 'integer'}
|
|
222
|
+
fallback={ghost as number | undefined}
|
|
223
|
+
id={domIds.control}
|
|
224
|
+
{describedBy}
|
|
225
|
+
onCommit={onCommitScalar}
|
|
226
|
+
/>
|
|
227
|
+
{:else if field.control === 'boolean'}
|
|
228
|
+
<BooleanField
|
|
229
|
+
value={value as boolean | undefined}
|
|
230
|
+
fallback={ghost as boolean | undefined}
|
|
231
|
+
id={domIds.control}
|
|
232
|
+
{describedBy}
|
|
233
|
+
onCommit={onCommitScalar}
|
|
234
|
+
/>
|
|
235
|
+
{:else if field.control === 'date'}
|
|
236
|
+
<DateField
|
|
237
|
+
bind:this={dateEl}
|
|
238
|
+
value={value as string | undefined}
|
|
239
|
+
fallback={defaultStr}
|
|
240
|
+
labelledBy={domIds.label}
|
|
241
|
+
{describedBy}
|
|
242
|
+
onCommit={onCommitScalar}
|
|
243
|
+
/>
|
|
244
|
+
{:else if field.control === 'array'}
|
|
245
|
+
<ArrayField
|
|
246
|
+
bind:this={arrayEl}
|
|
247
|
+
value={value as unknown[] | undefined}
|
|
248
|
+
items={field.schema.items}
|
|
249
|
+
label={field.label}
|
|
250
|
+
required={field.required}
|
|
251
|
+
description={field.description}
|
|
252
|
+
labelId={domIds.label}
|
|
253
|
+
descriptionId={domIds.description}
|
|
254
|
+
onCommit={onCommitScalar}
|
|
255
|
+
/>
|
|
256
|
+
{:else if field.control === 'object'}
|
|
257
|
+
<ObjectField
|
|
258
|
+
bind:this={objectEl}
|
|
259
|
+
value={value as Record<string, unknown> | undefined}
|
|
260
|
+
properties={field.schema.properties}
|
|
261
|
+
label={field.label}
|
|
262
|
+
labelledBy={domIds.label}
|
|
263
|
+
{describedBy}
|
|
264
|
+
onCommit={onCommitScalar}
|
|
265
|
+
/>
|
|
266
|
+
{:else}
|
|
267
|
+
<TextField
|
|
268
|
+
value={value as string | undefined}
|
|
269
|
+
placeholder={defaultStr}
|
|
270
|
+
id={domIds.control}
|
|
271
|
+
{describedBy}
|
|
272
|
+
onCommit={onCommitScalar}
|
|
273
|
+
/>
|
|
274
|
+
{/if}
|
|
275
|
+
</div>
|
|
271
276
|
|
|
272
|
-
|
|
277
|
+
<DiagnosticList {diagnostics} />
|
|
278
|
+
</div>
|
|
273
279
|
</div>
|
|
274
280
|
|
|
275
281
|
<style>
|
|
276
282
|
/* The `full` span, and the base every field starts from: its own row, a plain
|
|
277
283
|
stack. Nothing shares the row, so there are no internals to align against.
|
|
278
284
|
|
|
279
|
-
|
|
280
|
-
a row action
|
|
281
|
-
|
|
282
|
-
for the whole grid, which reaches only a field that stops short of it. The array
|
|
283
|
-
carries the only row action there is, and it always owns its row (structure.ts
|
|
284
|
-
`packable`), so it spans that reservation with every other column: the one field
|
|
285
|
-
a section could reserve for is the one it would miss. Per field the reservation
|
|
286
|
-
travels with the thing that uses it: one right edge per track, every control in a
|
|
287
|
-
row ending on it, and an array reaching back across its own to put its remove
|
|
288
|
-
past it (ArrayField). Every field pays the width, including one with no action in
|
|
289
|
-
it; a right edge that moved with a field's contents is the raggedness this
|
|
290
|
-
removes. */
|
|
285
|
+
No inset at the end: the right edge is the track's and every control in a row ends
|
|
286
|
+
on it, a row action sitting inside the element that carries it (ArrayField) rather
|
|
287
|
+
than in a column every field would hold clear. */
|
|
291
288
|
.qm-field {
|
|
292
289
|
display: flex;
|
|
293
290
|
flex-direction: column;
|
|
294
291
|
gap: var(--_qm-space);
|
|
295
292
|
grid-column: 1 / -1;
|
|
296
293
|
min-width: 0;
|
|
297
|
-
padding-right: var(--action-col);
|
|
298
294
|
}
|
|
299
295
|
/* A row-sharing field subgrids onto the section's row tracks instead of sizing its
|
|
300
|
-
own:
|
|
301
|
-
control in a visual row starts at the same y however tall a neighbour's
|
|
302
|
-
wrapped
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
296
|
+
own: two tracks, the label and the control's stack, taken from the parent, so
|
|
297
|
+
every control in a visual row starts at the same y however tall a neighbour's
|
|
298
|
+
label wrapped. Source order is track order; `align-items: start` keeps a short
|
|
299
|
+
control from stretching to a taller sibling's track. `row-gap` overrides the
|
|
300
|
+
section's inter-row gutter for the tracks this field spans: inside a field the
|
|
301
|
+
rhythm is tighter than between rows.
|
|
302
|
+
|
|
303
|
+
Two tracks, and not a third for the diagnostics: a track is permanent where a
|
|
304
|
+
diagnostic is occasional, and an empty track still costs the gutter above it, so
|
|
305
|
+
a third stands a rung of dead space under every field on the path where nothing
|
|
306
|
+
is wrong. That rung is the one place a field's box outruns its ink. It is what
|
|
307
|
+
makes a run of row-sharing fields read looser than the arrays and prose leaves
|
|
308
|
+
beside them, whose boxes end where their ink does. What a third track buys is the
|
|
309
|
+
diagnostics of one row starting at one y; the alignment a row is read by is the
|
|
310
|
+
controls', and the label track holds that. A diagnostic grows its row instead. */
|
|
307
311
|
.qm-field.cell,
|
|
308
312
|
.qm-field.lone {
|
|
309
313
|
display: grid;
|
|
310
|
-
grid-row: span
|
|
314
|
+
grid-row: span 2;
|
|
311
315
|
grid-template-rows: subgrid;
|
|
312
316
|
row-gap: var(--_qm-space);
|
|
313
317
|
align-items: start;
|
|
314
318
|
}
|
|
319
|
+
/* The stack the control's track holds: the control, and the diagnostics when
|
|
320
|
+
there are any. Its gap is the field's own, so a diagnostic sits the same rung
|
|
321
|
+
under its control that the control sits under its label. */
|
|
322
|
+
.qm-field-stack {
|
|
323
|
+
display: flex;
|
|
324
|
+
flex-direction: column;
|
|
325
|
+
gap: var(--_qm-space);
|
|
326
|
+
min-width: 0;
|
|
327
|
+
}
|
|
315
328
|
.qm-field.cell {
|
|
316
329
|
grid-column: span 1;
|
|
317
330
|
}
|
|
@@ -14,7 +14,7 @@ interface Props {
|
|
|
14
14
|
doc: Document;
|
|
15
15
|
/** The schema a prose leaf reads its content through (`ProseField`). */
|
|
16
16
|
quill: Quill;
|
|
17
|
-
/** This field's
|
|
17
|
+
/** This field's live address (getter-`card`, so a card reorder re-targets in
|
|
18
18
|
* place): the prose leaf commits to it, and a focus reports it. */
|
|
19
19
|
addr: Addr;
|
|
20
20
|
leafKey: string;
|