@quillmark/svelte 0.11.0 → 0.12.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/dist/core/address.d.ts +17 -10
- package/dist/core/address.js +26 -12
- package/dist/core/bloom.js +4 -3
- package/dist/core/codec/decode.js +21 -15
- package/dist/core/codec/encode.js +1 -1
- package/dist/core/codec/islands.d.ts +5 -4
- package/dist/core/codec/islands.js +23 -38
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/rungs.d.ts +8 -0
- package/dist/core/rungs.js +24 -0
- package/dist/core/theme.css +10 -0
- package/dist/visual/ArrayField.svelte +596 -167
- package/dist/visual/ArrayField.svelte.d.ts +14 -1
- package/dist/visual/BooleanField.svelte +10 -7
- package/dist/visual/Card.svelte +3 -22
- package/dist/visual/Field.svelte +69 -29
- package/dist/visual/MatrixField.svelte +352 -0
- package/dist/visual/MatrixField.svelte.d.ts +36 -0
- package/dist/visual/ObjectField.svelte +351 -133
- package/dist/visual/ObjectField.svelte.d.ts +17 -2
- package/dist/visual/VariantField.svelte +55 -2
- package/dist/visual/VariantField.svelte.d.ts +10 -1
- package/dist/visual/VisualEditorInner.svelte +53 -49
- package/dist/visual/controls.css +31 -0
- package/dist/visual/diagnostics.d.ts +26 -2
- package/dist/visual/diagnostics.js +52 -5
- package/dist/visual/hold.js +2 -2
- package/dist/visual/icons/nodes.d.ts +3 -0
- package/dist/visual/icons/nodes.js +2 -1
- package/dist/visual/leaves.d.ts +25 -12
- package/dist/visual/motion.d.ts +14 -5
- package/dist/visual/motion.js +13 -11
- package/dist/visual/strings.d.ts +19 -9
- package/dist/visual/strings.js +6 -2
- package/dist/visual/structure.d.ts +94 -9
- package/dist/visual/structure.js +229 -12
- package/package.json +2 -2
|
@@ -10,12 +10,16 @@
|
|
|
10
10
|
container with one cell written — and cells outside the drawn world ride through
|
|
11
11
|
untouched rather than needing to be merged back. A content cell reads at the key it
|
|
12
12
|
declares: the boundary's schema walk unions the worlds, so a dormant cell reads absent
|
|
13
|
-
rather than raising.
|
|
13
|
+
rather than raising. A cell may be a container of its own, which the subform draws at
|
|
14
|
+
the next rung.
|
|
14
15
|
-->
|
|
15
16
|
<script lang="ts">
|
|
16
17
|
import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
|
|
17
18
|
import EnumField from './EnumField.svelte';
|
|
18
19
|
import ObjectField from './ObjectField.svelte';
|
|
20
|
+
import DiagnosticList from './DiagnosticList.svelte';
|
|
21
|
+
import type { DeepDiagnostic } from './diagnostics.js';
|
|
22
|
+
import type { LandingBox } from './leaves.js';
|
|
19
23
|
import {
|
|
20
24
|
VARIANT_DISCRIMINANT,
|
|
21
25
|
commitDiscriminant,
|
|
@@ -43,6 +47,10 @@
|
|
|
43
47
|
onCommit: (v: Record<string, unknown> | undefined) => void;
|
|
44
48
|
optionAllowed?: (value: string) => boolean;
|
|
45
49
|
enumDisallowed?: 'hide' | 'disable';
|
|
50
|
+
/** The diagnostics routed into this field, each with its steps still to walk: one
|
|
51
|
+
* naming a live cell draws under it; the rest — the discriminant's, a dormant
|
|
52
|
+
* world's — draw at the field's foot. */
|
|
53
|
+
diagnostics?: readonly DeepDiagnostic[];
|
|
46
54
|
}
|
|
47
55
|
let {
|
|
48
56
|
value,
|
|
@@ -55,12 +63,52 @@
|
|
|
55
63
|
contentAt,
|
|
56
64
|
onCommit,
|
|
57
65
|
optionAllowed,
|
|
58
|
-
enumDisallowed
|
|
66
|
+
enumDisallowed,
|
|
67
|
+
diagnostics
|
|
59
68
|
}: Props = $props();
|
|
60
69
|
|
|
61
70
|
const member = $derived(variantMember(value, ghostMember));
|
|
62
71
|
const cells = $derived(variantCells(schema, member));
|
|
63
72
|
const discriminant = $derived(value?.[VARIANT_DISCRIMINANT] as string | undefined);
|
|
73
|
+
|
|
74
|
+
type Subform = {
|
|
75
|
+
focus: () => void;
|
|
76
|
+
focusPath: (steps: readonly PathStep[], pos?: number) => LandingBox;
|
|
77
|
+
};
|
|
78
|
+
let cellsEl = $state<Subform | undefined>();
|
|
79
|
+
|
|
80
|
+
/** Whether a step names a cell the live world draws. The one test the landing and
|
|
81
|
+
* the diagnostics both read, so the cell a caret lands in is the cell a message
|
|
82
|
+
* draws under. The discriminant is the field's own control and no world's cell. */
|
|
83
|
+
function live(step: PathStep | undefined): boolean {
|
|
84
|
+
return (
|
|
85
|
+
typeof step === 'string' &&
|
|
86
|
+
step !== VARIANT_DISCRIMINANT &&
|
|
87
|
+
!!cells &&
|
|
88
|
+
Object.hasOwn(cells, step)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
// The cells' own, and the field's: a diagnostic naming the discriminant or a world
|
|
92
|
+
// that is not drawn has no cell to sit under, and the subform's foot is inside a box
|
|
93
|
+
// holding the live world alone. So it draws under the field, where the control it is
|
|
94
|
+
// about is.
|
|
95
|
+
const inside = $derived((diagnostics ?? []).filter((d) => live(d.steps[0])));
|
|
96
|
+
const foot = $derived(
|
|
97
|
+
(diagnostics ?? []).filter((d) => !live(d.steps[0])).map((d) => d.diagnostic)
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
/** Take the caret: the discriminant's trigger, the field's own control. */
|
|
101
|
+
export function focus(): void {
|
|
102
|
+
if (id) document.getElementById(id)?.focus();
|
|
103
|
+
}
|
|
104
|
+
/** Land at `steps`: a live cell, and the rest of the walk inside it where the cell is
|
|
105
|
+
* a container; the discriminant, a dormant world's cell and a key no world declares
|
|
106
|
+
* land on the trigger ({@link FieldControl.focusPath}). */
|
|
107
|
+
export function focusPath(steps: readonly PathStep[], pos?: number): LandingBox {
|
|
108
|
+
if (live(steps[0]) && cellsEl) return cellsEl.focusPath(steps, pos);
|
|
109
|
+
focus();
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
64
112
|
</script>
|
|
65
113
|
|
|
66
114
|
<div class="qm-variant">
|
|
@@ -81,6 +129,7 @@
|
|
|
81
129
|
{#if cells}
|
|
82
130
|
{#key member}
|
|
83
131
|
<ObjectField
|
|
132
|
+
bind:this={cellsEl}
|
|
84
133
|
value={value ?? {}}
|
|
85
134
|
properties={cells}
|
|
86
135
|
{label}
|
|
@@ -89,9 +138,13 @@
|
|
|
89
138
|
{describedBy}
|
|
90
139
|
{contentAt}
|
|
91
140
|
onCommit={(obj) => onCommit(obj)}
|
|
141
|
+
diagnostics={inside}
|
|
92
142
|
/>
|
|
93
143
|
{/key}
|
|
94
144
|
{/if}
|
|
145
|
+
<!-- The field's own foot, drawn whether or not a world is: what the live world
|
|
146
|
+
cannot hold belongs under the field rather than inside the box its cells sit in. -->
|
|
147
|
+
<DiagnosticList diagnostics={foot} />
|
|
95
148
|
</div>
|
|
96
149
|
|
|
97
150
|
<style>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
|
|
2
|
+
import type { DeepDiagnostic } from './diagnostics.js';
|
|
3
|
+
import type { LandingBox } from './leaves.js';
|
|
2
4
|
interface Props {
|
|
3
5
|
/** The stored container, or undefined while the field is unset. */
|
|
4
6
|
value: Record<string, unknown> | undefined;
|
|
@@ -19,7 +21,14 @@ interface Props {
|
|
|
19
21
|
onCommit: (v: Record<string, unknown> | undefined) => void;
|
|
20
22
|
optionAllowed?: (value: string) => boolean;
|
|
21
23
|
enumDisallowed?: 'hide' | 'disable';
|
|
24
|
+
/** The diagnostics routed into this field, each with its steps still to walk: one
|
|
25
|
+
* naming a live cell draws under it; the rest — the discriminant's, a dormant
|
|
26
|
+
* world's — draw at the field's foot. */
|
|
27
|
+
diagnostics?: readonly DeepDiagnostic[];
|
|
22
28
|
}
|
|
23
|
-
declare const VariantField: import("svelte").Component<Props, {
|
|
29
|
+
declare const VariantField: import("svelte").Component<Props, {
|
|
30
|
+
focus: () => void;
|
|
31
|
+
focusPath: (steps: readonly PathStep[], pos?: number) => LandingBox;
|
|
32
|
+
}, "">;
|
|
24
33
|
type VariantField = ReturnType<typeof VariantField>;
|
|
25
34
|
export default VariantField;
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import {
|
|
25
25
|
addrForFieldPath,
|
|
26
26
|
cardPath,
|
|
27
|
-
|
|
27
|
+
nestedAddrForFieldPath,
|
|
28
28
|
fieldPathForAddr,
|
|
29
29
|
type DocPath,
|
|
30
30
|
type Landing
|
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
Addr,
|
|
37
37
|
CardAddr,
|
|
38
38
|
Diagnostic,
|
|
39
|
+
PathStep,
|
|
39
40
|
PayloadItem,
|
|
41
|
+
QuillFieldSchema,
|
|
40
42
|
Resolved,
|
|
41
43
|
ResolvedField
|
|
42
44
|
} from '@quillmark/wasm';
|
|
@@ -47,6 +49,8 @@
|
|
|
47
49
|
import {
|
|
48
50
|
IdSeq,
|
|
49
51
|
controlKind,
|
|
52
|
+
isContainer,
|
|
53
|
+
schemaAt,
|
|
50
54
|
fieldModels,
|
|
51
55
|
groupOrder,
|
|
52
56
|
groupSections,
|
|
@@ -75,7 +79,7 @@
|
|
|
75
79
|
} from './diagnostics.js';
|
|
76
80
|
import { fieldDomIds, groupPanelId } from './domid.js';
|
|
77
81
|
import { createLeafRegistry, type FieldControl } from './leaves.js';
|
|
78
|
-
import { reorder, reorderTrips } from './motion.js';
|
|
82
|
+
import { reorder, reorderArm, reorderTrips } from './motion.js';
|
|
79
83
|
import { tipsChannel } from './tips.js';
|
|
80
84
|
import { patchEditorExt } from './ext.js';
|
|
81
85
|
import Card from './Card.svelte';
|
|
@@ -424,26 +428,17 @@
|
|
|
424
428
|
return undefined;
|
|
425
429
|
}
|
|
426
430
|
}
|
|
427
|
-
// The reorder gesture's arming window: the reconcile that moves a
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
// microtask after the mutation and well inside the frame that disarms it.
|
|
432
|
-
let reordering = false;
|
|
433
|
-
let reorderFrame = 0;
|
|
434
|
-
function armReorder(): void {
|
|
435
|
-
reordering = true;
|
|
436
|
-
reorderFrame = requestAnimationFrame(() => (reordering = false));
|
|
437
|
-
}
|
|
438
|
-
span.onEnd(() => cancelAnimationFrame(reorderFrame));
|
|
439
|
-
const isReordering = (): boolean => reordering;
|
|
431
|
+
// The reorder gesture's arming window (`motion.ts`): the reconcile that moves a slot
|
|
432
|
+
// is the trip, and every other reconcile that happens to move one is not.
|
|
433
|
+
const arm = reorderArm();
|
|
434
|
+
span.onEnd(arm.cancel);
|
|
440
435
|
|
|
441
436
|
function moveCardById(id: string, dir: -1 | 1): void {
|
|
442
437
|
const from = cardIndexOf(id);
|
|
443
438
|
if (from < 0) return;
|
|
444
439
|
const to = from + dir;
|
|
445
440
|
if (to < 0 || to >= doc.cardCount) return;
|
|
446
|
-
|
|
441
|
+
arm.arm();
|
|
447
442
|
doc.moveCard(from, to);
|
|
448
443
|
const w = cardIds.slice();
|
|
449
444
|
const [x] = w.splice(from, 1);
|
|
@@ -691,7 +686,8 @@
|
|
|
691
686
|
// click is one discrete act, and its commonest target is the leaf already focused
|
|
692
687
|
// (where landing a caret changes nothing on screen) or one off-screen, where the
|
|
693
688
|
// browser's focus-scroll moves the page and leaves the caret to be hunted for.
|
|
694
|
-
|
|
689
|
+
const box = await land(found, at.granularity === 'segment' ? undefined : at.pos);
|
|
690
|
+
if (span.alive) bloomInside(box);
|
|
695
691
|
}
|
|
696
692
|
/** The active leaf's controller: the formatting popover's observation seam.
|
|
697
693
|
* `undefined` for a focused form control, which holds no marks to toggle. */
|
|
@@ -730,20 +726,22 @@
|
|
|
730
726
|
|
|
731
727
|
/**
|
|
732
728
|
* Put the caret in a revealed target, at the finest grain it can take: a USV offset
|
|
733
|
-
* in a prose leaf or
|
|
734
|
-
* has no coordinate to spend an offset in (an `<input type="number">`
|
|
735
|
-
* selection outright), which is also the whole of what a click on
|
|
736
|
-
* can mean.
|
|
737
|
-
*
|
|
729
|
+
* in a prose leaf or a nested content cell, a bare focus everywhere else. A form
|
|
730
|
+
* control has no coordinate to spend an offset in (an `<input type="number">`
|
|
731
|
+
* refuses a selection outright), which is also the whole of what a click on
|
|
732
|
+
* plate-placed ink can mean. A cell is no `createField` leaf, so the offset goes
|
|
733
|
+
* down the container's walk instead, where what it means is the leaf's (`leaves.ts`).
|
|
738
734
|
*
|
|
739
735
|
* Hands back the box the arrival wash blooms in, which is the landing's own
|
|
740
|
-
* granularity rather than the registry's:
|
|
741
|
-
*
|
|
742
|
-
* dropped — over the field's
|
|
736
|
+
* granularity rather than the registry's: a walk lands in one row and says so over
|
|
737
|
+
* the innermost row it opened, and everything else — the field, a property under a
|
|
738
|
+
* field-level subform, and a row the document has since dropped — over the field's
|
|
739
|
+
* box. Async because a row two closed boxes down exists only once the row above it
|
|
740
|
+
* has opened and flushed.
|
|
743
741
|
*/
|
|
744
|
-
function land(found: Landed, pos: number | undefined): HTMLElement {
|
|
745
|
-
if (found.
|
|
746
|
-
return found.control.
|
|
742
|
+
async function land(found: Landed, pos: number | undefined): Promise<HTMLElement> {
|
|
743
|
+
if (found.steps && found.control.focusPath) {
|
|
744
|
+
return (await found.control.focusPath(found.steps, pos)) ?? found.control.el;
|
|
747
745
|
}
|
|
748
746
|
if (pos != null) {
|
|
749
747
|
const prose = leaves.prose(found.key);
|
|
@@ -772,7 +770,8 @@
|
|
|
772
770
|
export async function focusField(field: DocPath): Promise<void> {
|
|
773
771
|
const found = await revealLeaf(field);
|
|
774
772
|
if (!found) return missed(`no mounted field at ${field}`, field);
|
|
775
|
-
|
|
773
|
+
const box = await land(found, undefined);
|
|
774
|
+
if (span.alive) bloomInside(box);
|
|
776
775
|
}
|
|
777
776
|
/** Seed a card of `kind` and insert it at `at` (default: the end). Returns the new
|
|
778
777
|
* card's session key, or `undefined` when the quill seeds no card of that kind. */
|
|
@@ -802,11 +801,11 @@
|
|
|
802
801
|
reportError(onError, { code: 'target-unknown', severity: 'dev', message, path });
|
|
803
802
|
}
|
|
804
803
|
|
|
805
|
-
/** What a `DocPath` resolves to in the mounted tree: a leaf key, and the
|
|
806
|
-
*
|
|
804
|
+
/** What a `DocPath` resolves to in the mounted tree: a leaf key, and the steps from
|
|
805
|
+
* that field to the leaf when the address names one inside it. */
|
|
807
806
|
interface LeafTarget {
|
|
808
807
|
key: string;
|
|
809
|
-
|
|
808
|
+
steps?: PathStep[];
|
|
810
809
|
}
|
|
811
810
|
/** A resolved target with the handle the registry holds for it. */
|
|
812
811
|
type Landed = LeafTarget & { control: FieldControl };
|
|
@@ -816,11 +815,13 @@
|
|
|
816
815
|
* diagnostics take.
|
|
817
816
|
*
|
|
818
817
|
* Two rungs, because the boundary mints addresses at a finer granularity than
|
|
819
|
-
* `Addr` can name: a `richtext[]` element surfaces as `main.keywords[0]
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
818
|
+
* `Addr` can name: a `richtext[]` element surfaces as `main.keywords[0]`, a nested
|
|
819
|
+
* row's cell as `main.vectors[0].tours[2].title`. The second rung reads the steps
|
|
820
|
+
* past the field against the schema, rung by rung — an index under a declared
|
|
821
|
+
* array, a key under a declared object, variant or matrix — which is why the ladder
|
|
822
|
+
* is the editor's (VISUAL_EDITOR.md §Surface): the preview carries no schema, and
|
|
823
|
+
* truncating the address there is worse than guessing, since `pos` is an offset
|
|
824
|
+
* into the leaf's own content.
|
|
824
825
|
*/
|
|
825
826
|
function leafTargetFor(field: DocPath): LeafTarget | undefined {
|
|
826
827
|
const direct = addrForFieldPath(field);
|
|
@@ -828,22 +829,25 @@
|
|
|
828
829
|
const resolved = resolveCardKey(direct, cardIds);
|
|
829
830
|
return resolved ? { key: fieldKeyToString(resolved) } : undefined;
|
|
830
831
|
}
|
|
831
|
-
const
|
|
832
|
-
if (!
|
|
833
|
-
const
|
|
834
|
-
|
|
832
|
+
const nested = nestedAddrForFieldPath(field);
|
|
833
|
+
if (!nested) return undefined;
|
|
834
|
+
const declared = declaredField(nested.field);
|
|
835
|
+
if (!declared || !isContainer(controlKind(declared))) return undefined;
|
|
836
|
+
if (!schemaAt(declared, nested.steps)) return undefined;
|
|
837
|
+
const resolved = resolveCardKey(nested.field, cardIds);
|
|
838
|
+
return resolved ? { key: fieldKeyToString(resolved), steps: nested.steps } : undefined;
|
|
835
839
|
}
|
|
836
840
|
|
|
837
|
-
/** The
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
|
|
841
|
-
|
|
841
|
+
/** The schema the field at `addr` declares, off the live card's kind: the guard the
|
|
842
|
+
* nested rung stands on. Asked of `controlKind` and `schemaAt`, so what the ladder
|
|
843
|
+
* tests is the control the tree mounted — the one holding a `focusPath` — and not a
|
|
844
|
+
* second reading of the schema beside it. */
|
|
845
|
+
function declaredField(addr: Addr): QuillFieldSchema | undefined {
|
|
846
|
+
if (addr.field == null) return undefined;
|
|
842
847
|
const kind = addr.card == null ? undefined : model.cards[addr.card]?.kind;
|
|
843
848
|
const schema = quill.schema;
|
|
844
849
|
const card = addr.card == null ? schema.main : kind ? schema.card_kinds?.[kind] : undefined;
|
|
845
|
-
|
|
846
|
-
return !!declared && controlKind(declared) === 'array';
|
|
850
|
+
return card?.fields?.[addr.field];
|
|
847
851
|
}
|
|
848
852
|
</script>
|
|
849
853
|
|
|
@@ -883,7 +887,7 @@
|
|
|
883
887
|
rather than being slid across. It is also the shape `animate:` asks for, being the
|
|
884
888
|
keyed block's only child. -->
|
|
885
889
|
{#each model.cards as c, i (c.id)}
|
|
886
|
-
<div class="qm-card-slot" bind:this={slotEls[i]} animate:reorder={
|
|
890
|
+
<div class="qm-card-slot" bind:this={slotEls[i]} animate:reorder={arm.armed}>
|
|
887
891
|
<Card
|
|
888
892
|
bind:this={cardRefs[i]}
|
|
889
893
|
card={titled(c)}
|
package/dist/visual/controls.css
CHANGED
|
@@ -255,6 +255,37 @@
|
|
|
255
255
|
transform: translate(-50%, -50%);
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
/* The capacity ladder: how many tracks a grid of fields steps to, off the width of
|
|
259
|
+
the query container it stands in. One recipe, because three grids read it — a section's
|
|
260
|
+
fields (`Card.svelte`), a subform's properties (`ObjectField`), each over its own
|
|
261
|
+
container, and a compact matrix's members (`MatrixField`) — and a ladder kept in
|
|
262
|
+
several files agrees until one is retuned. Capacity is
|
|
263
|
+
the container's, not JavaScript's: nothing measures, so there is no observer to loop
|
|
264
|
+
and no re-packing to restructure the DOM under a prose leaf.
|
|
265
|
+
|
|
266
|
+
It steps 1 → 2 → 4, skipping 3: each rung is the width at which a track still clears
|
|
267
|
+
the comfortable field minimum, and an even capacity is what lets the half a subform
|
|
268
|
+
of one takes (`--cols-half`) land on a track boundary. A field ends on its track's
|
|
269
|
+
own edge, so a rung is `n * 220px + gaps`. A subform three rungs in stands three
|
|
270
|
+
`--_qm-nest` and three verticals narrower than its section, and steps down where the
|
|
271
|
+
section does not: the count is the width the tracks actually get. */
|
|
272
|
+
:where(.qm-tracks) {
|
|
273
|
+
--cols: 1;
|
|
274
|
+
--cols-half: 1;
|
|
275
|
+
}
|
|
276
|
+
@container (min-width: 28rem) {
|
|
277
|
+
:where(.qm-tracks) {
|
|
278
|
+
--cols: 2;
|
|
279
|
+
--cols-half: 1;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
@container (min-width: 57rem) {
|
|
283
|
+
:where(.qm-tracks) {
|
|
284
|
+
--cols: 4;
|
|
285
|
+
--cols-half: 2;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
258
289
|
/* A floating list of choices: the enum's listbox and the card stack's kind menu.
|
|
259
290
|
Both portal into the nearest `[data-qm-root]`, and both are picked from, so the
|
|
260
291
|
surface (its edge, its inset, its ink) and the item (its pad, its radius, the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Diagnostic } from '@quillmark/wasm';
|
|
1
|
+
import type { Diagnostic, PathStep } from '@quillmark/wasm';
|
|
2
2
|
/** A field's routing address, and `/core`'s `Addr` structurally: an `Addr` is a
|
|
3
3
|
* positional `FieldKey`, so `nearestAddrForFieldPath` is the path→key walk and
|
|
4
4
|
* routing carries no second copy of the grammar. `card` is `undefined` for the main
|
|
@@ -40,10 +40,34 @@ export interface RoutedDiagnostic {
|
|
|
40
40
|
* Completeness stays a read the host makes on `quill.validate(doc)`.
|
|
41
41
|
*/
|
|
42
42
|
export declare function routeAndResolve(diagnostics: Diagnostic[] | undefined, cardIds: readonly string[]): RoutedDiagnostic[];
|
|
43
|
+
/** A diagnostic with the steps still to walk from the control holding it. */
|
|
44
|
+
export interface DeepDiagnostic {
|
|
45
|
+
steps: readonly PathStep[];
|
|
46
|
+
diagnostic: Diagnostic;
|
|
47
|
+
}
|
|
48
|
+
/** A field's routed diagnostics, each with its steps past the field: none for a
|
|
49
|
+
* diagnostic anchored at the field itself, or one whose path is not a nested one
|
|
50
|
+
* (a local commit error carries the field's own path). */
|
|
51
|
+
export declare function deepen(diagnostics: Diagnostic[] | undefined): DeepDiagnostic[];
|
|
52
|
+
/** One rung of the walk: the diagnostics anchored here, and the rest bucketed by the
|
|
53
|
+
* step they take next, each with that step consumed. */
|
|
54
|
+
export interface DeepSplit {
|
|
55
|
+
here: Diagnostic[];
|
|
56
|
+
below: Map<PathStep, DeepDiagnostic[]>;
|
|
57
|
+
}
|
|
58
|
+
export declare function splitDeep(list: readonly DeepDiagnostic[] | undefined): DeepSplit;
|
|
59
|
+
/** The diagnostics a rung cannot hand down: the ones anchored here, plus every one
|
|
60
|
+
* whose next step names nothing in `held`. Drawn at the rung's own foot, so a leaf the
|
|
61
|
+
* tree does not draw still has its message read somewhere the user can see. */
|
|
62
|
+
export declare function unrouted(split: DeepSplit, held: (step: PathStep) => boolean): Diagnostic[];
|
|
43
63
|
/**
|
|
44
64
|
* Merge N routed groups into `Map<fieldKeyToString(key), Diagnostic[]>`.
|
|
45
|
-
* Dedupes an identical `(severity, message)`
|
|
65
|
+
* Dedupes an identical `(path, severity, message)` quad landing on the same key from
|
|
46
66
|
* more than one group (e.g. the same error present in both `validate()` and
|
|
47
67
|
* an external feed). Producer order within a key.
|
|
68
|
+
*
|
|
69
|
+
* The key is the field, but the walk past it is the `path`'s ({@link deepen}), so two
|
|
70
|
+
* cells of one container share a key and differ only there: without `path` in the
|
|
71
|
+
* quad, one message failing two rows draws on the first row alone.
|
|
48
72
|
*/
|
|
49
73
|
export declare function mergeDiagnostics(...groups: RoutedDiagnostic[][]): Map<string, Diagnostic[]>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { nearestAddrForFieldPath } from '../core/address.js';
|
|
1
|
+
import { nearestAddrForFieldPath, nestedAddrForFieldPath } from '../core/address.js';
|
|
2
2
|
/** Canonical string key for a `FieldKey`: the routing `Map`'s key space. */
|
|
3
3
|
export function fieldKeyToString(k) {
|
|
4
4
|
return `${k.card ?? 'main'}:${k.field ?? '$body'}`;
|
|
@@ -42,11 +42,53 @@ export function routeAndResolve(diagnostics, cardIds) {
|
|
|
42
42
|
}
|
|
43
43
|
return out;
|
|
44
44
|
}
|
|
45
|
+
/** A field's routed diagnostics, each with its steps past the field: none for a
|
|
46
|
+
* diagnostic anchored at the field itself, or one whose path is not a nested one
|
|
47
|
+
* (a local commit error carries the field's own path). */
|
|
48
|
+
export function deepen(diagnostics) {
|
|
49
|
+
return (diagnostics ?? []).map((d) => ({
|
|
50
|
+
steps: (d.path && nestedAddrForFieldPath(d.path)?.steps) || [],
|
|
51
|
+
diagnostic: d
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
export function splitDeep(list) {
|
|
55
|
+
const here = [];
|
|
56
|
+
const below = new Map();
|
|
57
|
+
for (const d of list ?? []) {
|
|
58
|
+
if (d.steps.length === 0) {
|
|
59
|
+
here.push(d.diagnostic);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const [step, ...rest] = d.steps;
|
|
63
|
+
const bucket = below.get(step);
|
|
64
|
+
const next = { steps: rest, diagnostic: d.diagnostic };
|
|
65
|
+
if (bucket)
|
|
66
|
+
bucket.push(next);
|
|
67
|
+
else
|
|
68
|
+
below.set(step, [next]);
|
|
69
|
+
}
|
|
70
|
+
return { here, below };
|
|
71
|
+
}
|
|
72
|
+
/** The diagnostics a rung cannot hand down: the ones anchored here, plus every one
|
|
73
|
+
* whose next step names nothing in `held`. Drawn at the rung's own foot, so a leaf the
|
|
74
|
+
* tree does not draw still has its message read somewhere the user can see. */
|
|
75
|
+
export function unrouted(split, held) {
|
|
76
|
+
const out = [...split.here];
|
|
77
|
+
for (const [step, list] of split.below)
|
|
78
|
+
if (!held(step))
|
|
79
|
+
for (const d of list)
|
|
80
|
+
out.push(d.diagnostic);
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
45
83
|
/**
|
|
46
84
|
* Merge N routed groups into `Map<fieldKeyToString(key), Diagnostic[]>`.
|
|
47
|
-
* Dedupes an identical `(severity, message)`
|
|
85
|
+
* Dedupes an identical `(path, severity, message)` quad landing on the same key from
|
|
48
86
|
* more than one group (e.g. the same error present in both `validate()` and
|
|
49
87
|
* an external feed). Producer order within a key.
|
|
88
|
+
*
|
|
89
|
+
* The key is the field, but the walk past it is the `path`'s ({@link deepen}), so two
|
|
90
|
+
* cells of one container share a key and differ only there: without `path` in the
|
|
91
|
+
* quad, one message failing two rows draws on the first row alone.
|
|
50
92
|
*/
|
|
51
93
|
export function mergeDiagnostics(...groups) {
|
|
52
94
|
const byKey = new Map();
|
|
@@ -54,9 +96,14 @@ export function mergeDiagnostics(...groups) {
|
|
|
54
96
|
for (const group of groups) {
|
|
55
97
|
for (const { key, diagnostic } of group) {
|
|
56
98
|
const k = fieldKeyToString(key);
|
|
57
|
-
// Collision-proof key: JSON-encode the
|
|
58
|
-
// can never merge two distinct
|
|
59
|
-
const dedupeKey = JSON.stringify([
|
|
99
|
+
// Collision-proof key: JSON-encode the quad so arbitrary `message` text
|
|
100
|
+
// can never merge two distinct quads into one (a real diagnostic dropped).
|
|
101
|
+
const dedupeKey = JSON.stringify([
|
|
102
|
+
k,
|
|
103
|
+
diagnostic.path ?? null,
|
|
104
|
+
diagnostic.severity,
|
|
105
|
+
diagnostic.message
|
|
106
|
+
]);
|
|
60
107
|
if (seen.has(dedupeKey))
|
|
61
108
|
continue;
|
|
62
109
|
seen.add(dedupeKey);
|
package/dist/visual/hold.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// the layout, and what fills the viewport as a section collapses is that section's own
|
|
3
3
|
// content, whose top never moves.
|
|
4
4
|
import { flushSync } from 'svelte';
|
|
5
|
-
import { rungMs } from '
|
|
5
|
+
import { rungMs, rungStyle } from '../core/rungs.js';
|
|
6
6
|
/**
|
|
7
7
|
* Run `change` and keep `anchor` inside the fold for the length of the move.
|
|
8
8
|
*
|
|
@@ -26,7 +26,7 @@ export function holdInView(anchor, change) {
|
|
|
26
26
|
flushSync();
|
|
27
27
|
const reveal = () => anchor.scrollIntoView({ block: 'nearest', behavior: 'instant' });
|
|
28
28
|
reveal();
|
|
29
|
-
const rung = rungMs(
|
|
29
|
+
const rung = rungMs(rungStyle(anchor), '--_qm-duration-slow');
|
|
30
30
|
if (rung === undefined)
|
|
31
31
|
return;
|
|
32
32
|
const until = performance.now() + rung;
|
|
@@ -116,6 +116,9 @@ export declare const ICONS: {
|
|
|
116
116
|
readonly minus: readonly [readonly ["path", {
|
|
117
117
|
readonly d: "M5 12h14";
|
|
118
118
|
}]];
|
|
119
|
+
readonly check: readonly [readonly ["path", {
|
|
120
|
+
readonly d: "M20 6 9 17l-5-5";
|
|
121
|
+
}]];
|
|
119
122
|
};
|
|
120
123
|
/** The closed set. A name that is not here is a compile error at the call site,
|
|
121
124
|
* which is what a string key buys over thirteen imported components. */
|
package/dist/visual/leaves.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
import type { PathStep } from '@quillmark/wasm';
|
|
1
2
|
import type { FieldController } from '../core/codec/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* What a landing at depth hands back: the box the arrival wash blooms in, now or once
|
|
5
|
+
* the row it had to open has rendered. A row two closed boxes down does not exist
|
|
6
|
+
* until the row above it opens and flushes, so the innermost box is a promise wherever
|
|
7
|
+
* the walk had to open something on the way.
|
|
8
|
+
*/
|
|
9
|
+
export type LandingBox = HTMLElement | undefined | Promise<HTMLElement | undefined>;
|
|
2
10
|
/**
|
|
3
11
|
* A mounted field's landing handle: focus it, and the element the arrival wash blooms
|
|
4
12
|
* inside ({@link import('../core/bloom.js').bloomInside}).
|
|
@@ -10,22 +18,27 @@ import type { FieldController } from '../core/codec/index.js';
|
|
|
10
18
|
export interface FieldControl {
|
|
11
19
|
focus(): void;
|
|
12
20
|
/**
|
|
13
|
-
* Land
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
21
|
+
* Land at `steps` inside a control that has an inside — an array's element, an
|
|
22
|
+
* object's property, a variant's cell, a matrix's member, and any of them under any
|
|
23
|
+
* other — at USV `pos` where the leaf the walk ends in can take one. Each rung
|
|
24
|
+
* resolves its own step at the call (an index to a session id, a key to a mounted
|
|
25
|
+
* cell) and hands the rest to the control it opened, so nothing positional is
|
|
26
|
+
* carried. A step the tree does not hold falls back to that rung's own
|
|
27
|
+
* {@link focus}: the field is right and the row is gone. Absent on every control
|
|
28
|
+
* with no inside.
|
|
17
29
|
*
|
|
18
30
|
* A landing, not a focus: an absent `pos` is the placement rung, exactly as on
|
|
19
|
-
* `Landing`, and what a `pos` means is the
|
|
20
|
-
*
|
|
21
|
-
*
|
|
31
|
+
* `Landing`, and what a `pos` means is the leaf's, the same way focusing is. The
|
|
32
|
+
* place rides the call because the registry stays parent-keyed: a per-element key is
|
|
33
|
+
* positional, in a registry whose doctrine is dodging positional churn.
|
|
22
34
|
*
|
|
23
|
-
* Returns the
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* reads as
|
|
35
|
+
* Returns the innermost row the address named, which is what the wash blooms in: a
|
|
36
|
+
* box wider than the one the click resolved says the field where the pick said the
|
|
37
|
+
* row. `undefined` is a landing that named no row — a property under a field-level
|
|
38
|
+
* subform, or the fallback an index past the live list took — and reads as
|
|
39
|
+
* {@link el}.
|
|
27
40
|
*/
|
|
28
|
-
|
|
41
|
+
focusPath?(steps: readonly PathStep[], pos?: number): LandingBox;
|
|
29
42
|
readonly el: HTMLElement;
|
|
30
43
|
}
|
|
31
44
|
/**
|
package/dist/visual/motion.d.ts
CHANGED
|
@@ -3,11 +3,20 @@ import type { AnimationConfig } from 'svelte/animate';
|
|
|
3
3
|
* position for its whole length, so anything that measures the node's box waits on
|
|
4
4
|
* these rather than reading the one a transform is holding. */
|
|
5
5
|
export declare function reorderTrips(node: HTMLElement): Animation[];
|
|
6
|
-
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The reorder gesture's arming window, for a keyed list that takes the trip. The
|
|
8
|
+
* reconcile that moves a slot is the trip, and every other reconcile that happens to
|
|
9
|
+
* move one is not; so the command arms the gesture and the frame it lands in disarms
|
|
10
|
+
* it, `animate:` asking at apply time, a microtask after the mutation and well inside
|
|
11
|
+
* that frame. `armed` is a getter rather than a value, so the flag stays out of the
|
|
12
|
+
* template and needs no reactivity for it. `cancel` is for the surface's teardown.
|
|
13
|
+
*/
|
|
14
|
+
export interface ReorderArm {
|
|
15
|
+
arm(): void;
|
|
16
|
+
armed(): boolean;
|
|
17
|
+
cancel(): void;
|
|
18
|
+
}
|
|
19
|
+
export declare function reorderArm(): ReorderArm;
|
|
11
20
|
/**
|
|
12
21
|
* Slide a keyed slot from where it was to where it now is.
|
|
13
22
|
*
|