@quillmark/svelte 0.1.0 → 0.2.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 +15 -11
- package/THEMING.md +42 -23
- package/dist/core/address.d.ts +39 -4
- package/dist/core/address.js +47 -19
- package/dist/core/codec/encode.js +4 -1
- package/dist/core/codec/field.d.ts +1 -1
- package/dist/core/codec/prose.css +41 -18
- package/dist/core/codec/table.js +3 -1
- package/dist/core/errors.d.ts +15 -5
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +5 -4
- package/dist/core/lifecycle.d.ts +21 -5
- package/dist/core/lifecycle.js +34 -8
- package/dist/core/rebind.svelte.d.ts +16 -2
- package/dist/core/rebind.svelte.js +26 -8
- package/dist/core/theme.css +50 -18
- package/dist/preset/recipes.css +55 -8
- package/dist/preset/scale.css +5 -4
- package/dist/preview/Preview.svelte +10 -9
- package/dist/preview/Preview.svelte.d.ts +5 -5
- package/dist/preview/bridge.d.ts +6 -4
- package/dist/preview/bridge.js +17 -7
- package/dist/preview/controller.d.ts +13 -6
- package/dist/preview/controller.js +5 -2
- package/dist/preview/geometry.d.ts +16 -1
- package/dist/preview/geometry.js +20 -0
- package/dist/preview/index.d.ts +1 -0
- package/dist/preview/index.js +7 -0
- package/dist/preview/overlay.d.ts +2 -1
- package/dist/preview/overlay.js +24 -9
- package/dist/preview/paint.js +4 -0
- package/dist/visual/ArrayField.svelte +26 -20
- package/dist/visual/ArrayField.svelte.d.ts +4 -3
- package/dist/visual/Card.svelte +21 -19
- package/dist/visual/Card.svelte.d.ts +8 -3
- package/dist/visual/DiagnosticList.svelte +1 -1
- package/dist/visual/EnumField.svelte +34 -14
- package/dist/visual/EnumField.svelte.d.ts +4 -3
- package/dist/visual/Field.svelte +87 -25
- package/dist/visual/Field.svelte.d.ts +11 -7
- package/dist/visual/FieldHint.svelte +59 -10
- package/dist/visual/FormatPopover.svelte +3 -1
- package/dist/visual/ObjectField.svelte +21 -2
- package/dist/visual/ObjectField.svelte.d.ts +3 -1
- package/dist/visual/ProseArrayElement.svelte +1 -6
- package/dist/visual/ProseArrayElement.svelte.d.ts +0 -1
- package/dist/visual/ProseField.svelte +20 -12
- package/dist/visual/ProseField.svelte.d.ts +5 -3
- package/dist/visual/TipsCard.svelte +1 -1
- package/dist/visual/VisualEditor.svelte +12 -13
- package/dist/visual/VisualEditor.svelte.d.ts +2 -3
- package/dist/visual/VisualEditorInner.svelte +140 -66
- package/dist/visual/VisualEditorInner.svelte.d.ts +2 -3
- package/dist/visual/controls.css +14 -11
- package/dist/visual/diagnostics.d.ts +12 -20
- package/dist/visual/diagnostics.js +6 -18
- package/dist/visual/index.d.ts +1 -0
- package/dist/visual/index.js +7 -0
- package/dist/visual/leaves.d.ts +47 -0
- package/dist/visual/leaves.js +31 -0
- package/dist/visual/props.d.ts +27 -6
- package/dist/visual/tips.d.ts +3 -1
- package/dist/visual/tips.js +5 -2
- package/package.json +3 -5
package/dist/preview/geometry.js
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An address's boxes: `fieldBoxes`'s union, else its own `regions()` rects, else the
|
|
3
|
+
* rects of everything UNDER it. The one rule the overlay and the scroll both read, so
|
|
4
|
+
* an address `regions()` names never draws nothing (PREVIEW.md §"Three
|
|
5
|
+
* responsibilities").
|
|
6
|
+
*
|
|
7
|
+
* `fieldBoxes` answers `[]` for a scalar reference the plate places without tracking
|
|
8
|
+
* its content and for a `richtext[]` element; runtime.d.ts says what such a field's
|
|
9
|
+
* box is, a single `regions()` rect. The third rung is that sentence one granularity
|
|
10
|
+
* down — an array's ink is its elements' — and is a fallback, not a union, so an
|
|
11
|
+
* address with rects of its own never also draws its children's. The `.` keeps the
|
|
12
|
+
* prefix a path boundary: `main.references` matches `main.references.0`, not
|
|
13
|
+
* `main.references_note`.
|
|
14
|
+
*/
|
|
15
|
+
export function boxesForField(field, boxes, regions) {
|
|
16
|
+
if (boxes.length)
|
|
17
|
+
return boxes;
|
|
18
|
+
const own = regions.filter((r) => r.field === field);
|
|
19
|
+
return own.length ? own : regions.filter((r) => r.field.startsWith(`${field}.`));
|
|
20
|
+
}
|
|
1
21
|
/**
|
|
2
22
|
* Forward: a PDF-pt rect -> CSS % of the page box. Percent positioning tracks
|
|
3
23
|
* the page's displayed size across DPR and container resize for free; this is
|
package/dist/preview/index.d.ts
CHANGED
package/dist/preview/index.js
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
// (the codec, the VisualEditor) so a `@quillmark/preview` promotion stays a
|
|
6
6
|
// re-export, not a refactor: it may reach `/core` (the shared WASM boundary) and
|
|
7
7
|
// nothing else. Enforced by `check:deps`, which walks the subpath's import graph.
|
|
8
|
+
// The theme derivation, which the page slots and the overlay read through `var()`.
|
|
9
|
+
// Imported at the BARREL because a subpath is what a consumer gets: this surface
|
|
10
|
+
// reaches modules inside `core/` and never its entry, so a sheet hanging off that
|
|
11
|
+
// entry arrives only for a consumer importing `/core` for some other reason. The
|
|
12
|
+
// sheet rather than the entry, because the barrel needs the derivation and not
|
|
13
|
+
// `init`. `check:deps` (rule 4) walks this file's own graph and holds the reach.
|
|
14
|
+
import '../core/theme.css';
|
|
8
15
|
export { createPreview } from './controller.js';
|
|
9
16
|
export { default as Preview } from './Preview.svelte';
|
|
10
17
|
// What the preview SAYS when there is nothing to paint. Keyed and partial; the
|
|
@@ -3,7 +3,8 @@ import type { PageSlot } from './paint.js';
|
|
|
3
3
|
export interface OverlayController {
|
|
4
4
|
/** Re-read geometry and rebuild every box; call after a layout-affecting refresh. */
|
|
5
5
|
refresh(): void;
|
|
6
|
-
/** Bloom `field`
|
|
6
|
+
/** Bloom the boxes at `field` AND under it: transient, and a no-op when `field`
|
|
7
|
+
* is already the marked one. */
|
|
7
8
|
flashField(field: string): void;
|
|
8
9
|
destroy(): void;
|
|
9
10
|
}
|
package/dist/preview/overlay.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { bloom, bloomTiming, primeWash } from '../core/bloom.js';
|
|
2
|
-
import { rectToPercent, applyPercentRect } from './geometry.js';
|
|
2
|
+
import { boxesForField, rectToPercent, applyPercentRect } from './geometry.js';
|
|
3
3
|
const FIELD_ATTR = 'data-qm-field';
|
|
4
4
|
export function createOverlay(session, slots) {
|
|
5
5
|
const layers = slots.map((slot) => {
|
|
@@ -20,32 +20,47 @@ export function createOverlay(session, slots) {
|
|
|
20
20
|
let flash;
|
|
21
21
|
/** field → its boxes, rebuilt with them. The bloom's only lookup. */
|
|
22
22
|
let byField = new Map();
|
|
23
|
+
// The boxes a flash covers: the address's own, plus every one UNDER it, since the
|
|
24
|
+
// boxes are keyed as `regions()` names them (`main.references.0`) and an
|
|
25
|
+
// editor-side signal names the declared field (`main.references`). Resolved per
|
|
26
|
+
// apply rather than at `flashField`, so a rebuild picks up boxes the recompile
|
|
27
|
+
// moved or added.
|
|
28
|
+
function flashed(field) {
|
|
29
|
+
const els = [];
|
|
30
|
+
for (const [key, group] of byField) {
|
|
31
|
+
if (key === field || key.startsWith(`${field}.`))
|
|
32
|
+
els.push(...group);
|
|
33
|
+
}
|
|
34
|
+
return els;
|
|
35
|
+
}
|
|
23
36
|
// Start or RESUME the bloom on whatever boxes exist for it now: one path, because
|
|
24
37
|
// starting is resuming at ~0. A field surfaces several boxes and they share
|
|
25
38
|
// `startedAt`, so they bloom in step rather than shimmering unevenly.
|
|
26
39
|
function applyFlash() {
|
|
27
|
-
|
|
28
|
-
|
|
40
|
+
if (!flash)
|
|
41
|
+
return;
|
|
42
|
+
const els = flashed(flash.field);
|
|
43
|
+
if (!els.length)
|
|
29
44
|
return;
|
|
30
45
|
const elapsed = performance.now() - flash.startedAt;
|
|
31
46
|
const timing = bloomTiming(els[0]);
|
|
32
47
|
for (const el of els)
|
|
33
48
|
bloom(el, elapsed, timing);
|
|
34
49
|
}
|
|
35
|
-
// Field names come from `regions()` (the only session query that enumerates
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// or widget field, so a nameless-of-content field contributes no boxes.
|
|
50
|
+
// Field names come from `regions()` (the only session query that enumerates them;
|
|
51
|
+
// Preview carries no schema); the rects from `boxesForField`, whose fallback is
|
|
52
|
+
// what makes every address `regions()` names draw.
|
|
39
53
|
function build() {
|
|
40
54
|
for (const layer of layers)
|
|
41
55
|
layer.replaceChildren();
|
|
42
56
|
byField = new Map();
|
|
57
|
+
const regions = session.regions();
|
|
43
58
|
const fields = new Set();
|
|
44
|
-
for (const region of
|
|
59
|
+
for (const region of regions)
|
|
45
60
|
fields.add(region.field);
|
|
46
61
|
for (const field of fields) {
|
|
47
62
|
const els = [];
|
|
48
|
-
for (const box of session.fieldBoxes(field)) {
|
|
63
|
+
for (const box of boxesForField(field, session.fieldBoxes(field), regions)) {
|
|
49
64
|
const layer = layers[box.page];
|
|
50
65
|
const slot = slots[box.page];
|
|
51
66
|
if (!layer || !slot)
|
package/dist/preview/paint.js
CHANGED
|
@@ -43,6 +43,10 @@ onPaintError) {
|
|
|
43
43
|
// the paper.
|
|
44
44
|
const el = document.createElement('div');
|
|
45
45
|
el.className = 'qm-page';
|
|
46
|
+
// The page's index, in the DOM. `pageByEl` is this module's and a consumer
|
|
47
|
+
// drawing an overlay of its own needs the number: position among siblings is
|
|
48
|
+
// the only handle without it, which is right today and is not a contract.
|
|
49
|
+
el.dataset.page = String(page);
|
|
46
50
|
Object.assign(el.style, {
|
|
47
51
|
position: 'relative',
|
|
48
52
|
boxSizing: 'border-box',
|
|
@@ -55,20 +55,9 @@
|
|
|
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
62
|
// The ELEMENT control is the item schema's own; an array declaring no `items`
|
|
74
63
|
// has text elements.
|
|
@@ -102,7 +91,14 @@
|
|
|
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,6 +159,16 @@
|
|
|
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
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.
|
|
@@ -216,7 +223,7 @@
|
|
|
216
223
|
{label}
|
|
217
224
|
id={labelId}
|
|
218
225
|
{descriptionId}
|
|
219
|
-
onActivate={
|
|
226
|
+
onActivate={focus}
|
|
220
227
|
{required}
|
|
221
228
|
{description}
|
|
222
229
|
/>
|
|
@@ -237,7 +244,6 @@
|
|
|
237
244
|
label={label != null ? `${label} ${k + 1}` : undefined}
|
|
238
245
|
onChange={(rt) => commitElement(k, rt)}
|
|
239
246
|
onKey={(e) => onElementKey(e, k)}
|
|
240
|
-
{onFocusEl}
|
|
241
247
|
/>
|
|
242
248
|
{:else if control === 'object'}
|
|
243
249
|
<textarea
|
|
@@ -16,9 +16,10 @@ interface Props {
|
|
|
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;
|
package/dist/visual/Card.svelte
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import Icon from './icons/Icon.svelte';
|
|
17
17
|
import type { Document, Quill, Addr, Diagnostic } from '@quillmark/wasm';
|
|
18
18
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
19
|
-
import type {
|
|
19
|
+
import type { LeafRegistry } from './leaves.js';
|
|
20
20
|
import type { CardModel, FieldModel } from './structure.js';
|
|
21
21
|
import { placeFields, humanize, initialExpandedGroup } from './structure.js';
|
|
22
22
|
import type { FieldDomIds } from './domid.js';
|
|
@@ -51,12 +51,17 @@
|
|
|
51
51
|
isLast: boolean;
|
|
52
52
|
kinds: string[];
|
|
53
53
|
ops: CardOps;
|
|
54
|
+
/** How an option `ops.enumAllowed` refuses draws. Nothing about it is per-card,
|
|
55
|
+
* so it sits beside the bundle rather than in it. */
|
|
56
|
+
enumDisallowed?: 'hide' | 'disable';
|
|
54
57
|
onFocus?: (addr: Addr) => void;
|
|
55
58
|
onCaretMove?: (addr: Addr, pos: number) => void;
|
|
56
59
|
onChange?: (addr: Addr) => void;
|
|
57
60
|
onError?: EditorErrorHandler;
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
/** The editor's leaf registry (`leaves.ts`): every field in this card registers
|
|
62
|
+
* its landing handle, which is what the editor's `focusField`/`setCaret`
|
|
63
|
+
* resolve a `DocPath` to. */
|
|
64
|
+
leaves?: LeafRegistry;
|
|
60
65
|
}
|
|
61
66
|
let {
|
|
62
67
|
card,
|
|
@@ -67,12 +72,12 @@
|
|
|
67
72
|
isLast,
|
|
68
73
|
kinds,
|
|
69
74
|
ops,
|
|
75
|
+
enumDisallowed,
|
|
70
76
|
onFocus,
|
|
71
77
|
onCaretMove,
|
|
72
78
|
onChange,
|
|
73
79
|
onError,
|
|
74
|
-
|
|
75
|
-
unregister
|
|
80
|
+
leaves
|
|
76
81
|
}: Props = $props();
|
|
77
82
|
|
|
78
83
|
// Local title reconcile (external change only), like the scalar controls.
|
|
@@ -229,10 +234,9 @@
|
|
|
229
234
|
not a persistent box. `data-value` falls back to the
|
|
230
235
|
placeholder so an empty title still reserves its resolved-title width. -->
|
|
231
236
|
<span class="qm-card-title-sizer" data-value={localTitle || card.titlePlaceholder}>
|
|
232
|
-
<!-- `qm-focus-ring` is
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
edit unringed while a tab into it draws one. -->
|
|
237
|
+
<!-- `qm-focus-ring` is this control's ONLY focus indicator: the ink step
|
|
238
|
+
cannot be one, since hover already takes that step. `:focus-visible`, so
|
|
239
|
+
a press enters the edit unringed while a tab into it draws one. -->
|
|
236
240
|
<input
|
|
237
241
|
class="qm-card-title qm-focus-ring"
|
|
238
242
|
value={localTitle}
|
|
@@ -361,8 +365,7 @@
|
|
|
361
365
|
{onCaretMove}
|
|
362
366
|
{onChange}
|
|
363
367
|
{onError}
|
|
364
|
-
{
|
|
365
|
-
{unregister}
|
|
368
|
+
{leaves}
|
|
366
369
|
/>
|
|
367
370
|
<DiagnosticList diagnostics={ops.diagFor(undefined)} />
|
|
368
371
|
</div>
|
|
@@ -385,17 +388,17 @@
|
|
|
385
388
|
provenance={card.provenance[f.name]}
|
|
386
389
|
{doc}
|
|
387
390
|
{quill}
|
|
388
|
-
|
|
391
|
+
addr={ops.makeAddr(f.name)}
|
|
389
392
|
leafKey={ops.leafKey(f.name)}
|
|
390
393
|
domIds={ops.domIds(f.name)}
|
|
391
394
|
onCommitScalar={(v) => ops.commit(f.name, v)}
|
|
392
395
|
optionAllowed={(v) => ops.enumAllowed(f.name, v)}
|
|
396
|
+
{enumDisallowed}
|
|
393
397
|
{onFocus}
|
|
394
398
|
{onCaretMove}
|
|
395
399
|
{onChange}
|
|
396
400
|
{onError}
|
|
397
|
-
{
|
|
398
|
-
{unregister}
|
|
401
|
+
{leaves}
|
|
399
402
|
diagnostics={ops.diagFor(f.name)}
|
|
400
403
|
/>
|
|
401
404
|
{/each}
|
|
@@ -636,9 +639,8 @@
|
|
|
636
639
|
The ACTION COLUMN is minted here and spent one level down, on the field
|
|
637
640
|
(`Field.svelte` holds why it is the field's inset and not this container's). It
|
|
638
641
|
is declared on the query container because both of its consumers hang off one:
|
|
639
|
-
the fields inside, and the capacity ramp below, which
|
|
640
|
-
|
|
641
|
-
reservation already narrowed. */
|
|
642
|
+
the fields inside, and the capacity ramp below, which carries the column in its
|
|
643
|
+
own arithmetic because the query reads a box the reservation has not narrowed. */
|
|
642
644
|
.qm-section,
|
|
643
645
|
.qm-group-panel-inner {
|
|
644
646
|
--action-col: calc(var(--_qm-tap-min) + var(--_qm-space-2));
|
|
@@ -741,10 +743,10 @@
|
|
|
741
743
|
padding: var(--_qm-space) 0 var(--_qm-space) var(--_qm-space);
|
|
742
744
|
font: inherit;
|
|
743
745
|
font-size: var(--_qm-text-label);
|
|
744
|
-
font-weight: var(--_qm-weight-
|
|
746
|
+
font-weight: var(--_qm-weight-label);
|
|
745
747
|
line-height: var(--_qm-leading-tight);
|
|
746
748
|
cursor: pointer;
|
|
747
|
-
color: var(--_qm-ink-
|
|
749
|
+
color: var(--_qm-ink-label);
|
|
748
750
|
text-align: left;
|
|
749
751
|
transition: color var(--_qm-duration-fast) var(--_qm-ease-reverse);
|
|
750
752
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Document, Quill, Addr, Diagnostic } from '@quillmark/wasm';
|
|
2
2
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { LeafRegistry } from './leaves.js';
|
|
4
4
|
import type { CardModel } from './structure.js';
|
|
5
5
|
import type { FieldDomIds } from './domid.js';
|
|
6
6
|
/** The per-card operation bundle the VisualEditor builds (all resolve id→index lazily). */
|
|
@@ -28,12 +28,17 @@ interface Props {
|
|
|
28
28
|
isLast: boolean;
|
|
29
29
|
kinds: string[];
|
|
30
30
|
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
|
+
enumDisallowed?: 'hide' | 'disable';
|
|
31
34
|
onFocus?: (addr: Addr) => void;
|
|
32
35
|
onCaretMove?: (addr: Addr, pos: number) => void;
|
|
33
36
|
onChange?: (addr: Addr) => void;
|
|
34
37
|
onError?: EditorErrorHandler;
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
/** The editor's leaf registry (`leaves.ts`): every field in this card registers
|
|
39
|
+
* its landing handle, which is what the editor's `focusField`/`setCaret`
|
|
40
|
+
* resolve a `DocPath` to. */
|
|
41
|
+
leaves?: LeafRegistry;
|
|
37
42
|
}
|
|
38
43
|
declare const Card: import("svelte").Component<Props, {
|
|
39
44
|
revealLeaf: (key: string) => void;
|
|
@@ -34,13 +34,23 @@
|
|
|
34
34
|
/** The parked `description` (FieldLabel): announced after the name. */
|
|
35
35
|
describedBy?: string;
|
|
36
36
|
onCommit: (v: string | undefined) => void;
|
|
37
|
-
/** Consumer policy: `false`
|
|
38
|
-
*
|
|
39
|
-
* stripped or mutated. The UNSET sentinel is exempt: clear-to-default always works. */
|
|
37
|
+
/** Consumer policy: `false` marks an option unavailable, so it can't be picked.
|
|
38
|
+
* The UNSET sentinel is exempt: clear-to-default always works. */
|
|
40
39
|
optionAllowed?: (value: string) => boolean;
|
|
40
|
+
/** How a refused option draws: greyed and unpickable, or left out of the list. */
|
|
41
|
+
enumDisallowed?: 'hide' | 'disable';
|
|
41
42
|
}
|
|
42
|
-
let {
|
|
43
|
-
|
|
43
|
+
let {
|
|
44
|
+
value,
|
|
45
|
+
values,
|
|
46
|
+
fallback,
|
|
47
|
+
label,
|
|
48
|
+
id,
|
|
49
|
+
describedBy,
|
|
50
|
+
onCommit,
|
|
51
|
+
optionAllowed,
|
|
52
|
+
enumDisallowed = 'disable'
|
|
53
|
+
}: Props = $props();
|
|
44
54
|
|
|
45
55
|
// The sentinel's option value: a namespaced marker that no schema-authored
|
|
46
56
|
// enum member would ever be (`values` are classification markings, seal ids, and
|
|
@@ -107,15 +117,25 @@
|
|
|
107
117
|
<Select.Item class="qm-menu-item qm-select-item" value={UNSET} label={ghostText}>
|
|
108
118
|
<span class="qm-select-ghost">{ghostText}</span>
|
|
109
119
|
</Select.Item>
|
|
120
|
+
<!-- A refused option still draws under `'disable'`, and under EITHER
|
|
121
|
+
policy when it is the one selected: a listbox whose selected value
|
|
122
|
+
has no row shows nothing for what the document says, and offers none
|
|
123
|
+
for the primitive to mark, type-ahead to, or key onto
|
|
124
|
+
(VISUAL_EDITOR §"Enum policy"). Off `local.value`, not `value`: the
|
|
125
|
+
row that exists is the row the control has selected, through the
|
|
126
|
+
window where an own pick has not reconciled back. -->
|
|
110
127
|
{#each values as v (v)}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
128
|
+
{@const allowed = optionAllowed?.(v) !== false}
|
|
129
|
+
{#if allowed || enumDisallowed === 'disable' || v === local.value}
|
|
130
|
+
<Select.Item
|
|
131
|
+
class="qm-menu-item qm-select-item"
|
|
132
|
+
value={v}
|
|
133
|
+
label={dash(v)}
|
|
134
|
+
disabled={!allowed}
|
|
135
|
+
>
|
|
136
|
+
{dash(v)}
|
|
137
|
+
</Select.Item>
|
|
138
|
+
{/if}
|
|
119
139
|
{/each}
|
|
120
140
|
</Select.Viewport>
|
|
121
141
|
</div>
|
|
@@ -158,7 +178,7 @@
|
|
|
158
178
|
.qm-select-content :global(.qm-select-item[data-selected]) {
|
|
159
179
|
font-weight: var(--_qm-weight-label);
|
|
160
180
|
}
|
|
161
|
-
/* Consumer policy: offered
|
|
181
|
+
/* Consumer policy, for a refused option the list still draws: offered, unpickable. */
|
|
162
182
|
.qm-select-content :global(.qm-select-item[data-disabled]) {
|
|
163
183
|
opacity: var(--_qm-opacity-muted);
|
|
164
184
|
cursor: default;
|
|
@@ -15,10 +15,11 @@ interface Props {
|
|
|
15
15
|
/** The parked `description` (FieldLabel): announced after the name. */
|
|
16
16
|
describedBy?: string;
|
|
17
17
|
onCommit: (v: string | undefined) => void;
|
|
18
|
-
/** Consumer policy: `false`
|
|
19
|
-
*
|
|
20
|
-
* stripped or mutated. The UNSET sentinel is exempt: clear-to-default always works. */
|
|
18
|
+
/** Consumer policy: `false` marks an option unavailable, so it can't be picked.
|
|
19
|
+
* The UNSET sentinel is exempt: clear-to-default always works. */
|
|
21
20
|
optionAllowed?: (value: string) => boolean;
|
|
21
|
+
/** How a refused option draws: greyed and unpickable, or left out of the list. */
|
|
22
|
+
enumDisallowed?: 'hide' | 'disable';
|
|
22
23
|
}
|
|
23
24
|
declare const EnumField: import("svelte").Component<Props, {}, "">;
|
|
24
25
|
type EnumField = ReturnType<typeof EnumField>;
|