@quillmark/svelte 0.5.1 → 0.7.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 +6 -0
- package/THEMING.md +13 -17
- package/dist/core/address.d.ts +17 -3
- package/dist/core/address.js +20 -3
- package/dist/core/codec/blocks.d.ts +37 -0
- package/dist/core/codec/blocks.js +163 -0
- package/dist/core/codec/field.js +5 -6
- package/dist/core/codec/index.d.ts +1 -1
- package/dist/core/codec/index.js +3 -2
- package/dist/core/codec/inputrules.js +24 -144
- package/dist/core/codec/prose.css +14 -25
- package/dist/core/codec/schema.d.ts +8 -0
- package/dist/core/codec/schema.js +33 -1
- package/dist/core/codec/slash.d.ts +18 -16
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-view.d.ts +4 -0
- package/dist/core/codec/table-view.js +137 -158
- package/dist/core/codec/table.d.ts +0 -10
- package/dist/core/codec/table.js +0 -15
- package/dist/core/theme.css +357 -346
- package/dist/preset/recipes.css +25 -21
- package/dist/preview/Preview.svelte +4 -2
- package/dist/preview/bridge.d.ts +23 -0
- package/dist/preview/bridge.js +92 -35
- package/dist/preview/controller.d.ts +6 -5
- package/dist/preview/controller.js +13 -7
- package/dist/preview/geometry.d.ts +10 -9
- package/dist/preview/geometry.js +11 -13
- package/dist/preview/index.js +3 -2
- package/dist/preview/paint.js +9 -1
- package/dist/visual/ArrayField.svelte +246 -63
- package/dist/visual/ArrayField.svelte.d.ts +5 -1
- package/dist/visual/Card.svelte +84 -50
- package/dist/visual/EnumField.svelte +37 -3
- package/dist/visual/Field.svelte +28 -4
- package/dist/visual/FieldLabel.svelte +2 -2
- package/dist/visual/FormatPopover.svelte +13 -1
- package/dist/visual/ObjectField.svelte +128 -35
- package/dist/visual/ObjectField.svelte.d.ts +11 -4
- package/dist/visual/ProseArrayElement.svelte +22 -2
- package/dist/visual/ProseArrayElement.svelte.d.ts +1 -0
- package/dist/visual/SlashMenu.svelte +7 -0
- package/dist/visual/TipsCard.svelte +33 -13
- package/dist/visual/VariantField.svelte +95 -0
- package/dist/visual/VariantField.svelte.d.ts +21 -0
- package/dist/visual/VisualEditorInner.svelte +44 -21
- package/dist/visual/controls.css +282 -283
- package/dist/visual/diagnostics.d.ts +13 -11
- package/dist/visual/diagnostics.js +11 -9
- package/dist/visual/domid.d.ts +13 -0
- package/dist/visual/domid.js +16 -0
- package/dist/visual/hold.d.ts +17 -0
- package/dist/visual/hold.js +39 -0
- package/dist/visual/index.js +1 -1
- package/dist/visual/leaves.d.ts +9 -7
- package/dist/visual/links.d.ts +5 -2
- package/dist/visual/links.js +9 -3
- package/dist/visual/motion.d.ts +9 -0
- package/dist/visual/motion.js +10 -2
- package/dist/visual/strings.d.ts +15 -0
- package/dist/visual/strings.js +3 -0
- package/dist/visual/structure.d.ts +32 -6
- package/dist/visual/structure.js +49 -3
- package/package.json +2 -2
- package/dist/core/codec/clipboard.d.ts +0 -59
- package/dist/core/codec/clipboard.js +0 -202
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
only: a nested prose/array/object property renders a placeholder instead of
|
|
5
5
|
recursing.
|
|
6
6
|
|
|
7
|
+
The nesting is a vertical at `--_qm-border`, the ladder's last stroke: the card's edge,
|
|
8
|
+
an open section's vertical one `--_qm-nest` in where the field is in one, this one a rung
|
|
9
|
+
further, the properties a rung inside it (ARCHITECTURE §"A plane is a tone").
|
|
10
|
+
|
|
11
|
+
One figure wherever it mounts: a variant's cells under their discriminant and an array
|
|
12
|
+
element's properties under its summary row sit at the depth a field-level subform sits
|
|
13
|
+
at, and the box above them states position rather than depth.
|
|
14
|
+
|
|
7
15
|
A property's ghosted `default:` is the static schema `sub.default`, not the
|
|
8
16
|
resolved provenance the top-level ghosts read (FIELD_PROVENANCE): `resolve`
|
|
9
17
|
carries no per-property row (an object field resolves as one row whose value is
|
|
@@ -11,7 +19,10 @@
|
|
|
11
19
|
-->
|
|
12
20
|
<script lang="ts">
|
|
13
21
|
import type { QuillFieldSchema } from '@quillmark/wasm';
|
|
14
|
-
import { controlKind, humanize } from './structure.js';
|
|
22
|
+
import { controlKind, humanize, obliged } from './structure.js';
|
|
23
|
+
import { wording } from './strings.js';
|
|
24
|
+
import { propertyDomIds } from './domid.js';
|
|
25
|
+
import FieldLabel from './FieldLabel.svelte';
|
|
15
26
|
import TextField from './TextField.svelte';
|
|
16
27
|
import EnumField from './EnumField.svelte';
|
|
17
28
|
import NumberField from './NumberField.svelte';
|
|
@@ -21,21 +32,42 @@
|
|
|
21
32
|
interface Props {
|
|
22
33
|
value: Record<string, unknown> | undefined;
|
|
23
34
|
properties: Record<string, QuillFieldSchema> | undefined;
|
|
24
|
-
/** Accessible-name prefix for the property controls. */
|
|
25
|
-
label?: string;
|
|
26
35
|
/** The field label's own id. A subform is a group of controls, not one control
|
|
27
|
-
* `for` could reach, so the field's label names the set
|
|
28
|
-
*
|
|
36
|
+
* `for` could reach, so the field's label names the set; each property carries
|
|
37
|
+
* its own `<label for>` inside it. */
|
|
29
38
|
labelledBy?: string;
|
|
30
39
|
/** The parked `description` (FieldLabel): announced on entering the group. */
|
|
31
40
|
describedBy?: string;
|
|
41
|
+
/**
|
|
42
|
+
* The parent control's DOM id: the base each property's own three names derive
|
|
43
|
+
* from ({@link propertyDomIds}). Absent — a subform mounted with no field around
|
|
44
|
+
* it — the properties fall back to `aria-label`, which names them without a
|
|
45
|
+
* `for` target to click.
|
|
46
|
+
*/
|
|
47
|
+
idBase?: string;
|
|
48
|
+
/** Accessible-name prefix used only on the `aria-label` fallback above. */
|
|
49
|
+
label?: string;
|
|
32
50
|
onCommit: (obj: Record<string, unknown>) => void;
|
|
33
51
|
}
|
|
34
|
-
let { value, properties,
|
|
52
|
+
let { value, properties, labelledBy, describedBy, idBase, label, onCommit }: Props = $props();
|
|
35
53
|
|
|
54
|
+
const t = wording();
|
|
36
55
|
const entries = $derived(Object.entries(properties ?? {}));
|
|
37
56
|
const obj = $derived((value ?? {}) as Record<string, unknown>);
|
|
38
57
|
|
|
58
|
+
/** Obligation, read off the cell exactly as `fieldModels` reads it off a field
|
|
59
|
+
* ({@link obliged}). `validate` anchors it per leaf, so a property with no
|
|
60
|
+
* `default:` is obliged in its own right and the container around it holds none
|
|
61
|
+
* (VISUAL_EDITOR §"Enum variants"). */
|
|
62
|
+
const required = obliged;
|
|
63
|
+
|
|
64
|
+
const title = (key: string, sub: QuillFieldSchema): string => sub.ui?.title ?? humanize(key);
|
|
65
|
+
/** The `aria-label` fallback, for a subform mounted without a field's id space:
|
|
66
|
+
* the field's name and the property's, since nothing else names the control. */
|
|
67
|
+
const fallbackName = (key: string, sub: QuillFieldSchema): string =>
|
|
68
|
+
`${label != null ? `${label} ` : ''}${title(key, sub)}` +
|
|
69
|
+
(required(sub) ? ` ${t.strings.fieldRequired}` : '');
|
|
70
|
+
|
|
39
71
|
/** Take the caret: the first property's control, a subform having no single control
|
|
40
72
|
* of its own to land on. Resolved off the DOM rather than a ref per property: every
|
|
41
73
|
* property is a scalar control the DOM already knows how to focus (the date field's
|
|
@@ -43,10 +75,19 @@
|
|
|
43
75
|
* handle each would be five refs restating document order. An empty or wholly
|
|
44
76
|
* unsupported subform lands nothing. */
|
|
45
77
|
let rootEl = $state<HTMLElement | undefined>();
|
|
78
|
+
const FOCUSABLE = 'input, select, button, [tabindex]:not([tabindex="-1"])';
|
|
46
79
|
export function focus(): void {
|
|
47
|
-
rootEl
|
|
48
|
-
|
|
49
|
-
|
|
80
|
+
rootEl?.querySelector<HTMLElement>(FOCUSABLE)?.focus();
|
|
81
|
+
}
|
|
82
|
+
/** A label click for the one property `for` cannot reach: the date control, whose
|
|
83
|
+
* focus lives on a segment. Scoped to that property's own cell, so the click lands
|
|
84
|
+
* where the label is rather than on the subform's first control. */
|
|
85
|
+
function focusProp(key: string): void {
|
|
86
|
+
// Matched on the dataset: a key is the schema's own string, and spelling one into
|
|
87
|
+
// a selector needs `CSS.escape`, which the test DOM does not carry.
|
|
88
|
+
for (const cell of rootEl?.querySelectorAll<HTMLElement>('[data-qm-prop]') ?? []) {
|
|
89
|
+
if (cell.dataset.qmProp === key) return cell.querySelector<HTMLElement>(FOCUSABLE)?.focus();
|
|
90
|
+
}
|
|
50
91
|
}
|
|
51
92
|
|
|
52
93
|
function commitProp(key: string, v: unknown): void {
|
|
@@ -72,12 +113,31 @@
|
|
|
72
113
|
>
|
|
73
114
|
{#each entries as [key, sub] (key)}
|
|
74
115
|
{@const kind = controlKind(sub)}
|
|
75
|
-
{@const
|
|
76
|
-
|
|
77
|
-
|
|
116
|
+
{@const ids = idBase ? propertyDomIds(idBase, key) : undefined}
|
|
117
|
+
{@const named = ids ? undefined : fallbackName(key, sub)}
|
|
118
|
+
{@const describes = sub.description && ids ? ids.description : undefined}
|
|
119
|
+
<!-- `for` reaches the four labelable controls; the date field's focus lives on a
|
|
120
|
+
segment, so it takes the click handoff instead, exactly as `Field` does one
|
|
121
|
+
level up. -->
|
|
122
|
+
{@const labelable =
|
|
123
|
+
kind === 'text' || kind === 'enum' || kind === 'number' || kind === 'boolean'}
|
|
124
|
+
<div class="qm-object-prop" data-qm-prop={key}>
|
|
125
|
+
{#if ids}
|
|
126
|
+
<FieldLabel
|
|
127
|
+
label={title(key, sub)}
|
|
128
|
+
controlId={labelable ? ids.control : undefined}
|
|
129
|
+
id={ids.label}
|
|
130
|
+
descriptionId={describes}
|
|
131
|
+
onActivate={labelable ? undefined : () => focusProp(key)}
|
|
132
|
+
required={required(sub)}
|
|
133
|
+
description={sub.description}
|
|
134
|
+
/>
|
|
135
|
+
{/if}
|
|
78
136
|
{#if kind === 'enum'}
|
|
79
137
|
<EnumField
|
|
80
|
-
label={
|
|
138
|
+
label={named}
|
|
139
|
+
id={ids?.control}
|
|
140
|
+
describedBy={describes}
|
|
81
141
|
value={obj[key] as string | undefined}
|
|
82
142
|
values={sub.values ?? []}
|
|
83
143
|
fallback={sub.default as string | undefined}
|
|
@@ -85,7 +145,9 @@
|
|
|
85
145
|
/>
|
|
86
146
|
{:else if kind === 'number'}
|
|
87
147
|
<NumberField
|
|
88
|
-
label={
|
|
148
|
+
label={named}
|
|
149
|
+
id={ids?.control}
|
|
150
|
+
describedBy={describes}
|
|
89
151
|
value={obj[key] as number | undefined}
|
|
90
152
|
integer={sub.type === 'integer'}
|
|
91
153
|
fallback={sub.default as number | undefined}
|
|
@@ -93,54 +155,85 @@
|
|
|
93
155
|
/>
|
|
94
156
|
{:else if kind === 'boolean'}
|
|
95
157
|
<BooleanField
|
|
96
|
-
label={
|
|
158
|
+
label={named}
|
|
159
|
+
id={ids?.control}
|
|
160
|
+
describedBy={describes}
|
|
97
161
|
value={obj[key] as boolean | undefined}
|
|
98
162
|
fallback={sub.default as boolean | undefined}
|
|
99
163
|
onCommit={(v) => commitProp(key, v)}
|
|
100
164
|
/>
|
|
101
165
|
{:else if kind === 'date'}
|
|
102
166
|
<DateField
|
|
103
|
-
label={
|
|
167
|
+
label={named}
|
|
168
|
+
labelledBy={ids?.label}
|
|
169
|
+
describedBy={describes}
|
|
104
170
|
value={obj[key] as string | undefined}
|
|
105
171
|
onCommit={(v) => commitProp(key, v)}
|
|
106
172
|
/>
|
|
107
173
|
{:else if kind === 'text'}
|
|
108
174
|
<TextField
|
|
109
|
-
label={
|
|
175
|
+
label={named}
|
|
176
|
+
id={ids?.control}
|
|
177
|
+
describedBy={describes}
|
|
110
178
|
value={obj[key] as string | undefined}
|
|
111
179
|
placeholder={sub.default != null ? String(sub.default) : undefined}
|
|
112
180
|
onCommit={(v) => commitProp(key, v)}
|
|
113
181
|
/>
|
|
114
182
|
{:else}
|
|
115
|
-
|
|
183
|
+
<!-- A property the subform does not recurse into. What it says is what the
|
|
184
|
+
user can do about it, which is edit the field from the source surface;
|
|
185
|
+
a version number is the roadmap talking to somebody filling in a form. -->
|
|
186
|
+
<span class="qm-unsupported">{t.strings.nestedUnsupported(kind)}</span>
|
|
116
187
|
{/if}
|
|
117
188
|
</div>
|
|
118
189
|
{/each}
|
|
119
190
|
</div>
|
|
120
191
|
|
|
121
192
|
<style>
|
|
193
|
+
/* The vertical and the rung between it and the properties, `--_qm-nest` being the step
|
|
194
|
+
the card and the section hold too.
|
|
195
|
+
|
|
196
|
+
`--_qm-border` and not `--_qm-border-faint`: this is the stroke that structures
|
|
197
|
+
(ARCHITECTURE §"A plane is a tone"), the one the card's edge and the section's
|
|
198
|
+
vertical read. `faint` separates without structuring — a table's interior lines
|
|
199
|
+
under a frame that is doing the structuring — which is the other job.
|
|
200
|
+
|
|
201
|
+
`padding-block` is the stroke's end caps, the same rung and the same reason as a
|
|
202
|
+
section's (`Card.svelte`). A cap and not a leading gap: what stands the subform off
|
|
203
|
+
the label or the box above it is still that stacker's own.
|
|
204
|
+
|
|
205
|
+
The tracks are the section's count over the subform's own width — `--cols` inherits,
|
|
206
|
+
so the container query stepping the section's capacity steps this with it, and there
|
|
207
|
+
is no second query container and nothing measured. The edges are not the section's:
|
|
208
|
+
the inset is the nesting, and a cell lining up with the field above it would claim a
|
|
209
|
+
depth it is not at. */
|
|
122
210
|
.qm-object {
|
|
123
|
-
display:
|
|
124
|
-
|
|
211
|
+
display: grid;
|
|
212
|
+
grid-template-columns: repeat(var(--cols), 1fr);
|
|
125
213
|
gap: var(--_qm-space-2);
|
|
126
|
-
border-
|
|
127
|
-
padding-
|
|
214
|
+
border-inline-start: var(--_qm-border-width) solid var(--_qm-border);
|
|
215
|
+
padding-inline-start: var(--_qm-nest);
|
|
216
|
+
padding-block: var(--_qm-space);
|
|
128
217
|
}
|
|
218
|
+
/* A property measures like a field: two tracks over the subform's own rows, so a
|
|
219
|
+
label wrapping in one column leaves its control on the row's baseline rather than a
|
|
220
|
+
line below it — `Field.svelte`'s rule for a row-sharing field, one level in. The
|
|
221
|
+
tracks are this grid's own; the section's belong to the fields.
|
|
222
|
+
|
|
223
|
+
`min-width: 0` because a property overflowing its `1fr` grows the track, and the
|
|
224
|
+
subform with it, past the field that holds it. */
|
|
129
225
|
.qm-object-prop {
|
|
130
|
-
display:
|
|
131
|
-
|
|
132
|
-
|
|
226
|
+
display: grid;
|
|
227
|
+
grid-row: span 2;
|
|
228
|
+
grid-template-rows: subgrid;
|
|
229
|
+
row-gap: var(--_qm-space-half);
|
|
230
|
+
align-items: start;
|
|
231
|
+
min-width: 0;
|
|
133
232
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
here would state it twice and rank two names the ramp does not separate. */
|
|
139
|
-
font-weight: var(--_qm-weight-mid);
|
|
140
|
-
/* A label is a line, not a passage, so it overrides the root's reading
|
|
141
|
-
rhythm the way every other label rung does. */
|
|
142
|
-
line-height: var(--_qm-leading-tight);
|
|
143
|
-
color: var(--_qm-ink-label);
|
|
233
|
+
/* A subform of one takes half the capacity, the way a packable run of one does
|
|
234
|
+
(`.qm-field.lone`): at full capacity a single track reads as truncated. */
|
|
235
|
+
.qm-object-prop:only-child {
|
|
236
|
+
grid-column: span var(--cols-half);
|
|
144
237
|
}
|
|
145
238
|
.qm-unsupported {
|
|
146
239
|
font-size: var(--_qm-text-body);
|
|
@@ -2,14 +2,21 @@ import type { QuillFieldSchema } from '@quillmark/wasm';
|
|
|
2
2
|
interface Props {
|
|
3
3
|
value: Record<string, unknown> | undefined;
|
|
4
4
|
properties: Record<string, QuillFieldSchema> | undefined;
|
|
5
|
-
/** Accessible-name prefix for the property controls. */
|
|
6
|
-
label?: string;
|
|
7
5
|
/** The field label's own id. A subform is a group of controls, not one control
|
|
8
|
-
* `for` could reach, so the field's label names the set
|
|
9
|
-
*
|
|
6
|
+
* `for` could reach, so the field's label names the set; each property carries
|
|
7
|
+
* its own `<label for>` inside it. */
|
|
10
8
|
labelledBy?: string;
|
|
11
9
|
/** The parked `description` (FieldLabel): announced on entering the group. */
|
|
12
10
|
describedBy?: string;
|
|
11
|
+
/**
|
|
12
|
+
* The parent control's DOM id: the base each property's own three names derive
|
|
13
|
+
* from ({@link propertyDomIds}). Absent — a subform mounted with no field around
|
|
14
|
+
* it — the properties fall back to `aria-label`, which names them without a
|
|
15
|
+
* `for` target to click.
|
|
16
|
+
*/
|
|
17
|
+
idBase?: string;
|
|
18
|
+
/** Accessible-name prefix used only on the `aria-label` fallback above. */
|
|
19
|
+
label?: string;
|
|
13
20
|
onCommit: (obj: Record<string, unknown>) => void;
|
|
14
21
|
}
|
|
15
22
|
declare const ObjectField: import("svelte").Component<Props, {
|
|
@@ -17,14 +17,16 @@
|
|
|
17
17
|
-->
|
|
18
18
|
<script lang="ts">
|
|
19
19
|
import { onMount } from 'svelte';
|
|
20
|
-
import { EditorState } from 'prosemirror-state';
|
|
20
|
+
import { EditorState, Selection } from 'prosemirror-state';
|
|
21
21
|
import { EditorView } from 'prosemirror-view';
|
|
22
22
|
import {
|
|
23
23
|
decode,
|
|
24
24
|
pmToContent,
|
|
25
25
|
inlineSchema,
|
|
26
26
|
plaintextSchema,
|
|
27
|
-
proseLeafPlugins
|
|
27
|
+
proseLeafPlugins,
|
|
28
|
+
buildLineIndex,
|
|
29
|
+
usvToPM
|
|
28
30
|
} from '../core/codec/index.js';
|
|
29
31
|
import './controls.css';
|
|
30
32
|
import type { Content } from '@quillmark/wasm';
|
|
@@ -61,6 +63,24 @@
|
|
|
61
63
|
view.focus();
|
|
62
64
|
view.dom.scrollIntoView({ block: 'nearest' });
|
|
63
65
|
}
|
|
66
|
+
/** Take the caret to USV `pos` in this element's own content: what a landing on an
|
|
67
|
+
* element address resolves to when the compile answered a cluster-exact offset
|
|
68
|
+
* (`leaves.ts`). `createField`'s body over the view this component owns.
|
|
69
|
+
*
|
|
70
|
+
* The index is built here rather than held: a landing is a discrete act, and one
|
|
71
|
+
* doc walk per click is cheaper than a cache to invalidate per keystroke. `usvToPM`
|
|
72
|
+
* clamps, so an offset past a value the compile has moved on from lands the end.
|
|
73
|
+
*
|
|
74
|
+
* `Selection.near`, not `TextSelection.create`: the mapped position can be
|
|
75
|
+
* non-inline. Focus first, then the caret, flagged for scroll — the order and the
|
|
76
|
+
* reasons are `FieldController.setCaret`'s (`codec/field.ts`). */
|
|
77
|
+
export function setCaret(pos: number): void {
|
|
78
|
+
if (!view) return;
|
|
79
|
+
const pm = usvToPM(buildLineIndex(view.state.doc), pos);
|
|
80
|
+
const sel = Selection.near(view.state.doc.resolve(pm));
|
|
81
|
+
view.focus();
|
|
82
|
+
view.dispatch(view.state.tr.setSelection(sel).scrollIntoView());
|
|
83
|
+
}
|
|
64
84
|
|
|
65
85
|
onMount(() => {
|
|
66
86
|
if (!containerEl) return;
|
|
@@ -19,6 +19,7 @@ interface Props {
|
|
|
19
19
|
}
|
|
20
20
|
declare const ProseArrayElement: import("svelte").Component<Props, {
|
|
21
21
|
focus: () => void;
|
|
22
|
+
setCaret: (pos: number) => void;
|
|
22
23
|
}, "">;
|
|
23
24
|
type ProseArrayElement = ReturnType<typeof ProseArrayElement>;
|
|
24
25
|
export default ProseArrayElement;
|
|
@@ -72,8 +72,15 @@
|
|
|
72
72
|
edge under the character that opened the menu, and stays there as the query is
|
|
73
73
|
typed. Zero offset, because a gap reads as a surface floating near the caret
|
|
74
74
|
rather than one growing out of it. -->
|
|
75
|
+
<!-- The boundary is the root the surface portals into, the box that clips it where
|
|
76
|
+
the consumer scrolls one ({@link FormatPopover} states it in full). It is also
|
|
77
|
+
what keeps the row scroll above inside the list: `nearest` scrolls any ancestor
|
|
78
|
+
the row is not visible in, and a surface raised into room the pane clips leaves
|
|
79
|
+
the pane one of them — so the scroll moves the pane, and with it the caret this
|
|
80
|
+
menu is anchored to. -->
|
|
75
81
|
<Popover.Content
|
|
76
82
|
customAnchor={menu?.anchor ?? null}
|
|
83
|
+
collisionBoundary={portalTarget ?? []}
|
|
77
84
|
side="bottom"
|
|
78
85
|
align="start"
|
|
79
86
|
trapFocus={false}
|
|
@@ -97,27 +97,34 @@
|
|
|
97
97
|
paints over the rest. The two read as one block, which is what document-level
|
|
98
98
|
guidance is: the main card's, not a card of its own. Still in-flow, no lift.
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
No stroke of its own. A tint below the card reads as a slip; the same tint inside an
|
|
101
|
+
outline reads as a second card, which is the one thing the tuck exists to deny. What
|
|
102
|
+
shows below the seam is fill, and `main`'s hairline is the pair's only edge.
|
|
103
103
|
|
|
104
104
|
The bottom corners take the inner rung where the card above takes the outer: a slip
|
|
105
105
|
is cut crisper than the thing it is tucked under, at every setting of the dial.
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
Down, the insets are the document's own rhythm and they are measured to the ink, not
|
|
108
|
+
to the boxes. The top is a block gap below the seam, what two of the body's paragraphs
|
|
109
|
+
take, so the tip reads as the block after the prose; the bottom is the card's inset
|
|
110
|
+
rung; and the foot sits at half of either, being the message's own foot rather than
|
|
111
|
+
the next thing after it. The control gives its tap floor back as margin for that to
|
|
112
|
+
hold: a box standing on the floor rather than on its one-line label carries a rung of
|
|
113
|
+
slack at each edge, and spent inside the gap that is enough to make the tightest space
|
|
114
|
+
in the card measure as the widest.
|
|
115
|
+
|
|
116
|
+
Across, the inset is `main`'s stroke and the two nested insets under it, the card's
|
|
117
|
+
own and the prose control's, so the tip starts on the prose's edge rather than short
|
|
118
|
+
of it. */
|
|
112
119
|
.qm-tips {
|
|
113
120
|
margin-top: calc(var(--_qm-radius) * -1);
|
|
114
|
-
border: var(--_qm-border-width) solid var(--_qm-border);
|
|
115
121
|
border-radius: 0 0 var(--_qm-radius-inner) var(--_qm-radius-inner);
|
|
116
|
-
padding: calc(var(--_qm-space-3) + var(--_qm-radius))
|
|
122
|
+
padding: calc(var(--_qm-space-3) + var(--_qm-radius))
|
|
123
|
+
calc(var(--_qm-border-width) + var(--_qm-space-3) * 2) var(--_qm-space-3);
|
|
117
124
|
background: var(--_qm-tips-surface);
|
|
118
125
|
display: flex;
|
|
119
126
|
flex-direction: column;
|
|
120
|
-
gap: var(--_qm-space
|
|
127
|
+
gap: var(--_qm-space);
|
|
121
128
|
font-family: var(--_qm-tips-font);
|
|
122
129
|
font-size: var(--_qm-text-body);
|
|
123
130
|
line-height: var(--_qm-leading-body);
|
|
@@ -168,7 +175,11 @@
|
|
|
168
175
|
the spacing one — the foot's button clears the floor and walks the whole set from
|
|
169
176
|
the tip the card opens on, so no tip is reachable only by a dot. State is three
|
|
170
177
|
steps of one ink on the recede ladder, never a second hue and never a size: a dot
|
|
171
|
-
that grew would move the row it sits in.
|
|
178
|
+
that grew would move the row it sits in.
|
|
179
|
+
|
|
180
|
+
`color` is restated because a button takes the UA's and not its parent's, so
|
|
181
|
+
`currentColor` is the card's ink only once it is inherited; left to the UA it is
|
|
182
|
+
`buttontext`, which is the one neutral on a card that carries no neutrals. */
|
|
172
183
|
.qm-tips-dot {
|
|
173
184
|
box-sizing: content-box;
|
|
174
185
|
width: 5px;
|
|
@@ -176,6 +187,7 @@
|
|
|
176
187
|
padding: var(--_qm-space-half);
|
|
177
188
|
border: none;
|
|
178
189
|
border-radius: var(--_qm-radius-pill);
|
|
190
|
+
color: inherit;
|
|
179
191
|
background: currentColor;
|
|
180
192
|
background-clip: content-box;
|
|
181
193
|
opacity: var(--_qm-opacity-idle);
|
|
@@ -192,8 +204,16 @@
|
|
|
192
204
|
overrides is everything the family sets for a glyph among callers of its own
|
|
193
205
|
colour, a labelled control on a warm card wanting none of it. The hover included:
|
|
194
206
|
the family's neutral step would read as lent from the document above. Unlayered,
|
|
195
|
-
so it beats the base rule without out-specifying it.
|
|
207
|
+
so it beats the base rule without out-specifying it.
|
|
208
|
+
|
|
209
|
+
The box is the tap target and the label is what the rhythm measures, so both axes
|
|
210
|
+
give the difference back as margin: the inline rung lands the label on the body's
|
|
211
|
+
edge above, and the block rung takes the floor's slack out of the foot's height.
|
|
212
|
+
The box, and the hover it paints, still reaches past the label the way a control
|
|
213
|
+
does. */
|
|
196
214
|
.qm-tips-action {
|
|
215
|
+
margin-block: calc(var(--_qm-space) * -1);
|
|
216
|
+
margin-right: calc(var(--_qm-space-2) * -1);
|
|
197
217
|
padding: var(--_qm-space-half) var(--_qm-space-2);
|
|
198
218
|
font-family: inherit;
|
|
199
219
|
font-size: inherit;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
An `enum` declaring `variants:` → the discriminant's select, and under it the cells
|
|
3
|
+
the chosen world brings into play (VISUAL_EDITOR §"Enum variants"). The field rests as
|
|
4
|
+
a container, `{value: <member>, …that member's fields}`, and commits whole: it is one
|
|
5
|
+
cell to `Addr`, so there is no per-cell write address.
|
|
6
|
+
|
|
7
|
+
The cells are {@link ObjectField}, which already draws a scalar field set keyed by
|
|
8
|
+
name over a container its parent commits by value. It takes the whole container as its
|
|
9
|
+
`value` and the world's declaration as its `properties`, so what it hands back is the
|
|
10
|
+
container with one cell written — and cells outside the drawn world ride through
|
|
11
|
+
untouched rather than needing to be merged back.
|
|
12
|
+
-->
|
|
13
|
+
<script lang="ts">
|
|
14
|
+
import type { QuillFieldSchema } from '@quillmark/wasm';
|
|
15
|
+
import EnumField from './EnumField.svelte';
|
|
16
|
+
import ObjectField from './ObjectField.svelte';
|
|
17
|
+
import {
|
|
18
|
+
VARIANT_DISCRIMINANT,
|
|
19
|
+
commitDiscriminant,
|
|
20
|
+
variantCells,
|
|
21
|
+
variantMember
|
|
22
|
+
} from './structure.js';
|
|
23
|
+
|
|
24
|
+
interface Props {
|
|
25
|
+
/** The stored container, or undefined while the field is unset. */
|
|
26
|
+
value: Record<string, unknown> | undefined;
|
|
27
|
+
schema: QuillFieldSchema;
|
|
28
|
+
/** The resolved `default:`'s member: what an unset field renders as. */
|
|
29
|
+
ghostMember: string | undefined;
|
|
30
|
+
/** Field label, the naming prefix the cells' controls compose with. */
|
|
31
|
+
label: string;
|
|
32
|
+
/** `<label for>` target: the discriminant trigger, this control's one labelable
|
|
33
|
+
* element. The cells are named by their own composed labels. */
|
|
34
|
+
id?: string;
|
|
35
|
+
labelledBy?: string;
|
|
36
|
+
describedBy?: string;
|
|
37
|
+
onCommit: (v: Record<string, unknown> | undefined) => void;
|
|
38
|
+
optionAllowed?: (value: string) => boolean;
|
|
39
|
+
enumDisallowed?: 'hide' | 'disable';
|
|
40
|
+
}
|
|
41
|
+
let {
|
|
42
|
+
value,
|
|
43
|
+
schema,
|
|
44
|
+
ghostMember,
|
|
45
|
+
label,
|
|
46
|
+
id,
|
|
47
|
+
labelledBy,
|
|
48
|
+
describedBy,
|
|
49
|
+
onCommit,
|
|
50
|
+
optionAllowed,
|
|
51
|
+
enumDisallowed
|
|
52
|
+
}: Props = $props();
|
|
53
|
+
|
|
54
|
+
const member = $derived(variantMember(value, ghostMember));
|
|
55
|
+
const cells = $derived(variantCells(schema, member));
|
|
56
|
+
const discriminant = $derived(value?.[VARIANT_DISCRIMINANT] as string | undefined);
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<div class="qm-variant">
|
|
60
|
+
<EnumField
|
|
61
|
+
value={discriminant}
|
|
62
|
+
values={schema.values ?? []}
|
|
63
|
+
fallback={ghostMember}
|
|
64
|
+
{id}
|
|
65
|
+
{describedBy}
|
|
66
|
+
onCommit={(v) => onCommit(commitDiscriminant(value, v))}
|
|
67
|
+
{optionAllowed}
|
|
68
|
+
{enumDisallowed}
|
|
69
|
+
/>
|
|
70
|
+
<!-- Keyed on the member so a flip remounts the cells rather than re-targeting
|
|
71
|
+
them: two worlds' cells are different fields that happen to occupy one place,
|
|
72
|
+
and a control carrying local state (an unreconciled pick, a caret) across the
|
|
73
|
+
flip would carry it between them. -->
|
|
74
|
+
{#if cells}
|
|
75
|
+
{#key member}
|
|
76
|
+
<ObjectField
|
|
77
|
+
value={value ?? {}}
|
|
78
|
+
properties={cells}
|
|
79
|
+
{label}
|
|
80
|
+
idBase={id}
|
|
81
|
+
{labelledBy}
|
|
82
|
+
{describedBy}
|
|
83
|
+
onCommit={(obj) => onCommit(obj)}
|
|
84
|
+
/>
|
|
85
|
+
{/key}
|
|
86
|
+
{/if}
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<style>
|
|
90
|
+
.qm-variant {
|
|
91
|
+
display: flex;
|
|
92
|
+
flex-direction: column;
|
|
93
|
+
gap: var(--_qm-space-2);
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { QuillFieldSchema } from '@quillmark/wasm';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** The stored container, or undefined while the field is unset. */
|
|
4
|
+
value: Record<string, unknown> | undefined;
|
|
5
|
+
schema: QuillFieldSchema;
|
|
6
|
+
/** The resolved `default:`'s member: what an unset field renders as. */
|
|
7
|
+
ghostMember: string | undefined;
|
|
8
|
+
/** Field label, the naming prefix the cells' controls compose with. */
|
|
9
|
+
label: string;
|
|
10
|
+
/** `<label for>` target: the discriminant trigger, this control's one labelable
|
|
11
|
+
* element. The cells are named by their own composed labels. */
|
|
12
|
+
id?: string;
|
|
13
|
+
labelledBy?: string;
|
|
14
|
+
describedBy?: string;
|
|
15
|
+
onCommit: (v: Record<string, unknown> | undefined) => void;
|
|
16
|
+
optionAllowed?: (value: string) => boolean;
|
|
17
|
+
enumDisallowed?: 'hide' | 'disable';
|
|
18
|
+
}
|
|
19
|
+
declare const VariantField: import("svelte").Component<Props, {}, "">;
|
|
20
|
+
type VariantField = ReturnType<typeof VariantField>;
|
|
21
|
+
export default VariantField;
|