@quillmark/svelte 0.2.0 → 0.3.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 +2 -2
- package/THEMING.md +11 -7
- package/dist/core/address.d.ts +7 -7
- package/dist/core/address.js +6 -6
- package/dist/core/bloom.d.ts +5 -5
- package/dist/core/bloom.js +8 -8
- package/dist/core/codec/anchor.d.ts +1 -1
- package/dist/core/codec/anchor.js +1 -1
- package/dist/core/codec/atoms.d.ts +10 -0
- package/dist/core/codec/atoms.js +78 -0
- package/dist/core/codec/clipboard.d.ts +59 -0
- package/dist/core/codec/clipboard.js +202 -0
- package/dist/core/codec/code.d.ts +1 -1
- package/dist/core/codec/code.js +4 -4
- package/dist/core/codec/decode.d.ts +1 -1
- package/dist/core/codec/decode.js +8 -8
- package/dist/core/codec/encode.d.ts +4 -4
- package/dist/core/codec/encode.js +10 -10
- package/dist/core/codec/field.d.ts +21 -26
- package/dist/core/codec/field.js +33 -32
- package/dist/core/codec/index.js +5 -8
- package/dist/core/codec/inputrules.js +56 -18
- package/dist/core/codec/islands.d.ts +11 -7
- package/dist/core/codec/islands.js +16 -10
- package/dist/core/codec/keymap.d.ts +5 -6
- package/dist/core/codec/keymap.js +8 -7
- package/dist/core/codec/lists.d.ts +3 -3
- package/dist/core/codec/lists.js +32 -13
- package/dist/core/codec/marks.d.ts +1 -1
- package/dist/core/codec/marks.js +1 -1
- package/dist/core/codec/positions.js +1 -1
- package/dist/core/codec/prose.css +230 -215
- package/dist/core/codec/schema.js +4 -4
- package/dist/core/codec/slash.d.ts +39 -41
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-view.d.ts +12 -16
- package/dist/core/codec/table-view.js +488 -292
- package/dist/core/codec/table.d.ts +42 -15
- package/dist/core/codec/table.js +70 -26
- package/dist/core/errors.d.ts +5 -5
- package/dist/core/index.js +2 -2
- package/dist/core/lifecycle.js +1 -1
- package/dist/core/rebind.svelte.d.ts +2 -2
- package/dist/core/rebind.svelte.js +4 -4
- package/dist/core/teardown.d.ts +1 -1
- package/dist/core/teardown.js +1 -1
- package/dist/core/theme.css +144 -54
- package/dist/preset/index.css +1 -1
- package/dist/preset/recipes.css +31 -29
- package/dist/preset/scale.css +36 -25
- package/dist/preview/Preview.svelte +12 -10
- package/dist/preview/Preview.svelte.d.ts +3 -3
- package/dist/preview/bridge.js +4 -4
- package/dist/preview/controller.js +3 -3
- package/dist/preview/geometry.d.ts +2 -2
- package/dist/preview/geometry.js +2 -2
- package/dist/preview/index.js +3 -3
- package/dist/preview/overlay.d.ts +1 -1
- package/dist/preview/overlay.js +3 -3
- package/dist/preview/paint.js +15 -17
- package/dist/visual/ArrayField.svelte +80 -66
- package/dist/visual/ArrayField.svelte.d.ts +1 -1
- package/dist/visual/BooleanField.svelte +8 -6
- package/dist/visual/BooleanField.svelte.d.ts +2 -2
- package/dist/visual/Card.svelte +151 -325
- package/dist/visual/Card.svelte.d.ts +2 -6
- package/dist/visual/CardControls.svelte +1 -1
- package/dist/visual/DateField.svelte +19 -19
- package/dist/visual/DateField.svelte.d.ts +3 -3
- package/dist/visual/DiagnosticList.svelte +1 -1
- package/dist/visual/EnumField.svelte +16 -16
- package/dist/visual/EnumField.svelte.d.ts +2 -2
- package/dist/visual/Field.svelte +128 -115
- package/dist/visual/Field.svelte.d.ts +1 -1
- package/dist/visual/FieldHint.svelte +24 -42
- package/dist/visual/FieldHint.svelte.d.ts +1 -1
- package/dist/visual/FieldLabel.svelte +8 -8
- package/dist/visual/FormatPopover.svelte +80 -35
- package/dist/visual/NumberField.svelte +5 -5
- package/dist/visual/NumberField.svelte.d.ts +1 -1
- package/dist/visual/ObjectField.svelte +9 -9
- package/dist/visual/ObjectField.svelte.d.ts +1 -1
- package/dist/visual/ProseArrayElement.svelte +11 -13
- package/dist/visual/ProseArrayElement.svelte.d.ts +1 -1
- package/dist/visual/ProseField.svelte +53 -84
- package/dist/visual/ProseField.svelte.d.ts +12 -17
- package/dist/visual/SlashMenu.svelte +42 -16
- package/dist/visual/TextField.svelte +5 -5
- package/dist/visual/TextField.svelte.d.ts +1 -1
- package/dist/visual/TipsCard.svelte +43 -28
- package/dist/visual/VisualEditor.svelte +6 -6
- package/dist/visual/VisualEditorInner.svelte +122 -129
- package/dist/visual/controls.css +74 -30
- package/dist/visual/diagnostics.d.ts +5 -5
- package/dist/visual/diagnostics.js +3 -3
- package/dist/visual/domid.js +3 -3
- package/dist/visual/ext.d.ts +1 -1
- package/dist/visual/ext.js +1 -1
- package/dist/visual/icons/nodes.d.ts +3 -0
- package/dist/visual/icons/nodes.js +2 -1
- package/dist/visual/index.js +4 -4
- package/dist/visual/leaves.d.ts +3 -3
- package/dist/visual/leaves.js +1 -1
- package/dist/visual/links.d.ts +24 -0
- package/dist/visual/links.js +67 -0
- package/dist/visual/props.d.ts +8 -8
- package/dist/visual/signals.d.ts +6 -6
- package/dist/visual/strings.d.ts +14 -19
- package/dist/visual/strings.js +5 -7
- package/dist/visual/structure.d.ts +24 -17
- package/dist/visual/structure.js +8 -8
- package/dist/visual/synced.svelte.d.ts +1 -1
- package/dist/visual/synced.svelte.js +4 -4
- package/dist/visual/tips.d.ts +2 -2
- package/dist/visual/tips.js +3 -3
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
The federated WYSIWYG surface (VISUAL_EDITOR). A
|
|
3
|
-
small editors;
|
|
2
|
+
The federated WYSIWYG surface (VISUAL_EDITOR). A thin composition over many
|
|
3
|
+
small editors; not one PM document spanning the page.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Mounted once per document. `VisualEditor` keys this component on `doc`, so
|
|
6
6
|
everything below seeds from a handle that cannot change under it: `cardIds`, the
|
|
7
7
|
id `seq`, the leaf registry, the commit-error map, the active address and the
|
|
8
8
|
card refs are all mount-scoped, and the prose leaves close over the `doc` they
|
|
@@ -12,21 +12,21 @@
|
|
|
12
12
|
It owns:
|
|
13
13
|
• structure: the schema × payload join, re-derived from the live `Document`;
|
|
14
14
|
• stable card identity: a session-id array reordered in lockstep with the
|
|
15
|
-
content, resolved to an index
|
|
15
|
+
content, resolved to an index only at the mutation boundary;
|
|
16
16
|
• commit routing: prose leaves lower to `applyChange` (in the codec); scalars/
|
|
17
17
|
arrays/objects go through the typed `writer`; structure through the mutators;
|
|
18
18
|
• focus + the bridge outputs (`onActiveLeafChange`, `onCaretMove`) and the
|
|
19
19
|
`setCaret(at)` entry wired to the preview;
|
|
20
|
-
• the
|
|
20
|
+
• the one formatting popover (`FormatPopover`, mounted once, observing the
|
|
21
21
|
active leaf via `getActiveLeaf`) and diagnostics routing (`diagnostics.ts`:
|
|
22
22
|
quill.validate + local commit errors + the external `diagnostics` prop,
|
|
23
23
|
merged into `diagByKey` and threaded to each `<Field>`/card body).
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Reactivity across the WASM handle. The `Document` is opaque to Svelte, so a
|
|
26
26
|
`revision` counter is bumped after every scalar/structure mutation and the card
|
|
27
|
-
tree is `$derived` by
|
|
28
|
-
leaves are mounted
|
|
29
|
-
they commit to the doc directly and do
|
|
27
|
+
tree is `$derived` by re-reading `doc.main`/`doc.cards`/`quill.schema`. Prose
|
|
28
|
+
leaves are mounted once per stable leaf key (keyed `<Card>`/`<ProseField>`);
|
|
29
|
+
they commit to the doc directly and do not bump `revision`, so a re-derive never
|
|
30
30
|
remounts them or drops a caret. `doc.main`/`doc.cards` allocate per read: read
|
|
31
31
|
once per derive.
|
|
32
32
|
-->
|
|
@@ -66,7 +66,6 @@
|
|
|
66
66
|
stringifyGhost,
|
|
67
67
|
resolveBodyGhost,
|
|
68
68
|
NO_RESOLVED_ROWS,
|
|
69
|
-
type BodyPlaceholder,
|
|
70
69
|
type CardModel,
|
|
71
70
|
type ResolvedCardRows
|
|
72
71
|
} from './structure.js';
|
|
@@ -122,7 +121,7 @@
|
|
|
122
121
|
// ── Reactivity + session identity ───────────────────────────────────────────
|
|
123
122
|
let revision = $state(0);
|
|
124
123
|
// This editor's own id, prefixed onto every field's DOM ids (`domid.ts`). The
|
|
125
|
-
// leaf-key space is unique per
|
|
124
|
+
// leaf-key space is unique per editor, not per page: two editors mounted
|
|
126
125
|
// side-by-side both hold `main:subject`, and a duplicate `id` makes `for` resolve
|
|
127
126
|
// to whichever mounted first. `$props.id()` is stable across SSR and hydration,
|
|
128
127
|
// which a module counter is not.
|
|
@@ -150,13 +149,13 @@
|
|
|
150
149
|
/**
|
|
151
150
|
* Re-derive the card tree and report the edit. Called for the two lanes that go
|
|
152
151
|
* through this component; a prose leaf commits itself and reports through
|
|
153
|
-
* `proseChanged`, which does
|
|
152
|
+
* `proseChanged`, which does not bump the revision (bumping it would re-derive
|
|
154
153
|
* and remount every leaf, costing the caret on every keystroke).
|
|
155
154
|
*
|
|
156
155
|
* `cardId` is passed rather than derived from `at`: the sites below hold the id
|
|
157
156
|
* already, and the removal has no address to derive one from. `at` is an `Addr` for
|
|
158
|
-
* a leaf and a
|
|
159
|
-
* anything inside it; either way the path is minted
|
|
157
|
+
* a leaf and a card index for a structure op, whose subject is the card rather than
|
|
158
|
+
* anything inside it; either way the path is minted here, after the bump, because
|
|
160
159
|
* an op's own index is only addressable against the tree the bump re-derived (an
|
|
161
160
|
* insert's card does not exist in the previous one, and a retype's kind is the
|
|
162
161
|
* previous kind).
|
|
@@ -181,7 +180,7 @@
|
|
|
181
180
|
}
|
|
182
181
|
|
|
183
182
|
// ── Teardown (core/teardown.ts: unregister, cancel, then free) ──────────────
|
|
184
|
-
// A document swap is a
|
|
183
|
+
// A document swap is a remount (see the remount contract above), so a destroy is
|
|
185
184
|
// the only way this surface ends and one span covers both. Cancellers register
|
|
186
185
|
// below in the order they run: the registry that resolves new work first, then
|
|
187
186
|
// the work already scheduled. The document handle is the consumer's to free, so
|
|
@@ -207,7 +206,7 @@
|
|
|
207
206
|
let mainCard = $state<CardHandle | undefined>(undefined);
|
|
208
207
|
let cardRefs = $state<(CardHandle | undefined)[]>([]);
|
|
209
208
|
/** The stack's own element: it carries `data-qm-root`, so it is what the kind
|
|
210
|
-
* menu portals
|
|
209
|
+
* menu portals into, the way each leaf's surface resolves its nearest root. */
|
|
211
210
|
let rootEl = $state<HTMLElement | undefined>(undefined);
|
|
212
211
|
|
|
213
212
|
// ── Focus + bridge outputs ──────────────────────────────────────────────────
|
|
@@ -234,7 +233,7 @@
|
|
|
234
233
|
* common case allocates nothing. */
|
|
235
234
|
const NO_KINDS: readonly string[] = [];
|
|
236
235
|
/**
|
|
237
|
-
* The live kinds by content index, off the
|
|
236
|
+
* The live kinds by content index, off the derived card tree rather than
|
|
238
237
|
* `doc.cards`: they are already in hand, and `doc.cards` serializes every card on
|
|
239
238
|
* each read, which is not a thing to do per keystroke.
|
|
240
239
|
*/
|
|
@@ -248,13 +247,13 @@
|
|
|
248
247
|
function pathFor(addr: Addr): DocPath | undefined {
|
|
249
248
|
return fieldPathForAddr(addr, addr.card == null ? NO_KINDS : liveKinds());
|
|
250
249
|
}
|
|
251
|
-
// The last
|
|
250
|
+
// The last place reported, so a caret that did not move is not news. A leaf fires
|
|
252
251
|
// one caret signal per transaction, and a transaction need not have moved the
|
|
253
252
|
// caret to exist: a mark toggle over the same range, an anchor op, a plugin's own
|
|
254
253
|
// bookkeeping. The memo is the editor's rather than each leaf's, because a place
|
|
255
254
|
// spans leaves: a caret that leaves `main.body` at 5 for a card leaf and comes
|
|
256
255
|
// back to 5 made two moves, and a per-leaf memo would swallow the second. An edit
|
|
257
|
-
// that reflows the page
|
|
256
|
+
// that reflows the page under a caret holding its offset is the case this drops,
|
|
258
257
|
// and a preview does not need the signal to answer it: the place is unchanged, so
|
|
259
258
|
// the one it already followed is the one its recompile re-locates.
|
|
260
259
|
let lastCaretField: DocPath | undefined;
|
|
@@ -276,18 +275,18 @@
|
|
|
276
275
|
|
|
277
276
|
// ── Commit routing ──────────────────────────────────────────────────────────
|
|
278
277
|
// Scalars / arrays / objects → the typed writer (schema-checked). Prose leaves
|
|
279
|
-
// commit themselves via the codec (applyChange) and do
|
|
278
|
+
// commit themselves via the codec (applyChange) and do not pass through here.
|
|
280
279
|
// A scalar control commits `undefined` for a cleared entry: the unset lane
|
|
281
280
|
// below (`doc.removeField`), not a write.
|
|
282
281
|
//
|
|
283
|
-
// A bad value makes `writer.set`
|
|
282
|
+
// A bad value makes `writer.set` throw a `QuillmarkError`, whose
|
|
284
283
|
// `diagnostics[0]` carries a `code` and a canonical `path` (e.g.
|
|
285
284
|
// `edit::field_coercion_failed` at `main.font_size`, or `edit::unknown_field`). The
|
|
286
|
-
// editor already
|
|
287
|
-
// from
|
|
285
|
+
// editor already knows the field/card being committed, so it keys the entry
|
|
286
|
+
// from that address: id-keyed so it survives a later card reorder, never
|
|
288
287
|
// parsed from the positional path; while surfacing the thrown diagnostic
|
|
289
288
|
// verbatim (its `code`/`message`) as the payload (VISUAL_EDITOR §Diagnostics,
|
|
290
|
-
// producer #2). It is stashed in `commitErrors`; a subsequent
|
|
289
|
+
// producer #2). It is stashed in `commitErrors`; a subsequent successful commit
|
|
291
290
|
// for the same field clears it. Nothing here gates: the value is not written
|
|
292
291
|
// (the document is unchanged on throw, per the boundary's own transactional
|
|
293
292
|
// contract) and editing continues.
|
|
@@ -296,14 +295,14 @@
|
|
|
296
295
|
const keyStr = fieldKeyToString(key);
|
|
297
296
|
try {
|
|
298
297
|
if (value === undefined) {
|
|
299
|
-
// The
|
|
300
|
-
// VISUAL_EDITOR §"Structure mirrors the schema"):
|
|
298
|
+
// The unset rung of the commitment ladder (a cleared scalar control,
|
|
299
|
+
// VISUAL_EDITOR §"Structure mirrors the schema"): remove the field so the
|
|
301
300
|
// engine's authored › `default:` › zero-fill resolve renders the
|
|
302
301
|
// default, rather than baking a snapshot the schema can't track
|
|
303
302
|
// (canon SCHEMAS.md: the engine never persists a default; nor do we).
|
|
304
303
|
// Removal writes no value, so there is nothing for a schema to conform:
|
|
305
304
|
// it goes through the quill-free `doc.removeField` (bare string = main
|
|
306
|
-
// `{ field }`; `{ card, field }` for a card),
|
|
305
|
+
// `{ field }`; `{ card, field }` for a card), not the typed writer.
|
|
307
306
|
if (isMain) {
|
|
308
307
|
doc.removeField(name);
|
|
309
308
|
} else {
|
|
@@ -436,7 +435,7 @@
|
|
|
436
435
|
});
|
|
437
436
|
// No addr: the removed card has no address left, and the surviving cards'
|
|
438
437
|
// addresses all shifted. The stack changed, not a leaf. The id still names
|
|
439
|
-
//
|
|
438
|
+
// which card went, which is the one thing a host keying on it needs, and the
|
|
440
439
|
// only handle the removal leaves it.
|
|
441
440
|
bump('structure', id);
|
|
442
441
|
}
|
|
@@ -462,7 +461,7 @@
|
|
|
462
461
|
bump('structure', id, i);
|
|
463
462
|
}
|
|
464
463
|
/**
|
|
465
|
-
* Clear the tips channel: the dismissal write, and the
|
|
464
|
+
* Clear the tips channel: the dismissal write, and the only write
|
|
466
465
|
* tips make. `undefined` drops the key while `title` and any later sibling ride
|
|
467
466
|
* through; `ext.ts` holds why that matters.
|
|
468
467
|
*/
|
|
@@ -475,7 +474,7 @@
|
|
|
475
474
|
}
|
|
476
475
|
|
|
477
476
|
// ── Addressing + per-card op bundle ─────────────────────────────────────────
|
|
478
|
-
/** A
|
|
477
|
+
/** A live card address: `card` is a getter, so a reorder re-targets in place. */
|
|
479
478
|
function cardAddr(id: string, field?: string): Addr {
|
|
480
479
|
return field != null
|
|
481
480
|
? ({
|
|
@@ -496,9 +495,7 @@
|
|
|
496
495
|
}
|
|
497
496
|
|
|
498
497
|
// ── Diagnostics routing (VISUAL_EDITOR §Diagnostics) ────────────────────────
|
|
499
|
-
// Producer #1: quill.validate(doc), re-run every revision
|
|
500
|
-
// `[]` for usaf_memo (no field in the fixture carries a `!must_fill` marker)
|
|
501
|
-
// but the routing exists for the general contract, not just this fixture).
|
|
498
|
+
// Producer #1: quill.validate(doc), re-run every revision.
|
|
502
499
|
const validation = $derived.by(() => {
|
|
503
500
|
revision; // re-run on every mutation, per VISUAL_EDITOR §Diagnostics
|
|
504
501
|
try {
|
|
@@ -525,7 +522,7 @@
|
|
|
525
522
|
return mergeDiagnostics(fromValidate, fromExternal, [...commitErrors.values()]);
|
|
526
523
|
});
|
|
527
524
|
function opsFor(id: string, isMain: boolean) {
|
|
528
|
-
//
|
|
525
|
+
// One identity per field: the leaf registry, the DOM's three names, and the
|
|
529
526
|
// diagnostics map all key off this string, so a leaf, its label and its
|
|
530
527
|
// diagnostics can only ever resolve together.
|
|
531
528
|
const leafKey = (field?: string) => fieldKeyToString({ card: isMain ? undefined : id, field });
|
|
@@ -547,33 +544,6 @@
|
|
|
547
544
|
};
|
|
548
545
|
}
|
|
549
546
|
|
|
550
|
-
// ── The empty-body ghost's consumer wording ─────────────────────────────────
|
|
551
|
-
// The hook is consulted once per KIND and its answer kept for the session, which
|
|
552
|
-
// is the whole determinism guarantee: a hook that samples a witty set at random
|
|
553
|
-
// is impure by design, and this cache is what makes its answer look chosen:
|
|
554
|
-
// same string for every card of a kind, and the same one after a remount or any
|
|
555
|
-
// re-derive. Keyed by kind rather than by card id deliberately: two empty cards
|
|
556
|
-
// that are the same kind ARE the same invitation, and disagreeing ghosts read as
|
|
557
|
-
// a glitch. Retyping a card crosses to another key and re-asks, which is right:
|
|
558
|
-
// it is a different card now. Plain (non-`$state`) on purpose: memoization, so
|
|
559
|
-
// filling it during a derive must not feed back into one.
|
|
560
|
-
let ghostHook: BodyPlaceholder | undefined;
|
|
561
|
-
const ghostByKind = new Map<string, string | undefined>();
|
|
562
|
-
function customBodyGhost(kind: string, isMain: boolean): string | undefined {
|
|
563
|
-
const bodyPlaceholder = merged.bodyPlaceholder;
|
|
564
|
-
// A swapped hook invalidates every answer the old one gave.
|
|
565
|
-
if (bodyPlaceholder !== ghostHook) {
|
|
566
|
-
ghostByKind.clear();
|
|
567
|
-
ghostHook = bodyPlaceholder;
|
|
568
|
-
}
|
|
569
|
-
if (!bodyPlaceholder) return undefined;
|
|
570
|
-
// `main` is not a `card_kinds` key, so a kind that spells it collides without
|
|
571
|
-
// the flag in the key.
|
|
572
|
-
const key = `${isMain ? '1' : '0'}\0${kind}`;
|
|
573
|
-
if (!ghostByKind.has(key)) ghostByKind.set(key, bodyPlaceholder({ kind, isMain }));
|
|
574
|
-
return ghostByKind.get(key);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
547
|
// ── The derived card tree (schema × payload join) ───────────────────────────
|
|
578
548
|
function buildCard(
|
|
579
549
|
id: string,
|
|
@@ -612,12 +582,14 @@
|
|
|
612
582
|
// same text-ghost projection `<Field>` applies to a field's row; and falls
|
|
613
583
|
// back to an invitation where a scalar shows nothing, because an empty body
|
|
614
584
|
// is a surface to write on and an empty control is a value not yet given.
|
|
615
|
-
// Asked only for a card that
|
|
616
|
-
// about one that renders none.
|
|
585
|
+
// Asked only for a card that has a body, so the hook is never consulted
|
|
586
|
+
// about one that renders none. The hook is pure by contract, so it is called
|
|
587
|
+
// straight from the derive and its answer kept nowhere: a ghost holds still
|
|
588
|
+
// across a re-derive because the function does.
|
|
617
589
|
bodyGhost: hasBody
|
|
618
590
|
? resolveBodyGhost(
|
|
619
591
|
stringifyGhost(ghostDefault(rows.body ?? undefined)),
|
|
620
|
-
|
|
592
|
+
merged.bodyPlaceholder?.({ cardId: id, kind, isMain }),
|
|
621
593
|
merged.bodyGhost
|
|
622
594
|
)
|
|
623
595
|
: undefined
|
|
@@ -671,13 +643,13 @@
|
|
|
671
643
|
/**
|
|
672
644
|
* Resolve a preview {@link Landing} to a mounted leaf and land in it.
|
|
673
645
|
*
|
|
674
|
-
* Async because the reveal has to
|
|
646
|
+
* Async because the reveal has to render before the landing: a collapsed group
|
|
675
647
|
* is `inert`, which swallows a focus silently, so a caret placed in the same
|
|
676
648
|
* tick as the reveal would go nowhere and report nothing. The consumer's
|
|
677
649
|
* `onPick` ignores the promise: awaiting it is for a caller that wants to
|
|
678
650
|
* observe where the caret went.
|
|
679
651
|
*
|
|
680
|
-
* A destroy lands
|
|
652
|
+
* A destroy lands inside this one: `leaf` is looked up before the flush and
|
|
681
653
|
* dispatched into after it, and a PM view destroyed in that window throws on the
|
|
682
654
|
* dispatch. A consumer's `onPick` outlives the surface it points at too, so the
|
|
683
655
|
* span is asked on the way in as well as after the await.
|
|
@@ -686,15 +658,15 @@
|
|
|
686
658
|
const found = await revealLeaf(at.field);
|
|
687
659
|
if (!found) return missed(`no mounted field at ${at.field}`, at.field);
|
|
688
660
|
// A `'segment'` hit landed on origin-less ink (list markers, a code fence's
|
|
689
|
-
// interior): `pos` is the segment
|
|
661
|
+
// interior): `pos` is the segment start, not a cluster-exact caret
|
|
690
662
|
// (HitGranularity), so it is dropped rather than snapped to a spot the click
|
|
691
663
|
// did not resolve. `'cluster'` (and an absent granularity: the backend did not
|
|
692
|
-
// report it, treat as exact) places the caret. An absent `pos` is the
|
|
664
|
+
// report it, treat as exact) places the caret. An absent `pos` is the placement
|
|
693
665
|
// rung and reaches the same floor.
|
|
694
666
|
land(found, at.granularity === 'segment' ? undefined : at.pos);
|
|
695
667
|
// The arrival cue. Unconditional, unlike the preview side's change-guarded
|
|
696
668
|
// bloom: a preview click is one discrete act, and its commonest target is the
|
|
697
|
-
// leaf
|
|
669
|
+
// leaf already focused (where landing a caret changes nothing on screen) or
|
|
698
670
|
// one off-screen, where the browser's focus-scroll moves the page and leaves
|
|
699
671
|
// the caret to be hunted for in a long form.
|
|
700
672
|
bloomInside(found.control.el);
|
|
@@ -709,7 +681,7 @@
|
|
|
709
681
|
|
|
710
682
|
/**
|
|
711
683
|
* Reveal the field at `field` and hand back its landing handle plus the target it
|
|
712
|
-
* resolved to, once the reveal has
|
|
684
|
+
* resolved to, once the reveal has rendered. The shared half of the two landing
|
|
713
685
|
* verbs: a collapsed group is clipped to zero height and sits inside an `inert`
|
|
714
686
|
* panel, which swallows a focus silently, so a caret placed in the same tick as the
|
|
715
687
|
* reveal goes nowhere and reports nothing. Exactly one card holds the key; the rest
|
|
@@ -717,10 +689,10 @@
|
|
|
717
689
|
*
|
|
718
690
|
* The target comes back with the handle because the caller needs its key to ask for
|
|
719
691
|
* the codec lane (`leaves.prose`), and minting it twice would be two parses of one
|
|
720
|
-
* path. The reveal itself is the
|
|
692
|
+
* path. The reveal itself is the key's: a group holds the field, and an element
|
|
721
693
|
* address opens the same group its array sits in.
|
|
722
694
|
*
|
|
723
|
-
* A destroy lands
|
|
695
|
+
* A destroy lands inside this: the field is looked up before the flush and
|
|
724
696
|
* dispatched into after it, and a PM view destroyed in that window throws on the
|
|
725
697
|
* dispatch. A consumer's call outlives the surface it points at too, so the span is
|
|
726
698
|
* asked on the way in as well as after the await.
|
|
@@ -739,10 +711,10 @@
|
|
|
739
711
|
/**
|
|
740
712
|
* Put the caret in a revealed target, at the finest grain it can take.
|
|
741
713
|
*
|
|
742
|
-
* - An
|
|
714
|
+
* - An element address focuses that row, with no caret inside it: an array element
|
|
743
715
|
* is no `createField` leaf and its handle is a bare focus.
|
|
744
716
|
* - A prose leaf with a `pos` takes the caret at that USV offset.
|
|
745
|
-
* - Everything else takes the focus alone: the offset is a position in
|
|
717
|
+
* - Everything else takes the focus alone: the offset is a position in rendered
|
|
746
718
|
* content and a form control has no coordinate to spend it in (an
|
|
747
719
|
* `<input type="number">` refuses a selection outright), which is also the whole
|
|
748
720
|
* of what a click on plate-placed ink can mean.
|
|
@@ -765,9 +737,9 @@
|
|
|
765
737
|
// They speak the public vocabulary — a `DocPath` for a place, a `CardId` for a
|
|
766
738
|
// card — so a host drives them with what the hooks handed it.
|
|
767
739
|
//
|
|
768
|
-
// A target the surface does not hold is a
|
|
740
|
+
// A target the surface does not hold is a no-op that reports `target-unknown` at
|
|
769
741
|
// `dev`: a key from a previous session or a card already removed, or a path naming
|
|
770
|
-
// no declared field — an
|
|
742
|
+
// no declared field — an element path (`main.references.0`) whose array is
|
|
771
743
|
// undeclared or unmounted included, that being the only way one misses. `setCaret`
|
|
772
744
|
// reports it too, being the verb a preview click drives: a landing that resolved
|
|
773
745
|
// nothing is indistinguishable from one that landed, and a consumer wiring the
|
|
@@ -815,7 +787,7 @@
|
|
|
815
787
|
}
|
|
816
788
|
|
|
817
789
|
/** What a `DocPath` resolves to in the mounted tree: a leaf key, and the array
|
|
818
|
-
*
|
|
790
|
+
* element within it when the address names one. */
|
|
819
791
|
interface LeafTarget {
|
|
820
792
|
key: string;
|
|
821
793
|
element?: number;
|
|
@@ -828,13 +800,13 @@
|
|
|
828
800
|
* `addrForFieldPath` route the diagnostics take, the absolute card index resolved
|
|
829
801
|
* to its live stable id, then the shared `fieldKeyToString` form.
|
|
830
802
|
*
|
|
831
|
-
*
|
|
803
|
+
* Two rungs, because the boundary mints addresses at a finer granularity than
|
|
832
804
|
* `Addr` can name: a `richtext[]` element surfaces as `main.references.0`, which
|
|
833
805
|
* the grammar reads as a field literally named `"0"`. The second rung reads that
|
|
834
|
-
* trailing segment as an
|
|
806
|
+
* trailing segment as an index, under a field the schema declares an array — which
|
|
835
807
|
* is why the ladder is the editor's (VISUAL_EDITOR.md §Surface): the preview
|
|
836
808
|
* carries no schema, and truncating the address there is worse than guessing, since
|
|
837
|
-
* `pos` is an offset into the
|
|
809
|
+
* `pos` is an offset into the element's own content.
|
|
838
810
|
*/
|
|
839
811
|
function leafTargetFor(field: DocPath): LeafTarget | undefined {
|
|
840
812
|
const direct = addrForFieldPath(field);
|
|
@@ -863,7 +835,7 @@
|
|
|
863
835
|
</script>
|
|
864
836
|
|
|
865
837
|
<div class="qm-editor {className ?? ''}" {style} data-qm-root bind:this={rootEl}>
|
|
866
|
-
<!-- `main` and the tips card are
|
|
838
|
+
<!-- `main` and the tips card are one block in the stack: the tips card tucks under
|
|
867
839
|
`main`'s bottom corners, so the two share a seam rather than a gutter and the
|
|
868
840
|
wrapper is what holds them to it. -->
|
|
869
841
|
<div class="qm-primary">
|
|
@@ -895,7 +867,7 @@
|
|
|
895
867
|
</div>
|
|
896
868
|
|
|
897
869
|
{@render addAffordance(0)}
|
|
898
|
-
<!-- A card and the gap under it are one
|
|
870
|
+
<!-- A card and the gap under it are one slot, which is what a reorder moves: the
|
|
899
871
|
strip below a card is the same strip wherever the card lands, so it rides along
|
|
900
872
|
rather than being slid across. It is also the shape `animate:` asks for, being the
|
|
901
873
|
keyed block's only child. -->
|
|
@@ -925,47 +897,46 @@
|
|
|
925
897
|
|
|
926
898
|
<FormatPopover {getActiveLeaf} />
|
|
927
899
|
|
|
928
|
-
<!-- Cards always render; the
|
|
900
|
+
<!-- Cards always render; the add affordance is gated on the schema declaring
|
|
929
901
|
`card_kinds`, since there is nothing to seed otherwise. A card already in the
|
|
930
902
|
document shows regardless of its kind: a kind with no schema (foreign, or a
|
|
931
903
|
schema with no `card_kinds` at all) degrades to a recovery shell inside <Card>
|
|
932
904
|
(retype + delete), never gated away, so its content is neither dropped nor
|
|
933
|
-
trapped. The gate lives
|
|
905
|
+
trapped. The gate lives here rather than at each call site: the strip is one
|
|
934
906
|
decision, and two copies of it drift into a stack with a gap at one end. -->
|
|
935
907
|
{#snippet addAffordance(atIndex: number)}
|
|
936
908
|
{#if kinds.length}
|
|
937
|
-
<!-- The strip past the last card is the
|
|
909
|
+
<!-- The strip past the last card is the append point, the one a reader looks for
|
|
938
910
|
rather than finds by position, so it alone states itself in words at rest. The
|
|
939
|
-
gaps between cards stay bare: the strip
|
|
911
|
+
gaps between cards stay bare: the strip is the gap, and the pill each fills on
|
|
940
912
|
hover draws what a label would state, the space the new card takes. -->
|
|
941
913
|
{@const marked = atIndex === model.cards.length}
|
|
942
914
|
<div class="qm-add-card" class:qm-add-card-marked={marked}>
|
|
943
|
-
<!-- Marked, the words
|
|
944
|
-
them; bare, the
|
|
945
|
-
carry
|
|
915
|
+
<!-- Marked, the words are the accessible name and no `aria-label` doubles
|
|
916
|
+
them; bare, the same words land as the label, the strip having no
|
|
917
|
+
geometry to carry them. -->
|
|
946
918
|
{#if kinds.length === 1}
|
|
947
919
|
<button
|
|
948
920
|
type="button"
|
|
949
|
-
class="qm-add-btn qm-add-affordance"
|
|
950
|
-
aria-label={marked ? undefined : merged.
|
|
951
|
-
onclick={() => addCard(atIndex, kinds[0])}
|
|
952
|
-
>{marked ? merged.addCardMark(humanize(kinds[0])) : ''}</button
|
|
921
|
+
class="qm-add-btn qm-add-affordance qm-tap-floor"
|
|
922
|
+
aria-label={marked ? undefined : merged.addCard}
|
|
923
|
+
onclick={() => addCard(atIndex, kinds[0])}>{marked ? merged.addCard : ''}</button
|
|
953
924
|
>
|
|
954
925
|
{:else}
|
|
955
|
-
<!-- Multi-kind add: pick the kind, then seed + insert. A
|
|
926
|
+
<!-- Multi-kind add: pick the kind, then seed + insert. A menu rather than a
|
|
956
927
|
disclosure: it floats out of the stack, so raising it moves no card, and
|
|
957
928
|
it dismisses on pick, on Escape and on a click outside, none of which a
|
|
958
929
|
`<details>` does. The trigger is bits-ui's `<button>`, which is why the
|
|
959
930
|
rules below reach it through `:global`. -->
|
|
960
931
|
<DropdownMenu.Root>
|
|
961
932
|
<DropdownMenu.Trigger
|
|
962
|
-
class="qm-add-btn qm-add-affordance"
|
|
933
|
+
class="qm-add-btn qm-add-affordance qm-tap-floor"
|
|
963
934
|
aria-label={marked ? undefined : merged.addCard}
|
|
964
|
-
>{marked ? merged.
|
|
935
|
+
>{marked ? merged.addCard : ''}</DropdownMenu.Trigger
|
|
965
936
|
>
|
|
966
937
|
<DropdownMenu.Portal to={rootEl}>
|
|
967
938
|
<DropdownMenu.Content sideOffset={4}>
|
|
968
|
-
<!-- Portalled out of the row but
|
|
939
|
+
<!-- Portalled out of the row but into the stack's root, and carrying the
|
|
969
940
|
marker itself: floating is still a detached subtree to the
|
|
970
941
|
derivation, like FormatPopover and the enum listbox. -->
|
|
971
942
|
<div class="qm-menu-surface" data-qm-root>
|
|
@@ -985,33 +956,40 @@
|
|
|
985
956
|
|
|
986
957
|
<style>
|
|
987
958
|
/* The private scale lands via `data-qm-root` on the root element above: this is
|
|
988
|
-
a
|
|
959
|
+
a detached root, one of those core/theme.css applies the derivation to. The
|
|
989
960
|
root rule also carries the baseline font and colour, so nothing here restates
|
|
990
961
|
them. Nothing here mints; `check:style` enforces that. */
|
|
991
|
-
/* The
|
|
962
|
+
/* The column, not only the cards in it: the gutter the stack sits in and the tone
|
|
992
963
|
behind it are the surface's own, so a bare `<div>` is a mounting site and
|
|
993
964
|
nothing is owed before the editor looks right. Consumer CSS is unlayered and
|
|
994
965
|
beats all of it, which is how a host that wants the column back takes it.
|
|
995
|
-
`border-box`, so a height from the caller is the height this draws.
|
|
966
|
+
`border-box`, so a height from the caller is the height this draws.
|
|
967
|
+
|
|
968
|
+
The sunken rung, which is what makes a card an island: the cards are the base
|
|
969
|
+
plane and this is the one they float on, so the gutter reads as ground rather
|
|
970
|
+
than as more card (ARCHITECTURE §Styling). The inset is the widest rung on the
|
|
971
|
+
ramp, which is what gives the ground enough of itself to read as ground: at a
|
|
972
|
+
narrower one the stack meets whatever frames the surface and the islands are
|
|
973
|
+
back to being one block clipped at its edges. */
|
|
996
974
|
.qm-editor {
|
|
997
975
|
box-sizing: border-box;
|
|
998
976
|
display: flex;
|
|
999
977
|
flex-direction: column;
|
|
1000
978
|
gap: var(--_qm-space-2);
|
|
1001
|
-
padding: var(--_qm-space-
|
|
1002
|
-
background: var(--_qm-surface);
|
|
979
|
+
padding: var(--_qm-space-4);
|
|
980
|
+
background: var(--_qm-surface-sunken);
|
|
1003
981
|
color: var(--_qm-ink);
|
|
1004
982
|
}
|
|
1005
|
-
/* A fixed-height pane, which the stack is
|
|
983
|
+
/* A fixed-height pane, which the stack is not by default, and the four popovers
|
|
1006
984
|
are the reason: each resolves its portal target with
|
|
1007
985
|
`closest('[data-qm-root]')`, which is this element, so an `overflow` here
|
|
1008
986
|
clips the menu a leaf raised. Mounted in a page that scrolls, nothing clips;
|
|
1009
987
|
mounted in a pane that does, the host says so and takes the clipping it would
|
|
1010
|
-
have had from its own scrolling frame either way. The
|
|
988
|
+
have had from its own scrolling frame either way. The tail rides with the
|
|
1011
989
|
scroller because it is only meaningful under one: dead space below the last
|
|
1012
990
|
card, so it can be read mid-pane rather than against the bottom edge.
|
|
1013
991
|
|
|
1014
|
-
The
|
|
992
|
+
The height is what makes the rest of the rule mean anything, and it is the
|
|
1015
993
|
pane's rather than a number of ours: `overflow` on a box that grows to its
|
|
1016
994
|
content never has anything to scroll, so the stack would run past the pane
|
|
1017
995
|
and be clipped by whatever the host frames it with. Taking the pane's height
|
|
@@ -1029,7 +1007,7 @@
|
|
|
1029
1007
|
padding-block-end: var(--_qm-tail);
|
|
1030
1008
|
}
|
|
1031
1009
|
/* The stack's one gapless seam, which `TipsCard` draws and this holds the two
|
|
1032
|
-
blocks to. `main` has to paint
|
|
1010
|
+
blocks to. `main` has to paint over the tip: a later sibling paints over an earlier
|
|
1033
1011
|
one otherwise, and the tip's background would cover the corners it is there to
|
|
1034
1012
|
fill. `isolation` keeps that z-index local, so one wrapper owns the ordering and no
|
|
1035
1013
|
card below inherits an argument about it. The wrapper is a single flex item, so the
|
|
@@ -1051,56 +1029,71 @@
|
|
|
1051
1029
|
gap: var(--_qm-space-2);
|
|
1052
1030
|
}
|
|
1053
1031
|
/* The strip between two blocks, and the whole of it is the target: a gap is found
|
|
1054
|
-
by
|
|
1055
|
-
to aim at in the middle of it. It takes the editor's gap back on
|
|
1056
|
-
it is not a control sitting in the gutter; it
|
|
1057
|
-
cards is the trigger's own height
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1032
|
+
by position, so reveal and hit region are the full-bleed row rather than a word
|
|
1033
|
+
to aim at in the middle of it. It takes the editor's gap back on both sides, so
|
|
1034
|
+
it is not a control sitting in the gutter; it is the gutter: what separates two
|
|
1035
|
+
cards is the trigger's own height and nothing else, and the pill it fills on hover
|
|
1036
|
+
is edge to edge the space the new card opens into. No band is held back as
|
|
1037
|
+
miss-tolerance: gutter that reads as the trigger and inserts nothing unsays what
|
|
1038
|
+
the fill claims, so a press anywhere between two cards inserts. Absorbed rather
|
|
1039
|
+
than removed, because `gap` is also what separates the one seam no strip sits in:
|
|
1040
|
+
every card from the next under a quill declaring no kinds, where the affordance
|
|
1041
|
+
does not render at all. */
|
|
1064
1042
|
.qm-add-card {
|
|
1065
1043
|
display: flex;
|
|
1066
1044
|
margin-top: calc(var(--_qm-space-2) * -1);
|
|
1067
1045
|
margin-bottom: calc(var(--_qm-space-2) * -1);
|
|
1068
1046
|
}
|
|
1069
1047
|
/* Unboxed, like every button, and not dashed: a
|
|
1070
|
-
dashed edge is the
|
|
1048
|
+
dashed edge is the placeholder idiom ("nothing is here yet") which on a button
|
|
1071
1049
|
reads as disabled or as a drop target. It stays honest in one place, the
|
|
1072
1050
|
un-schemable card (`Card.svelte`), which is a state rather than a control. A bare
|
|
1073
|
-
trigger rests invisible, so the
|
|
1051
|
+
trigger rests invisible, so the fill is the whole of what it shows: the pill fills
|
|
1074
1052
|
the strip because the strip is what was pressed, and the strip is the gap, so the
|
|
1075
1053
|
fill is the card-to-be drawn where it will land. Every gap is an equal entry point,
|
|
1076
1054
|
reached by the name rather than by a mark: a card goes anywhere in the stack, not
|
|
1077
1055
|
just after the last one.
|
|
1078
1056
|
|
|
1057
|
+
No inset of its own: the pill and the gap are the same rectangle, and a bare strip is
|
|
1058
|
+
a band of gutter, so its height is a rhythm rung. The press floor the box gives up is
|
|
1059
|
+
`.qm-tap-floor`'s (controls.css); what it overhangs is the card's own inset above and
|
|
1060
|
+
below, which holds no target, so the floor costs a neighbour nothing.
|
|
1061
|
+
|
|
1062
|
+
Ink is pinned to the label tone in every state, taking back the family's hover step:
|
|
1063
|
+
the trigger arrives and leaves on opacity, and a tone moving under an opacity already
|
|
1064
|
+
moving reads as two trips at different speeds.
|
|
1065
|
+
|
|
1079
1066
|
`:global`, because the multi-kind trigger is bits-ui's own element and a `class`
|
|
1080
1067
|
passed to a primitive is a plain string that never picks up the scoping hash:
|
|
1081
1068
|
the same seam the enum trigger is styled through. */
|
|
1082
1069
|
.qm-add-card :global(.qm-add-btn) {
|
|
1083
|
-
/* No inset of its own: the pill and the gap are the same rectangle, and the tap
|
|
1084
|
-
floor is the whole of the height, which is what the marked strip's words
|
|
1085
|
-
centre in. */
|
|
1086
1070
|
display: flex;
|
|
1087
1071
|
align-items: center;
|
|
1088
1072
|
justify-content: center;
|
|
1089
1073
|
width: 100%;
|
|
1074
|
+
height: var(--_qm-space-4);
|
|
1075
|
+
color: var(--_qm-ink-label);
|
|
1076
|
+
opacity: 0;
|
|
1077
|
+
transition:
|
|
1078
|
+
opacity var(--_qm-reveal),
|
|
1079
|
+
background-color var(--_qm-reveal);
|
|
1090
1080
|
}
|
|
1091
|
-
/*
|
|
1092
|
-
|
|
1093
|
-
(`controls.css`), which is the size the fields above are READ at. */
|
|
1081
|
+
/* The strip that holds words at rest takes the height that centres them, and rests dim
|
|
1082
|
+
rather than absent, having something to be seen. */
|
|
1094
1083
|
.qm-add-card-marked :global(.qm-add-btn) {
|
|
1084
|
+
height: var(--_qm-tap-min);
|
|
1095
1085
|
font-size: var(--_qm-text-label);
|
|
1086
|
+
font-weight: var(--_qm-weight-mid);
|
|
1087
|
+
opacity: var(--_qm-opacity-idle);
|
|
1096
1088
|
}
|
|
1097
|
-
/*
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
strip, so a pointer moving onto an item has left the row whose hover was drawing
|
|
1089
|
+
/* One arrival, three ways in. Keyboard focus, because the fill is the only thing that
|
|
1090
|
+
reads as the trigger on a bare strip; and an open menu, because it portals out of
|
|
1091
|
+
the strip, so a pointer moving onto an item has left the row whose hover was drawing
|
|
1101
1092
|
the pill the menu hangs from. */
|
|
1093
|
+
.qm-add-card :global(.qm-add-btn:hover),
|
|
1102
1094
|
.qm-add-card :global(.qm-add-btn:focus-visible),
|
|
1103
1095
|
.qm-add-card :global(.qm-add-btn[data-state='open']) {
|
|
1104
|
-
|
|
1096
|
+
opacity: 1;
|
|
1097
|
+
background: var(--_qm-add-fill);
|
|
1105
1098
|
}
|
|
1106
1099
|
</style>
|