@quillmark/svelte 0.17.0 → 0.18.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 +35 -43
- package/dist/visual/ArrayField.svelte.d.ts +0 -1
- 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 +7 -8
- 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 +8 -12
- 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';
|
|
@@ -214,7 +214,7 @@
|
|
|
214
214
|
const rowEls: Record<string, HTMLElement | undefined> = $state({});
|
|
215
215
|
// A table row's cells, on the same key: the subform a landing and the column-keeping
|
|
216
216
|
// Enter hop reach into. Every table row is open, so there is one per row where the
|
|
217
|
-
// list holds one
|
|
217
|
+
// list holds one per open row.
|
|
218
218
|
type Subform = {
|
|
219
219
|
focus: () => void;
|
|
220
220
|
focusPath: (steps: readonly PathStep[], pos?: number) => LandingBox;
|
|
@@ -226,14 +226,14 @@
|
|
|
226
226
|
delete els[id];
|
|
227
227
|
delete rowEls[id];
|
|
228
228
|
delete cellEls[id];
|
|
229
|
+
delete objEls[id];
|
|
230
|
+
open.delete(id);
|
|
229
231
|
}
|
|
230
232
|
|
|
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>();
|
|
233
|
+
// ── Object elements: each row open on its own ────────────────────────────────
|
|
234
|
+
const open = new SvelteSet<string>();
|
|
235
|
+
// An open row's subform, on the row's id: the landing below walks into it.
|
|
236
|
+
const objEls: Record<string, Subform | undefined> = $state({});
|
|
237
237
|
|
|
238
238
|
/** A collapsed row's own words, or `undefined` while it has none. */
|
|
239
239
|
function elementTitle(k: number): string | undefined {
|
|
@@ -244,12 +244,9 @@
|
|
|
244
244
|
function untitled(k: number): string {
|
|
245
245
|
return label != null ? t.strings.elementUntitled(label, k + 1) : String(k + 1);
|
|
246
246
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
holdInView(summary, () => {
|
|
251
|
-
openId = openId === id ? undefined : id;
|
|
252
|
-
});
|
|
247
|
+
function toggleRow(id: string): void {
|
|
248
|
+
if (open.has(id)) open.delete(id);
|
|
249
|
+
else open.add(id);
|
|
253
250
|
}
|
|
254
251
|
const rowName = (k: number): string | undefined =>
|
|
255
252
|
label != null ? `${label} ${k + 1}` : undefined;
|
|
@@ -300,7 +297,7 @@
|
|
|
300
297
|
onCommit(next);
|
|
301
298
|
// A row added is a row to fill in, so an object element arrives open: landing on
|
|
302
299
|
// a collapsed empty summary would make adding one a two-press gesture.
|
|
303
|
-
if (control === 'object' && !table)
|
|
300
|
+
if (control === 'object' && !table) open.add(id);
|
|
304
301
|
void focusAfterFlush(id, column);
|
|
305
302
|
}
|
|
306
303
|
function add(): void {
|
|
@@ -319,9 +316,6 @@
|
|
|
319
316
|
const next = ids.filter((_, i) => i !== k);
|
|
320
317
|
ids = next;
|
|
321
318
|
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
319
|
onCommit(arr.filter((_, i) => i !== k));
|
|
326
320
|
void focusAfterFlush(next[Math.max(k - 1, 0)], column, fromRemove);
|
|
327
321
|
}
|
|
@@ -370,7 +364,8 @@
|
|
|
370
364
|
/** An object row's landing: inside the subform when that row is the open one, on
|
|
371
365
|
* the row's own summary otherwise — a collapsed row's control is its summary. */
|
|
372
366
|
function focusObjectRow(id: string): void {
|
|
373
|
-
|
|
367
|
+
const sub = objEls[id];
|
|
368
|
+
if (open.has(id) && sub) return sub.focus();
|
|
374
369
|
rowEls[id]?.querySelector<HTMLElement>('.qm-element-summary')?.focus();
|
|
375
370
|
}
|
|
376
371
|
/** The box an arrival wash blooms in (`leaves.ts`, `core/bloom.ts`): the elements,
|
|
@@ -413,10 +408,11 @@
|
|
|
413
408
|
return rowEls[id];
|
|
414
409
|
}
|
|
415
410
|
if (control === 'object') {
|
|
416
|
-
|
|
411
|
+
open.add(id);
|
|
417
412
|
return (async () => {
|
|
418
413
|
if (!(await span.resumes(tick()))) return undefined;
|
|
419
|
-
|
|
414
|
+
const sub = objEls[id];
|
|
415
|
+
if (rest.length && sub) return (await sub.focusPath(rest, pos)) ?? rowEls[id];
|
|
420
416
|
focusObjectRow(id);
|
|
421
417
|
return rowEls[id];
|
|
422
418
|
})();
|
|
@@ -539,8 +535,7 @@
|
|
|
539
535
|
</script>
|
|
540
536
|
|
|
541
537
|
<div
|
|
542
|
-
class=
|
|
543
|
-
class:empty={ids.length === 0}
|
|
538
|
+
class={['qm-array', { empty: ids.length === 0 }]}
|
|
544
539
|
role="group"
|
|
545
540
|
aria-labelledby={label != null ? labelId : undefined}
|
|
546
541
|
aria-describedby={description ? descriptionId : undefined}
|
|
@@ -579,8 +574,7 @@
|
|
|
579
574
|
each block's one child: a branch inside the block would stand between them. -->
|
|
580
575
|
{#if table}
|
|
581
576
|
<div
|
|
582
|
-
class=
|
|
583
|
-
class:empty={ids.length === 0}
|
|
577
|
+
class={['qm-array-rows qm-array-table', { empty: ids.length === 0 }]}
|
|
584
578
|
data-default={defaulted ? '' : undefined}
|
|
585
579
|
bind:this={rowsEl}
|
|
586
580
|
>
|
|
@@ -592,9 +586,10 @@
|
|
|
592
586
|
<div class="qm-array-table-head">
|
|
593
587
|
{#each columns as [key, sub] (key)}
|
|
594
588
|
<div
|
|
595
|
-
class=
|
|
596
|
-
|
|
597
|
-
|
|
589
|
+
class={[
|
|
590
|
+
'qm-array-table-col',
|
|
591
|
+
{ mark: controlKind(sub) === 'boolean', prose: controlKind(sub) === 'prose' }
|
|
592
|
+
]}
|
|
598
593
|
>
|
|
599
594
|
<FieldLabel
|
|
600
595
|
label={columnTitle(key, sub)}
|
|
@@ -625,8 +620,7 @@
|
|
|
625
620
|
</div>
|
|
626
621
|
{:else if control === 'object'}
|
|
627
622
|
<div
|
|
628
|
-
class=
|
|
629
|
-
class:empty={ids.length === 0}
|
|
623
|
+
class={['qm-array-rows', { empty: ids.length === 0 }]}
|
|
630
624
|
data-default={defaulted ? '' : undefined}
|
|
631
625
|
bind:this={rowsEl}
|
|
632
626
|
>
|
|
@@ -635,8 +629,7 @@
|
|
|
635
629
|
<!-- The handler catches a key from the controls inside the row and adds no
|
|
636
630
|
interaction of the row's own: the row is no tab stop. -->
|
|
637
631
|
<div
|
|
638
|
-
class=
|
|
639
|
-
class:open={openId === id}
|
|
632
|
+
class={['qm-array-row qm-element', { open: open.has(id) }]}
|
|
640
633
|
bind:this={rowEls[id]}
|
|
641
634
|
animate:reorder={arm.armed}
|
|
642
635
|
onkeydown={(e) => onRowKey(e, k)}
|
|
@@ -649,8 +642,8 @@
|
|
|
649
642
|
<button
|
|
650
643
|
type="button"
|
|
651
644
|
class="qm-control-box qm-focus-ring qm-element-summary"
|
|
652
|
-
aria-expanded={
|
|
653
|
-
onclick={(
|
|
645
|
+
aria-expanded={open.has(id)}
|
|
646
|
+
onclick={() => toggleRow(id)}
|
|
654
647
|
>
|
|
655
648
|
<!-- Leading, and it rotates: trailing is the figure for pushing a new
|
|
656
649
|
screen, where this unfolds in place. Same glyph, same rotation and
|
|
@@ -666,9 +659,9 @@
|
|
|
666
659
|
{@render rowActions(k)}
|
|
667
660
|
</div>
|
|
668
661
|
</div>
|
|
669
|
-
{#if
|
|
662
|
+
{#if open.has(id)}
|
|
670
663
|
<ObjectField
|
|
671
|
-
bind:this={
|
|
664
|
+
bind:this={objEls[id]}
|
|
672
665
|
value={(arr[k] ?? {}) as Record<string, unknown>}
|
|
673
666
|
properties={items?.properties}
|
|
674
667
|
label={rowName(k)}
|
|
@@ -687,8 +680,7 @@
|
|
|
687
680
|
</div>
|
|
688
681
|
{:else}
|
|
689
682
|
<div
|
|
690
|
-
class=
|
|
691
|
-
class:empty={ids.length === 0}
|
|
683
|
+
class={['qm-array-rows', { empty: ids.length === 0 }]}
|
|
692
684
|
data-default={defaulted ? '' : undefined}
|
|
693
685
|
bind:this={rowsEl}
|
|
694
686
|
>
|
|
@@ -2,7 +2,6 @@ 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
6
|
value: unknown[] | undefined;
|
|
8
7
|
/** The declared `default:`: the rows an unset array draws, which a gesture takes. */
|
|
@@ -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
|
}}
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
<script lang="ts">
|
|
16
16
|
import { Select } from 'bits-ui';
|
|
17
17
|
import Icon from './icons/Icon.svelte';
|
|
18
|
-
import { syncedLocal } from './synced.svelte.js';
|
|
19
18
|
import { wording } from './strings.js';
|
|
20
19
|
import './controls.css';
|
|
21
20
|
|
|
@@ -67,23 +66,21 @@
|
|
|
67
66
|
// the like), so it never collides with a real option.
|
|
68
67
|
const UNSET = '__qm_unset__';
|
|
69
68
|
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
// the package's rather than the primitive's.
|
|
74
|
-
const local = syncedLocal(() => value ?? UNSET);
|
|
69
|
+
// Driven controlled (`value` + `onValueChange`, never `bind:`) so reconciliation
|
|
70
|
+
// stays the package's rather than the primitive's.
|
|
71
|
+
let local = $derived(value ?? UNSET);
|
|
75
72
|
|
|
76
73
|
/** The blank has no glyph of its own: the quill's `ui.blank_title` stands in for
|
|
77
74
|
* it, else an em dash. */
|
|
78
75
|
const dash = (v: string | undefined) => v || blankTitle || '—';
|
|
79
76
|
|
|
80
|
-
const unset = $derived(local
|
|
77
|
+
const unset = $derived(local === UNSET);
|
|
81
78
|
/** The ghost is a member the render prints, rather than a word for the blank or for
|
|
82
79
|
* `none`: it draws at the default rung (theme.css). */
|
|
83
80
|
const ghostsMember = $derived(!optional && !!fallback);
|
|
84
81
|
const ghostText = $derived(optional ? t.strings.optionalGhost : dash(fallback));
|
|
85
82
|
/** What the closed trigger shows: the pick, or the ghosted default while unset. */
|
|
86
|
-
const shown = $derived(unset ? ghostText : dash(local
|
|
83
|
+
const shown = $derived(unset ? ghostText : dash(local));
|
|
87
84
|
|
|
88
85
|
/** The root to portal into: `document.body` would escape the consumer's dials
|
|
89
86
|
* along with the editor's subtree. `undefined` falls back to bits-ui's default. */
|
|
@@ -99,10 +96,10 @@
|
|
|
99
96
|
<Select.Root
|
|
100
97
|
type="single"
|
|
101
98
|
allowDeselect={false}
|
|
102
|
-
value={local
|
|
99
|
+
value={local}
|
|
103
100
|
onValueChange={(v) => {
|
|
104
101
|
if (v == null) return;
|
|
105
|
-
local
|
|
102
|
+
local = v;
|
|
106
103
|
// Sentinel → unset (parent `removeField`, default renders); any real pick
|
|
107
104
|
// (incl. the default value) → a genuine write.
|
|
108
105
|
onCommit(v === UNSET ? undefined : v);
|
|
@@ -152,12 +149,12 @@
|
|
|
152
149
|
policy when it is the one selected: a listbox whose selected value
|
|
153
150
|
has no row shows nothing for what the document says, and offers none
|
|
154
151
|
for the primitive to mark, type-ahead to, or key onto
|
|
155
|
-
(VISUAL_EDITOR §"Enum policy"). Off `local
|
|
152
|
+
(VISUAL_EDITOR §"Enum policy"). Off `local`, not `value`: the
|
|
156
153
|
row that exists is the row the control has selected, through the
|
|
157
154
|
window where an own pick has not reconciled back. -->
|
|
158
155
|
{#each values as v (v)}
|
|
159
156
|
{@const allowed = optionAllowed?.(v) !== false}
|
|
160
|
-
{#if allowed || enumDisallowed === 'disable' || v === local
|
|
157
|
+
{#if allowed || enumDisallowed === 'disable' || v === local}
|
|
161
158
|
<Select.Item
|
|
162
159
|
class="qm-menu-item qm-select-item"
|
|
163
160
|
value={v}
|