@quillmark/svelte 0.16.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 +4 -2
- package/dist/core/codec/index.d.ts +0 -3
- package/dist/core/rebind.svelte.js +5 -8
- package/dist/core/theme.css +12 -1
- 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 +44 -41
- package/dist/visual/ArrayField.svelte.d.ts +0 -1
- package/dist/visual/BooleanField.svelte +16 -14
- package/dist/visual/BooleanField.svelte.d.ts +0 -1
- package/dist/visual/Card.svelte +65 -35
- 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 +17 -10
- package/dist/visual/Field.svelte.d.ts +3 -0
- 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 +210 -0
- package/dist/visual/MatrixAdd.svelte.d.ts +15 -0
- package/dist/visual/MatrixField.svelte +329 -84
- package/dist/visual/MatrixField.svelte.d.ts +8 -6
- package/dist/visual/NumberField.svelte +6 -10
- package/dist/visual/NumberField.svelte.d.ts +0 -1
- package/dist/visual/ObjectField.svelte +30 -17
- package/dist/visual/ObjectField.svelte.d.ts +3 -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 +21 -14
- package/dist/visual/VisualEditorInner.svelte.d.ts +0 -1
- package/dist/visual/checklists.d.ts +82 -0
- package/dist/visual/checklists.js +171 -0
- package/dist/visual/index.d.ts +0 -1
- package/dist/visual/strings.d.ts +15 -1
- package/dist/visual/strings.js +6 -0
- package/dist/visual/structure.d.ts +63 -31
- package/dist/visual/structure.js +122 -44
- 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
|
|
|
@@ -46,6 +46,8 @@ const session = await new Engine().open(quill, doc);
|
|
|
46
46
|
// free() on teardown the handles you MINTED: this quill, this doc, this session.
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
`<VisualEditor>` needs no session, and the first `open` loads the render build, so a host that mounts the editor over `doc` and `quill` first shows it without waiting on that load. The session opens behind it: `await engine.load(quill)`, then `engine.open(quill, doc)`, so its first compile reads what was typed meanwhile ([HOSTING.md](prose/canon/HOSTING.md)).
|
|
50
|
+
|
|
49
51
|
## The quill resolves from the document
|
|
50
52
|
|
|
51
53
|
A stored document carries none of its quill's bytes, only a reference: `doc.quillRef` is `name@version`, persisted in the markdown itself. Resolution is **host code**: read the ref, map it to a `Quill`, open. No surface resolves, so one resolution per document holds by construction.
|
|
@@ -58,7 +60,7 @@ const session = await new Engine().open(quill, doc);
|
|
|
58
60
|
|
|
59
61
|
`@quillmark/quiver` is one such registry, and the quill it hands back is **borrowed**: it is cached per ref and shared with every caller, so freeing it strands the next one.
|
|
60
62
|
|
|
61
|
-
Opening a document that names a **different** quill is the same sequence, in order: resolve the new ref,
|
|
63
|
+
Opening a document that names a **different** quill is the same sequence, in order: resolve the new ref, swap the editor's props, free the replaced handles that were yours to free, then open the next session behind the editor. `<VisualEditor>` re-keys itself on the new `doc` (see below); `<Preview>` swaps by remount (`{#key session}`).
|
|
62
64
|
|
|
63
65
|
## What a document is trusted to be
|
|
64
66
|
|
|
@@ -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
|
package/dist/core/theme.css
CHANGED
|
@@ -316,7 +316,14 @@
|
|
|
316
316
|
for the type half. Asymmetric because the vertical is what leading leaves once
|
|
317
317
|
it has taken the line box, and the horizontal is how far in the text starts,
|
|
318
318
|
which nothing else fixes. */
|
|
319
|
-
--_qm-inset-control: var(--_qm-space)
|
|
319
|
+
--_qm-inset-control-block: var(--_qm-space);
|
|
320
|
+
--_qm-inset-control: var(--_qm-inset-control-block) var(--_qm-space-3);
|
|
321
|
+
/* A box's first line, drawn: its line box and the inset over and under it. What a
|
|
322
|
+
row of boxes lines up on however far one of them wraps, and so what a mark with no
|
|
323
|
+
line of its own centres on to stand in that row (`BooleanField`). */
|
|
324
|
+
--_qm-box-line: calc(
|
|
325
|
+
var(--_qm-text-body) * var(--_qm-leading-body) + 2 * var(--_qm-inset-control-block)
|
|
326
|
+
);
|
|
320
327
|
/* The nesting step: the distance from one structural stroke to the next, and what
|
|
321
328
|
makes the depths read as a ladder rather than as indents that happen to look
|
|
322
329
|
alike. A rung because the boxes holding that distance are in two components and
|
|
@@ -349,6 +356,10 @@
|
|
|
349
356
|
name it and no label of its own to fit. A threshold rather than a rhythm choice,
|
|
350
357
|
so it does not move with `--qm-space`. */
|
|
351
358
|
--_qm-track-min: 7rem;
|
|
359
|
+
/* A prose column's floor: its cell wraps, and at `--_qm-track-min` a line holds a
|
|
360
|
+
word, standing the row many lines tall beside cells one line tall. A multiple of
|
|
361
|
+
the floor, so the two move together. */
|
|
362
|
+
--_qm-track-prose: calc(var(--_qm-track-min) * 2);
|
|
352
363
|
|
|
353
364
|
/* ── Type ────────────────────────────────────────────────────────────────
|
|
354
365
|
`--qm-font-size` anchors the chrome rung; 1.125 is the ratio between adjacent
|
|
@@ -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
|
>
|
|
@@ -591,7 +585,12 @@
|
|
|
591
585
|
column's rather than any one cell's. -->
|
|
592
586
|
<div class="qm-array-table-head">
|
|
593
587
|
{#each columns as [key, sub] (key)}
|
|
594
|
-
<div
|
|
588
|
+
<div
|
|
589
|
+
class={[
|
|
590
|
+
'qm-array-table-col',
|
|
591
|
+
{ mark: controlKind(sub) === 'boolean', prose: controlKind(sub) === 'prose' }
|
|
592
|
+
]}
|
|
593
|
+
>
|
|
595
594
|
<FieldLabel
|
|
596
595
|
label={columnTitle(key, sub)}
|
|
597
596
|
required={obliged(sub)}
|
|
@@ -621,8 +620,7 @@
|
|
|
621
620
|
</div>
|
|
622
621
|
{:else if control === 'object'}
|
|
623
622
|
<div
|
|
624
|
-
class=
|
|
625
|
-
class:empty={ids.length === 0}
|
|
623
|
+
class={['qm-array-rows', { empty: ids.length === 0 }]}
|
|
626
624
|
data-default={defaulted ? '' : undefined}
|
|
627
625
|
bind:this={rowsEl}
|
|
628
626
|
>
|
|
@@ -631,8 +629,7 @@
|
|
|
631
629
|
<!-- The handler catches a key from the controls inside the row and adds no
|
|
632
630
|
interaction of the row's own: the row is no tab stop. -->
|
|
633
631
|
<div
|
|
634
|
-
class=
|
|
635
|
-
class:open={openId === id}
|
|
632
|
+
class={['qm-array-row qm-element', { open: open.has(id) }]}
|
|
636
633
|
bind:this={rowEls[id]}
|
|
637
634
|
animate:reorder={arm.armed}
|
|
638
635
|
onkeydown={(e) => onRowKey(e, k)}
|
|
@@ -645,8 +642,8 @@
|
|
|
645
642
|
<button
|
|
646
643
|
type="button"
|
|
647
644
|
class="qm-control-box qm-focus-ring qm-element-summary"
|
|
648
|
-
aria-expanded={
|
|
649
|
-
onclick={(
|
|
645
|
+
aria-expanded={open.has(id)}
|
|
646
|
+
onclick={() => toggleRow(id)}
|
|
650
647
|
>
|
|
651
648
|
<!-- Leading, and it rotates: trailing is the figure for pushing a new
|
|
652
649
|
screen, where this unfolds in place. Same glyph, same rotation and
|
|
@@ -662,9 +659,9 @@
|
|
|
662
659
|
{@render rowActions(k)}
|
|
663
660
|
</div>
|
|
664
661
|
</div>
|
|
665
|
-
{#if
|
|
662
|
+
{#if open.has(id)}
|
|
666
663
|
<ObjectField
|
|
667
|
-
bind:this={
|
|
664
|
+
bind:this={objEls[id]}
|
|
668
665
|
value={(arr[k] ?? {}) as Record<string, unknown>}
|
|
669
666
|
properties={items?.properties}
|
|
670
667
|
label={rowName(k)}
|
|
@@ -683,8 +680,7 @@
|
|
|
683
680
|
</div>
|
|
684
681
|
{:else}
|
|
685
682
|
<div
|
|
686
|
-
class=
|
|
687
|
-
class:empty={ids.length === 0}
|
|
683
|
+
class={['qm-array-rows', { empty: ids.length === 0 }]}
|
|
688
684
|
data-default={defaulted ? '' : undefined}
|
|
689
685
|
bind:this={rowsEl}
|
|
690
686
|
>
|
|
@@ -1044,6 +1040,9 @@
|
|
|
1044
1040
|
.qm-array-table-col.mark {
|
|
1045
1041
|
min-width: 0;
|
|
1046
1042
|
}
|
|
1043
|
+
.qm-array-table-col.prose {
|
|
1044
|
+
min-width: var(--_qm-track-prose);
|
|
1045
|
+
}
|
|
1047
1046
|
/* The remove is the row's last track, pinned to the end edge of the box the table
|
|
1048
1047
|
scrolls in: sticky, so it rests in its track while the table fits and rides over
|
|
1049
1048
|
the cells once the box scrolls, on the card's plane so what passes under it is
|
|
@@ -1066,8 +1065,12 @@
|
|
|
1066
1065
|
background: var(--_qm-surface);
|
|
1067
1066
|
box-shadow: 0 0 0 var(--_qm-ring-reach) var(--_qm-surface);
|
|
1068
1067
|
}
|
|
1068
|
+
/* The glyph stands on the row's first line with the cells, however far the slab
|
|
1069
|
+
stretches under it. */
|
|
1069
1070
|
.qm-array-table-row > .qm-remove {
|
|
1070
1071
|
align-self: stretch;
|
|
1072
|
+
align-items: start;
|
|
1073
|
+
padding-block-start: calc((var(--_qm-box-line) - var(--_qm-glyph-control)) / 2);
|
|
1071
1074
|
height: auto;
|
|
1072
1075
|
border-radius: var(--_qm-radius-inner);
|
|
1073
1076
|
color: var(--_qm-ink-label);
|
|
@@ -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
|
>
|
|
@@ -74,6 +69,13 @@
|
|
|
74
69
|
</span>
|
|
75
70
|
|
|
76
71
|
<style>
|
|
72
|
+
/* The switch stands on the first line of the boxes beside it: a row of controls tops
|
|
73
|
+
out together, and a mark carries no line of its own to sit on. */
|
|
74
|
+
.qm-toggle-wrap {
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
min-height: var(--_qm-box-line);
|
|
78
|
+
}
|
|
77
79
|
/* A primitive renders its own element, which a scoped selector cannot reach:
|
|
78
80
|
styled through the wrapper with `:global`. */
|
|
79
81
|
/* The track is the drawn box and the target is `.qm-tap-floor`'s (controls.css): a
|