@quillmark/svelte 0.21.0 → 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/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 +1 -1
- package/dist/core/codec/blocks.js +4 -2
- package/dist/core/codec/code.d.ts +1 -1
- package/dist/core/codec/code.js +1 -1
- 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 +15 -10
- package/dist/core/codec/field.js +46 -17
- package/dist/core/codec/index.d.ts +1 -1
- package/dist/core/codec/index.js +2 -2
- package/dist/core/codec/inputrules.js +1 -1
- package/dist/core/codec/lists.d.ts +3 -2
- package/dist/core/codec/lists.js +21 -5
- package/dist/core/codec/nesting.d.ts +20 -0
- package/dist/core/codec/nesting.js +62 -0
- package/dist/core/codec/prose.css +74 -20
- package/dist/core/codec/schema.d.ts +2 -2
- package/dist/core/codec/schema.js +16 -7
- 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 +1 -7
- package/dist/core/codec/table-view.js +70 -119
- 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/visual/ArrayField.svelte +101 -63
- package/dist/visual/ArrayField.svelte.d.ts +0 -2
- package/dist/visual/Card.svelte +9 -3
- package/dist/visual/Card.svelte.d.ts +2 -2
- package/dist/visual/Field.svelte +20 -16
- package/dist/visual/Field.svelte.d.ts +4 -2
- package/dist/visual/FieldLabel.svelte +5 -33
- package/dist/visual/FieldLabel.svelte.d.ts +0 -2
- package/dist/visual/FormatPopover.svelte +54 -73
- package/dist/visual/MatrixField.svelte +18 -12
- package/dist/visual/NumberField.svelte +32 -3
- package/dist/visual/ObjectField.svelte +38 -20
- 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 +5 -2
- package/dist/visual/VariantField.svelte +1 -1
- package/dist/visual/VariantField.svelte.d.ts +1 -1
- package/dist/visual/VisualEditorInner.svelte +13 -13
- package/dist/visual/controls.css +19 -1
- package/dist/visual/icons/nodes.d.ts +0 -21
- package/dist/visual/icons/nodes.js +1 -7
- package/dist/visual/leaves.d.ts +12 -9
- package/dist/visual/leaves.js +19 -0
- package/dist/visual/strings.d.ts +10 -5
- package/dist/visual/strings.js +16 -9
- 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
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
controlKind,
|
|
72
72
|
declaredContent,
|
|
73
73
|
humanize,
|
|
74
|
-
|
|
74
|
+
literalText,
|
|
75
75
|
rowSummary,
|
|
76
76
|
schemaAt,
|
|
77
77
|
type ArrayLayout
|
|
@@ -105,8 +105,6 @@
|
|
|
105
105
|
max?: number;
|
|
106
106
|
/** Accessible-name prefix for the element controls (`label` + 1-based index). */
|
|
107
107
|
label?: string;
|
|
108
|
-
/** No-default field → a persistent required `*` on the label. */
|
|
109
|
-
required?: boolean;
|
|
110
108
|
/** Schema `description`: the label's help affordance. */
|
|
111
109
|
description?: string;
|
|
112
110
|
/** The label's own DOM id. An array is a group (N inputs, no single `for`
|
|
@@ -140,7 +138,6 @@
|
|
|
140
138
|
layout = 'list',
|
|
141
139
|
max,
|
|
142
140
|
label,
|
|
143
|
-
required,
|
|
144
141
|
description,
|
|
145
142
|
labelId,
|
|
146
143
|
descriptionId,
|
|
@@ -519,9 +516,10 @@
|
|
|
519
516
|
* any cell an open row holds: Alt+↑/↓, the table island's own binding. The nearest
|
|
520
517
|
* row answers, or none does: a press inside a nested row is that row's whether it
|
|
521
518
|
* holds a reorder or not, so a key in a table's cell or a list of strings' input
|
|
522
|
-
* 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. */
|
|
523
521
|
function onRowKey(e: KeyboardEvent, k: number): void {
|
|
524
|
-
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;
|
|
525
523
|
// A cell that took the key — a select opening, a date segment stepping, a prose
|
|
526
524
|
// leaf joining — owns it.
|
|
527
525
|
if (e.defaultPrevented) return;
|
|
@@ -536,42 +534,53 @@
|
|
|
536
534
|
|
|
537
535
|
// ── The drag handle ──────────────────────────────────────────────────────────
|
|
538
536
|
// Press-and-drag the handle moves its row; a press that does not travel past the dead
|
|
539
|
-
// zone does nothing. The
|
|
540
|
-
//
|
|
541
|
-
//
|
|
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.
|
|
542
544
|
const DEAD_ZONE = 3;
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
heads: { start: number; end: number }[];
|
|
549
|
-
};
|
|
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
550
|
let drag = $state<RowDrag | undefined>();
|
|
551
|
+
let follow: { grip: Element; port: Element; armed: boolean; frame: number } | undefined;
|
|
551
552
|
function onHandleDown(e: PointerEvent, k: number): void {
|
|
552
553
|
if (e.button !== 0) return;
|
|
553
554
|
e.preventDefault();
|
|
554
|
-
(
|
|
555
|
-
|
|
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 };
|
|
556
560
|
}
|
|
557
561
|
function onHandleMove(e: PointerEvent): void {
|
|
558
562
|
if (!drag) return;
|
|
563
|
+
drag.y = e.clientY;
|
|
559
564
|
if (!drag.engaged) {
|
|
560
565
|
if (Math.abs(e.clientY - drag.origin) < DEAD_ZONE) return;
|
|
561
566
|
drag.engaged = true;
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
return { start: box?.top ?? 0, end: box?.bottom ?? 0 };
|
|
565
|
-
});
|
|
567
|
+
addEventListener('scroll', repick, { capture: true, passive: true });
|
|
568
|
+
if (follow) follow.frame = requestAnimationFrame(edgeScroll);
|
|
566
569
|
}
|
|
567
|
-
|
|
570
|
+
repick();
|
|
571
|
+
}
|
|
572
|
+
function repick(): void {
|
|
573
|
+
if (drag?.engaged) drag.drop = dropIndex(drag.k, drag.y);
|
|
568
574
|
}
|
|
575
|
+
const rowBox = (i: number): DOMRect | undefined => rowEls[ids[i]]?.getBoundingClientRect();
|
|
569
576
|
/** The row the pointer is over, or the nearest one past either end. */
|
|
570
|
-
function dropIndex(
|
|
571
|
-
let best =
|
|
577
|
+
function dropIndex(k: number, y: number): number {
|
|
578
|
+
let best = k;
|
|
572
579
|
let nearest = Infinity;
|
|
573
|
-
|
|
574
|
-
const
|
|
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;
|
|
575
584
|
if (gap < nearest) {
|
|
576
585
|
nearest = gap;
|
|
577
586
|
best = i;
|
|
@@ -579,9 +588,45 @@
|
|
|
579
588
|
});
|
|
580
589
|
return best;
|
|
581
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);
|
|
582
626
|
function onHandleUp(e: PointerEvent): void {
|
|
583
627
|
const done = drag;
|
|
584
628
|
drag = undefined;
|
|
629
|
+
release();
|
|
585
630
|
if (!done?.engaged || e.type !== 'pointerup') return;
|
|
586
631
|
move(done.k, done.drop);
|
|
587
632
|
}
|
|
@@ -600,14 +645,7 @@
|
|
|
600
645
|
>
|
|
601
646
|
<div class="qm-array-header">
|
|
602
647
|
{#if label != null}
|
|
603
|
-
<FieldLabel
|
|
604
|
-
{label}
|
|
605
|
-
id={labelId}
|
|
606
|
-
{descriptionId}
|
|
607
|
-
onActivate={focus}
|
|
608
|
-
{required}
|
|
609
|
-
{description}
|
|
610
|
-
/>
|
|
648
|
+
<FieldLabel {label} id={labelId} {descriptionId} onActivate={focus} {description} />
|
|
611
649
|
{:else}
|
|
612
650
|
<span></span>
|
|
613
651
|
{/if}
|
|
@@ -632,15 +670,15 @@
|
|
|
632
670
|
each block's one child: a branch inside the block would stand between them. -->
|
|
633
671
|
{#if table}
|
|
634
672
|
<div
|
|
635
|
-
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 }]}
|
|
636
674
|
data-default={defaulted ? '' : undefined}
|
|
637
675
|
bind:this={rowsEl}
|
|
638
676
|
>
|
|
639
677
|
<div class="qm-array-table-scroller" style:--table-cols={columns.length}>
|
|
640
678
|
<!-- The header names the columns, so a cell carries no label of its own: its
|
|
641
679
|
accessible name is the row's and the column's composed (`ObjectField`). The
|
|
642
|
-
|
|
643
|
-
|
|
680
|
+
guidance affordance rides the header, being the column's rather than any
|
|
681
|
+
one cell's. -->
|
|
644
682
|
<div class="qm-array-table-head">
|
|
645
683
|
{#each columns as [key, sub] (key)}
|
|
646
684
|
<div
|
|
@@ -652,7 +690,6 @@
|
|
|
652
690
|
<FieldLabel
|
|
653
691
|
label={columnTitle(key, sub)}
|
|
654
692
|
descriptionId={idBase != null ? propertyDomIds(idBase, key).description : undefined}
|
|
655
|
-
required={obliged(sub)}
|
|
656
693
|
description={sub.description}
|
|
657
694
|
/>
|
|
658
695
|
</div>
|
|
@@ -660,7 +697,11 @@
|
|
|
660
697
|
<span class="qm-array-table-head-end"></span>
|
|
661
698
|
</div>
|
|
662
699
|
{#each ids as id, k (id)}
|
|
663
|
-
<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
|
+
>
|
|
664
705
|
<ObjectField
|
|
665
706
|
bare
|
|
666
707
|
bind:this={cellEls[id]}
|
|
@@ -680,7 +721,7 @@
|
|
|
680
721
|
</div>
|
|
681
722
|
{:else if control === 'object'}
|
|
682
723
|
<div
|
|
683
|
-
class={['qm-array-rows', { empty: ids.length === 0 }]}
|
|
724
|
+
class={['qm-array-rows qm-wash', { empty: ids.length === 0 }]}
|
|
684
725
|
data-default={defaulted ? '' : undefined}
|
|
685
726
|
bind:this={rowsEl}
|
|
686
727
|
>
|
|
@@ -690,10 +731,11 @@
|
|
|
690
731
|
interaction of the row's own: the row is no tab stop. -->
|
|
691
732
|
<div
|
|
692
733
|
class={[
|
|
693
|
-
'qm-array-row qm-element',
|
|
734
|
+
'qm-array-row qm-element qm-wash',
|
|
694
735
|
{ open: open.has(id), lifted: drag?.engaged && drag.k === k }
|
|
695
736
|
]}
|
|
696
737
|
data-drop={dropSide(k)}
|
|
738
|
+
data-qm-index={k}
|
|
697
739
|
bind:this={rowEls[id]}
|
|
698
740
|
animate:reorder={arm.armed}
|
|
699
741
|
onkeydown={(e) => onRowKey(e, k)}
|
|
@@ -708,6 +750,7 @@
|
|
|
708
750
|
type="button"
|
|
709
751
|
class="qm-control-box qm-focus-ring qm-element-summary"
|
|
710
752
|
aria-expanded={open.has(id)}
|
|
753
|
+
aria-keyshortcuts="Alt+ArrowUp Alt+ArrowDown"
|
|
711
754
|
onclick={() => toggleRow(id)}
|
|
712
755
|
>
|
|
713
756
|
<!-- Leading, and it rotates: trailing is the figure for pushing a new
|
|
@@ -745,18 +788,18 @@
|
|
|
745
788
|
</div>
|
|
746
789
|
{:else}
|
|
747
790
|
<div
|
|
748
|
-
class={['qm-array-rows', { empty: ids.length === 0 }]}
|
|
791
|
+
class={['qm-array-rows qm-wash', { empty: ids.length === 0 }]}
|
|
749
792
|
data-default={defaulted ? '' : undefined}
|
|
750
793
|
bind:this={rowsEl}
|
|
751
794
|
>
|
|
752
795
|
{#each ids as id, k (id)}
|
|
753
|
-
<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]}>
|
|
754
797
|
{#if control === 'prose'}
|
|
755
798
|
<ProseValue
|
|
756
799
|
bind:this={els[id]}
|
|
757
800
|
content={() => readAt([k])}
|
|
758
801
|
value={arr[k]}
|
|
759
|
-
plaintext={items != null &&
|
|
802
|
+
plaintext={items != null && literalText(items)}
|
|
760
803
|
label={rowName(k)}
|
|
761
804
|
onChange={(rt) => commitElement(k, rt)}
|
|
762
805
|
onKey={(e) => onElementKey(e, k)}
|
|
@@ -794,20 +837,20 @@
|
|
|
794
837
|
{/snippet}
|
|
795
838
|
|
|
796
839
|
<!-- An `object` row's drag handle, a table grip's twin hung off the row's start edge.
|
|
797
|
-
|
|
798
|
-
|
|
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. -->
|
|
799
843
|
{#snippet handle(k: number)}
|
|
800
|
-
<
|
|
801
|
-
type="button"
|
|
844
|
+
<span
|
|
802
845
|
class="qm-row-handle"
|
|
803
|
-
|
|
846
|
+
aria-hidden="true"
|
|
804
847
|
title={t.strings.arrayDrag}
|
|
805
|
-
aria-label={rowAction(t.strings.arrayDrag, k)}
|
|
806
848
|
onpointerdown={(e) => onHandleDown(e, k)}
|
|
807
849
|
onpointermove={onHandleMove}
|
|
808
850
|
onpointerup={onHandleUp}
|
|
809
851
|
onpointercancel={onHandleUp}
|
|
810
|
-
|
|
852
|
+
onlostpointercapture={onHandleUp}
|
|
853
|
+
><span class="qm-row-handle-bar"><Icon name="grip-vertical" /></span></span
|
|
811
854
|
>
|
|
812
855
|
{/snippet}
|
|
813
856
|
|
|
@@ -841,14 +884,13 @@
|
|
|
841
884
|
}
|
|
842
885
|
/* The elements, in a box of their own: the arrival wash blooms here rather than over
|
|
843
886
|
the wrapper `Field` hands this component (`washBox`), which is the label's box too.
|
|
844
|
-
|
|
845
|
-
|
|
887
|
+
Rounded to the rung a row's own box draws, the way `.qm-field-control` is for every
|
|
888
|
+
other control.
|
|
846
889
|
|
|
847
890
|
`display: none` when there are no elements, so the header does not stand a gap above
|
|
848
891
|
an empty box; a wash over it then paints nothing, which is what an array with
|
|
849
892
|
nothing in it has to show. */
|
|
850
893
|
.qm-array-rows {
|
|
851
|
-
position: relative;
|
|
852
894
|
display: flex;
|
|
853
895
|
flex-direction: column;
|
|
854
896
|
gap: var(--_qm-space);
|
|
@@ -873,7 +915,6 @@
|
|
|
873
915
|
against. `minmax(0, …)` because a long unbroken value grows an `auto` track, and the
|
|
874
916
|
edge with it. */
|
|
875
917
|
.qm-array-row {
|
|
876
|
-
position: relative;
|
|
877
918
|
display: grid;
|
|
878
919
|
grid-template-columns: minmax(0, 1fr);
|
|
879
920
|
row-gap: var(--_qm-space);
|
|
@@ -953,9 +994,6 @@
|
|
|
953
994
|
display: flex;
|
|
954
995
|
align-items: center;
|
|
955
996
|
justify-content: center;
|
|
956
|
-
padding: 0;
|
|
957
|
-
border: none;
|
|
958
|
-
background: none;
|
|
959
997
|
color: var(--_qm-ink-label);
|
|
960
998
|
cursor: grab;
|
|
961
999
|
touch-action: none;
|
|
@@ -988,7 +1026,7 @@
|
|
|
988
1026
|
.qm-element.lifted > .qm-element-head > .qm-element-summary {
|
|
989
1027
|
opacity: var(--_qm-opacity-muted);
|
|
990
1028
|
}
|
|
991
|
-
.qm-element[data-drop]::
|
|
1029
|
+
.qm-element[data-drop]::before {
|
|
992
1030
|
content: '';
|
|
993
1031
|
position: absolute;
|
|
994
1032
|
inset-inline: 0;
|
|
@@ -997,10 +1035,10 @@
|
|
|
997
1035
|
border-radius: var(--_qm-radius-pill);
|
|
998
1036
|
pointer-events: none;
|
|
999
1037
|
}
|
|
1000
|
-
.qm-element[data-drop='before']::
|
|
1038
|
+
.qm-element[data-drop='before']::before {
|
|
1001
1039
|
top: calc(-0.5 * (var(--_qm-space) + var(--_qm-ring-width)));
|
|
1002
1040
|
}
|
|
1003
|
-
.qm-element[data-drop='after']::
|
|
1041
|
+
.qm-element[data-drop='after']::before {
|
|
1004
1042
|
bottom: calc(-0.5 * (var(--_qm-space) + var(--_qm-ring-width)));
|
|
1005
1043
|
}
|
|
1006
1044
|
/* ── An object element ──────────────────────────────────────────────────────
|
|
@@ -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`
|
package/dist/visual/Card.svelte
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// component renders standalone too.
|
|
11
11
|
const t = wording();
|
|
12
12
|
import Icon from './icons/Icon.svelte';
|
|
13
|
-
import type { Document, Quill, Addr, Diagnostic } from '@quillmark/wasm';
|
|
13
|
+
import type { Document, Quill, Addr, Diagnostic, PathStep } from '@quillmark/wasm';
|
|
14
14
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
15
15
|
import type { LeafRegistry } from './leaves.js';
|
|
16
16
|
import type { CardModel, FieldModel } from './structure.js';
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
kinds: string[];
|
|
49
49
|
ops: CardOps;
|
|
50
50
|
enumDisallowed?: 'hide' | 'disable';
|
|
51
|
-
onFocus?: (addr: Addr) => void;
|
|
51
|
+
onFocus?: (addr: Addr, steps?: readonly PathStep[]) => void;
|
|
52
52
|
onCaretMove?: (addr: Addr, pos: number) => void;
|
|
53
53
|
onChange?: (addr: Addr) => void;
|
|
54
54
|
onError?: EditorErrorHandler;
|
|
@@ -279,7 +279,7 @@
|
|
|
279
279
|
<div class="qm-group-head">
|
|
280
280
|
<button
|
|
281
281
|
type="button"
|
|
282
|
-
class="qm-group-header"
|
|
282
|
+
class="qm-group-header qm-focus-ring"
|
|
283
283
|
id={headIds?.label}
|
|
284
284
|
bind:this={headers[group]}
|
|
285
285
|
aria-expanded={isOpen}
|
|
@@ -538,6 +538,12 @@
|
|
|
538
538
|
.qm-card-title:focus {
|
|
539
539
|
color: var(--_qm-ink);
|
|
540
540
|
}
|
|
541
|
+
/* The automatic title, at the label ink every placeholder takes rather than the UA's
|
|
542
|
+
grey, and steady under focus, where a typed title steps to the ink. */
|
|
543
|
+
.qm-card-title::placeholder {
|
|
544
|
+
color: var(--_qm-ink-label);
|
|
545
|
+
opacity: 1;
|
|
546
|
+
}
|
|
541
547
|
.qm-card-body {
|
|
542
548
|
display: flex;
|
|
543
549
|
flex-direction: column;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Document, Quill, Addr, Diagnostic } from '@quillmark/wasm';
|
|
1
|
+
import type { Document, Quill, Addr, Diagnostic, PathStep } from '@quillmark/wasm';
|
|
2
2
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
3
3
|
import type { LeafRegistry } from './leaves.js';
|
|
4
4
|
import type { CardModel } from './structure.js';
|
|
@@ -27,7 +27,7 @@ interface Props {
|
|
|
27
27
|
kinds: string[];
|
|
28
28
|
ops: CardOps;
|
|
29
29
|
enumDisallowed?: 'hide' | 'disable';
|
|
30
|
-
onFocus?: (addr: Addr) => void;
|
|
30
|
+
onFocus?: (addr: Addr, steps?: readonly PathStep[]) => void;
|
|
31
31
|
onCaretMove?: (addr: Addr, pos: number) => void;
|
|
32
32
|
onChange?: (addr: Addr) => void;
|
|
33
33
|
onError?: EditorErrorHandler;
|
package/dist/visual/Field.svelte
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
ResolvedField
|
|
28
28
|
} from '@quillmark/wasm';
|
|
29
29
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
30
|
-
import { seatValueLeaves, type LandingBox, type LeafRegistry } from './leaves.js';
|
|
30
|
+
import { seatValueLeaves, stepsTo, type LandingBox, type LeafRegistry } from './leaves.js';
|
|
31
31
|
import type { FieldModel, FieldSpan } from './structure.js';
|
|
32
32
|
import {
|
|
33
33
|
VARIANT_DISCRIMINANT,
|
|
@@ -82,7 +82,9 @@
|
|
|
82
82
|
optionAllowed?: (value: string) => boolean;
|
|
83
83
|
/** How a refused option draws: greyed (default) or left out. */
|
|
84
84
|
enumDisallowed?: 'hide' | 'disable';
|
|
85
|
-
|
|
85
|
+
/** A focus inside the field: its address, and the steps down to the cell holding
|
|
86
|
+
* the focus ({@link stepsTo}), `[]` on the field's own control. */
|
|
87
|
+
onFocus?: (addr: Addr, steps?: readonly PathStep[]) => void;
|
|
86
88
|
onCaretMove?: (addr: Addr, pos: number) => void;
|
|
87
89
|
onChange?: (addr: Addr) => void;
|
|
88
90
|
onError?: EditorErrorHandler;
|
|
@@ -221,7 +223,7 @@
|
|
|
221
223
|
|
|
222
224
|
/**
|
|
223
225
|
* This field's landing handle. The wrapper is the bloom host rather than the
|
|
224
|
-
* control:
|
|
226
|
+
* control: the wash is a box's `::after`, which an `<input>` draws none of. The
|
|
225
227
|
* label is outside it, and stays out of the wash: an arrival marks where the caret
|
|
226
228
|
* landed, which is the control. A control that owns its label (`ownsLabel`: the array
|
|
227
229
|
* and the matrix) holds that row inside itself, so it names its own box
|
|
@@ -251,7 +253,8 @@
|
|
|
251
253
|
}
|
|
252
254
|
|
|
253
255
|
// A by-value prose leaf anywhere inside this field's containers is reached under
|
|
254
|
-
// this field's key: the active address
|
|
256
|
+
// this field's key: the registry and the active address name the field, never the
|
|
257
|
+
// element or cell.
|
|
255
258
|
seatValueLeaves({
|
|
256
259
|
focus: (leaf) => leaves?.focusValue(leafKey, leaf),
|
|
257
260
|
release: (leaf) => leaves?.releaseValue(leafKey, leaf)
|
|
@@ -261,11 +264,16 @@
|
|
|
261
264
|
* A form control has no controller to report its focus through, so the wrapper
|
|
262
265
|
* reports: `focusin` bubbles, so one handler covers a plain input, an array's N
|
|
263
266
|
* elements and an object's properties alike, and the active leaf names a scalar
|
|
264
|
-
* field the way it names a prose one. A prose leaf reports through its own
|
|
267
|
+
* field the way it names a prose one, down to the element or cell the focus is in. A prose leaf reports through its own
|
|
265
268
|
* controller — the source that drives its caret signals too — and is excluded here
|
|
266
269
|
* rather than counted twice.
|
|
267
270
|
*/
|
|
268
|
-
const reportFocus = $derived(
|
|
271
|
+
const reportFocus = $derived(
|
|
272
|
+
field.control === 'prose'
|
|
273
|
+
? undefined
|
|
274
|
+
: (e: FocusEvent & { currentTarget: HTMLElement }) =>
|
|
275
|
+
onFocus?.(addr, stepsTo(e.target, e.currentTarget))
|
|
276
|
+
);
|
|
269
277
|
</script>
|
|
270
278
|
|
|
271
279
|
<div class={['qm-field', { cell: span === 'cell' }]}>
|
|
@@ -276,7 +284,6 @@
|
|
|
276
284
|
id={domIds.label}
|
|
277
285
|
descriptionId={domIds.description}
|
|
278
286
|
{onActivate}
|
|
279
|
-
required={field.required}
|
|
280
287
|
description={field.description}
|
|
281
288
|
/>
|
|
282
289
|
{/if}
|
|
@@ -285,7 +292,7 @@
|
|
|
285
292
|
rule below holds why). One markup for both spans; a field owning its row nests a
|
|
286
293
|
level and measures the same. -->
|
|
287
294
|
<div class="qm-field-stack">
|
|
288
|
-
<div class="qm-field-control" {@attach registerControl} onfocusin={reportFocus}>
|
|
295
|
+
<div class="qm-field-control qm-wash" {@attach registerControl} onfocusin={reportFocus}>
|
|
289
296
|
{#if field.control === 'prose'}
|
|
290
297
|
<ProseField
|
|
291
298
|
{quill}
|
|
@@ -296,7 +303,7 @@
|
|
|
296
303
|
plaintext={field.plaintext}
|
|
297
304
|
fallback={printed ? (ghost as Content) : undefined}
|
|
298
305
|
placeholder={none}
|
|
299
|
-
|
|
306
|
+
emptyUnsets={optional}
|
|
300
307
|
labelledBy={domIds.label}
|
|
301
308
|
{describedBy}
|
|
302
309
|
{leafKey}
|
|
@@ -374,7 +381,6 @@
|
|
|
374
381
|
layout={arrayLayout(field.schema)}
|
|
375
382
|
max={field.schema.max}
|
|
376
383
|
label={field.label}
|
|
377
|
-
required={field.required}
|
|
378
384
|
description={field.description}
|
|
379
385
|
labelId={domIds.label}
|
|
380
386
|
descriptionId={domIds.description}
|
|
@@ -389,6 +395,7 @@
|
|
|
389
395
|
value={value as Record<string, unknown> | undefined}
|
|
390
396
|
properties={field.schema.properties}
|
|
391
397
|
{flat}
|
|
398
|
+
placed
|
|
392
399
|
label={field.label}
|
|
393
400
|
idBase={domIds.control}
|
|
394
401
|
labelledBy={domIds.label}
|
|
@@ -476,13 +483,10 @@
|
|
|
476
483
|
gap: var(--_qm-space);
|
|
477
484
|
min-width: 0;
|
|
478
485
|
}
|
|
479
|
-
/*
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
square over a rounded one; on an array or a subform it bounds a group, where
|
|
483
|
-
there is no single box for it to disagree with. */
|
|
486
|
+
/* The arrival wash's host (`.qm-wash`). The radius is the control's own, so the
|
|
487
|
+
wash's corners are the box's rather than square over a rounded one; on an array or
|
|
488
|
+
a subform it bounds a group, where there is no single box for it to disagree with. */
|
|
484
489
|
.qm-field-control {
|
|
485
|
-
position: relative;
|
|
486
490
|
border-radius: var(--_qm-radius-inner);
|
|
487
491
|
}
|
|
488
492
|
</style>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Document, Quill, Addr, Diagnostic, ResolvedField } from '@quillmark/wasm';
|
|
1
|
+
import type { Document, Quill, Addr, Diagnostic, PathStep, ResolvedField } from '@quillmark/wasm';
|
|
2
2
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
3
3
|
import { type LeafRegistry } from './leaves.js';
|
|
4
4
|
import type { FieldModel, FieldSpan } from './structure.js';
|
|
@@ -27,7 +27,9 @@ interface Props {
|
|
|
27
27
|
optionAllowed?: (value: string) => boolean;
|
|
28
28
|
/** How a refused option draws: greyed (default) or left out. */
|
|
29
29
|
enumDisallowed?: 'hide' | 'disable';
|
|
30
|
-
|
|
30
|
+
/** A focus inside the field: its address, and the steps down to the cell holding
|
|
31
|
+
* the focus ({@link stepsTo}), `[]` on the field's own control. */
|
|
32
|
+
onFocus?: (addr: Addr, steps?: readonly PathStep[]) => void;
|
|
31
33
|
onCaretMove?: (addr: Addr, pos: number) => void;
|
|
32
34
|
onChange?: (addr: Addr) => void;
|
|
33
35
|
onError?: EditorErrorHandler;
|
|
@@ -10,15 +10,8 @@
|
|
|
10
10
|
hands the click to `onActivate`. Both halves are needed: `for` on a non-labelable
|
|
11
11
|
element is inert and invalid.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
`default:` (DOCUMENT_MODEL). Its
|
|
16
|
-
accessible name is "required", so a screen reader announces the word, not the
|
|
17
|
-
glyph; inside the `<label>`, so it names the control along with the text
|
|
18
|
-
("Subject required") rather than announcing as a node adjacent to it;
|
|
19
|
-
|
|
20
|
-
• the `description` as an info marker raising a themed popover
|
|
21
|
-
({@link FieldHint}).
|
|
13
|
+
One non-gating label decoration: the `description` as an info marker raising a
|
|
14
|
+
themed popover ({@link FieldHint}).
|
|
22
15
|
|
|
23
16
|
The description describes, it does not name. It is parked in an always-present
|
|
24
17
|
visually-hidden node here and the control carries `aria-describedby`: name then
|
|
@@ -32,11 +25,6 @@
|
|
|
32
25
|
control's label decorates the same way.
|
|
33
26
|
-->
|
|
34
27
|
<script lang="ts">
|
|
35
|
-
import { wording } from './strings.js';
|
|
36
|
-
|
|
37
|
-
// The surface's words, ambient from the editor root; the package's English
|
|
38
|
-
// off-tree, so this component renders standalone too.
|
|
39
|
-
const t = wording();
|
|
40
28
|
import FieldHint from './FieldHint.svelte';
|
|
41
29
|
|
|
42
30
|
interface Props {
|
|
@@ -50,12 +38,10 @@
|
|
|
50
38
|
descriptionId?: string;
|
|
51
39
|
/** Focus handoff for a control `for` cannot reach; a label click calls it. */
|
|
52
40
|
onActivate?: () => void;
|
|
53
|
-
/** No-default field → a persistent required `*`. */
|
|
54
|
-
required?: boolean;
|
|
55
41
|
/** Schema `description`, or undefined: the affordance renders only when set. */
|
|
56
42
|
description?: string;
|
|
57
43
|
}
|
|
58
|
-
let { label, controlId, id, descriptionId, onActivate,
|
|
44
|
+
let { label, controlId, id, descriptionId, onActivate, description }: Props = $props();
|
|
59
45
|
</script>
|
|
60
46
|
|
|
61
47
|
<div class="qm-field-label-row">
|
|
@@ -71,9 +57,6 @@
|
|
|
71
57
|
onclick={onActivate}
|
|
72
58
|
>
|
|
73
59
|
<span>{label}</span>
|
|
74
|
-
{#if required}
|
|
75
|
-
<span class="qm-field-required" aria-label={t.strings.fieldRequired}>*</span>
|
|
76
|
-
{/if}
|
|
77
60
|
</label>
|
|
78
61
|
{#if description}
|
|
79
62
|
<!-- Outside the `<label>`, both of them: a trigger inside it would put its own
|
|
@@ -91,10 +74,8 @@
|
|
|
91
74
|
/* The row, not the label, is the field's grid child: `Field`'s subgrid counts
|
|
92
75
|
two tracks and the hint sits beside the label rather than under it.
|
|
93
76
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
marker is a second affordance beside it. The marker's box is its glyph
|
|
97
|
-
(`FieldHint`), so this gap is the whole of what stands between them. */
|
|
77
|
+
The marker's box is its glyph (`FieldHint`), so this gap is the whole of what
|
|
78
|
+
stands between it and the label. */
|
|
98
79
|
.qm-field-label-row {
|
|
99
80
|
display: flex;
|
|
100
81
|
align-items: center;
|
|
@@ -104,7 +85,6 @@
|
|
|
104
85
|
.qm-field-label {
|
|
105
86
|
display: inline-flex;
|
|
106
87
|
align-items: center;
|
|
107
|
-
gap: var(--_qm-space-half);
|
|
108
88
|
font-size: var(--_qm-text-label);
|
|
109
89
|
font-weight: var(--_qm-weight-mid);
|
|
110
90
|
line-height: var(--_qm-leading-tight);
|
|
@@ -116,14 +96,6 @@
|
|
|
116
96
|
.qm-field-label.targeted {
|
|
117
97
|
cursor: pointer;
|
|
118
98
|
}
|
|
119
|
-
/* Required marker: the label's own ink, not an alarm; required-ness is guidance,
|
|
120
|
-
the document still edits and renders unmet, and the danger hue is a diagnostic's. */
|
|
121
|
-
.qm-field-required {
|
|
122
|
-
display: inline-flex;
|
|
123
|
-
align-items: center;
|
|
124
|
-
color: var(--_qm-ink-label);
|
|
125
|
-
line-height: 1;
|
|
126
|
-
}
|
|
127
99
|
/* The parked description: in the accessibility tree, out of the layout, and never
|
|
128
100
|
a focus stop. Clipped rather than `display: none` / `hidden`, which take a node
|
|
129
101
|
out of the tree entirely and would make the `aria-describedby` dangle. Out of
|
|
@@ -9,8 +9,6 @@ interface Props {
|
|
|
9
9
|
descriptionId?: string;
|
|
10
10
|
/** Focus handoff for a control `for` cannot reach; a label click calls it. */
|
|
11
11
|
onActivate?: () => void;
|
|
12
|
-
/** No-default field → a persistent required `*`. */
|
|
13
|
-
required?: boolean;
|
|
14
12
|
/** Schema `description`, or undefined: the affordance renders only when set. */
|
|
15
13
|
description?: string;
|
|
16
14
|
}
|