@quillmark/svelte 0.11.0 → 0.13.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.d.ts +17 -0
- package/dist/core/codec/decode.js +78 -19
- package/dist/core/codec/encode.js +1 -1
- package/dist/core/codec/field.d.ts +64 -15
- package/dist/core/codec/field.js +201 -65
- package/dist/core/codec/index.d.ts +3 -3
- package/dist/core/codec/index.js +3 -3
- package/dist/core/codec/islands.d.ts +5 -4
- package/dist/core/codec/islands.js +23 -38
- package/dist/core/codec/keymap.d.ts +5 -4
- package/dist/core/codec/keymap.js +5 -4
- package/dist/core/codec/prose.css +42 -0
- package/dist/core/codec/schema.d.ts +24 -5
- package/dist/core/codec/schema.js +40 -9
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/errors.d.ts +3 -2
- package/dist/core/rungs.d.ts +8 -0
- package/dist/core/rungs.js +24 -0
- package/dist/core/theme.css +19 -6
- package/dist/preview/paint.js +21 -17
- package/dist/visual/ArrayField.svelte +649 -176
- package/dist/visual/ArrayField.svelte.d.ts +16 -1
- package/dist/visual/BooleanField.svelte +36 -11
- package/dist/visual/BooleanField.svelte.d.ts +2 -0
- package/dist/visual/Card.svelte +6 -25
- package/dist/visual/DateField.svelte +66 -31
- package/dist/visual/EnumField.svelte +27 -9
- package/dist/visual/EnumField.svelte.d.ts +5 -0
- package/dist/visual/Field.svelte +113 -38
- package/dist/visual/FormatPopover.svelte +4 -3
- package/dist/visual/MatrixField.svelte +353 -0
- package/dist/visual/MatrixField.svelte.d.ts +36 -0
- package/dist/visual/NumberField.svelte +31 -14
- package/dist/visual/NumberField.svelte.d.ts +5 -1
- package/dist/visual/ObjectField.svelte +404 -139
- package/dist/visual/ObjectField.svelte.d.ts +17 -2
- package/dist/visual/ProseField.svelte +53 -23
- package/dist/visual/ProseField.svelte.d.ts +15 -3
- package/dist/visual/ProseValue.svelte +102 -46
- package/dist/visual/ProseValue.svelte.d.ts +17 -5
- package/dist/visual/TextField.svelte +35 -18
- package/dist/visual/TextField.svelte.d.ts +7 -0
- package/dist/visual/VariantField.svelte +56 -2
- package/dist/visual/VariantField.svelte.d.ts +10 -1
- package/dist/visual/VisualEditorInner.svelte +77 -73
- package/dist/visual/controls.css +47 -5
- 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 +27 -10
- package/dist/visual/strings.js +8 -2
- package/dist/visual/structure.d.ts +159 -40
- package/dist/visual/structure.js +337 -45
- 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">
|
|
@@ -68,6 +116,7 @@
|
|
|
68
116
|
value={discriminant}
|
|
69
117
|
values={schema.values ?? []}
|
|
70
118
|
fallback={ghostMember}
|
|
119
|
+
blankTitle={schema.ui?.blank_title}
|
|
71
120
|
{id}
|
|
72
121
|
{describedBy}
|
|
73
122
|
onCommit={(v) => onCommit(commitDiscriminant(value, v))}
|
|
@@ -81,6 +130,7 @@
|
|
|
81
130
|
{#if cells}
|
|
82
131
|
{#key member}
|
|
83
132
|
<ObjectField
|
|
133
|
+
bind:this={cellsEl}
|
|
84
134
|
value={value ?? {}}
|
|
85
135
|
properties={cells}
|
|
86
136
|
{label}
|
|
@@ -89,9 +139,13 @@
|
|
|
89
139
|
{describedBy}
|
|
90
140
|
{contentAt}
|
|
91
141
|
onCommit={(obj) => onCommit(obj)}
|
|
142
|
+
diagnostics={inside}
|
|
92
143
|
/>
|
|
93
144
|
{/key}
|
|
94
145
|
{/if}
|
|
146
|
+
<!-- The field's own foot, drawn whether or not a world is: what the live world
|
|
147
|
+
cannot hold belongs under the field rather than inside the box its cells sit in. -->
|
|
148
|
+
<DiagnosticList diagnostics={foot} />
|
|
95
149
|
</div>
|
|
96
150
|
|
|
97
151
|
<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,19 +49,22 @@
|
|
|
47
49
|
import {
|
|
48
50
|
IdSeq,
|
|
49
51
|
controlKind,
|
|
52
|
+
isContainer,
|
|
53
|
+
shortCell,
|
|
54
|
+
schemaAt,
|
|
50
55
|
fieldModels,
|
|
51
56
|
groupOrder,
|
|
52
57
|
groupSections,
|
|
53
58
|
groupLabel,
|
|
54
59
|
cardTitle,
|
|
55
|
-
|
|
60
|
+
kindTitle,
|
|
56
61
|
fieldValues,
|
|
57
62
|
bodyEnabled,
|
|
58
|
-
humanize,
|
|
59
63
|
provenanceMap,
|
|
60
64
|
resolvedByCardIndex,
|
|
61
65
|
ghostDefault,
|
|
62
|
-
|
|
66
|
+
declaredGhost,
|
|
67
|
+
titleText,
|
|
63
68
|
resolveBodyGhost,
|
|
64
69
|
NO_RESOLVED_ROWS,
|
|
65
70
|
type CardModel,
|
|
@@ -75,7 +80,7 @@
|
|
|
75
80
|
} from './diagnostics.js';
|
|
76
81
|
import { fieldDomIds, groupPanelId } from './domid.js';
|
|
77
82
|
import { createLeafRegistry, type FieldControl } from './leaves.js';
|
|
78
|
-
import { reorder, reorderTrips } from './motion.js';
|
|
83
|
+
import { reorder, reorderArm, reorderTrips } from './motion.js';
|
|
79
84
|
import { tipsChannel } from './tips.js';
|
|
80
85
|
import { patchEditorExt } from './ext.js';
|
|
81
86
|
import Card from './Card.svelte';
|
|
@@ -282,24 +287,22 @@
|
|
|
282
287
|
onChange?.({ source: 'prose', cardId, path: pathFor(plain) });
|
|
283
288
|
}
|
|
284
289
|
|
|
285
|
-
/** A card's
|
|
286
|
-
* one piece of the model that lane
|
|
287
|
-
* the next bump, which rebuilds it from the document. The
|
|
288
|
-
*
|
|
289
|
-
* names the field. */
|
|
290
|
+
/** A header named by the card's own values, after a prose commit to one that can name
|
|
291
|
+
* it: the one piece of the model that lane moves, held beside the model and dropped
|
|
292
|
+
* on the next bump, which rebuilds it from the document. The card is read only for a
|
|
293
|
+
* one-line prose field of a kind declaring no `title` (`cardTitle`). */
|
|
290
294
|
let liveTitles = $state<Record<CardId, string>>({});
|
|
291
295
|
function refreshTitle(addr: Addr, cardId: CardId): void {
|
|
292
296
|
const { card: at, field } = addr;
|
|
293
|
-
if (field == null) return;
|
|
294
|
-
const shown =
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (!live) return;
|
|
297
|
+
if (at == null || field == null) return;
|
|
298
|
+
const shown = model.cards[at];
|
|
299
|
+
const cardSchema = shown && quill.schema.card_kinds?.[shown.kind];
|
|
300
|
+
if (!cardSchema || cardSchema.title?.trim()) return;
|
|
301
|
+
const sub = Object.hasOwn(cardSchema.fields, field) ? cardSchema.fields[field] : undefined;
|
|
302
|
+
if (!sub || !shortCell(sub)) return;
|
|
300
303
|
liveTitles = {
|
|
301
304
|
...liveTitles,
|
|
302
|
-
[cardId]: cardTitle(cardSchema, shown.kind, fieldValues(
|
|
305
|
+
[cardId]: cardTitle(cardSchema, shown.kind, fieldValues(doc.card(at).payloadItems), undefined)
|
|
303
306
|
};
|
|
304
307
|
}
|
|
305
308
|
function titled(c: CardModel): CardModel {
|
|
@@ -324,11 +327,11 @@
|
|
|
324
327
|
const keyStr = fieldKeyToString(key);
|
|
325
328
|
try {
|
|
326
329
|
if (value === undefined) {
|
|
327
|
-
// The unset rung
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
330
|
+
// The unset rung (VISUAL_EDITOR §"The commitment ladder"): removing the field
|
|
331
|
+
// leaves the engine's authored › `default:` › blank-fill resolve to render the
|
|
332
|
+
// default, where writing one bakes a snapshot the schema cannot track (canon
|
|
333
|
+
// SCHEMAS.md: the engine never writes a default; the editor writes one only
|
|
334
|
+
// as the edit that takes it). Removal writes no value, so there is
|
|
332
335
|
// nothing for a schema to conform and the lane is the quill-free one.
|
|
333
336
|
if (isMain) {
|
|
334
337
|
doc.removeField(name);
|
|
@@ -424,26 +427,17 @@
|
|
|
424
427
|
return undefined;
|
|
425
428
|
}
|
|
426
429
|
}
|
|
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;
|
|
430
|
+
// The reorder gesture's arming window (`motion.ts`): the reconcile that moves a slot
|
|
431
|
+
// is the trip, and every other reconcile that happens to move one is not.
|
|
432
|
+
const arm = reorderArm();
|
|
433
|
+
span.onEnd(arm.cancel);
|
|
440
434
|
|
|
441
435
|
function moveCardById(id: string, dir: -1 | 1): void {
|
|
442
436
|
const from = cardIndexOf(id);
|
|
443
437
|
if (from < 0) return;
|
|
444
438
|
const to = from + dir;
|
|
445
439
|
if (to < 0 || to >= doc.cardCount) return;
|
|
446
|
-
|
|
440
|
+
arm.arm();
|
|
447
441
|
doc.moveCard(from, to);
|
|
448
442
|
const w = cardIds.slice();
|
|
449
443
|
const [x] = w.splice(from, 1);
|
|
@@ -617,7 +611,8 @@
|
|
|
617
611
|
// still across a re-derive because the function does.
|
|
618
612
|
bodyGhost: hasBody
|
|
619
613
|
? resolveBodyGhost(
|
|
620
|
-
|
|
614
|
+
titleText(ghostDefault(rows.body ?? undefined)) || undefined,
|
|
615
|
+
declaredGhost(cardSchema?.body?.example, true),
|
|
621
616
|
merged.bodyPlaceholder?.({ cardId: id, kind, isMain }),
|
|
622
617
|
merged.bodyGhost
|
|
623
618
|
)
|
|
@@ -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
|
|
|
@@ -854,7 +858,7 @@
|
|
|
854
858
|
<div class="qm-primary">
|
|
855
859
|
<Card
|
|
856
860
|
bind:this={mainCard}
|
|
857
|
-
card={
|
|
861
|
+
card={model.main}
|
|
858
862
|
{doc}
|
|
859
863
|
{quill}
|
|
860
864
|
isFirst={true}
|
|
@@ -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)}
|
|
@@ -953,7 +957,7 @@
|
|
|
953
957
|
<div class="qm-menu-surface" data-qm-root>
|
|
954
958
|
{#each kinds as k (k)}
|
|
955
959
|
<DropdownMenu.Item class="qm-menu-item" onSelect={() => addCard(atIndex, k)}
|
|
956
|
-
>{
|
|
960
|
+
>{kindTitle(quill.schema.card_kinds?.[k], k)}</DropdownMenu.Item
|
|
957
961
|
>
|
|
958
962
|
{/each}
|
|
959
963
|
</div>
|
package/dist/visual/controls.css
CHANGED
|
@@ -105,14 +105,25 @@
|
|
|
105
105
|
width: 100%;
|
|
106
106
|
box-sizing: border-box;
|
|
107
107
|
}
|
|
108
|
-
/*
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
/* An unset field's text where its `default:` prints (`TextField`, `NumberField`, a
|
|
109
|
+
prose leaf): the value, at the default rung, so it reads as an answer and never as a
|
|
110
|
+
written one. The first edit writes it and the rung goes with the attribute. */
|
|
111
|
+
:where(.qm-input[data-default], .ProseMirror[data-default]) {
|
|
112
|
+
color: var(--_qm-ink-default);
|
|
113
|
+
}
|
|
114
|
+
/* The native placeholder is the label rung, not the UA's own grey: what an input
|
|
115
|
+
ghosts is words about a value it does not hold — an `example:`, `None` — which is
|
|
116
|
+
content and reads at a content's contrast, the same step the prose leaf's ghost
|
|
117
|
+
reads (`prose.css`). */
|
|
113
118
|
:where(.qm-input)::placeholder {
|
|
114
119
|
color: var(--_qm-ink-label);
|
|
115
120
|
}
|
|
121
|
+
/* The note a held prose leaf draws inside its box, under the text it is about. */
|
|
122
|
+
:where(.qm-prose-held-note) {
|
|
123
|
+
display: block;
|
|
124
|
+
font-size: var(--_qm-text-label);
|
|
125
|
+
color: var(--_qm-ink-label);
|
|
126
|
+
}
|
|
116
127
|
|
|
117
128
|
/* The second recipe, and it is not the box above: a field is a box because you
|
|
118
129
|
type into it. A button rests with no fill; the surface arrives on hover, off the
|
|
@@ -255,6 +266,37 @@
|
|
|
255
266
|
transform: translate(-50%, -50%);
|
|
256
267
|
}
|
|
257
268
|
|
|
269
|
+
/* The capacity ladder: how many tracks a grid of fields steps to, off the width of
|
|
270
|
+
the query container it stands in. One recipe, because three grids read it — a section's
|
|
271
|
+
fields (`Card.svelte`), a subform's properties (`ObjectField`), each over its own
|
|
272
|
+
container, and a compact matrix's members (`MatrixField`) — and a ladder kept in
|
|
273
|
+
several files agrees until one is retuned. Capacity is
|
|
274
|
+
the container's, not JavaScript's: nothing measures, so there is no observer to loop
|
|
275
|
+
and no re-packing to restructure the DOM under a prose leaf.
|
|
276
|
+
|
|
277
|
+
It steps 1 → 2 → 4, skipping 3: each rung is the width at which a track still clears
|
|
278
|
+
the comfortable field minimum, and an even capacity is what lets the half a subform
|
|
279
|
+
of one takes (`--cols-half`) land on a track boundary. A field ends on its track's
|
|
280
|
+
own edge, so a rung is `n * 220px + gaps`. A subform three rungs in stands three
|
|
281
|
+
`--_qm-nest` and three verticals narrower than its section, and steps down where the
|
|
282
|
+
section does not: the count is the width the tracks actually get. */
|
|
283
|
+
:where(.qm-tracks) {
|
|
284
|
+
--cols: 1;
|
|
285
|
+
--cols-half: 1;
|
|
286
|
+
}
|
|
287
|
+
@container (min-width: 28rem) {
|
|
288
|
+
:where(.qm-tracks) {
|
|
289
|
+
--cols: 2;
|
|
290
|
+
--cols-half: 1;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
@container (min-width: 57rem) {
|
|
294
|
+
:where(.qm-tracks) {
|
|
295
|
+
--cols: 4;
|
|
296
|
+
--cols-half: 2;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
258
300
|
/* A floating list of choices: the enum's listbox and the card stack's kind menu.
|
|
259
301
|
Both portal into the nearest `[data-qm-root]`, and both are picked from, so the
|
|
260
302
|
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. */
|