@quillmark/svelte 0.17.0 → 0.19.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 +1 -1
- package/dist/core/codec/index.d.ts +0 -3
- package/dist/core/rebind.svelte.js +5 -8
- package/dist/preset/index.d.ts +1 -0
- package/dist/preview/Preview.svelte +14 -19
- package/dist/preview/Preview.svelte.d.ts +3 -3
- package/dist/preview/index.d.ts +0 -1
- package/dist/visual/ArrayField.svelte +48 -49
- package/dist/visual/ArrayField.svelte.d.ts +1 -2
- package/dist/visual/BooleanField.svelte +9 -14
- package/dist/visual/BooleanField.svelte.d.ts +0 -1
- package/dist/visual/Card.svelte +5 -18
- package/dist/visual/CardControls.svelte.d.ts +0 -1
- package/dist/visual/DateField.svelte +14 -17
- package/dist/visual/DateField.svelte.d.ts +0 -1
- package/dist/visual/EnumField.svelte +9 -12
- package/dist/visual/EnumField.svelte.d.ts +0 -1
- package/dist/visual/Field.svelte +8 -9
- package/dist/visual/FieldHint.svelte.d.ts +0 -1
- package/dist/visual/FieldLabel.svelte +1 -2
- package/dist/visual/FormatPopover.svelte +22 -27
- package/dist/visual/FormatPopover.svelte.d.ts +0 -1
- package/dist/visual/MatrixAdd.svelte +1 -2
- package/dist/visual/MatrixAdd.svelte.d.ts +0 -1
- package/dist/visual/MatrixField.svelte +3 -5
- package/dist/visual/MatrixField.svelte.d.ts +0 -1
- package/dist/visual/NumberField.svelte +6 -10
- package/dist/visual/NumberField.svelte.d.ts +0 -1
- package/dist/visual/ObjectField.svelte +9 -13
- package/dist/visual/ObjectField.svelte.d.ts +0 -1
- package/dist/visual/ProseField.svelte +42 -41
- package/dist/visual/ProseField.svelte.d.ts +0 -1
- package/dist/visual/ProseValue.svelte +56 -56
- package/dist/visual/ProseValue.svelte.d.ts +0 -1
- package/dist/visual/SlashMenu.svelte +5 -15
- package/dist/visual/SlashMenu.svelte.d.ts +0 -1
- package/dist/visual/TextField.svelte +5 -9
- package/dist/visual/TextField.svelte.d.ts +0 -1
- package/dist/visual/TipsCard.svelte +10 -11
- package/dist/visual/TipsCard.svelte.d.ts +0 -1
- package/dist/visual/VisualEditorInner.svelte +6 -14
- package/dist/visual/VisualEditorInner.svelte.d.ts +0 -1
- package/dist/visual/index.d.ts +0 -1
- package/dist/visual/structure.d.ts +4 -0
- package/dist/visual/structure.js +12 -0
- package/package.json +20 -16
- package/dist/visual/synced.svelte.d.ts +0 -15
- package/dist/visual/synced.svelte.js +0 -34
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Editor + live-preview components for [Quillmark](https://github.com/borb-sh/quil
|
|
|
8
8
|
npm install @quillmark/svelte
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
`svelte@^5` and `@quillmark/wasm` are peer dependencies: the session's handles cross the package boundary, so the consumer supplies the one copy both sides mint them from.
|
|
11
|
+
`svelte@^5.33` and `@quillmark/wasm` are peer dependencies: the session's handles cross the package boundary, so the consumer supplies the one copy both sides mint them from.
|
|
12
12
|
|
|
13
13
|
## Subpaths
|
|
14
14
|
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import 'prosemirror-view/style/prosemirror.css';
|
|
2
|
-
import 'prosemirror-gapcursor/style/gapcursor.css';
|
|
3
|
-
import './prose.css';
|
|
4
1
|
export { rangeAnchor } from './anchor.js';
|
|
5
2
|
export type { RangeAnchor } from './anchor.js';
|
|
6
3
|
export { createField, emptyContent, heldAttributes, proseAttributes, proseLeafPlugins } from './field.js';
|
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
// The remount contract, enforced. `Preview` binds its props once,
|
|
2
|
-
// observes nothing after: the vanilla core below it owns scroll position,
|
|
3
|
-
// slots and observer sets that a remount would discard on every apply. So a
|
|
4
|
-
// swapped in place is reported, not dropped in silence, and thrown in a dev build.
|
|
1
|
+
// The remount contract, enforced. `Preview` binds its props once, when its container
|
|
2
|
+
// mounts, and observes nothing after: the vanilla core below it owns scroll position,
|
|
3
|
+
// mounted slots and observer sets that a remount would discard on every apply. So a
|
|
4
|
+
// prop swapped in place is reported, not dropped in silence, and thrown in a dev build.
|
|
5
5
|
//
|
|
6
6
|
// A guard over one prop of a set is worse than none: a consumer who learns the
|
|
7
7
|
// surface complains when it ignores a prop reads silence on the rest as reactivity.
|
|
8
8
|
// The snapshot is every once-bound prop, and one report names whichever went stale.
|
|
9
|
+
import { DEV } from 'esm-env';
|
|
9
10
|
import { reportError } from './errors.js';
|
|
10
|
-
// True in a dev build whose bundler injects `import.meta.env`. Cast, since the package
|
|
11
|
-
// declares no bundler's ambient types; false where nothing injects it, so a toolchain
|
|
12
|
-
// that does not answer gets the report alone.
|
|
13
|
-
const DEV = import.meta.env?.DEV === true;
|
|
14
11
|
/**
|
|
15
12
|
* Report the violation, then throw it in a dev build. Both surfaces raise it here; the
|
|
16
13
|
* guards themselves stay separate, since the preview snapshots its props once and the
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
imperatively (e.g. after `session.apply` elsewhere).
|
|
8
8
|
-->
|
|
9
9
|
<script lang="ts">
|
|
10
|
-
import {
|
|
10
|
+
import { untrack } from 'svelte';
|
|
11
11
|
import { createPreview, type PreviewController } from './controller.js';
|
|
12
12
|
import { guardRebind } from '../core/rebind.svelte.js';
|
|
13
13
|
import type { PreviewStringsInput } from './strings.js';
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Remount contract. `createPreview` binds once
|
|
20
|
-
* prop it closed over (`session`, `margin`, `onPick`, `onError`,
|
|
21
|
-
* observed, and each reports `rebind-ignored` when swapped. Swap
|
|
19
|
+
* Remount contract. `createPreview` binds once, when its container mounts; a later
|
|
20
|
+
* change to any prop it closed over (`session`, `margin`, `onPick`, `onError`,
|
|
21
|
+
* `strings`) is not observed, and each reports `rebind-ignored` when swapped. Swap
|
|
22
22
|
* the session by remounting (`{#key session}`, as the playground does); drive
|
|
23
23
|
* in-place edits through the `refresh(change)` method, not a prop change.
|
|
24
24
|
*
|
|
@@ -44,7 +44,6 @@
|
|
|
44
44
|
|
|
45
45
|
let { session, margin, onPick, onError, strings, class: className, style }: Props = $props();
|
|
46
46
|
|
|
47
|
-
let containerEl: HTMLDivElement | undefined = $state();
|
|
48
47
|
let controller: PreviewController | undefined;
|
|
49
48
|
|
|
50
49
|
guardRebind(
|
|
@@ -52,20 +51,16 @@
|
|
|
52
51
|
'Remount the preview ({#key session}) to rebind.'
|
|
53
52
|
);
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
function mountPreview(container: HTMLDivElement): () => void {
|
|
55
|
+
return untrack(() => {
|
|
56
|
+
const mounted = createPreview(session, { container, margin, onPick, onError, strings });
|
|
57
|
+
controller = mounted;
|
|
58
|
+
return () => {
|
|
59
|
+
mounted.destroy();
|
|
60
|
+
controller = undefined;
|
|
61
|
+
};
|
|
63
62
|
});
|
|
64
|
-
|
|
65
|
-
controller?.destroy();
|
|
66
|
-
controller = undefined;
|
|
67
|
-
};
|
|
68
|
-
});
|
|
63
|
+
}
|
|
69
64
|
|
|
70
65
|
export function refresh(change: ChangeSet): void {
|
|
71
66
|
controller?.refresh(change);
|
|
@@ -88,7 +83,7 @@
|
|
|
88
83
|
}
|
|
89
84
|
</script>
|
|
90
85
|
|
|
91
|
-
<div
|
|
86
|
+
<div {@attach mountPreview} class="qm-preview {className ?? ''}" {style} data-qm-root></div>
|
|
92
87
|
|
|
93
88
|
<style>
|
|
94
89
|
/* A detached root: the preview is not a descendant of the editor, so it carries
|
|
@@ -3,9 +3,9 @@ import type { LiveSession, ChangeSet } from '@quillmark/wasm';
|
|
|
3
3
|
import type { DocPath, Landing, Place } from '../core/address.js';
|
|
4
4
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
5
5
|
/**
|
|
6
|
-
* Remount contract. `createPreview` binds once
|
|
7
|
-
* prop it closed over (`session`, `margin`, `onPick`, `onError`,
|
|
8
|
-
* observed, and each reports `rebind-ignored` when swapped. Swap
|
|
6
|
+
* Remount contract. `createPreview` binds once, when its container mounts; a later
|
|
7
|
+
* change to any prop it closed over (`session`, `margin`, `onPick`, `onError`,
|
|
8
|
+
* `strings`) is not observed, and each reports `rebind-ignored` when swapped. Swap
|
|
9
9
|
* the session by remounting (`{#key session}`, as the playground does); drive
|
|
10
10
|
* in-place edits through the `refresh(change)` method, not a prop change.
|
|
11
11
|
*
|
package/dist/preview/index.d.ts
CHANGED
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
neither a screen reader nor a touch user, and the count is the carrier.
|
|
13
13
|
|
|
14
14
|
An `object` element collapses: the row is its own summary — a box, titled by the
|
|
15
|
-
row's first short text cell ({@link rowSummary}) — and opens onto {@link ObjectField}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Opening is
|
|
15
|
+
row's first short text cell ({@link rowSummary}) — and opens onto {@link ObjectField}.
|
|
16
|
+
Each row opens and closes on its own: a press on a summary toggles that row, and
|
|
17
|
+
opening one leaves every other where it stands, so a landing never folds the row the
|
|
18
|
+
author was in. Opening is part of a landing rather than something the user does first:
|
|
19
19
|
`focusPath` opens the row it is aimed at before it focuses, and hands the rest of the
|
|
20
|
-
walk to
|
|
20
|
+
walk to that row's subform, which is how a row two closed boxes down is reached.
|
|
21
21
|
|
|
22
22
|
The table (`layout: 'table'`, VISUAL_EDITOR §"Structure mirrors the schema") is the
|
|
23
23
|
same row machine in another presentation: the same ids, the same splices, the same
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
Removing the last row commits `[]`, the empty answer.
|
|
55
55
|
-->
|
|
56
56
|
<script lang="ts">
|
|
57
|
+
import { SvelteSet } from 'svelte/reactivity';
|
|
57
58
|
import { wording } from './strings.js';
|
|
58
59
|
|
|
59
60
|
// The surface's words, ambient from the editor root; the package's English
|
|
@@ -76,7 +77,6 @@
|
|
|
76
77
|
} from './structure.js';
|
|
77
78
|
import { splitDeep, unrouted, type DeepDiagnostic } from './diagnostics.js';
|
|
78
79
|
import type { LandingBox } from './leaves.js';
|
|
79
|
-
import { holdInView } from './hold.js';
|
|
80
80
|
import { reorder, reorderArm } from './motion.js';
|
|
81
81
|
import Icon from './icons/Icon.svelte';
|
|
82
82
|
import TextField from './TextField.svelte';
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
const CHEVRON = 16;
|
|
92
92
|
|
|
93
93
|
interface Props {
|
|
94
|
-
value: unknown
|
|
94
|
+
value: unknown;
|
|
95
95
|
/** The declared `default:`: the rows an unset array draws, which a gesture takes. */
|
|
96
96
|
fallback?: unknown[];
|
|
97
97
|
items: QuillFieldSchema | undefined;
|
|
@@ -156,11 +156,18 @@
|
|
|
156
156
|
const table = $derived(layout === 'table' && control === 'object');
|
|
157
157
|
const columns = $derived(Object.entries(items?.properties ?? {}));
|
|
158
158
|
const defaulted = $derived(value == null && (fallback?.length ?? 0) > 0);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
// A bare value is the one-element list the render floor wraps it into (canon
|
|
160
|
+
// `SCHEMAS.md` §"Type coercion"), and the reader answers no element of it.
|
|
161
|
+
const bare = $derived(value != null && !Array.isArray(value));
|
|
162
|
+
const arr = $derived(rowsOf(value));
|
|
163
|
+
function rowsOf(v: unknown): unknown[] {
|
|
164
|
+
if (v == null) return fallback ?? [];
|
|
165
|
+
return Array.isArray(v) ? v : [v];
|
|
166
|
+
}
|
|
167
|
+
/** The rows' content read: the boundary's, or, while the rows are the default or a
|
|
168
|
+
* bare value, the literal's own leaf at the codec its declared type names. */
|
|
162
169
|
function readAt(path: PathStep[]): Content | undefined {
|
|
163
|
-
if (!defaulted) return contentAt(path);
|
|
170
|
+
if (!defaulted && !bare) return contentAt(path);
|
|
164
171
|
const leaf = schemaAt({ type: 'array', items }, path);
|
|
165
172
|
let v: unknown = arr;
|
|
166
173
|
for (const step of path) v = (v as Record<string | number, unknown> | undefined)?.[step];
|
|
@@ -177,7 +184,7 @@
|
|
|
177
184
|
// an effect-only seed mounts every element editor in a second render.
|
|
178
185
|
const seq = new IdSeq();
|
|
179
186
|
// svelte-ignore state_referenced_locally
|
|
180
|
-
let ids = $state<string[]>(seq.take((value
|
|
187
|
+
let ids = $state<string[]>(seq.take(rowsOf(value).length));
|
|
181
188
|
// Length reconcile (defend against an out-of-band length change);
|
|
182
189
|
// order is maintained by the mutators, not here.
|
|
183
190
|
$effect(() => {
|
|
@@ -214,7 +221,7 @@
|
|
|
214
221
|
const rowEls: Record<string, HTMLElement | undefined> = $state({});
|
|
215
222
|
// A table row's cells, on the same key: the subform a landing and the column-keeping
|
|
216
223
|
// Enter hop reach into. Every table row is open, so there is one per row where the
|
|
217
|
-
// list holds one
|
|
224
|
+
// list holds one per open row.
|
|
218
225
|
type Subform = {
|
|
219
226
|
focus: () => void;
|
|
220
227
|
focusPath: (steps: readonly PathStep[], pos?: number) => LandingBox;
|
|
@@ -226,14 +233,14 @@
|
|
|
226
233
|
delete els[id];
|
|
227
234
|
delete rowEls[id];
|
|
228
235
|
delete cellEls[id];
|
|
236
|
+
delete objEls[id];
|
|
237
|
+
open.delete(id);
|
|
229
238
|
}
|
|
230
239
|
|
|
231
|
-
// ── Object elements:
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
// row is open, so the ref is singular by the same rule the state is.
|
|
236
|
-
let openObjEl = $state<Subform | undefined>();
|
|
240
|
+
// ── Object elements: each row open on its own ────────────────────────────────
|
|
241
|
+
const open = new SvelteSet<string>();
|
|
242
|
+
// An open row's subform, on the row's id: the landing below walks into it.
|
|
243
|
+
const objEls: Record<string, Subform | undefined> = $state({});
|
|
237
244
|
|
|
238
245
|
/** A collapsed row's own words, or `undefined` while it has none. */
|
|
239
246
|
function elementTitle(k: number): string | undefined {
|
|
@@ -244,12 +251,9 @@
|
|
|
244
251
|
function untitled(k: number): string {
|
|
245
252
|
return label != null ? t.strings.elementUntitled(label, k + 1) : String(k + 1);
|
|
246
253
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
holdInView(summary, () => {
|
|
251
|
-
openId = openId === id ? undefined : id;
|
|
252
|
-
});
|
|
254
|
+
function toggleRow(id: string): void {
|
|
255
|
+
if (open.has(id)) open.delete(id);
|
|
256
|
+
else open.add(id);
|
|
253
257
|
}
|
|
254
258
|
const rowName = (k: number): string | undefined =>
|
|
255
259
|
label != null ? `${label} ${k + 1}` : undefined;
|
|
@@ -300,7 +304,7 @@
|
|
|
300
304
|
onCommit(next);
|
|
301
305
|
// A row added is a row to fill in, so an object element arrives open: landing on
|
|
302
306
|
// a collapsed empty summary would make adding one a two-press gesture.
|
|
303
|
-
if (control === 'object' && !table)
|
|
307
|
+
if (control === 'object' && !table) open.add(id);
|
|
304
308
|
void focusAfterFlush(id, column);
|
|
305
309
|
}
|
|
306
310
|
function add(): void {
|
|
@@ -319,9 +323,6 @@
|
|
|
319
323
|
const next = ids.filter((_, i) => i !== k);
|
|
320
324
|
ids = next;
|
|
321
325
|
drop(dropped);
|
|
322
|
-
// The open row can be the one removed; `openId` is cleared with it rather than
|
|
323
|
-
// left naming an element that has gone.
|
|
324
|
-
if (openId === dropped) openId = undefined;
|
|
325
326
|
onCommit(arr.filter((_, i) => i !== k));
|
|
326
327
|
void focusAfterFlush(next[Math.max(k - 1, 0)], column, fromRemove);
|
|
327
328
|
}
|
|
@@ -370,7 +371,8 @@
|
|
|
370
371
|
/** An object row's landing: inside the subform when that row is the open one, on
|
|
371
372
|
* the row's own summary otherwise — a collapsed row's control is its summary. */
|
|
372
373
|
function focusObjectRow(id: string): void {
|
|
373
|
-
|
|
374
|
+
const sub = objEls[id];
|
|
375
|
+
if (open.has(id) && sub) return sub.focus();
|
|
374
376
|
rowEls[id]?.querySelector<HTMLElement>('.qm-element-summary')?.focus();
|
|
375
377
|
}
|
|
376
378
|
/** The box an arrival wash blooms in (`leaves.ts`, `core/bloom.ts`): the elements,
|
|
@@ -413,10 +415,11 @@
|
|
|
413
415
|
return rowEls[id];
|
|
414
416
|
}
|
|
415
417
|
if (control === 'object') {
|
|
416
|
-
|
|
418
|
+
open.add(id);
|
|
417
419
|
return (async () => {
|
|
418
420
|
if (!(await span.resumes(tick()))) return undefined;
|
|
419
|
-
|
|
421
|
+
const sub = objEls[id];
|
|
422
|
+
if (rest.length && sub) return (await sub.focusPath(rest, pos)) ?? rowEls[id];
|
|
420
423
|
focusObjectRow(id);
|
|
421
424
|
return rowEls[id];
|
|
422
425
|
})();
|
|
@@ -539,8 +542,7 @@
|
|
|
539
542
|
</script>
|
|
540
543
|
|
|
541
544
|
<div
|
|
542
|
-
class=
|
|
543
|
-
class:empty={ids.length === 0}
|
|
545
|
+
class={['qm-array', { empty: ids.length === 0 }]}
|
|
544
546
|
role="group"
|
|
545
547
|
aria-labelledby={label != null ? labelId : undefined}
|
|
546
548
|
aria-describedby={description ? descriptionId : undefined}
|
|
@@ -579,8 +581,7 @@
|
|
|
579
581
|
each block's one child: a branch inside the block would stand between them. -->
|
|
580
582
|
{#if table}
|
|
581
583
|
<div
|
|
582
|
-
class=
|
|
583
|
-
class:empty={ids.length === 0}
|
|
584
|
+
class={['qm-array-rows qm-array-table', { empty: ids.length === 0 }]}
|
|
584
585
|
data-default={defaulted ? '' : undefined}
|
|
585
586
|
bind:this={rowsEl}
|
|
586
587
|
>
|
|
@@ -592,9 +593,10 @@
|
|
|
592
593
|
<div class="qm-array-table-head">
|
|
593
594
|
{#each columns as [key, sub] (key)}
|
|
594
595
|
<div
|
|
595
|
-
class=
|
|
596
|
-
|
|
597
|
-
|
|
596
|
+
class={[
|
|
597
|
+
'qm-array-table-col',
|
|
598
|
+
{ mark: controlKind(sub) === 'boolean', prose: controlKind(sub) === 'prose' }
|
|
599
|
+
]}
|
|
598
600
|
>
|
|
599
601
|
<FieldLabel
|
|
600
602
|
label={columnTitle(key, sub)}
|
|
@@ -625,8 +627,7 @@
|
|
|
625
627
|
</div>
|
|
626
628
|
{:else if control === 'object'}
|
|
627
629
|
<div
|
|
628
|
-
class=
|
|
629
|
-
class:empty={ids.length === 0}
|
|
630
|
+
class={['qm-array-rows', { empty: ids.length === 0 }]}
|
|
630
631
|
data-default={defaulted ? '' : undefined}
|
|
631
632
|
bind:this={rowsEl}
|
|
632
633
|
>
|
|
@@ -635,8 +636,7 @@
|
|
|
635
636
|
<!-- The handler catches a key from the controls inside the row and adds no
|
|
636
637
|
interaction of the row's own: the row is no tab stop. -->
|
|
637
638
|
<div
|
|
638
|
-
class=
|
|
639
|
-
class:open={openId === id}
|
|
639
|
+
class={['qm-array-row qm-element', { open: open.has(id) }]}
|
|
640
640
|
bind:this={rowEls[id]}
|
|
641
641
|
animate:reorder={arm.armed}
|
|
642
642
|
onkeydown={(e) => onRowKey(e, k)}
|
|
@@ -649,8 +649,8 @@
|
|
|
649
649
|
<button
|
|
650
650
|
type="button"
|
|
651
651
|
class="qm-control-box qm-focus-ring qm-element-summary"
|
|
652
|
-
aria-expanded={
|
|
653
|
-
onclick={(
|
|
652
|
+
aria-expanded={open.has(id)}
|
|
653
|
+
onclick={() => toggleRow(id)}
|
|
654
654
|
>
|
|
655
655
|
<!-- Leading, and it rotates: trailing is the figure for pushing a new
|
|
656
656
|
screen, where this unfolds in place. Same glyph, same rotation and
|
|
@@ -666,9 +666,9 @@
|
|
|
666
666
|
{@render rowActions(k)}
|
|
667
667
|
</div>
|
|
668
668
|
</div>
|
|
669
|
-
{#if
|
|
669
|
+
{#if open.has(id)}
|
|
670
670
|
<ObjectField
|
|
671
|
-
bind:this={
|
|
671
|
+
bind:this={objEls[id]}
|
|
672
672
|
value={(arr[k] ?? {}) as Record<string, unknown>}
|
|
673
673
|
properties={items?.properties}
|
|
674
674
|
label={rowName(k)}
|
|
@@ -687,8 +687,7 @@
|
|
|
687
687
|
</div>
|
|
688
688
|
{:else}
|
|
689
689
|
<div
|
|
690
|
-
class=
|
|
691
|
-
class:empty={ids.length === 0}
|
|
690
|
+
class={['qm-array-rows', { empty: ids.length === 0 }]}
|
|
692
691
|
data-default={defaulted ? '' : undefined}
|
|
693
692
|
bind:this={rowsEl}
|
|
694
693
|
>
|
|
@@ -2,9 +2,8 @@ import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
|
|
|
2
2
|
import { type ArrayLayout } from './structure.js';
|
|
3
3
|
import { type DeepDiagnostic } from './diagnostics.js';
|
|
4
4
|
import type { LandingBox } from './leaves.js';
|
|
5
|
-
import './controls.css';
|
|
6
5
|
interface Props {
|
|
7
|
-
value: unknown
|
|
6
|
+
value: unknown;
|
|
8
7
|
/** The declared `default:`: the rows an unset array draws, which a gesture takes. */
|
|
9
8
|
fallback?: unknown[];
|
|
10
9
|
items: QuillFieldSchema | undefined;
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
-->
|
|
14
14
|
<script lang="ts">
|
|
15
15
|
import { Switch } from 'bits-ui';
|
|
16
|
-
import { syncedLocal } from './synced.svelte.js';
|
|
17
16
|
import './controls.css';
|
|
18
17
|
|
|
19
18
|
interface Props {
|
|
@@ -36,18 +35,14 @@
|
|
|
36
35
|
}
|
|
37
36
|
let { value, fallback, optional = false, label, id, describedBy, onCommit }: Props = $props();
|
|
38
37
|
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const local = syncedLocal<boolean | undefined>(
|
|
46
|
-
() => value ?? fallback ?? (optional ? undefined : false)
|
|
47
|
-
);
|
|
48
|
-
const unset = $derived(local.value === undefined);
|
|
38
|
+
// The primitive is driven controlled (`checked` + `onCheckedChange`, never `bind:`)
|
|
39
|
+
// so reconciliation stays the package's: a two-way bind hands the primitive a lane
|
|
40
|
+
// around it, which repeats the reconciliation hazard in miniature. `undefined` is
|
|
41
|
+
// the unset optional cell, the one state with no default to stand in for it.
|
|
42
|
+
let local = $derived<boolean | undefined>(value ?? fallback ?? (optional ? undefined : false));
|
|
43
|
+
const unset = $derived(local === undefined);
|
|
49
44
|
const tristate = $derived(
|
|
50
|
-
optional ? { role: 'checkbox', 'aria-checked': unset ? ('mixed' as const) : !!local
|
|
45
|
+
optional ? { role: 'checkbox', 'aria-checked': unset ? ('mixed' as const) : !!local } : {}
|
|
51
46
|
);
|
|
52
47
|
</script>
|
|
53
48
|
|
|
@@ -57,13 +52,13 @@
|
|
|
57
52
|
<span class="qm-toggle-wrap" data-unset={unset ? '' : undefined}>
|
|
58
53
|
<Switch.Root
|
|
59
54
|
class="qm-toggle qm-focus-ring qm-tap-floor"
|
|
60
|
-
checked={!!local
|
|
55
|
+
checked={!!local}
|
|
61
56
|
{id}
|
|
62
57
|
aria-label={id ? undefined : label}
|
|
63
58
|
aria-describedby={describedBy}
|
|
64
59
|
onCheckedChange={(v) => {
|
|
65
60
|
// From unset the primitive reads the unchecked face, so its toggle is `true`.
|
|
66
|
-
local
|
|
61
|
+
local = v;
|
|
67
62
|
onCommit(v);
|
|
68
63
|
}}
|
|
69
64
|
>
|
package/dist/visual/Card.svelte
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
// Ambient from the editor root, with the package's English off-tree, so this
|
|
10
10
|
// component renders standalone too.
|
|
11
11
|
const t = wording();
|
|
12
|
-
import { untrack } from 'svelte';
|
|
13
12
|
import Icon from './icons/Icon.svelte';
|
|
14
13
|
import type { Document, Quill, Addr, Diagnostic } from '@quillmark/wasm';
|
|
15
14
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
@@ -73,15 +72,7 @@
|
|
|
73
72
|
leaves
|
|
74
73
|
}: Props = $props();
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
// svelte-ignore state_referenced_locally
|
|
78
|
-
let localTitle = $state(card.titleOverride);
|
|
79
|
-
$effect(() => {
|
|
80
|
-
const incoming = card.titleOverride;
|
|
81
|
-
untrack(() => {
|
|
82
|
-
if (incoming !== localTitle) localTitle = incoming;
|
|
83
|
-
});
|
|
84
|
-
});
|
|
75
|
+
let localTitle = $derived(card.titleOverride);
|
|
85
76
|
|
|
86
77
|
// The pre-edit value Escape rolls back to; the rename itself stays live on input.
|
|
87
78
|
let titleAtFocus = '';
|
|
@@ -209,10 +200,8 @@
|
|
|
209
200
|
ancestor and nothing else marks the box its guidance belongs in. -->
|
|
210
201
|
<section
|
|
211
202
|
bind:this={el}
|
|
212
|
-
class=
|
|
203
|
+
class={['qm-card', { 'qm-main': card.isMain, 'qm-unschemable': card.unschemable }]}
|
|
213
204
|
data-qm-card
|
|
214
|
-
class:qm-main={card.isMain}
|
|
215
|
-
class:qm-unschemable={card.unschemable}
|
|
216
205
|
>
|
|
217
206
|
{#if card.unschemable}
|
|
218
207
|
{@render recoveryShell()}
|
|
@@ -253,9 +242,7 @@
|
|
|
253
242
|
<div class="qm-card-body">
|
|
254
243
|
{#if hasMeta}
|
|
255
244
|
<div
|
|
256
|
-
class=
|
|
257
|
-
class:qm-meta-top={!card.isMain}
|
|
258
|
-
class:qm-meta-bottom={card.hasBody}
|
|
245
|
+
class={['qm-card-meta', { 'qm-meta-top': !card.isMain, 'qm-meta-bottom': card.hasBody }]}
|
|
259
246
|
>
|
|
260
247
|
{#each ungrouped as section (section.group ?? '_ungrouped')}
|
|
261
248
|
<div class="qm-section">
|
|
@@ -266,14 +253,14 @@
|
|
|
266
253
|
<!-- One open at a time. Rendered only when a card has groups, so an empty
|
|
267
254
|
accordion does not stand a gap under the ungrouped fields. -->
|
|
268
255
|
{#if grouped.length}
|
|
269
|
-
<div class=
|
|
256
|
+
<div class={['qm-groups', { 'qm-instant': !animate }]}>
|
|
270
257
|
{#each grouped as section (section.group)}
|
|
271
258
|
{@const group = section.group as string}
|
|
272
259
|
{@const isOpen = expanded === group}
|
|
273
260
|
{@const panelId = ops.panelId(group)}
|
|
274
261
|
{@const headIds = section.head ? ops.domIds(section.head.name) : undefined}
|
|
275
262
|
{@const headHint = section.head?.description}
|
|
276
|
-
<div class=
|
|
263
|
+
<div class={['qm-group', { 'qm-open': isOpen }]}>
|
|
277
264
|
<!-- A flat field alone in its section is named by the header, which
|
|
278
265
|
takes its label id and parks its description: the field draws no
|
|
279
266
|
label of its own to say the section's name twice. -->
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
values are data, not input: `parseDate` throws on anything malformed, so a bad
|
|
26
26
|
string degrades to an empty field rather than taking the editor down with it.
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
parsed value: a fresh `CalendarDate` is never `===` the last one,
|
|
30
|
-
|
|
28
|
+
The local is a `$derived`, which compares by identity, so it holds the string, not
|
|
29
|
+
the parsed value: a fresh `CalendarDate` is never `===` the last one, so the
|
|
30
|
+
re-derivation each commit causes would re-render all seven segments.
|
|
31
31
|
|
|
32
32
|
The ghost is the default's digits, not a format hint. An unset field
|
|
33
33
|
carrying a `default:` prints the default's digits in the segments at the default
|
|
@@ -51,7 +51,6 @@
|
|
|
51
51
|
import { getLocalTimeZone, parseDate, today, type DateValue } from '@internationalized/date';
|
|
52
52
|
import Icon from './icons/Icon.svelte';
|
|
53
53
|
import { wording } from './strings.js';
|
|
54
|
-
import { syncedLocal } from './synced.svelte.js';
|
|
55
54
|
import './controls.css';
|
|
56
55
|
|
|
57
56
|
const t = wording();
|
|
@@ -109,31 +108,29 @@
|
|
|
109
108
|
|
|
110
109
|
const fallbackDate = $derived(toDateValue(fallback?.slice(0, 10) ?? ''));
|
|
111
110
|
/** The default is the primitive's value: from entering an unset field until an
|
|
112
|
-
* edit, a cleared segment or leaving. Part of the projection, so a
|
|
111
|
+
* edit, a cleared segment or leaving. Part of the projection, so a re-derivation
|
|
113
112
|
* neither drops a seat nor restores one over the segments a clear left. */
|
|
114
113
|
let seat = $state(false);
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const local = syncedLocal(
|
|
120
|
-
() => value?.slice(0, 10) ?? (seat && fallbackDate ? fallbackDate.toString() : '')
|
|
114
|
+
// Driven controlled (`value` + `onValueChange`, never `bind:`) so reconciliation
|
|
115
|
+
// stays the package's.
|
|
116
|
+
let local = $derived(
|
|
117
|
+
value?.slice(0, 10) ?? (seat && fallbackDate ? fallbackDate.toString() : '')
|
|
121
118
|
);
|
|
122
|
-
const parsed = $derived(toDateValue(local
|
|
119
|
+
const parsed = $derived(toDateValue(local));
|
|
123
120
|
// The date the empty segments ghost, or undefined when there is nothing to ghost:
|
|
124
121
|
// the field is unset and the default has a date form. A non-blank local that
|
|
125
122
|
// fails to parse is AUTHORED-but-malformed, which the empty field states
|
|
126
123
|
// honestly: the ghost would claim it unset. Held as the parsed value rather than
|
|
127
124
|
// a boolean so the substitution below narrows on the one fact it needs.
|
|
128
|
-
const ghost = $derived(local
|
|
129
|
-
const seated = $derived(value == null && seat && local
|
|
125
|
+
const ghost = $derived(local === '' ? fallbackDate : undefined);
|
|
126
|
+
const seated = $derived(value == null && seat && local !== '');
|
|
130
127
|
/** Who put `today` here, while the field follows the render date. */
|
|
131
128
|
const follows = $derived(
|
|
132
|
-
local
|
|
129
|
+
local === TODAY ? 'authored' : value == null && fallback === TODAY ? 'default' : undefined
|
|
133
130
|
);
|
|
134
131
|
|
|
135
132
|
function write(next: string): void {
|
|
136
|
-
local
|
|
133
|
+
local = next;
|
|
137
134
|
seat = false;
|
|
138
135
|
onCommit(next);
|
|
139
136
|
}
|
|
@@ -207,7 +204,7 @@
|
|
|
207
204
|
// seated default or `today` would write it.
|
|
208
205
|
const next = d?.toString() ?? '';
|
|
209
206
|
if (next === (parsed?.toString() ?? '')) return;
|
|
210
|
-
local
|
|
207
|
+
local = next;
|
|
211
208
|
if (!d) seat = false;
|
|
212
209
|
onCommit(d?.toString());
|
|
213
210
|
}}
|