@quillmark/svelte 0.10.1 → 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/README.md +1 -1
- package/THEMING.md +2 -2
- 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/blocks.js +21 -1
- package/dist/core/codec/breaks.d.ts +10 -2
- package/dist/core/codec/breaks.js +65 -9
- package/dist/core/codec/code.d.ts +6 -4
- package/dist/core/codec/code.js +7 -4
- package/dist/core/codec/decode.js +103 -68
- package/dist/core/codec/encode.d.ts +5 -8
- package/dist/core/codec/encode.js +56 -86
- package/dist/core/codec/field.js +43 -24
- package/dist/core/codec/index.d.ts +2 -1
- package/dist/core/codec/index.js +6 -3
- package/dist/core/codec/islands.d.ts +16 -11
- package/dist/core/codec/islands.js +70 -59
- package/dist/core/codec/keymap.js +2 -1
- package/dist/core/codec/marks.d.ts +17 -13
- package/dist/core/codec/marks.js +39 -42
- package/dist/core/codec/positions.d.ts +4 -1
- package/dist/core/codec/positions.js +5 -1
- package/dist/core/codec/reconcile.d.ts +6 -0
- package/dist/core/codec/reconcile.js +19 -0
- package/dist/core/codec/schema.d.ts +5 -8
- package/dist/core/codec/schema.js +30 -156
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-view.js +77 -26
- package/dist/core/codec/table.d.ts +6 -5
- package/dist/core/codec/table.js +14 -31
- package/dist/core/codec/urls.d.ts +20 -0
- package/dist/core/codec/urls.js +45 -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/preset/recipes.css +1 -1
- package/dist/preset/scale.css +7 -4
- package/dist/preview/bridge.d.ts +8 -1
- package/dist/preview/bridge.js +31 -25
- package/dist/preview/controller.js +13 -13
- package/dist/preview/geometry.d.ts +12 -1
- package/dist/preview/geometry.js +14 -1
- package/dist/preview/paint.js +6 -4
- package/dist/preview/strings.d.ts +0 -2
- package/dist/preview/strings.js +5 -6
- package/dist/visual/ArrayField.svelte +600 -173
- package/dist/visual/ArrayField.svelte.d.ts +14 -1
- package/dist/visual/BooleanField.svelte +10 -7
- package/dist/visual/Card.svelte +8 -30
- package/dist/visual/DateField.svelte +2 -2
- package/dist/visual/Field.svelte +76 -32
- package/dist/visual/FieldHint.svelte +1 -1
- package/dist/visual/MatrixField.svelte +352 -0
- package/dist/visual/MatrixField.svelte.d.ts +36 -0
- package/dist/visual/ObjectField.svelte +356 -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/VisualEditor.svelte +1 -1
- package/dist/visual/VisualEditorInner.svelte +118 -67
- package/dist/visual/controls.css +39 -9
- package/dist/visual/diagnostics.d.ts +26 -2
- package/dist/visual/diagnostics.js +52 -5
- package/dist/visual/ext.d.ts +12 -13
- package/dist/visual/ext.js +22 -19
- 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/index.js +2 -3
- package/dist/visual/leaves.d.ts +25 -12
- package/dist/visual/links.d.ts +4 -0
- package/dist/visual/links.js +15 -4
- package/dist/visual/motion.d.ts +14 -5
- package/dist/visual/motion.js +13 -11
- package/dist/visual/strings.d.ts +26 -13
- package/dist/visual/strings.js +7 -2
- package/dist/visual/structure.d.ts +101 -8
- package/dist/visual/structure.js +252 -13
- 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;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
`doc` prop remounts the editor under it, so a consumer holding one mount across
|
|
4
4
|
two documents gets a surface that names the live handle.
|
|
5
5
|
|
|
6
|
-
It is
|
|
6
|
+
It is a `{#key}`, not a reseed. The state a swap invalidates is not one field:
|
|
7
7
|
composable cards key on session id, the main card is keyed on nothing, and each
|
|
8
8
|
prose leaf mounts once per stable leaf key with `createField` closing over the
|
|
9
9
|
`doc` it mounted against — so after an in-place swap the main card's leaves
|
|
@@ -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
|
|
@@ -32,7 +32,16 @@
|
|
|
32
32
|
import { errorMessage, reportError } from '../core/errors.js';
|
|
33
33
|
import { bloomInside } from '../core/bloom.js';
|
|
34
34
|
import { createLifespan } from '../core/teardown.js';
|
|
35
|
-
import type {
|
|
35
|
+
import type {
|
|
36
|
+
Addr,
|
|
37
|
+
CardAddr,
|
|
38
|
+
Diagnostic,
|
|
39
|
+
PathStep,
|
|
40
|
+
PayloadItem,
|
|
41
|
+
QuillFieldSchema,
|
|
42
|
+
Resolved,
|
|
43
|
+
ResolvedField
|
|
44
|
+
} from '@quillmark/wasm';
|
|
36
45
|
import type { VisualEditorProps } from './props.js';
|
|
37
46
|
import { mergeStrings, setWording } from './strings.js';
|
|
38
47
|
import type { CardId, ChangeSource } from './signals.js';
|
|
@@ -40,11 +49,15 @@
|
|
|
40
49
|
import {
|
|
41
50
|
IdSeq,
|
|
42
51
|
controlKind,
|
|
52
|
+
isContainer,
|
|
53
|
+
schemaAt,
|
|
43
54
|
fieldModels,
|
|
44
55
|
groupOrder,
|
|
45
56
|
groupSections,
|
|
46
57
|
groupLabel,
|
|
47
58
|
cardTitle,
|
|
59
|
+
titleFields,
|
|
60
|
+
fieldValues,
|
|
48
61
|
bodyEnabled,
|
|
49
62
|
humanize,
|
|
50
63
|
provenanceMap,
|
|
@@ -66,7 +79,7 @@
|
|
|
66
79
|
} from './diagnostics.js';
|
|
67
80
|
import { fieldDomIds, groupPanelId } from './domid.js';
|
|
68
81
|
import { createLeafRegistry, type FieldControl } from './leaves.js';
|
|
69
|
-
import { reorder, reorderTrips } from './motion.js';
|
|
82
|
+
import { reorder, reorderArm, reorderTrips } from './motion.js';
|
|
70
83
|
import { tipsChannel } from './tips.js';
|
|
71
84
|
import { patchEditorExt } from './ext.js';
|
|
72
85
|
import Card from './Card.svelte';
|
|
@@ -144,6 +157,7 @@
|
|
|
144
157
|
*/
|
|
145
158
|
function bump(source: ChangeSource, cardId?: CardId, at?: Addr | number): void {
|
|
146
159
|
revision++;
|
|
160
|
+
liveTitles = {};
|
|
147
161
|
const path =
|
|
148
162
|
at == null ? undefined : typeof at === 'number' ? cardPath(at, liveKinds()) : pathFor(at);
|
|
149
163
|
onChange?.({ source, cardId, path });
|
|
@@ -154,8 +168,11 @@
|
|
|
154
168
|
return cardIds.indexOf(id);
|
|
155
169
|
}
|
|
156
170
|
/** The inverse, for the two signals that arrive as an address and nothing else (a
|
|
157
|
-
* focus, a prose commit), off the same array the mutation boundary reads.
|
|
158
|
-
|
|
171
|
+
* focus, a prose commit), off the same array the mutation boundary reads.
|
|
172
|
+
* `undefined` for an address outside the live card array, as {@link pathFor}: the
|
|
173
|
+
* key is a host's only handle on a card, so a stale one is dropped rather than
|
|
174
|
+
* reported. */
|
|
175
|
+
function cardIdOf(addr: Addr): CardId | undefined {
|
|
159
176
|
return addr.card == null ? 'main' : cardIds[addr.card];
|
|
160
177
|
}
|
|
161
178
|
|
|
@@ -214,7 +231,9 @@
|
|
|
214
231
|
lastCaretField = undefined;
|
|
215
232
|
lastCaretPos = undefined;
|
|
216
233
|
const field = pathFor(plain);
|
|
217
|
-
|
|
234
|
+
// Both halves or neither: a stale addr names no path and no live card.
|
|
235
|
+
if (field != null && activeCardId != null)
|
|
236
|
+
onActiveLeafChange?.({ field, cardId: activeCardId });
|
|
218
237
|
}
|
|
219
238
|
/** No card kinds are read for a main address; the shared empty stands in so the
|
|
220
239
|
* common case allocates nothing. */
|
|
@@ -261,7 +280,35 @@
|
|
|
261
280
|
* bump `revision` (see {@link bump}). */
|
|
262
281
|
function proseChanged(addr: Addr): void {
|
|
263
282
|
const plain = normalize(addr);
|
|
264
|
-
|
|
283
|
+
const cardId = cardIdOf(plain);
|
|
284
|
+
if (cardId == null) return;
|
|
285
|
+
refreshTitle(plain, cardId);
|
|
286
|
+
onChange?.({ source: 'prose', cardId, path: pathFor(plain) });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** A card's header after a prose commit to a field its `{field}` title names: the
|
|
290
|
+
* one piece of the model that lane changes, held beside the model and dropped on
|
|
291
|
+
* the next bump, which rebuilds it from the document. The kind is the derived
|
|
292
|
+
* tree's ({@link liveKinds}), so the document is read only for a title that
|
|
293
|
+
* names the field. */
|
|
294
|
+
let liveTitles = $state<Record<CardId, string>>({});
|
|
295
|
+
function refreshTitle(addr: Addr, cardId: CardId): void {
|
|
296
|
+
const { card: at, field } = addr;
|
|
297
|
+
if (field == null) return;
|
|
298
|
+
const shown = at == null ? model.main : model.cards[at];
|
|
299
|
+
if (!shown) return;
|
|
300
|
+
const cardSchema = at == null ? quill.schema.main : quill.schema.card_kinds?.[shown.kind];
|
|
301
|
+
if (!titleFields(cardSchema?.ui?.title).includes(field)) return;
|
|
302
|
+
const live = at == null ? doc.main : doc.cards[at];
|
|
303
|
+
if (!live) return;
|
|
304
|
+
liveTitles = {
|
|
305
|
+
...liveTitles,
|
|
306
|
+
[cardId]: cardTitle(cardSchema, shown.kind, fieldValues(live.payloadItems), undefined)
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function titled(c: CardModel): CardModel {
|
|
310
|
+
const t = liveTitles[c.id];
|
|
311
|
+
return t == null ? c : { ...c, titlePlaceholder: t };
|
|
265
312
|
}
|
|
266
313
|
|
|
267
314
|
// ── Commit routing ──────────────────────────────────────────────────────────
|
|
@@ -381,26 +428,17 @@
|
|
|
381
428
|
return undefined;
|
|
382
429
|
}
|
|
383
430
|
}
|
|
384
|
-
// The reorder gesture's arming window: the reconcile that moves a
|
|
385
|
-
//
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
// microtask after the mutation and well inside the frame that disarms it.
|
|
389
|
-
let reordering = false;
|
|
390
|
-
let reorderFrame = 0;
|
|
391
|
-
function armReorder(): void {
|
|
392
|
-
reordering = true;
|
|
393
|
-
reorderFrame = requestAnimationFrame(() => (reordering = false));
|
|
394
|
-
}
|
|
395
|
-
span.onEnd(() => cancelAnimationFrame(reorderFrame));
|
|
396
|
-
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);
|
|
397
435
|
|
|
398
436
|
function moveCardById(id: string, dir: -1 | 1): void {
|
|
399
437
|
const from = cardIndexOf(id);
|
|
400
438
|
if (from < 0) return;
|
|
401
439
|
const to = from + dir;
|
|
402
440
|
if (to < 0 || to >= doc.cardCount) return;
|
|
403
|
-
|
|
441
|
+
arm.arm();
|
|
404
442
|
doc.moveCard(from, to);
|
|
405
443
|
const w = cardIds.slice();
|
|
406
444
|
const [x] = w.splice(from, 1);
|
|
@@ -423,10 +461,11 @@
|
|
|
423
461
|
// Ids are never reused, so an entry keyed to the gone card is unreachable rather
|
|
424
462
|
// than mis-attributed to whichever card takes this position: it needs dropping
|
|
425
463
|
// because nothing else will ever clear it.
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
464
|
+
const prefix = `${id}:`;
|
|
465
|
+
const stale = [...commitErrors.keys()].filter((k) => k.startsWith(prefix));
|
|
466
|
+
// Guarded: `editCommitErrors` clones the map, and a removal matching nothing has
|
|
467
|
+
// nothing to re-derive.
|
|
468
|
+
if (stale.length) editCommitErrors((m) => stale.forEach((k) => m.delete(k)));
|
|
430
469
|
// No addr: the removed card has none left and every survivor's shifted. The
|
|
431
470
|
// stack changed, not a leaf, and the id is the only handle the removal leaves.
|
|
432
471
|
bump('structure', id);
|
|
@@ -435,7 +474,12 @@
|
|
|
435
474
|
const i = cardIndexOf(id);
|
|
436
475
|
if (i < 0) return;
|
|
437
476
|
try {
|
|
438
|
-
|
|
477
|
+
// Insert the retyped card ahead of the original and drop the original after.
|
|
478
|
+
// `insertCard` validates the whole card before it mutates and `removeCard`
|
|
479
|
+
// cannot throw, so a refusal leaves the document as it was; the reverse order
|
|
480
|
+
// would leave it a card short.
|
|
481
|
+
doc.insertCard({ ...doc.card(i), kind }, i);
|
|
482
|
+
doc.removeCard(i + 1);
|
|
439
483
|
bump('structure', id, i);
|
|
440
484
|
} catch (e) {
|
|
441
485
|
reportError(onError, {
|
|
@@ -536,15 +580,13 @@
|
|
|
536
580
|
isMain: boolean,
|
|
537
581
|
kind: string,
|
|
538
582
|
card: {
|
|
539
|
-
payloadItems:
|
|
583
|
+
payloadItems: readonly PayloadItem[];
|
|
540
584
|
ext?: Record<string, unknown>;
|
|
541
585
|
},
|
|
542
586
|
cardSchema: Parameters<typeof fieldModels>[0] | undefined,
|
|
543
587
|
rows: ResolvedCardRows
|
|
544
588
|
): CardModel {
|
|
545
|
-
const values
|
|
546
|
-
for (const p of card.payloadItems)
|
|
547
|
-
if (p.type === 'field' && p.key != null) values[p.key] = p.value;
|
|
589
|
+
const values = fieldValues(card.payloadItems);
|
|
548
590
|
const fields = cardSchema ? fieldModels(cardSchema) : [];
|
|
549
591
|
const sections = cardSchema
|
|
550
592
|
? groupSections(fields, groupOrder(cardSchema), (g) => groupLabel(cardSchema, g))
|
|
@@ -588,12 +630,12 @@
|
|
|
588
630
|
// chrome, so a resolve failure degrades to no ghosts, never a blank form.
|
|
589
631
|
let resolved: Resolved | undefined;
|
|
590
632
|
try {
|
|
591
|
-
resolved = quill.
|
|
633
|
+
resolved = quill.reader(doc).resolve();
|
|
592
634
|
} catch (e) {
|
|
593
635
|
reportError(onError, {
|
|
594
636
|
code: 'resolve-failed',
|
|
595
637
|
severity: 'error',
|
|
596
|
-
message: `
|
|
638
|
+
message: `reader.resolve threw; ghosted defaults fall back to none: ${errorMessage(e)}`,
|
|
597
639
|
cause: e
|
|
598
640
|
});
|
|
599
641
|
}
|
|
@@ -644,7 +686,8 @@
|
|
|
644
686
|
// click is one discrete act, and its commonest target is the leaf already focused
|
|
645
687
|
// (where landing a caret changes nothing on screen) or one off-screen, where the
|
|
646
688
|
// browser's focus-scroll moves the page and leaves the caret to be hunted for.
|
|
647
|
-
|
|
689
|
+
const box = await land(found, at.granularity === 'segment' ? undefined : at.pos);
|
|
690
|
+
if (span.alive) bloomInside(box);
|
|
648
691
|
}
|
|
649
692
|
/** The active leaf's controller: the formatting popover's observation seam.
|
|
650
693
|
* `undefined` for a focused form control, which holds no marks to toggle. */
|
|
@@ -683,20 +726,22 @@
|
|
|
683
726
|
|
|
684
727
|
/**
|
|
685
728
|
* Put the caret in a revealed target, at the finest grain it can take: a USV offset
|
|
686
|
-
* in a prose leaf or
|
|
687
|
-
* has no coordinate to spend an offset in (an `<input type="number">`
|
|
688
|
-
* selection outright), which is also the whole of what a click on
|
|
689
|
-
* can mean.
|
|
690
|
-
*
|
|
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`).
|
|
691
734
|
*
|
|
692
735
|
* Hands back the box the arrival wash blooms in, which is the landing's own
|
|
693
|
-
* granularity rather than the registry's:
|
|
694
|
-
*
|
|
695
|
-
* 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.
|
|
696
741
|
*/
|
|
697
|
-
function land(found: Landed, pos: number | undefined): HTMLElement {
|
|
698
|
-
if (found.
|
|
699
|
-
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;
|
|
700
745
|
}
|
|
701
746
|
if (pos != null) {
|
|
702
747
|
const prose = leaves.prose(found.key);
|
|
@@ -725,7 +770,8 @@
|
|
|
725
770
|
export async function focusField(field: DocPath): Promise<void> {
|
|
726
771
|
const found = await revealLeaf(field);
|
|
727
772
|
if (!found) return missed(`no mounted field at ${field}`, field);
|
|
728
|
-
|
|
773
|
+
const box = await land(found, undefined);
|
|
774
|
+
if (span.alive) bloomInside(box);
|
|
729
775
|
}
|
|
730
776
|
/** Seed a card of `kind` and insert it at `at` (default: the end). Returns the new
|
|
731
777
|
* card's session key, or `undefined` when the quill seeds no card of that kind. */
|
|
@@ -755,11 +801,11 @@
|
|
|
755
801
|
reportError(onError, { code: 'target-unknown', severity: 'dev', message, path });
|
|
756
802
|
}
|
|
757
803
|
|
|
758
|
-
/** What a `DocPath` resolves to in the mounted tree: a leaf key, and the
|
|
759
|
-
*
|
|
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. */
|
|
760
806
|
interface LeafTarget {
|
|
761
807
|
key: string;
|
|
762
|
-
|
|
808
|
+
steps?: PathStep[];
|
|
763
809
|
}
|
|
764
810
|
/** A resolved target with the handle the registry holds for it. */
|
|
765
811
|
type Landed = LeafTarget & { control: FieldControl };
|
|
@@ -769,11 +815,13 @@
|
|
|
769
815
|
* diagnostics take.
|
|
770
816
|
*
|
|
771
817
|
* Two rungs, because the boundary mints addresses at a finer granularity than
|
|
772
|
-
* `Addr` can name: a `richtext[]` element surfaces as `main.keywords[0]
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
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.
|
|
777
825
|
*/
|
|
778
826
|
function leafTargetFor(field: DocPath): LeafTarget | undefined {
|
|
779
827
|
const direct = addrForFieldPath(field);
|
|
@@ -781,22 +829,25 @@
|
|
|
781
829
|
const resolved = resolveCardKey(direct, cardIds);
|
|
782
830
|
return resolved ? { key: fieldKeyToString(resolved) } : undefined;
|
|
783
831
|
}
|
|
784
|
-
const
|
|
785
|
-
if (!
|
|
786
|
-
const
|
|
787
|
-
|
|
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;
|
|
788
839
|
}
|
|
789
840
|
|
|
790
|
-
/** The
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
|
|
794
|
-
|
|
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;
|
|
795
847
|
const kind = addr.card == null ? undefined : model.cards[addr.card]?.kind;
|
|
796
848
|
const schema = quill.schema;
|
|
797
849
|
const card = addr.card == null ? schema.main : kind ? schema.card_kinds?.[kind] : undefined;
|
|
798
|
-
|
|
799
|
-
return !!declared && controlKind(declared) === 'array';
|
|
850
|
+
return card?.fields?.[addr.field];
|
|
800
851
|
}
|
|
801
852
|
</script>
|
|
802
853
|
|
|
@@ -807,7 +858,7 @@
|
|
|
807
858
|
<div class="qm-primary">
|
|
808
859
|
<Card
|
|
809
860
|
bind:this={mainCard}
|
|
810
|
-
card={model.main}
|
|
861
|
+
card={titled(model.main)}
|
|
811
862
|
{doc}
|
|
812
863
|
{quill}
|
|
813
864
|
isFirst={true}
|
|
@@ -836,10 +887,10 @@
|
|
|
836
887
|
rather than being slid across. It is also the shape `animate:` asks for, being the
|
|
837
888
|
keyed block's only child. -->
|
|
838
889
|
{#each model.cards as c, i (c.id)}
|
|
839
|
-
<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}>
|
|
840
891
|
<Card
|
|
841
892
|
bind:this={cardRefs[i]}
|
|
842
|
-
card={c}
|
|
893
|
+
card={titled(c)}
|
|
843
894
|
{doc}
|
|
844
895
|
{quill}
|
|
845
896
|
isFirst={i === 0}
|
package/dist/visual/controls.css
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
Every selector here is `:where()`-wrapped, so a recipe computes at specificity zero.
|
|
13
13
|
That is the point twice over: a consumer's stylesheet beats the chrome without
|
|
14
14
|
`!important`, and a component's own scoped block overrides its shared base —
|
|
15
|
-
`.qm-card-delete` recolours a `.qm-chip` under the pointer
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
`.qm-card-delete` recolours a `.qm-chip` under the pointer — with no specificity
|
|
16
|
+
fight. Unlayered with it, so a host's own layered reset never reaches these rules
|
|
17
|
+
whatever order the sheets load in (`core/theme.css`). */
|
|
18
18
|
|
|
19
19
|
/* The themed focus ring in place of the raw UA outline, as a
|
|
20
20
|
rule rather than a pair of rungs each control assembles. `--_qm-ring`
|
|
@@ -63,11 +63,10 @@
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/* A typed value's box (every scalar control and every prose leaf): the text and
|
|
66
|
-
numeric inputs, the
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
edited, and nothing raises it (no literal is minted, so `check:style` stays
|
|
66
|
+
numeric inputs, the enum trigger, the date field, and the inline richtext leaves.
|
|
67
|
+
One rule, because a leaf that agrees with the input beside it "by construction" has
|
|
68
|
+
to draw the same declarations, not a copy of them: five hand-kept copies agree
|
|
69
|
+
until one is edited, and nothing raises it (no literal is minted, so `check:style` stays
|
|
71
70
|
green while a control renders subtly the wrong height).
|
|
72
71
|
|
|
73
72
|
Leading sits on the wide list rather than on the box's own, because a box's
|
|
@@ -91,7 +90,7 @@
|
|
|
91
90
|
color: var(--_qm-ink);
|
|
92
91
|
caret-color: currentColor;
|
|
93
92
|
}
|
|
94
|
-
/* The box is
|
|
93
|
+
/* The box is a fill and not an edge (ARCHITECTURE §Styling), and the fill is the
|
|
95
94
|
well rung: a field is a hole cut into the card rather than a plane of its own,
|
|
96
95
|
and with no edge to carry it the step is the whole of what says so. */
|
|
97
96
|
:where(.qm-control-box),
|
|
@@ -256,6 +255,37 @@
|
|
|
256
255
|
transform: translate(-50%, -50%);
|
|
257
256
|
}
|
|
258
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
|
+
|
|
259
289
|
/* A floating list of choices: the enum's listbox and the card stack's kind menu.
|
|
260
290
|
Both portal into the nearest `[data-qm-root]`, and both are picked from, so the
|
|
261
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[]>;
|