@quillmark/svelte 0.1.1 → 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 +16 -12
- package/THEMING.md +48 -25
- package/dist/core/address.d.ts +42 -7
- package/dist/core/address.js +49 -21
- 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 +14 -11
- package/dist/core/codec/field.d.ts +22 -27
- 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 +266 -228
- 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 +73 -27
- package/dist/core/errors.d.ts +17 -7
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +6 -5
- package/dist/core/lifecycle.d.ts +21 -5
- package/dist/core/lifecycle.js +34 -8
- package/dist/core/rebind.svelte.d.ts +18 -4
- package/dist/core/rebind.svelte.js +30 -12
- package/dist/core/teardown.d.ts +1 -1
- package/dist/core/teardown.js +1 -1
- package/dist/core/theme.css +173 -58
- package/dist/preset/index.css +1 -1
- package/dist/preset/recipes.css +79 -30
- package/dist/preset/scale.css +40 -28
- package/dist/preview/Preview.svelte +22 -19
- package/dist/preview/Preview.svelte.d.ts +8 -8
- package/dist/preview/bridge.d.ts +6 -4
- package/dist/preview/bridge.js +19 -9
- package/dist/preview/controller.d.ts +13 -6
- package/dist/preview/controller.js +8 -5
- package/dist/preview/geometry.d.ts +17 -2
- package/dist/preview/geometry.js +21 -1
- package/dist/preview/index.d.ts +1 -0
- package/dist/preview/index.js +9 -2
- package/dist/preview/overlay.d.ts +2 -1
- package/dist/preview/overlay.js +26 -11
- package/dist/preview/paint.js +19 -17
- package/dist/visual/ArrayField.svelte +103 -83
- package/dist/visual/ArrayField.svelte.d.ts +5 -4
- package/dist/visual/BooleanField.svelte +8 -6
- package/dist/visual/BooleanField.svelte.d.ts +2 -2
- package/dist/visual/Card.svelte +161 -333
- package/dist/visual/Card.svelte.d.ts +5 -4
- 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 +2 -2
- package/dist/visual/EnumField.svelte +48 -28
- package/dist/visual/EnumField.svelte.d.ts +5 -4
- package/dist/visual/Field.svelte +207 -132
- package/dist/visual/Field.svelte.d.ts +11 -7
- package/dist/visual/FieldHint.svelte +79 -48
- package/dist/visual/FieldHint.svelte.d.ts +1 -1
- package/dist/visual/FieldLabel.svelte +8 -8
- package/dist/visual/FormatPopover.svelte +82 -35
- package/dist/visual/NumberField.svelte +5 -5
- package/dist/visual/NumberField.svelte.d.ts +1 -1
- package/dist/visual/ObjectField.svelte +28 -9
- package/dist/visual/ObjectField.svelte.d.ts +4 -2
- package/dist/visual/ProseArrayElement.svelte +12 -19
- package/dist/visual/ProseArrayElement.svelte.d.ts +1 -2
- package/dist/visual/ProseField.svelte +62 -85
- package/dist/visual/ProseField.svelte.d.ts +14 -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 +18 -19
- package/dist/visual/VisualEditor.svelte.d.ts +2 -3
- package/dist/visual/VisualEditorInner.svelte +251 -184
- package/dist/visual/VisualEditorInner.svelte.d.ts +2 -3
- package/dist/visual/controls.css +85 -38
- package/dist/visual/diagnostics.d.ts +13 -21
- package/dist/visual/diagnostics.js +7 -19
- 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.d.ts +1 -0
- package/dist/visual/index.js +10 -3
- package/dist/visual/leaves.d.ts +47 -0
- package/dist/visual/leaves.js +31 -0
- package/dist/visual/links.d.ts +24 -0
- package/dist/visual/links.js +67 -0
- package/dist/visual/props.d.ts +33 -12
- 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 +4 -2
- package/dist/visual/tips.js +7 -4
- package/package.json +2 -2
package/dist/preview/paint.js
CHANGED
|
@@ -33,23 +33,25 @@ onPaintError) {
|
|
|
33
33
|
}, { root: container, threshold: 0 });
|
|
34
34
|
function makeSlot(page) {
|
|
35
35
|
const size = session.pageSize(page);
|
|
36
|
-
// The sheet
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// boundary
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// the paper.
|
|
36
|
+
// The sheet is a plane and the desk is the one under it: paper sits at
|
|
37
|
+
// `--_qm-surface` and the container paints `--_qm-surface-sunken`, so the
|
|
38
|
+
// edge is where two tones meet and no stroke is drawn around it
|
|
39
|
+
// (ARCHITECTURE §Styling). It leaves the overlay's no-ink-at-rest rule
|
|
40
|
+
// untouched, since a boundary that is not painted is not a mark on the page
|
|
41
|
+
// (PREVIEW §Overlay). `border-box` costs nothing here and keeps a slot a
|
|
42
|
+
// consumer insets from measuring wider than its paper.
|
|
44
43
|
const el = document.createElement('div');
|
|
45
44
|
el.className = 'qm-page';
|
|
45
|
+
// The page's index, in the DOM. `pageByEl` is this module's and a consumer
|
|
46
|
+
// drawing an overlay of its own needs the number: position among siblings is
|
|
47
|
+
// the only handle without it, which is right today and is not a contract.
|
|
48
|
+
el.dataset.page = String(page);
|
|
46
49
|
Object.assign(el.style, {
|
|
47
50
|
position: 'relative',
|
|
48
51
|
boxSizing: 'border-box',
|
|
49
52
|
width: '100%',
|
|
50
53
|
aspectRatio: `${size.widthPt} / ${size.heightPt}`,
|
|
51
|
-
background: 'var(--_qm-surface)'
|
|
52
|
-
border: 'var(--_qm-border-width) solid var(--_qm-border)'
|
|
54
|
+
background: 'var(--_qm-surface)'
|
|
53
55
|
});
|
|
54
56
|
if (page > 0)
|
|
55
57
|
el.style.marginTop = PAGE_GAP;
|
|
@@ -59,7 +61,7 @@ onPaintError) {
|
|
|
59
61
|
return { page, size, el };
|
|
60
62
|
}
|
|
61
63
|
// Rasterize `slot` into its canvas (creating the canvas if this is its first
|
|
62
|
-
// paint since mounting). `layoutScale` is derived from the slot's
|
|
64
|
+
// paint since mounting). `layoutScale` is derived from the slot's own current
|
|
63
65
|
// CSS width, so layout width tracks the container per the zoom settled
|
|
64
66
|
// decision; `canvas.style.*` is then set from the authoritative PaintResult,
|
|
65
67
|
// never guessed.
|
|
@@ -70,7 +72,7 @@ onPaintError) {
|
|
|
70
72
|
canvas.className = 'qm-page-canvas';
|
|
71
73
|
Object.assign(canvas.style, { position: 'absolute', inset: '0', display: 'block' });
|
|
72
74
|
}
|
|
73
|
-
// Contextless canvas: bail
|
|
75
|
+
// Contextless canvas: bail before registering, so `updateBand` still sees
|
|
74
76
|
// the page as unmounted and retries instead of keeping a blank page.
|
|
75
77
|
const ctx = canvas.getContext('2d');
|
|
76
78
|
if (!ctx)
|
|
@@ -104,7 +106,7 @@ onPaintError) {
|
|
|
104
106
|
canvases.delete(page);
|
|
105
107
|
}
|
|
106
108
|
// Mount pages newly inside [min(visible)-margin, max(visible)+margin], unmount
|
|
107
|
-
// pages that fell outside it. Only touches pages
|
|
109
|
+
// pages that fell outside it. Only touches pages changing band membership;
|
|
108
110
|
// an already-mounted page in the band is left alone (paint contract: an idle
|
|
109
111
|
// canvas keeps its pixels for free; repainting it here would be pure waste).
|
|
110
112
|
function updateBand() {
|
|
@@ -154,13 +156,13 @@ onPaintError) {
|
|
|
154
156
|
}
|
|
155
157
|
}
|
|
156
158
|
// Slots are built on the first `refresh` (the controller calls it immediately),
|
|
157
|
-
//
|
|
159
|
+
// not here: reconcile → `session.pageSize` must run after the controller's
|
|
158
160
|
// `supportsCanvas`/`pageCount` gate, since a `supportsCanvas: false` compile with
|
|
159
161
|
// pages would throw on `pageSize` (runtime.d.ts: it succeeds iff `supportsCanvas`)
|
|
160
162
|
// before the controller can surface its "unsupported" message.
|
|
161
163
|
// ── Keep mounted rasters in step with the display ───────────────────────────
|
|
162
|
-
// Every paint freezes `canvas.style.width/height` to the box width
|
|
163
|
-
//
|
|
164
|
+
// Every paint freezes `canvas.style.width/height` to the box width at that
|
|
165
|
+
// paint (paintSlot). A page that stays mounted while the container's CSS width
|
|
164
166
|
// or `devicePixelRatio` shifts would otherwise keep a stale raster the %-space
|
|
165
167
|
// overlay/click math silently drifts off of: the box tracks the container, the
|
|
166
168
|
// ink does not. Two observers close the gap by repainting mounted pages from
|
|
@@ -208,7 +210,7 @@ onPaintError) {
|
|
|
208
210
|
resizeObserver?.observe(container);
|
|
209
211
|
// `devicePixelRatio` changes (window dragged to a different-DPI monitor, browser
|
|
210
212
|
// zoom) leave CSS-px width untouched, so the ResizeObserver never sees them;
|
|
211
|
-
// watch DPR directly. A `(resolution: …dppx)` query pins the
|
|
213
|
+
// watch DPR directly. A `(resolution: …dppx)` query pins the current ratio and
|
|
212
214
|
// is therefore one-shot: re-arm it against the new ratio on each change. Guarded
|
|
213
215
|
// for environments (jsdom) without `matchMedia`.
|
|
214
216
|
let dprQuery;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
An `array` field → an add/remove repeater. Elements commit by
|
|
2
|
+
An `array` field → an add/remove repeater. Elements commit by value: every
|
|
3
3
|
edit / add / remove rebuilds the whole array and hands it to the parent's typed
|
|
4
4
|
`writer.set(field, wholeArray)` (arrays are not op-addressed). Element control
|
|
5
5
|
by `items.type`: `string` → text input, `richtext` → a prose element
|
|
@@ -8,19 +8,19 @@
|
|
|
8
8
|
label header row (space-between with the field label); {@link Field} skips its
|
|
9
9
|
own label for array controls and hands this component the label track with it.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
The remove is inside the element: a slab over the end of the element's own box, taking
|
|
12
|
+
its two end-side corners. So a row's box is the element's box, and an array's rows end
|
|
13
|
+
where every other field's control does.
|
|
14
14
|
|
|
15
15
|
Keys carry the list without the mouse: Enter inserts a
|
|
16
|
-
sibling below and takes the caret there, Backspace on an
|
|
16
|
+
sibling below and takes the caret there, Backspace on an empty element removes it
|
|
17
17
|
and hands focus back up the list.
|
|
18
18
|
|
|
19
19
|
No reorder: an array's order is fixed at declaration/entry order. Elements
|
|
20
20
|
carry a parallel session-id list, spliced with the values as one operation at
|
|
21
21
|
whatever index a mutation names; so an element holds its id for life and the
|
|
22
22
|
surviving order never permutes, which is what lets a keyed prose element survive an
|
|
23
|
-
insert or a remove
|
|
23
|
+
insert or a remove above it rather than remounting.
|
|
24
24
|
-->
|
|
25
25
|
<script lang="ts">
|
|
26
26
|
import { wording } from './strings.js';
|
|
@@ -48,36 +48,25 @@
|
|
|
48
48
|
required?: boolean;
|
|
49
49
|
/** Schema `description`: the label's help affordance. */
|
|
50
50
|
description?: string;
|
|
51
|
-
/** The label's own DOM id. An array is a
|
|
51
|
+
/** The label's own DOM id. An array is a group (N inputs, no single `for`
|
|
52
52
|
* target) so the label names the set and each element keeps its indexed
|
|
53
53
|
* `aria-label`. */
|
|
54
54
|
labelId?: string;
|
|
55
55
|
/** Where the description parks, for the group's `aria-describedby`. */
|
|
56
56
|
descriptionId?: string;
|
|
57
57
|
onCommit: (arr: unknown[]) => void;
|
|
58
|
-
/** A prose element gained focus: joins the field in the focus federation. */
|
|
59
|
-
onFocusEl?: () => void;
|
|
60
58
|
}
|
|
61
|
-
let {
|
|
62
|
-
|
|
63
|
-
items,
|
|
64
|
-
label,
|
|
65
|
-
required,
|
|
66
|
-
description,
|
|
67
|
-
labelId,
|
|
68
|
-
descriptionId,
|
|
69
|
-
onCommit,
|
|
70
|
-
onFocusEl
|
|
71
|
-
}: Props = $props();
|
|
59
|
+
let { value, items, label, required, description, labelId, descriptionId, onCommit }: Props =
|
|
60
|
+
$props();
|
|
72
61
|
|
|
73
|
-
// The
|
|
62
|
+
// The element control is the item schema's own; an array declaring no `items`
|
|
74
63
|
// has text elements.
|
|
75
64
|
const control = $derived(items ? controlKind(items) : 'text');
|
|
76
65
|
const plaintext = $derived(items?.type === 'plaintext');
|
|
77
66
|
const arr = $derived((value ?? []) as unknown[]);
|
|
78
67
|
|
|
79
68
|
// Parallel stable ids, one per element, kept in lockstep with the data below.
|
|
80
|
-
// Seeded eagerly so a non-empty array renders its rows on the
|
|
69
|
+
// Seeded eagerly so a non-empty array renders its rows on the first pass:
|
|
81
70
|
// an effect-only seed mounts every element editor in a second render.
|
|
82
71
|
const seq = new IdSeq();
|
|
83
72
|
// svelte-ignore state_referenced_locally
|
|
@@ -100,9 +89,16 @@
|
|
|
100
89
|
// is stated on `ProseArrayElement.focus`, which owns it.
|
|
101
90
|
//
|
|
102
91
|
// Every path that drops an id deletes its entry: `bind:this` teardown nulls the
|
|
103
|
-
//
|
|
92
|
+
// value on unmount and leaves the key, so a card that outlives its elements
|
|
104
93
|
// accumulates one dead key per element ever created.
|
|
105
|
-
|
|
94
|
+
//
|
|
95
|
+
// `$state` for the binding's sake, not this component's: nothing here reads `els`
|
|
96
|
+
// reactively (every read is inside an event handler or a post-flush focus hop), but
|
|
97
|
+
// `bind:this` into a property of a plain object is a write Svelte cannot track, and
|
|
98
|
+
// it says so once per element per render. Thirteen lines on one memo's first paint,
|
|
99
|
+
// in the console a consumer is reading to find its own defects. Same shape
|
|
100
|
+
// {@link Card} keeps its header/panel refs in.
|
|
101
|
+
const els: Record<string, { focus: () => void } | undefined> = $state({});
|
|
106
102
|
let addEl: HTMLButtonElement | undefined = $state();
|
|
107
103
|
let rootEl: HTMLElement | undefined = $state();
|
|
108
104
|
|
|
@@ -151,10 +147,11 @@
|
|
|
151
147
|
// key does: the button under the pointer is part of what it destroys.
|
|
152
148
|
focusAfterFlush(next[Math.max(k - 1, 0)]);
|
|
153
149
|
}
|
|
154
|
-
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
|
|
150
|
+
/** Take the caret: the first element, or the add affordance when the list is empty;
|
|
151
|
+
* which is then the only thing there is to land on, and the next thing the user
|
|
152
|
+
* wants anyway. Reached by a label click and by the editor's landing verbs, which
|
|
153
|
+
* ask one function so they cannot disagree (`Field`, `leaves.ts`). */
|
|
154
|
+
export function focus(): void {
|
|
158
155
|
if (ids.length === 0) return void addEl?.focus();
|
|
159
156
|
const first = els[ids[0]];
|
|
160
157
|
if (first) return first.focus();
|
|
@@ -162,11 +159,21 @@
|
|
|
162
159
|
// nothing about focusing it that the DOM does not already know.
|
|
163
160
|
rootEl?.querySelector<HTMLTextAreaElement>('.qm-array-row textarea')?.focus();
|
|
164
161
|
}
|
|
162
|
+
/** Take the caret to element `k`: what a landing on an element address resolves to
|
|
163
|
+
* (`leaves.ts`). The index resolves to the element's session id here, at the call,
|
|
164
|
+
* never carried as one — an index is stale the moment anything above it splices.
|
|
165
|
+
* Past the live list it falls back to {@link focus}: the field is right and the row
|
|
166
|
+
* is gone, which is a landing off a compile the document has moved past. */
|
|
167
|
+
export function focusElement(k: number): void {
|
|
168
|
+
const el = els[ids[k]];
|
|
169
|
+
if (el) el.focus();
|
|
170
|
+
else focus();
|
|
171
|
+
}
|
|
165
172
|
/** Focus element `id` after the flush, never in the same tick: a mutation commits
|
|
166
|
-
* the array
|
|
173
|
+
* the array by value, so the parent re-derives and the row does not exist until
|
|
167
174
|
* then. `undefined` is the empty list: the add affordance.
|
|
168
175
|
*
|
|
169
|
-
* The commit that schedules this can also remove the
|
|
176
|
+
* The commit that schedules this can also remove the card holding the field, which
|
|
170
177
|
* unmounts this component inside the window (core/teardown.ts). */
|
|
171
178
|
async function focusAfterFlush(id: string | undefined): Promise<void> {
|
|
172
179
|
if (!(await span.resumes(tick()))) return;
|
|
@@ -174,7 +181,7 @@
|
|
|
174
181
|
else els[id]?.focus();
|
|
175
182
|
}
|
|
176
183
|
/** Whether element `k` reads empty to the user. A text element's committed value
|
|
177
|
-
*
|
|
184
|
+
* lags the input: a cleared field commits at `change`, not per keystroke
|
|
178
185
|
* ({@link TextField}); so the input's own value is the truth; a prose element
|
|
179
186
|
* commits every edit, so the committed `Content` is. */
|
|
180
187
|
function elementEmpty(k: number, target: EventTarget | null): boolean {
|
|
@@ -195,7 +202,7 @@
|
|
|
195
202
|
} else if (e.key === 'Backspace' && !e.repeat && elementEmpty(k, e.target)) {
|
|
196
203
|
// Destructive with nothing to undo it, so it takes a deliberate press:
|
|
197
204
|
// `repeat` is a held key running on past the character it just cleared, and
|
|
198
|
-
// the emptiness test reads the state
|
|
205
|
+
// the emptiness test reads the state before this keystroke applies; so the
|
|
199
206
|
// press that empties an element never also removes it.
|
|
200
207
|
e.preventDefault();
|
|
201
208
|
remove(k);
|
|
@@ -216,15 +223,18 @@
|
|
|
216
223
|
{label}
|
|
217
224
|
id={labelId}
|
|
218
225
|
{descriptionId}
|
|
219
|
-
onActivate={
|
|
226
|
+
onActivate={focus}
|
|
220
227
|
{required}
|
|
221
228
|
{description}
|
|
222
229
|
/>
|
|
223
230
|
{:else}
|
|
224
231
|
<span></span>
|
|
225
232
|
{/if}
|
|
226
|
-
<button
|
|
227
|
-
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
class="qm-add-el qm-add-affordance qm-tap-floor"
|
|
236
|
+
bind:this={addEl}
|
|
237
|
+
onclick={add}>{t.strings.arrayAdd}</button
|
|
228
238
|
>
|
|
229
239
|
</div>
|
|
230
240
|
{#each ids as id, k (id)}
|
|
@@ -237,7 +247,6 @@
|
|
|
237
247
|
label={label != null ? `${label} ${k + 1}` : undefined}
|
|
238
248
|
onChange={(rt) => commitElement(k, rt)}
|
|
239
249
|
onKey={(e) => onElementKey(e, k)}
|
|
240
|
-
{onFocusEl}
|
|
241
250
|
/>
|
|
242
251
|
{:else if control === 'object'}
|
|
243
252
|
<textarea
|
|
@@ -263,9 +272,9 @@
|
|
|
263
272
|
{/if}
|
|
264
273
|
<button
|
|
265
274
|
type="button"
|
|
266
|
-
class="qm-icon-btn qm-remove"
|
|
275
|
+
class="qm-icon-btn qm-remove qm-focus-ring"
|
|
267
276
|
title={t.strings.arrayRemove}
|
|
268
|
-
onclick={() => remove(k)}><Icon name="
|
|
277
|
+
onclick={() => remove(k)}><Icon name="minus" /></button
|
|
269
278
|
>
|
|
270
279
|
</div>
|
|
271
280
|
{/each}
|
|
@@ -276,14 +285,8 @@
|
|
|
276
285
|
display: flex;
|
|
277
286
|
flex-direction: column;
|
|
278
287
|
gap: var(--_qm-space);
|
|
279
|
-
/* The one field that reaches across the reserved action column: its rows END in
|
|
280
|
-
that column, so an element control stops exactly where the scalar beside it
|
|
281
|
-
does and the remove never sits over a long value. The reservation is the
|
|
282
|
-
FIELD's inset (Field.svelte), so this cancels it wherever the field landed:
|
|
283
|
-
its own row, or one track of a shared one. */
|
|
284
|
-
margin-right: calc(-1 * var(--action-col));
|
|
285
288
|
}
|
|
286
|
-
/* The array's first line
|
|
289
|
+
/* The array's first line is the row's label line: this component owns the label
|
|
287
290
|
track (Field.svelte), so the header has to measure what a `.qm-field-label-row`
|
|
288
291
|
measures or an array shares a row with a scalar and their labels sit apart. The
|
|
289
292
|
label is the same component; what had to give was the add affordance's height,
|
|
@@ -294,19 +297,56 @@
|
|
|
294
297
|
justify-content: space-between;
|
|
295
298
|
gap: var(--_qm-space-2);
|
|
296
299
|
}
|
|
297
|
-
/*
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
content: a long unbroken value grows the track otherwise, and the edge with it. */
|
|
300
|
+
/* A grid rather than a block: an `<input>` and a `<textarea>` are inline-level and
|
|
301
|
+
would sit on a baseline, standing the row a descender taller than the box the slab
|
|
302
|
+
measures itself against. `minmax(0, …)` because a long unbroken value grows an `auto`
|
|
303
|
+
track, and the edge with it. */
|
|
302
304
|
.qm-array-row {
|
|
305
|
+
position: relative;
|
|
303
306
|
display: grid;
|
|
304
|
-
grid-template-columns: minmax(0, 1fr)
|
|
305
|
-
|
|
306
|
-
|
|
307
|
+
grid-template-columns: minmax(0, 1fr);
|
|
308
|
+
}
|
|
309
|
+
/* The end inset the slab stands in, taken off whichever box the element drew:
|
|
310
|
+
`.qm-input` is the text element and the JSON textarea, `.qm-control-box` the prose
|
|
311
|
+
one. `:global`, because the box belongs to the child component's markup and the scope
|
|
312
|
+
class stops at this component's. The longhand beats the family's `padding` shorthand
|
|
313
|
+
without a specificity fight: this block is unlayered and `controls.css` is not. */
|
|
314
|
+
.qm-array-row :global(.qm-input),
|
|
315
|
+
.qm-array-row :global(.qm-control-box) {
|
|
316
|
+
padding-inline-end: var(--_qm-tap-min);
|
|
307
317
|
}
|
|
318
|
+
/* The box's end wall, floor to ceiling: a height of its own would leave a sliver of
|
|
319
|
+
well above or below, and the corners it takes are the box's — the end-side pair keeps
|
|
320
|
+
`.qm-icon-btn`'s radius, which is the same rung the box draws.
|
|
321
|
+
|
|
322
|
+
It comes up on its own row rather than on the field: a destructive control is offered
|
|
323
|
+
by the row the pointer is on, not by every row at once. Hover is where it says
|
|
324
|
+
destructive, ink with fill, a tint alone being a wash under a label-toned glyph. */
|
|
308
325
|
.qm-remove {
|
|
309
|
-
|
|
326
|
+
position: absolute;
|
|
327
|
+
inset-block: 0;
|
|
328
|
+
inset-inline-end: 0;
|
|
329
|
+
width: var(--_qm-tap-min);
|
|
330
|
+
color: var(--_qm-ink-label);
|
|
331
|
+
opacity: var(--_qm-opacity-idle);
|
|
332
|
+
border-start-start-radius: 0;
|
|
333
|
+
border-end-start-radius: 0;
|
|
334
|
+
transition:
|
|
335
|
+
opacity var(--_qm-duration-fast) var(--_qm-ease-reverse),
|
|
336
|
+
background-color var(--_qm-duration-fast) var(--_qm-ease-reverse),
|
|
337
|
+
color var(--_qm-duration-fast) var(--_qm-ease-reverse);
|
|
338
|
+
}
|
|
339
|
+
.qm-remove :global(svg) {
|
|
340
|
+
width: var(--_qm-glyph-control);
|
|
341
|
+
height: var(--_qm-glyph-control);
|
|
342
|
+
}
|
|
343
|
+
.qm-array-row:hover .qm-remove,
|
|
344
|
+
.qm-array-row:focus-within .qm-remove {
|
|
345
|
+
opacity: 1;
|
|
346
|
+
}
|
|
347
|
+
.qm-remove:hover {
|
|
348
|
+
background: var(--_qm-danger-tint);
|
|
349
|
+
color: var(--_qm-danger);
|
|
310
350
|
}
|
|
311
351
|
/* The JSON element is a `.qm-input` (controls.css): box, focus ring, and all;
|
|
312
352
|
what a textarea adds over an input is the face and a floor on its height. */
|
|
@@ -314,48 +354,28 @@
|
|
|
314
354
|
font-family: var(--_qm-font-mono);
|
|
315
355
|
min-height: 2.5rem;
|
|
316
356
|
}
|
|
317
|
-
/*
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
trigger at the head of a row of controls is found by looking at the array, not by
|
|
321
|
-
grazing its first line.
|
|
357
|
+
/* It rests dim and comes up on hover of the field rather than of itself: one trigger
|
|
358
|
+
at the head of a row of controls is found by looking at the array, not by grazing its
|
|
359
|
+
first line.
|
|
322
360
|
|
|
323
|
-
|
|
324
|
-
the field label's guidance marker takes, for the same reason and one row over.
|
|
325
|
-
This is the other affordance that sits IN a line of text rather than in a row of
|
|
326
|
-
its own, and the line is a label's, so a
|
|
361
|
+
The box is the label's line box and the target is `.qm-tap-floor`'s (controls.css): a
|
|
327
362
|
target-sized box would stand the header 8px taller than the `.qm-field-label-row`
|
|
328
|
-
beside it in a shared row and put the two labels on different lines.
|
|
329
|
-
floor comes off the box and goes out of flow, unpainted and centred on it: the row
|
|
330
|
-
keeps the label's line box, the press keeps the floor. The horizontal edges are the
|
|
331
|
-
button's own, the word being already wider than the threshold.
|
|
363
|
+
beside it in a shared row and put the two labels on different lines.
|
|
332
364
|
|
|
333
|
-
The
|
|
334
|
-
|
|
335
|
-
wants). It reads as one more thing on the label line rather than as a control that
|
|
336
|
-
wandered up from the row below. */
|
|
365
|
+
The label rung in place of the family's body rung, which the family invites: it reads
|
|
366
|
+
as one more thing on the label line rather than as a control from the row below. */
|
|
337
367
|
.qm-add-el {
|
|
338
|
-
position: relative;
|
|
339
|
-
min-height: 0;
|
|
340
368
|
padding: 0 var(--_qm-space);
|
|
341
369
|
font-size: var(--_qm-text-label);
|
|
342
370
|
opacity: var(--_qm-opacity-idle);
|
|
343
371
|
}
|
|
344
|
-
.qm-add-el::after {
|
|
345
|
-
content: '';
|
|
346
|
-
position: absolute;
|
|
347
|
-
inset-inline: 0;
|
|
348
|
-
top: 50%;
|
|
349
|
-
height: var(--_qm-tap-min);
|
|
350
|
-
transform: translateY(-50%);
|
|
351
|
-
}
|
|
352
372
|
.qm-array:hover .qm-add-el,
|
|
353
373
|
.qm-add-el:focus-visible {
|
|
354
374
|
opacity: 1;
|
|
355
375
|
}
|
|
356
|
-
/* Touch has no hover: keep a faint always-on affordance so add stays reachable. */
|
|
357
376
|
@media (hover: none) {
|
|
358
|
-
.qm-add-el
|
|
377
|
+
.qm-add-el,
|
|
378
|
+
.qm-remove {
|
|
359
379
|
opacity: var(--_qm-opacity-muted);
|
|
360
380
|
}
|
|
361
381
|
}
|
|
@@ -9,16 +9,17 @@ interface Props {
|
|
|
9
9
|
required?: boolean;
|
|
10
10
|
/** Schema `description`: the label's help affordance. */
|
|
11
11
|
description?: string;
|
|
12
|
-
/** The label's own DOM id. An array is a
|
|
12
|
+
/** The label's own DOM id. An array is a group (N inputs, no single `for`
|
|
13
13
|
* target) so the label names the set and each element keeps its indexed
|
|
14
14
|
* `aria-label`. */
|
|
15
15
|
labelId?: string;
|
|
16
16
|
/** Where the description parks, for the group's `aria-describedby`. */
|
|
17
17
|
descriptionId?: string;
|
|
18
18
|
onCommit: (arr: unknown[]) => void;
|
|
19
|
-
/** A prose element gained focus: joins the field in the focus federation. */
|
|
20
|
-
onFocusEl?: () => void;
|
|
21
19
|
}
|
|
22
|
-
declare const ArrayField: import("svelte").Component<Props, {
|
|
20
|
+
declare const ArrayField: import("svelte").Component<Props, {
|
|
21
|
+
focus: () => void;
|
|
22
|
+
focusElement: (k: number) => void;
|
|
23
|
+
}, "">;
|
|
23
24
|
type ArrayField = ReturnType<typeof ArrayField>;
|
|
24
25
|
export default ArrayField;
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
interface Props {
|
|
14
14
|
value: boolean | undefined;
|
|
15
15
|
fallback?: boolean;
|
|
16
|
-
/** Accessible name for a switch
|
|
16
|
+
/** Accessible name for a switch nothing else names: an object property, whose
|
|
17
17
|
* name is the field label plus the property's. A field's own switch takes `id`
|
|
18
18
|
* instead and is named by the `<label for>` beside it. */
|
|
19
19
|
label?: string;
|
|
20
|
-
/** `<label for>` target. `Switch.Root` renders a `<button>`, which
|
|
20
|
+
/** `<label for>` target. `Switch.Root` renders a `<button>`, which is labelable,
|
|
21
21
|
* so the click a label forwards toggles the switch: correct for this control,
|
|
22
22
|
* and single-fire: the label is a sibling, not a wrapper, so there is no second
|
|
23
23
|
* click bubbling back up to be re-dispatched. */
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
// Local toggle state synced to `value`; own-toggles stay local, only an external
|
|
32
32
|
// change reconciles back in (see `syncedLocal`). The primitive is driven
|
|
33
|
-
//
|
|
33
|
+
// controlled (`checked` + `onCheckedChange`, never `bind:`) so reconciliation
|
|
34
34
|
// stays the package's: a two-way bind hands the primitive a lane around it,
|
|
35
35
|
// which repeats the reconciliation hazard in miniature.
|
|
36
36
|
const local = syncedLocal(() => value ?? fallback ?? false);
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
</span>
|
|
54
54
|
|
|
55
55
|
<style>
|
|
56
|
-
/* A primitive renders its
|
|
56
|
+
/* A primitive renders its own element, which a scoped selector cannot reach:
|
|
57
57
|
styled through the wrapper with `:global`. */
|
|
58
58
|
.qm-switch-wrap :global(.qm-switch) {
|
|
59
59
|
display: inline-flex;
|
|
@@ -61,15 +61,17 @@
|
|
|
61
61
|
width: 1.75rem;
|
|
62
62
|
height: 1rem;
|
|
63
63
|
padding: var(--_qm-space-half);
|
|
64
|
-
border:
|
|
64
|
+
border: none;
|
|
65
65
|
border-radius: var(--_qm-radius-pill);
|
|
66
|
+
/* The track is a fill and nothing else: an edge around it would be a second
|
|
67
|
+
statement of a shape the tone already draws, and the checked state swaps that
|
|
68
|
+
one fill for the accent (ARCHITECTURE §Styling). */
|
|
66
69
|
background: var(--_qm-surface-hover);
|
|
67
70
|
cursor: pointer;
|
|
68
71
|
transition: background var(--_qm-duration-fast) var(--_qm-ease-reverse);
|
|
69
72
|
}
|
|
70
73
|
.qm-switch-wrap :global(.qm-switch[data-state='checked']) {
|
|
71
74
|
background: var(--_qm-accent);
|
|
72
|
-
border-color: var(--_qm-accent);
|
|
73
75
|
}
|
|
74
76
|
/* The focus ring rides `.qm-focus-ring` on the switch (controls.css). */
|
|
75
77
|
/* The thumb reads on tone alone: the base surface over the track's `hover` rung
|
|
@@ -2,11 +2,11 @@ import './controls.css';
|
|
|
2
2
|
interface Props {
|
|
3
3
|
value: boolean | undefined;
|
|
4
4
|
fallback?: boolean;
|
|
5
|
-
/** Accessible name for a switch
|
|
5
|
+
/** Accessible name for a switch nothing else names: an object property, whose
|
|
6
6
|
* name is the field label plus the property's. A field's own switch takes `id`
|
|
7
7
|
* instead and is named by the `<label for>` beside it. */
|
|
8
8
|
label?: string;
|
|
9
|
-
/** `<label for>` target. `Switch.Root` renders a `<button>`, which
|
|
9
|
+
/** `<label for>` target. `Switch.Root` renders a `<button>`, which is labelable,
|
|
10
10
|
* so the click a label forwards toggles the switch: correct for this control,
|
|
11
11
|
* and single-fire: the label is a sibling, not a wrapper, so there is no second
|
|
12
12
|
* click bubbling back up to be re-dispatched. */
|