@quillmark/svelte 0.20.1 → 0.22.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/NOTICE +6 -7
- package/README.md +1 -1
- package/THEMING.md +1 -1
- package/dist/core/address.d.ts +6 -1
- package/dist/core/address.js +12 -3
- package/dist/core/bloom.d.ts +9 -4
- package/dist/core/bloom.js +18 -49
- package/dist/core/codec/atoms.d.ts +1 -1
- package/dist/core/codec/atoms.js +26 -11
- package/dist/core/codec/blocks.d.ts +5 -5
- package/dist/core/codec/blocks.js +27 -8
- package/dist/core/codec/code.d.ts +4 -4
- package/dist/core/codec/code.js +52 -22
- package/dist/core/codec/decode.d.ts +5 -5
- package/dist/core/codec/decode.js +18 -12
- package/dist/core/codec/encode.js +6 -1
- package/dist/core/codec/field.d.ts +22 -15
- package/dist/core/codec/field.js +66 -26
- package/dist/core/codec/index.d.ts +1 -1
- package/dist/core/codec/index.js +2 -2
- package/dist/core/codec/inputrules.d.ts +4 -0
- package/dist/core/codec/inputrules.js +44 -6
- package/dist/core/codec/link-paste.d.ts +11 -0
- package/dist/core/codec/link-paste.js +32 -0
- package/dist/core/codec/lists.d.ts +5 -4
- package/dist/core/codec/lists.js +87 -31
- package/dist/core/codec/nesting.d.ts +20 -0
- package/dist/core/codec/nesting.js +62 -0
- package/dist/core/codec/prose.css +145 -63
- package/dist/core/codec/schema.d.ts +2 -2
- package/dist/core/codec/schema.js +71 -18
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-copy.d.ts +17 -0
- package/dist/core/codec/table-copy.js +84 -0
- package/dist/core/codec/table-view.d.ts +6 -7
- package/dist/core/codec/table-view.js +281 -201
- package/dist/core/codec/table.d.ts +2 -9
- package/dist/core/codec/table.js +10 -19
- package/dist/core/codec/tasks.d.ts +19 -0
- package/dist/core/codec/tasks.js +140 -0
- package/dist/core/errors.d.ts +2 -2
- package/dist/core/rungs.d.ts +6 -0
- package/dist/core/rungs.js +11 -0
- package/dist/core/theme.css +24 -12
- package/dist/visual/ArrayField.svelte +236 -78
- package/dist/visual/ArrayField.svelte.d.ts +0 -2
- package/dist/visual/BooleanField.svelte +12 -1
- package/dist/visual/Card.svelte +41 -23
- package/dist/visual/Card.svelte.d.ts +3 -2
- package/dist/visual/CardControls.svelte +15 -15
- package/dist/visual/DateField.svelte +93 -64
- package/dist/visual/EnumField.svelte +9 -4
- package/dist/visual/Field.svelte +20 -16
- package/dist/visual/Field.svelte.d.ts +4 -2
- package/dist/visual/FieldHint.svelte +22 -5
- package/dist/visual/FieldLabel.svelte +5 -33
- package/dist/visual/FieldLabel.svelte.d.ts +0 -2
- package/dist/visual/FormatPopover.svelte +78 -81
- package/dist/visual/MatrixField.svelte +28 -19
- package/dist/visual/NumberField.svelte +32 -3
- package/dist/visual/ObjectField.svelte +40 -24
- package/dist/visual/ObjectField.svelte.d.ts +6 -0
- package/dist/visual/ProseField.svelte +10 -13
- package/dist/visual/ProseField.svelte.d.ts +5 -2
- package/dist/visual/ProseValue.svelte +25 -19
- package/dist/visual/ProseValue.svelte.d.ts +4 -4
- package/dist/visual/TextField.svelte +3 -2
- package/dist/visual/TipsCard.svelte +7 -7
- package/dist/visual/VariantField.svelte +1 -1
- package/dist/visual/VariantField.svelte.d.ts +1 -1
- package/dist/visual/VisualEditorInner.svelte +121 -39
- package/dist/visual/controls.css +21 -2
- package/dist/visual/icons/Icon.svelte +3 -2
- package/dist/visual/icons/Icon.svelte.d.ts +3 -2
- package/dist/visual/icons/nodes.d.ts +25 -21
- package/dist/visual/icons/nodes.js +9 -7
- package/dist/visual/leaves.d.ts +12 -9
- package/dist/visual/leaves.js +19 -0
- package/dist/visual/motion.d.ts +25 -3
- package/dist/visual/motion.js +58 -9
- package/dist/visual/strings.d.ts +15 -9
- package/dist/visual/strings.js +19 -12
- package/dist/visual/structure.d.ts +21 -31
- package/dist/visual/structure.js +54 -51
- package/package.json +3 -3
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
An `array` field → an add/remove repeater. Elements commit by value: every
|
|
3
3
|
edit / add / remove / move rebuilds the whole array and hands it to the parent's typed
|
|
4
4
|
`writer.set(field, wholeArray)` (arrays are not op-addressed). Element control
|
|
5
|
-
by `items.type`: `richtext` / `
|
|
5
|
+
by `items.type`: `richtext` / `text` → a prose element
|
|
6
6
|
({@link ProseValue}), `object` → a summary row that opens onto a subform, or a
|
|
7
7
|
table's row where the array asks for one and the cells are short, everything else →
|
|
8
8
|
a text input. The add chip sits in the label header row (space-between with the field
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
container or a block prose cell.
|
|
30
30
|
|
|
31
31
|
The row's controls are inside the element: a slab over the end of the element's own
|
|
32
|
-
box, taking its two end-side corners
|
|
33
|
-
|
|
32
|
+
box, taking its two end-side corners: the remove. An `object` row's drag handle hangs
|
|
33
|
+
off its start edge instead, as a table grip does. So a row's box is the element's box, and an array's rows end where
|
|
34
34
|
every other field's control does. On an object element that box is the summary rather
|
|
35
35
|
than the open row: a destructive control belongs to the line it sits on, not to
|
|
36
36
|
everything that line has unfolded. A table's row carries the remove alone, in its last
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
Keys carry the list without the mouse: Enter inserts a sibling below and takes the
|
|
42
42
|
caret there, Backspace on an empty element removes it and hands focus back up the
|
|
43
43
|
list, Alt+↑/↓ anywhere in an `object` row of the list moves it, the nearest row
|
|
44
|
-
answering and never the record around a nested one
|
|
44
|
+
answering and never the record around a nested one, and is the twin of the handle's
|
|
45
|
+
drag. A move is one splice of the ids
|
|
45
46
|
and of the values together, the mechanism insert and remove use, so an element keeps
|
|
46
47
|
its id for life and no prose leaf inside it remounts; the open row stays open across
|
|
47
48
|
its own move, and `animate:reorder` holds the moving row as it holds a card.
|
|
@@ -70,7 +71,7 @@
|
|
|
70
71
|
controlKind,
|
|
71
72
|
declaredContent,
|
|
72
73
|
humanize,
|
|
73
|
-
|
|
74
|
+
literalText,
|
|
74
75
|
rowSummary,
|
|
75
76
|
schemaAt,
|
|
76
77
|
type ArrayLayout
|
|
@@ -104,8 +105,6 @@
|
|
|
104
105
|
max?: number;
|
|
105
106
|
/** Accessible-name prefix for the element controls (`label` + 1-based index). */
|
|
106
107
|
label?: string;
|
|
107
|
-
/** No-default field → a persistent required `*` on the label. */
|
|
108
|
-
required?: boolean;
|
|
109
108
|
/** Schema `description`: the label's help affordance. */
|
|
110
109
|
description?: string;
|
|
111
110
|
/** The label's own DOM id. An array is a group (N inputs, no single `for`
|
|
@@ -139,7 +138,6 @@
|
|
|
139
138
|
layout = 'list',
|
|
140
139
|
max,
|
|
141
140
|
label,
|
|
142
|
-
required,
|
|
143
141
|
description,
|
|
144
142
|
labelId,
|
|
145
143
|
descriptionId,
|
|
@@ -328,15 +326,13 @@
|
|
|
328
326
|
void focusAfterFlush(next[Math.max(k - 1, 0)], column, fromRemove);
|
|
329
327
|
}
|
|
330
328
|
/**
|
|
331
|
-
* Move element `k`
|
|
332
|
-
* the element keeps its id and everything mounted under it. A no-op
|
|
333
|
-
* The control that
|
|
334
|
-
* flush has moved it: a node moved in the DOM is a node the browser blurred.
|
|
335
|
-
* the move carried to its edge is disabled and takes no focus, so its twin does.
|
|
329
|
+
* Move element `k` to slot `to`: one splice of the ids and of the values together, so
|
|
330
|
+
* the element keeps its id and everything mounted under it. A no-op outside the list.
|
|
331
|
+
* The control that held the focus rides with the row, so it is focused again once the
|
|
332
|
+
* flush has moved it: a node moved in the DOM is a node the browser blurred.
|
|
336
333
|
*/
|
|
337
|
-
function move(k: number,
|
|
338
|
-
|
|
339
|
-
if (to < 0 || to >= ids.length) return;
|
|
334
|
+
function move(k: number, to: number): void {
|
|
335
|
+
if (to === k || to < 0 || to >= ids.length) return;
|
|
340
336
|
const pressed = document.activeElement;
|
|
341
337
|
arm.arm();
|
|
342
338
|
const nextIds = ids.slice();
|
|
@@ -349,14 +345,8 @@
|
|
|
349
345
|
onCommit(next);
|
|
350
346
|
void (async () => {
|
|
351
347
|
if (!(await span.resumes(tick()))) return;
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
pressed instanceof HTMLButtonElement && pressed.disabled
|
|
355
|
-
? [pressed.previousElementSibling, pressed.nextElementSibling].find(
|
|
356
|
-
(b) => b instanceof HTMLButtonElement && !b.disabled
|
|
357
|
-
)
|
|
358
|
-
: pressed;
|
|
359
|
-
if (target instanceof HTMLElement) target.focus();
|
|
348
|
+
if (pressed instanceof HTMLElement && pressed.isConnected && rowEls[id]?.contains(pressed))
|
|
349
|
+
pressed.focus();
|
|
360
350
|
})();
|
|
361
351
|
}
|
|
362
352
|
/** Take the caret: the first element, or the add affordance when the list is empty;
|
|
@@ -526,9 +516,10 @@
|
|
|
526
516
|
* any cell an open row holds: Alt+↑/↓, the table island's own binding. The nearest
|
|
527
517
|
* row answers, or none does: a press inside a nested row is that row's whether it
|
|
528
518
|
* holds a reorder or not, so a key in a table's cell or a list of strings' input
|
|
529
|
-
* moves nothing, and never the record around it.
|
|
519
|
+
* moves nothing, and never the record around it. A held drag owns the row until its
|
|
520
|
+
* release. */
|
|
530
521
|
function onRowKey(e: KeyboardEvent, k: number): void {
|
|
531
|
-
if (!e.altKey || e.shiftKey || e.ctrlKey || e.metaKey || e.isComposing) return;
|
|
522
|
+
if (!e.altKey || e.shiftKey || e.ctrlKey || e.metaKey || e.isComposing || drag) return;
|
|
532
523
|
// A cell that took the key — a select opening, a date segment stepping, a prose
|
|
533
524
|
// leaf joining — owns it.
|
|
534
525
|
if (e.defaultPrevented) return;
|
|
@@ -538,7 +529,111 @@
|
|
|
538
529
|
const dir = e.key === 'ArrowUp' ? -1 : 1;
|
|
539
530
|
if (k + dir < 0 || k + dir >= ids.length) return;
|
|
540
531
|
e.preventDefault();
|
|
541
|
-
move(k, dir);
|
|
532
|
+
move(k, k + dir);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ── The drag handle ──────────────────────────────────────────────────────────
|
|
536
|
+
// Press-and-drag the handle moves its row; a press that does not travel past the dead
|
|
537
|
+
// zone does nothing. The drop is the row under the pointer, read off the rows where
|
|
538
|
+
// they stand, so a scroll by any hand re-picks it. Held in an edge band of its
|
|
539
|
+
// scrollport, the drag scrolls it toward the rows out of view, once the pointer has
|
|
540
|
+
// been outside the band: a press on a row already in the band does not scroll. Nothing
|
|
541
|
+
// reflows until the release (the lift is a tone and the drop rule is out of flow), and
|
|
542
|
+
// the release is the one splice `move` makes. A handle that loses the pointer, or
|
|
543
|
+
// leaves the DOM, ends the drag with no move.
|
|
544
|
+
const DEAD_ZONE = 3;
|
|
545
|
+
/** The band at a scrollport's top and bottom edges that scrolls it under a held drag,
|
|
546
|
+
* and the most one frame scrolls, reached at the edge itself. */
|
|
547
|
+
const EDGE = 48;
|
|
548
|
+
const STEP = 16;
|
|
549
|
+
type RowDrag = { k: number; origin: number; y: number; engaged: boolean; drop: number };
|
|
550
|
+
let drag = $state<RowDrag | undefined>();
|
|
551
|
+
let follow: { grip: Element; port: Element; armed: boolean; frame: number } | undefined;
|
|
552
|
+
function onHandleDown(e: PointerEvent, k: number): void {
|
|
553
|
+
if (e.button !== 0) return;
|
|
554
|
+
e.preventDefault();
|
|
555
|
+
release();
|
|
556
|
+
const grip = e.currentTarget as HTMLElement;
|
|
557
|
+
grip.setPointerCapture?.(e.pointerId);
|
|
558
|
+
drag = { k, origin: e.clientY, y: e.clientY, engaged: false, drop: k };
|
|
559
|
+
follow = { grip, port: scrollport(grip), armed: false, frame: 0 };
|
|
560
|
+
}
|
|
561
|
+
function onHandleMove(e: PointerEvent): void {
|
|
562
|
+
if (!drag) return;
|
|
563
|
+
drag.y = e.clientY;
|
|
564
|
+
if (!drag.engaged) {
|
|
565
|
+
if (Math.abs(e.clientY - drag.origin) < DEAD_ZONE) return;
|
|
566
|
+
drag.engaged = true;
|
|
567
|
+
addEventListener('scroll', repick, { capture: true, passive: true });
|
|
568
|
+
if (follow) follow.frame = requestAnimationFrame(edgeScroll);
|
|
569
|
+
}
|
|
570
|
+
repick();
|
|
571
|
+
}
|
|
572
|
+
function repick(): void {
|
|
573
|
+
if (drag?.engaged) drag.drop = dropIndex(drag.k, drag.y);
|
|
574
|
+
}
|
|
575
|
+
const rowBox = (i: number): DOMRect | undefined => rowEls[ids[i]]?.getBoundingClientRect();
|
|
576
|
+
/** The row the pointer is over, or the nearest one past either end. */
|
|
577
|
+
function dropIndex(k: number, y: number): number {
|
|
578
|
+
let best = k;
|
|
579
|
+
let nearest = Infinity;
|
|
580
|
+
ids.forEach((_, i) => {
|
|
581
|
+
const box = rowBox(i);
|
|
582
|
+
if (!box) return;
|
|
583
|
+
const gap = y < box.top ? box.top - y : y > box.bottom ? y - box.bottom : 0;
|
|
584
|
+
if (gap < nearest) {
|
|
585
|
+
nearest = gap;
|
|
586
|
+
best = i;
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
return best;
|
|
590
|
+
}
|
|
591
|
+
const page = (): Element => document.scrollingElement ?? document.documentElement;
|
|
592
|
+
/** The nearest ancestor that scrolls vertically, else the page. */
|
|
593
|
+
function scrollport(from: Element): Element {
|
|
594
|
+
for (let el = from.parentElement; el; el = el.parentElement) {
|
|
595
|
+
const { overflowY } = getComputedStyle(el);
|
|
596
|
+
if (/auto|scroll/.test(overflowY) && el.scrollHeight > el.clientHeight) return el;
|
|
597
|
+
}
|
|
598
|
+
return page();
|
|
599
|
+
}
|
|
600
|
+
function edgeScroll(): void {
|
|
601
|
+
if (!follow || !drag?.engaged) return;
|
|
602
|
+
if (!follow.grip.isConnected) {
|
|
603
|
+
drag = undefined;
|
|
604
|
+
release();
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const { port } = follow;
|
|
608
|
+
const box = port === page() ? undefined : port.getBoundingClientRect();
|
|
609
|
+
const top = Math.max(box?.top ?? 0, 0);
|
|
610
|
+
const bottom = Math.min(box?.bottom ?? innerHeight, innerHeight);
|
|
611
|
+
const into = Math.min(drag.y - top - EDGE, 0) + Math.max(drag.y - bottom + EDGE, 0);
|
|
612
|
+
if (!into) follow.armed = true;
|
|
613
|
+
// Only toward rows still out of view: past the list's end there is nothing to reach.
|
|
614
|
+
const beyond =
|
|
615
|
+
into < 0 ? (rowBox(0)?.top ?? top) < top : (rowBox(ids.length - 1)?.bottom ?? 0) > bottom;
|
|
616
|
+
if (into && follow.armed && beyond)
|
|
617
|
+
port.scrollBy({ top: STEP * Math.max(-1, Math.min(1, into / EDGE)), behavior: 'instant' });
|
|
618
|
+
follow.frame = requestAnimationFrame(edgeScroll);
|
|
619
|
+
}
|
|
620
|
+
function release(): void {
|
|
621
|
+
if (follow) cancelAnimationFrame(follow.frame);
|
|
622
|
+
follow = undefined;
|
|
623
|
+
removeEventListener('scroll', repick, { capture: true });
|
|
624
|
+
}
|
|
625
|
+
onDestroy(release);
|
|
626
|
+
function onHandleUp(e: PointerEvent): void {
|
|
627
|
+
const done = drag;
|
|
628
|
+
drag = undefined;
|
|
629
|
+
release();
|
|
630
|
+
if (!done?.engaged || e.type !== 'pointerup') return;
|
|
631
|
+
move(done.k, done.drop);
|
|
632
|
+
}
|
|
633
|
+
/** Where the drop rule draws on row `k`, while a drag would land the row there. */
|
|
634
|
+
function dropSide(k: number): 'before' | 'after' | undefined {
|
|
635
|
+
if (!drag?.engaged || drag.drop !== k || drag.drop === drag.k) return undefined;
|
|
636
|
+
return drag.drop > drag.k ? 'after' : 'before';
|
|
542
637
|
}
|
|
543
638
|
</script>
|
|
544
639
|
|
|
@@ -550,14 +645,7 @@
|
|
|
550
645
|
>
|
|
551
646
|
<div class="qm-array-header">
|
|
552
647
|
{#if label != null}
|
|
553
|
-
<FieldLabel
|
|
554
|
-
{label}
|
|
555
|
-
id={labelId}
|
|
556
|
-
{descriptionId}
|
|
557
|
-
onActivate={focus}
|
|
558
|
-
{required}
|
|
559
|
-
{description}
|
|
560
|
-
/>
|
|
648
|
+
<FieldLabel {label} id={labelId} {descriptionId} onActivate={focus} {description} />
|
|
561
649
|
{:else}
|
|
562
650
|
<span></span>
|
|
563
651
|
{/if}
|
|
@@ -582,15 +670,15 @@
|
|
|
582
670
|
each block's one child: a branch inside the block would stand between them. -->
|
|
583
671
|
{#if table}
|
|
584
672
|
<div
|
|
585
|
-
class={['qm-array-rows qm-array-table', { empty: ids.length === 0 }]}
|
|
673
|
+
class={['qm-array-rows qm-array-table qm-wash', { empty: ids.length === 0 }]}
|
|
586
674
|
data-default={defaulted ? '' : undefined}
|
|
587
675
|
bind:this={rowsEl}
|
|
588
676
|
>
|
|
589
677
|
<div class="qm-array-table-scroller" style:--table-cols={columns.length}>
|
|
590
678
|
<!-- The header names the columns, so a cell carries no label of its own: its
|
|
591
679
|
accessible name is the row's and the column's composed (`ObjectField`). The
|
|
592
|
-
|
|
593
|
-
|
|
680
|
+
guidance affordance rides the header, being the column's rather than any
|
|
681
|
+
one cell's. -->
|
|
594
682
|
<div class="qm-array-table-head">
|
|
595
683
|
{#each columns as [key, sub] (key)}
|
|
596
684
|
<div
|
|
@@ -602,7 +690,6 @@
|
|
|
602
690
|
<FieldLabel
|
|
603
691
|
label={columnTitle(key, sub)}
|
|
604
692
|
descriptionId={idBase != null ? propertyDomIds(idBase, key).description : undefined}
|
|
605
|
-
required={obliged(sub)}
|
|
606
693
|
description={sub.description}
|
|
607
694
|
/>
|
|
608
695
|
</div>
|
|
@@ -610,7 +697,11 @@
|
|
|
610
697
|
<span class="qm-array-table-head-end"></span>
|
|
611
698
|
</div>
|
|
612
699
|
{#each ids as id, k (id)}
|
|
613
|
-
<div
|
|
700
|
+
<div
|
|
701
|
+
class="qm-array-row qm-array-table-row qm-wash"
|
|
702
|
+
data-qm-index={k}
|
|
703
|
+
bind:this={rowEls[id]}
|
|
704
|
+
>
|
|
614
705
|
<ObjectField
|
|
615
706
|
bare
|
|
616
707
|
bind:this={cellEls[id]}
|
|
@@ -630,7 +721,7 @@
|
|
|
630
721
|
</div>
|
|
631
722
|
{:else if control === 'object'}
|
|
632
723
|
<div
|
|
633
|
-
class={['qm-array-rows', { empty: ids.length === 0 }]}
|
|
724
|
+
class={['qm-array-rows qm-wash', { empty: ids.length === 0 }]}
|
|
634
725
|
data-default={defaulted ? '' : undefined}
|
|
635
726
|
bind:this={rowsEl}
|
|
636
727
|
>
|
|
@@ -639,7 +730,12 @@
|
|
|
639
730
|
<!-- The handler catches a key from the controls inside the row and adds no
|
|
640
731
|
interaction of the row's own: the row is no tab stop. -->
|
|
641
732
|
<div
|
|
642
|
-
class={[
|
|
733
|
+
class={[
|
|
734
|
+
'qm-array-row qm-element qm-wash',
|
|
735
|
+
{ open: open.has(id), lifted: drag?.engaged && drag.k === k }
|
|
736
|
+
]}
|
|
737
|
+
data-drop={dropSide(k)}
|
|
738
|
+
data-qm-index={k}
|
|
643
739
|
bind:this={rowEls[id]}
|
|
644
740
|
animate:reorder={arm.armed}
|
|
645
741
|
onkeydown={(e) => onRowKey(e, k)}
|
|
@@ -649,10 +745,12 @@
|
|
|
649
745
|
end-side corners) needs corners to take. So a list of records measures
|
|
650
746
|
like a list of inputs whatever the element type. -->
|
|
651
747
|
<div class="qm-element-head">
|
|
748
|
+
{@render handle(k)}
|
|
652
749
|
<button
|
|
653
750
|
type="button"
|
|
654
751
|
class="qm-control-box qm-focus-ring qm-element-summary"
|
|
655
752
|
aria-expanded={open.has(id)}
|
|
753
|
+
aria-keyshortcuts="Alt+ArrowUp Alt+ArrowDown"
|
|
656
754
|
onclick={() => toggleRow(id)}
|
|
657
755
|
>
|
|
658
756
|
<!-- Leading, and it rotates: trailing is the figure for pushing a new
|
|
@@ -666,7 +764,7 @@
|
|
|
666
764
|
{/if}
|
|
667
765
|
</button>
|
|
668
766
|
<div class="qm-row-actions">
|
|
669
|
-
{@render
|
|
767
|
+
{@render removeButton(k)}
|
|
670
768
|
</div>
|
|
671
769
|
</div>
|
|
672
770
|
{#if open.has(id)}
|
|
@@ -690,18 +788,18 @@
|
|
|
690
788
|
</div>
|
|
691
789
|
{:else}
|
|
692
790
|
<div
|
|
693
|
-
class={['qm-array-rows', { empty: ids.length === 0 }]}
|
|
791
|
+
class={['qm-array-rows qm-wash', { empty: ids.length === 0 }]}
|
|
694
792
|
data-default={defaulted ? '' : undefined}
|
|
695
793
|
bind:this={rowsEl}
|
|
696
794
|
>
|
|
697
795
|
{#each ids as id, k (id)}
|
|
698
|
-
<div class="qm-array-row" bind:this={rowEls[id]}>
|
|
796
|
+
<div class="qm-array-row qm-wash" data-qm-index={k} bind:this={rowEls[id]}>
|
|
699
797
|
{#if control === 'prose'}
|
|
700
798
|
<ProseValue
|
|
701
799
|
bind:this={els[id]}
|
|
702
800
|
content={() => readAt([k])}
|
|
703
801
|
value={arr[k]}
|
|
704
|
-
plaintext={items != null &&
|
|
802
|
+
plaintext={items != null && literalText(items)}
|
|
705
803
|
label={rowName(k)}
|
|
706
804
|
onChange={(rt) => commitElement(k, rt)}
|
|
707
805
|
onKey={(e) => onElementKey(e, k)}
|
|
@@ -738,26 +836,22 @@
|
|
|
738
836
|
>
|
|
739
837
|
{/snippet}
|
|
740
838
|
|
|
741
|
-
<!-- An `object` row's
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
aria-
|
|
749
|
-
|
|
750
|
-
|
|
839
|
+
<!-- An `object` row's drag handle, a table grip's twin hung off the row's start edge.
|
|
840
|
+
Pointer-only and out of the accessibility tree: a press on it does nothing, so a
|
|
841
|
+
button there would be one a screen reader activates to no effect. Its twin is
|
|
842
|
+
Alt+↑/↓ from anywhere in the row, which the summary announces. -->
|
|
843
|
+
{#snippet handle(k: number)}
|
|
844
|
+
<span
|
|
845
|
+
class="qm-row-handle"
|
|
846
|
+
aria-hidden="true"
|
|
847
|
+
title={t.strings.arrayDrag}
|
|
848
|
+
onpointerdown={(e) => onHandleDown(e, k)}
|
|
849
|
+
onpointermove={onHandleMove}
|
|
850
|
+
onpointerup={onHandleUp}
|
|
851
|
+
onpointercancel={onHandleUp}
|
|
852
|
+
onlostpointercapture={onHandleUp}
|
|
853
|
+
><span class="qm-row-handle-bar"><Icon name="grip-vertical" /></span></span
|
|
751
854
|
>
|
|
752
|
-
<button
|
|
753
|
-
type="button"
|
|
754
|
-
class="qm-icon-btn qm-row-btn qm-focus-ring"
|
|
755
|
-
title={t.strings.arrayMoveDown}
|
|
756
|
-
aria-label={rowAction(t.strings.arrayMoveDown, k)}
|
|
757
|
-
disabled={k === ids.length - 1}
|
|
758
|
-
onclick={() => move(k, 1)}><Icon name="chevron-down" /></button
|
|
759
|
-
>
|
|
760
|
-
{@render removeButton(k)}
|
|
761
855
|
{/snippet}
|
|
762
856
|
|
|
763
857
|
<style>
|
|
@@ -790,14 +884,13 @@
|
|
|
790
884
|
}
|
|
791
885
|
/* The elements, in a box of their own: the arrival wash blooms here rather than over
|
|
792
886
|
the wrapper `Field` hands this component (`washBox`), which is the label's box too.
|
|
793
|
-
|
|
794
|
-
|
|
887
|
+
Rounded to the rung a row's own box draws, the way `.qm-field-control` is for every
|
|
888
|
+
other control.
|
|
795
889
|
|
|
796
890
|
`display: none` when there are no elements, so the header does not stand a gap above
|
|
797
891
|
an empty box; a wash over it then paints nothing, which is what an array with
|
|
798
892
|
nothing in it has to show. */
|
|
799
893
|
.qm-array-rows {
|
|
800
|
-
position: relative;
|
|
801
894
|
display: flex;
|
|
802
895
|
flex-direction: column;
|
|
803
896
|
gap: var(--_qm-space);
|
|
@@ -807,18 +900,21 @@
|
|
|
807
900
|
display: none;
|
|
808
901
|
}
|
|
809
902
|
/* The default's rows are what prints and nothing written: every value they draw takes
|
|
810
|
-
the default rung (theme.css),
|
|
903
|
+
the default rung (theme.css), a checked switch's track included, and the first
|
|
904
|
+
gesture, writing them, takes it off. */
|
|
811
905
|
.qm-array-rows[data-default] :global(:is(.qm-input, .ProseMirror, .qm-date)),
|
|
812
906
|
.qm-array-rows[data-default] :global(.qm-select:not([data-ghosted])),
|
|
813
907
|
.qm-array-rows[data-default] .qm-element-title:not(.untitled) {
|
|
814
908
|
color: var(--_qm-ink-default);
|
|
815
909
|
}
|
|
910
|
+
.qm-array-rows[data-default] :global(.qm-toggle[data-state='checked']) {
|
|
911
|
+
background: var(--_qm-accent-default);
|
|
912
|
+
}
|
|
816
913
|
/* A grid rather than a block: an `<input>` is inline-level and would sit on a
|
|
817
914
|
baseline, standing the row a descender taller than the box the slab measures itself
|
|
818
915
|
against. `minmax(0, …)` because a long unbroken value grows an `auto` track, and the
|
|
819
916
|
edge with it. */
|
|
820
917
|
.qm-array-row {
|
|
821
|
-
position: relative;
|
|
822
918
|
display: grid;
|
|
823
919
|
grid-template-columns: minmax(0, 1fr);
|
|
824
920
|
row-gap: var(--_qm-space);
|
|
@@ -830,7 +926,7 @@
|
|
|
830
926
|
/* The end inset the slabs stand in, taken off whichever box the element drew:
|
|
831
927
|
`.qm-input` is the text element, `.qm-control-box` the prose one — the row's own
|
|
832
928
|
child, so a table's cells, which are boxes too, keep their inset — and an object
|
|
833
|
-
row's summary, which stands
|
|
929
|
+
row's summary, which stands one slab off its end. `:global`, because the box
|
|
834
930
|
belongs to the child component's markup and the scope class stops at this
|
|
835
931
|
component's. The longhand beats the family's `padding` shorthand without a
|
|
836
932
|
specificity fight: `controls.css` ranks the family at zero inside `:where()`. */
|
|
@@ -839,7 +935,7 @@
|
|
|
839
935
|
padding-inline-end: var(--_qm-tap-min);
|
|
840
936
|
}
|
|
841
937
|
.qm-element-summary {
|
|
842
|
-
padding-inline-end:
|
|
938
|
+
padding-inline-end: var(--_qm-tap-min);
|
|
843
939
|
}
|
|
844
940
|
/* The box's end wall, floor to ceiling: a height of its own would leave a sliver of
|
|
845
941
|
well above or below, and the corners it takes are the box's — the end-side pair keeps
|
|
@@ -847,8 +943,7 @@
|
|
|
847
943
|
another gives up its corners on both sides.
|
|
848
944
|
|
|
849
945
|
It comes up on its own row rather than on the field: a destructive control is offered
|
|
850
|
-
by the row the pointer is on, not by every row at once
|
|
851
|
-
it comes up with it. Hover is where the remove says destructive, ink with fill, a
|
|
946
|
+
by the row the pointer is on, not by every row at once. Hover is where the remove says destructive, ink with fill, a
|
|
852
947
|
tint alone being a wash under a label-toned glyph. */
|
|
853
948
|
.qm-row-actions {
|
|
854
949
|
position: absolute;
|
|
@@ -872,10 +967,6 @@
|
|
|
872
967
|
border-start-end-radius: var(--_qm-radius-inner);
|
|
873
968
|
border-end-end-radius: var(--_qm-radius-inner);
|
|
874
969
|
}
|
|
875
|
-
.qm-row-btn :global(svg) {
|
|
876
|
-
width: var(--_qm-glyph-control);
|
|
877
|
-
height: var(--_qm-glyph-control);
|
|
878
|
-
}
|
|
879
970
|
/* Child combinators: a nested array shares this scope, and a row's hover is not its
|
|
880
971
|
open subform's rows'. */
|
|
881
972
|
.qm-array-row:hover > .qm-row-actions,
|
|
@@ -888,6 +979,68 @@
|
|
|
888
979
|
background: var(--_qm-danger-tint);
|
|
889
980
|
color: var(--_qm-danger);
|
|
890
981
|
}
|
|
982
|
+
/* The handle is a table grip's twin (prose.css §"One grip per line"): a pill carrying
|
|
983
|
+
the grip's marks, hung off the row's start edge in the `--_qm-nest` every row stands
|
|
984
|
+
in from the stroke outside it, which a group's panel clips at. It rests
|
|
985
|
+
at nothing, where the remove rests faint, and comes up with the row the pointer is
|
|
986
|
+
on, staying up through the drag it carries; a band flush with the row keeps the
|
|
987
|
+
pointer on the row all the way to it. The pointer owns the gesture, not the page's
|
|
988
|
+
scroll. */
|
|
989
|
+
.qm-row-handle {
|
|
990
|
+
position: absolute;
|
|
991
|
+
inset-block: 0;
|
|
992
|
+
inset-inline-end: 100%;
|
|
993
|
+
width: var(--_qm-nest);
|
|
994
|
+
display: flex;
|
|
995
|
+
align-items: center;
|
|
996
|
+
justify-content: center;
|
|
997
|
+
color: var(--_qm-ink-label);
|
|
998
|
+
cursor: grab;
|
|
999
|
+
touch-action: none;
|
|
1000
|
+
opacity: 0;
|
|
1001
|
+
transition: opacity var(--_qm-duration-fast) var(--_qm-ease-reverse);
|
|
1002
|
+
}
|
|
1003
|
+
.qm-row-handle-bar {
|
|
1004
|
+
display: flex;
|
|
1005
|
+
align-items: center;
|
|
1006
|
+
justify-content: center;
|
|
1007
|
+
width: 100%;
|
|
1008
|
+
height: var(--_qm-tap-min);
|
|
1009
|
+
border-radius: var(--_qm-radius-pill);
|
|
1010
|
+
background: var(--_qm-surface-band);
|
|
1011
|
+
}
|
|
1012
|
+
.qm-row-handle-bar :global(svg) {
|
|
1013
|
+
width: var(--_qm-space-3);
|
|
1014
|
+
height: var(--_qm-space-3);
|
|
1015
|
+
}
|
|
1016
|
+
.qm-array-row:hover > .qm-element-head > .qm-row-handle,
|
|
1017
|
+
.qm-element.lifted > .qm-element-head > .qm-row-handle {
|
|
1018
|
+
opacity: 1;
|
|
1019
|
+
}
|
|
1020
|
+
.qm-element.lifted > .qm-element-head > .qm-row-handle {
|
|
1021
|
+
cursor: grabbing;
|
|
1022
|
+
}
|
|
1023
|
+
/* The lifted row is a tone, and the drop is one rule on the edge it would land
|
|
1024
|
+
against, drawn out of flow so nothing reflows under the drag: the table island's
|
|
1025
|
+
own pair. */
|
|
1026
|
+
.qm-element.lifted > .qm-element-head > .qm-element-summary {
|
|
1027
|
+
opacity: var(--_qm-opacity-muted);
|
|
1028
|
+
}
|
|
1029
|
+
.qm-element[data-drop]::before {
|
|
1030
|
+
content: '';
|
|
1031
|
+
position: absolute;
|
|
1032
|
+
inset-inline: 0;
|
|
1033
|
+
height: var(--_qm-ring-width);
|
|
1034
|
+
background: var(--_qm-accent);
|
|
1035
|
+
border-radius: var(--_qm-radius-pill);
|
|
1036
|
+
pointer-events: none;
|
|
1037
|
+
}
|
|
1038
|
+
.qm-element[data-drop='before']::before {
|
|
1039
|
+
top: calc(-0.5 * (var(--_qm-space) + var(--_qm-ring-width)));
|
|
1040
|
+
}
|
|
1041
|
+
.qm-element[data-drop='after']::before {
|
|
1042
|
+
bottom: calc(-0.5 * (var(--_qm-space) + var(--_qm-ring-width)));
|
|
1043
|
+
}
|
|
891
1044
|
/* ── An object element ──────────────────────────────────────────────────────
|
|
892
1045
|
The row is a summary and the subform hangs under it. `row-gap` rather than a margin
|
|
893
1046
|
on the subform, because the distance between a control and what it has unfolded
|
|
@@ -1106,10 +1259,15 @@
|
|
|
1106
1259
|
.qm-array.empty .qm-add-el {
|
|
1107
1260
|
opacity: 1;
|
|
1108
1261
|
}
|
|
1262
|
+
/* No hover to come up on: the controls stand at the muted rung, the handle with
|
|
1263
|
+
them. */
|
|
1109
1264
|
@media (hover: none) {
|
|
1110
1265
|
.qm-add-el,
|
|
1111
1266
|
.qm-row-actions {
|
|
1112
1267
|
opacity: var(--_qm-opacity-muted);
|
|
1113
1268
|
}
|
|
1269
|
+
.qm-row-handle {
|
|
1270
|
+
opacity: 1;
|
|
1271
|
+
}
|
|
1114
1272
|
}
|
|
1115
1273
|
</style>
|
|
@@ -15,8 +15,6 @@ interface Props {
|
|
|
15
15
|
max?: number;
|
|
16
16
|
/** Accessible-name prefix for the element controls (`label` + 1-based index). */
|
|
17
17
|
label?: string;
|
|
18
|
-
/** No-default field → a persistent required `*` on the label. */
|
|
19
|
-
required?: boolean;
|
|
20
18
|
/** Schema `description`: the label's help affordance. */
|
|
21
19
|
description?: string;
|
|
22
20
|
/** The label's own DOM id. An array is a group (N inputs, no single `for`
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
`mixed` there reads as false), so an optional boolean's control is a `checkbox`, the
|
|
11
11
|
toggle role that does, announcing `mixed` while unset. A press from unset writes
|
|
12
12
|
`true`; nothing here returns the cell to unset.
|
|
13
|
+
|
|
14
|
+
An unset switch draws its `default:` at the default rung, as every control draws a
|
|
15
|
+
printed default: a checked track takes the accent's default rung.
|
|
13
16
|
-->
|
|
14
17
|
<script lang="ts">
|
|
15
18
|
import { Switch } from 'bits-ui';
|
|
@@ -41,6 +44,7 @@
|
|
|
41
44
|
// the unset optional cell, the one state with no default to stand in for it.
|
|
42
45
|
let local = $derived<boolean | undefined>(value ?? fallback ?? (optional ? undefined : false));
|
|
43
46
|
const unset = $derived(local === undefined);
|
|
47
|
+
const defaulted = $derived(value === undefined && fallback !== undefined);
|
|
44
48
|
const tristate = $derived(
|
|
45
49
|
optional ? { role: 'checkbox', 'aria-checked': unset ? ('mixed' as const) : !!local } : {}
|
|
46
50
|
);
|
|
@@ -49,7 +53,11 @@
|
|
|
49
53
|
<!-- Not `.qm-switch`: that name is the preset's pane band, whose narrow-viewport rule
|
|
50
54
|
stands every child of it at `--qmh-tap`, so a track wearing it grows a thumb three times
|
|
51
55
|
its own height over the labels around it (`preset/recipes.css`, held by `check:style`). -->
|
|
52
|
-
<span
|
|
56
|
+
<span
|
|
57
|
+
class="qm-toggle-wrap"
|
|
58
|
+
data-unset={unset ? '' : undefined}
|
|
59
|
+
data-default={defaulted ? '' : undefined}
|
|
60
|
+
>
|
|
53
61
|
<Switch.Root
|
|
54
62
|
class="qm-toggle qm-focus-ring qm-tap-floor"
|
|
55
63
|
checked={!!local}
|
|
@@ -101,6 +109,9 @@
|
|
|
101
109
|
.qm-toggle-wrap :global(.qm-toggle[data-state='checked']) {
|
|
102
110
|
background: var(--_qm-accent);
|
|
103
111
|
}
|
|
112
|
+
.qm-toggle-wrap[data-default] :global(.qm-toggle[data-state='checked']) {
|
|
113
|
+
background: var(--_qm-accent-default);
|
|
114
|
+
}
|
|
104
115
|
/* The focus ring rides `.qm-focus-ring` on the switch (controls.css). */
|
|
105
116
|
/* The thumb reads on tone alone: the base surface over the track's `hover` rung
|
|
106
117
|
unchecked, and over `--_qm-accent` checked. No edge of its own: a hairline around
|