@quillmark/svelte 0.1.1 → 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 +16 -12
- package/THEMING.md +48 -25
- package/dist/core/address.d.ts +42 -7
- package/dist/core/address.js +49 -21
- 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 +14 -11
- package/dist/core/codec/field.d.ts +22 -27
- 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 +266 -228
- 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 +73 -27
- package/dist/core/errors.d.ts +17 -7
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +6 -5
- package/dist/core/lifecycle.d.ts +21 -5
- package/dist/core/lifecycle.js +34 -8
- package/dist/core/rebind.svelte.d.ts +18 -4
- package/dist/core/rebind.svelte.js +30 -12
- package/dist/core/teardown.d.ts +1 -1
- package/dist/core/teardown.js +1 -1
- package/dist/core/theme.css +173 -58
- package/dist/preset/index.css +1 -1
- package/dist/preset/recipes.css +79 -30
- package/dist/preset/scale.css +40 -28
- package/dist/preview/Preview.svelte +22 -19
- package/dist/preview/Preview.svelte.d.ts +8 -8
- package/dist/preview/bridge.d.ts +6 -4
- package/dist/preview/bridge.js +19 -9
- package/dist/preview/controller.d.ts +13 -6
- package/dist/preview/controller.js +8 -5
- package/dist/preview/geometry.d.ts +17 -2
- package/dist/preview/geometry.js +21 -1
- package/dist/preview/index.d.ts +1 -0
- package/dist/preview/index.js +9 -2
- package/dist/preview/overlay.d.ts +2 -1
- package/dist/preview/overlay.js +26 -11
- package/dist/preview/paint.js +19 -17
- package/dist/visual/ArrayField.svelte +103 -83
- package/dist/visual/ArrayField.svelte.d.ts +5 -4
- package/dist/visual/BooleanField.svelte +8 -6
- package/dist/visual/BooleanField.svelte.d.ts +2 -2
- package/dist/visual/Card.svelte +161 -333
- package/dist/visual/Card.svelte.d.ts +5 -4
- 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 +2 -2
- package/dist/visual/EnumField.svelte +48 -28
- package/dist/visual/EnumField.svelte.d.ts +5 -4
- package/dist/visual/Field.svelte +207 -132
- package/dist/visual/Field.svelte.d.ts +11 -7
- package/dist/visual/FieldHint.svelte +79 -48
- package/dist/visual/FieldHint.svelte.d.ts +1 -1
- package/dist/visual/FieldLabel.svelte +8 -8
- package/dist/visual/FormatPopover.svelte +82 -35
- package/dist/visual/NumberField.svelte +5 -5
- package/dist/visual/NumberField.svelte.d.ts +1 -1
- package/dist/visual/ObjectField.svelte +28 -9
- package/dist/visual/ObjectField.svelte.d.ts +4 -2
- package/dist/visual/ProseArrayElement.svelte +12 -19
- package/dist/visual/ProseArrayElement.svelte.d.ts +1 -2
- package/dist/visual/ProseField.svelte +62 -85
- package/dist/visual/ProseField.svelte.d.ts +14 -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 +18 -19
- package/dist/visual/VisualEditor.svelte.d.ts +2 -3
- package/dist/visual/VisualEditorInner.svelte +251 -184
- package/dist/visual/VisualEditorInner.svelte.d.ts +2 -3
- package/dist/visual/controls.css +85 -38
- package/dist/visual/diagnostics.d.ts +13 -21
- package/dist/visual/diagnostics.js +7 -19
- 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.d.ts +1 -0
- package/dist/visual/index.js +10 -3
- package/dist/visual/leaves.d.ts +47 -0
- package/dist/visual/leaves.js +31 -0
- package/dist/visual/links.d.ts +24 -0
- package/dist/visual/links.js +67 -0
- package/dist/visual/props.d.ts +33 -12
- 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 +4 -2
- package/dist/visual/tips.js +7 -4
- package/package.json +2 -2
|
@@ -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
|
-
`setCaret(
|
|
20
|
-
• the
|
|
19
|
+
`setCaret(at)` entry wired to the preview;
|
|
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
|
-->
|
|
@@ -34,24 +34,25 @@
|
|
|
34
34
|
import { onDestroy, tick } from 'svelte';
|
|
35
35
|
import { DropdownMenu } from 'bits-ui';
|
|
36
36
|
import { isQuillmarkError, MAIN_CARD_ADDR } from '@quillmark/wasm';
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
addrForFieldPath,
|
|
39
|
+
cardPath,
|
|
40
|
+
elementAddrForFieldPath,
|
|
41
|
+
fieldPathForAddr,
|
|
42
|
+
type DocPath,
|
|
43
|
+
type Landing
|
|
44
|
+
} from '../core/address.js';
|
|
38
45
|
import { errorMessage, reportError } from '../core/errors.js';
|
|
39
46
|
import { bloomInside } from '../core/bloom.js';
|
|
40
47
|
import { createLifespan } from '../core/teardown.js';
|
|
41
|
-
import type {
|
|
42
|
-
Addr,
|
|
43
|
-
CardAddr,
|
|
44
|
-
ContentHit,
|
|
45
|
-
Diagnostic,
|
|
46
|
-
Resolved,
|
|
47
|
-
ResolvedField
|
|
48
|
-
} from '@quillmark/wasm';
|
|
48
|
+
import type { Addr, CardAddr, Diagnostic, Resolved, ResolvedField } from '@quillmark/wasm';
|
|
49
49
|
import type { VisualEditorProps } from './props.js';
|
|
50
50
|
import { mergeStrings, setWording } from './strings.js';
|
|
51
51
|
import type { CardId, ChangeSource } from './signals.js';
|
|
52
52
|
import type { FieldController } from '../core/codec/index.js';
|
|
53
53
|
import {
|
|
54
54
|
IdSeq,
|
|
55
|
+
controlKind,
|
|
55
56
|
fieldModels,
|
|
56
57
|
groupOrder,
|
|
57
58
|
groupSections,
|
|
@@ -65,13 +66,11 @@
|
|
|
65
66
|
stringifyGhost,
|
|
66
67
|
resolveBodyGhost,
|
|
67
68
|
NO_RESOLVED_ROWS,
|
|
68
|
-
type BodyPlaceholder,
|
|
69
69
|
type CardModel,
|
|
70
70
|
type ResolvedCardRows
|
|
71
71
|
} from './structure.js';
|
|
72
72
|
import {
|
|
73
73
|
fieldKeyToString,
|
|
74
|
-
parsePath,
|
|
75
74
|
resolveCardKey,
|
|
76
75
|
routeAndResolve,
|
|
77
76
|
mergeDiagnostics,
|
|
@@ -79,6 +78,7 @@
|
|
|
79
78
|
type RoutedDiagnostic
|
|
80
79
|
} from './diagnostics.js';
|
|
81
80
|
import { fieldDomIds, groupPanelId } from './domid.js';
|
|
81
|
+
import { createLeafRegistry, type FieldControl } from './leaves.js';
|
|
82
82
|
import { reorder } from './motion.js';
|
|
83
83
|
import { tipsChannel } from './tips.js';
|
|
84
84
|
import { patchEditorExt } from './ext.js';
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
onError,
|
|
99
99
|
diagnostics,
|
|
100
100
|
enumOptionAllowed,
|
|
101
|
+
enumDisallowed = 'disable',
|
|
101
102
|
strings,
|
|
102
103
|
formatDiagnostic,
|
|
103
104
|
class: className,
|
|
@@ -120,7 +121,7 @@
|
|
|
120
121
|
// ── Reactivity + session identity ───────────────────────────────────────────
|
|
121
122
|
let revision = $state(0);
|
|
122
123
|
// This editor's own id, prefixed onto every field's DOM ids (`domid.ts`). The
|
|
123
|
-
// leaf-key space is unique per
|
|
124
|
+
// leaf-key space is unique per editor, not per page: two editors mounted
|
|
124
125
|
// side-by-side both hold `main:subject`, and a duplicate `id` makes `for` resolve
|
|
125
126
|
// to whichever mounted first. `$props.id()` is stable across SSR and hydration,
|
|
126
127
|
// which a module counter is not.
|
|
@@ -148,13 +149,13 @@
|
|
|
148
149
|
/**
|
|
149
150
|
* Re-derive the card tree and report the edit. Called for the two lanes that go
|
|
150
151
|
* through this component; a prose leaf commits itself and reports through
|
|
151
|
-
* `proseChanged`, which does
|
|
152
|
+
* `proseChanged`, which does not bump the revision (bumping it would re-derive
|
|
152
153
|
* and remount every leaf, costing the caret on every keystroke).
|
|
153
154
|
*
|
|
154
155
|
* `cardId` is passed rather than derived from `at`: the sites below hold the id
|
|
155
156
|
* already, and the removal has no address to derive one from. `at` is an `Addr` for
|
|
156
|
-
* a leaf and a
|
|
157
|
-
* 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
|
|
158
159
|
* an op's own index is only addressable against the tree the bump re-derived (an
|
|
159
160
|
* insert's card does not exist in the previous one, and a retype's kind is the
|
|
160
161
|
* previous kind).
|
|
@@ -179,7 +180,7 @@
|
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
// ── Teardown (core/teardown.ts: unregister, cancel, then free) ──────────────
|
|
182
|
-
// A document swap is a
|
|
183
|
+
// A document swap is a remount (see the remount contract above), so a destroy is
|
|
183
184
|
// the only way this surface ends and one span covers both. Cancellers register
|
|
184
185
|
// below in the order they run: the registry that resolves new work first, then
|
|
185
186
|
// the work already scheduled. The document handle is the consumer's to free, so
|
|
@@ -187,8 +188,11 @@
|
|
|
187
188
|
const span = createLifespan();
|
|
188
189
|
onDestroy(() => span.end());
|
|
189
190
|
|
|
190
|
-
// ── Leaf registry (
|
|
191
|
-
|
|
191
|
+
// ── Leaf registry (landing target lookup + the active-leaf seam) ────────────
|
|
192
|
+
// Every mounted field is in it, prose leaf and form control alike (`leaves.ts`):
|
|
193
|
+
// a landing needs a focus and a box to bloom, which every control has, and the
|
|
194
|
+
// codec lane is asked for only where there is an offset to place.
|
|
195
|
+
const leaves = createLeafRegistry();
|
|
192
196
|
// Each leaf unregisters itself on its own teardown; this covers the surface
|
|
193
197
|
// going away as a whole, where a leaf's cleanup order relative to the parent's
|
|
194
198
|
// is Svelte's business and not something to depend on.
|
|
@@ -202,14 +206,8 @@
|
|
|
202
206
|
let mainCard = $state<CardHandle | undefined>(undefined);
|
|
203
207
|
let cardRefs = $state<(CardHandle | undefined)[]>([]);
|
|
204
208
|
/** The stack's own element: it carries `data-qm-root`, so it is what the kind
|
|
205
|
-
* menu portals
|
|
209
|
+
* menu portals into, the way each leaf's surface resolves its nearest root. */
|
|
206
210
|
let rootEl = $state<HTMLElement | undefined>(undefined);
|
|
207
|
-
function register(key: string, controller: FieldController): void {
|
|
208
|
-
leaves.set(key, controller);
|
|
209
|
-
}
|
|
210
|
-
function unregister(key: string): void {
|
|
211
|
-
leaves.delete(key);
|
|
212
|
-
}
|
|
213
211
|
|
|
214
212
|
// ── Focus + bridge outputs ──────────────────────────────────────────────────
|
|
215
213
|
// `activeCardId` is the id-keyed half of `activeAddr` (whose `card` is positional),
|
|
@@ -235,7 +233,7 @@
|
|
|
235
233
|
* common case allocates nothing. */
|
|
236
234
|
const NO_KINDS: readonly string[] = [];
|
|
237
235
|
/**
|
|
238
|
-
* The live kinds by content index, off the
|
|
236
|
+
* The live kinds by content index, off the derived card tree rather than
|
|
239
237
|
* `doc.cards`: they are already in hand, and `doc.cards` serializes every card on
|
|
240
238
|
* each read, which is not a thing to do per keystroke.
|
|
241
239
|
*/
|
|
@@ -249,13 +247,13 @@
|
|
|
249
247
|
function pathFor(addr: Addr): DocPath | undefined {
|
|
250
248
|
return fieldPathForAddr(addr, addr.card == null ? NO_KINDS : liveKinds());
|
|
251
249
|
}
|
|
252
|
-
// The last
|
|
250
|
+
// The last place reported, so a caret that did not move is not news. A leaf fires
|
|
253
251
|
// one caret signal per transaction, and a transaction need not have moved the
|
|
254
252
|
// caret to exist: a mark toggle over the same range, an anchor op, a plugin's own
|
|
255
253
|
// bookkeeping. The memo is the editor's rather than each leaf's, because a place
|
|
256
254
|
// spans leaves: a caret that leaves `main.body` at 5 for a card leaf and comes
|
|
257
255
|
// back to 5 made two moves, and a per-leaf memo would swallow the second. An edit
|
|
258
|
-
// that reflows the page
|
|
256
|
+
// that reflows the page under a caret holding its offset is the case this drops,
|
|
259
257
|
// and a preview does not need the signal to answer it: the place is unchanged, so
|
|
260
258
|
// the one it already followed is the one its recompile re-locates.
|
|
261
259
|
let lastCaretField: DocPath | undefined;
|
|
@@ -277,18 +275,18 @@
|
|
|
277
275
|
|
|
278
276
|
// ── Commit routing ──────────────────────────────────────────────────────────
|
|
279
277
|
// Scalars / arrays / objects → the typed writer (schema-checked). Prose leaves
|
|
280
|
-
// commit themselves via the codec (applyChange) and do
|
|
278
|
+
// commit themselves via the codec (applyChange) and do not pass through here.
|
|
281
279
|
// A scalar control commits `undefined` for a cleared entry: the unset lane
|
|
282
280
|
// below (`doc.removeField`), not a write.
|
|
283
281
|
//
|
|
284
|
-
// A bad value makes `writer.set`
|
|
282
|
+
// A bad value makes `writer.set` throw a `QuillmarkError`, whose
|
|
285
283
|
// `diagnostics[0]` carries a `code` and a canonical `path` (e.g.
|
|
286
284
|
// `edit::field_coercion_failed` at `main.font_size`, or `edit::unknown_field`). The
|
|
287
|
-
// editor already
|
|
288
|
-
// 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
|
|
289
287
|
// parsed from the positional path; while surfacing the thrown diagnostic
|
|
290
288
|
// verbatim (its `code`/`message`) as the payload (VISUAL_EDITOR §Diagnostics,
|
|
291
|
-
// producer #2). It is stashed in `commitErrors`; a subsequent
|
|
289
|
+
// producer #2). It is stashed in `commitErrors`; a subsequent successful commit
|
|
292
290
|
// for the same field clears it. Nothing here gates: the value is not written
|
|
293
291
|
// (the document is unchanged on throw, per the boundary's own transactional
|
|
294
292
|
// contract) and editing continues.
|
|
@@ -297,14 +295,14 @@
|
|
|
297
295
|
const keyStr = fieldKeyToString(key);
|
|
298
296
|
try {
|
|
299
297
|
if (value === undefined) {
|
|
300
|
-
// The
|
|
301
|
-
// 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
|
|
302
300
|
// engine's authored › `default:` › zero-fill resolve renders the
|
|
303
301
|
// default, rather than baking a snapshot the schema can't track
|
|
304
302
|
// (canon SCHEMAS.md: the engine never persists a default; nor do we).
|
|
305
303
|
// Removal writes no value, so there is nothing for a schema to conform:
|
|
306
304
|
// it goes through the quill-free `doc.removeField` (bare string = main
|
|
307
|
-
// `{ field }`; `{ card, field }` for a card),
|
|
305
|
+
// `{ field }`; `{ card, field }` for a card), not the typed writer.
|
|
308
306
|
if (isMain) {
|
|
309
307
|
doc.removeField(name);
|
|
310
308
|
} else {
|
|
@@ -437,7 +435,7 @@
|
|
|
437
435
|
});
|
|
438
436
|
// No addr: the removed card has no address left, and the surviving cards'
|
|
439
437
|
// addresses all shifted. The stack changed, not a leaf. The id still names
|
|
440
|
-
//
|
|
438
|
+
// which card went, which is the one thing a host keying on it needs, and the
|
|
441
439
|
// only handle the removal leaves it.
|
|
442
440
|
bump('structure', id);
|
|
443
441
|
}
|
|
@@ -463,7 +461,7 @@
|
|
|
463
461
|
bump('structure', id, i);
|
|
464
462
|
}
|
|
465
463
|
/**
|
|
466
|
-
* Clear the tips channel: the dismissal write, and the
|
|
464
|
+
* Clear the tips channel: the dismissal write, and the only write
|
|
467
465
|
* tips make. `undefined` drops the key while `title` and any later sibling ride
|
|
468
466
|
* through; `ext.ts` holds why that matters.
|
|
469
467
|
*/
|
|
@@ -476,7 +474,7 @@
|
|
|
476
474
|
}
|
|
477
475
|
|
|
478
476
|
// ── Addressing + per-card op bundle ─────────────────────────────────────────
|
|
479
|
-
/** A
|
|
477
|
+
/** A live card address: `card` is a getter, so a reorder re-targets in place. */
|
|
480
478
|
function cardAddr(id: string, field?: string): Addr {
|
|
481
479
|
return field != null
|
|
482
480
|
? ({
|
|
@@ -497,9 +495,7 @@
|
|
|
497
495
|
}
|
|
498
496
|
|
|
499
497
|
// ── Diagnostics routing (VISUAL_EDITOR §Diagnostics) ────────────────────────
|
|
500
|
-
// Producer #1: quill.validate(doc), re-run every revision
|
|
501
|
-
// `[]` for usaf_memo (no field in the fixture carries a `!must_fill` marker)
|
|
502
|
-
// but the routing exists for the general contract, not just this fixture).
|
|
498
|
+
// Producer #1: quill.validate(doc), re-run every revision.
|
|
503
499
|
const validation = $derived.by(() => {
|
|
504
500
|
revision; // re-run on every mutation, per VISUAL_EDITOR §Diagnostics
|
|
505
501
|
try {
|
|
@@ -526,7 +522,7 @@
|
|
|
526
522
|
return mergeDiagnostics(fromValidate, fromExternal, [...commitErrors.values()]);
|
|
527
523
|
});
|
|
528
524
|
function opsFor(id: string, isMain: boolean) {
|
|
529
|
-
//
|
|
525
|
+
// One identity per field: the leaf registry, the DOM's three names, and the
|
|
530
526
|
// diagnostics map all key off this string, so a leaf, its label and its
|
|
531
527
|
// diagnostics can only ever resolve together.
|
|
532
528
|
const leafKey = (field?: string) => fieldKeyToString({ card: isMain ? undefined : id, field });
|
|
@@ -548,33 +544,6 @@
|
|
|
548
544
|
};
|
|
549
545
|
}
|
|
550
546
|
|
|
551
|
-
// ── The empty-body ghost's consumer wording ─────────────────────────────────
|
|
552
|
-
// The hook is consulted once per KIND and its answer kept for the session, which
|
|
553
|
-
// is the whole determinism guarantee: a hook that samples a witty set at random
|
|
554
|
-
// is impure by design, and this cache is what makes its answer look chosen:
|
|
555
|
-
// same string for every card of a kind, and the same one after a remount or any
|
|
556
|
-
// re-derive. Keyed by kind rather than by card id deliberately: two empty cards
|
|
557
|
-
// that are the same kind ARE the same invitation, and disagreeing ghosts read as
|
|
558
|
-
// a glitch. Retyping a card crosses to another key and re-asks, which is right:
|
|
559
|
-
// it is a different card now. Plain (non-`$state`) on purpose: memoization, so
|
|
560
|
-
// filling it during a derive must not feed back into one.
|
|
561
|
-
let ghostHook: BodyPlaceholder | undefined;
|
|
562
|
-
const ghostByKind = new Map<string, string | undefined>();
|
|
563
|
-
function customBodyGhost(kind: string, isMain: boolean): string | undefined {
|
|
564
|
-
const bodyPlaceholder = merged.bodyPlaceholder;
|
|
565
|
-
// A swapped hook invalidates every answer the old one gave.
|
|
566
|
-
if (bodyPlaceholder !== ghostHook) {
|
|
567
|
-
ghostByKind.clear();
|
|
568
|
-
ghostHook = bodyPlaceholder;
|
|
569
|
-
}
|
|
570
|
-
if (!bodyPlaceholder) return undefined;
|
|
571
|
-
// `main` is not a `card_kinds` key, so a kind that spells it collides without
|
|
572
|
-
// the flag in the key.
|
|
573
|
-
const key = `${isMain ? '1' : '0'}\0${kind}`;
|
|
574
|
-
if (!ghostByKind.has(key)) ghostByKind.set(key, bodyPlaceholder({ kind, isMain }));
|
|
575
|
-
return ghostByKind.get(key);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
547
|
// ── The derived card tree (schema × payload join) ───────────────────────────
|
|
579
548
|
function buildCard(
|
|
580
549
|
id: string,
|
|
@@ -613,12 +582,14 @@
|
|
|
613
582
|
// same text-ghost projection `<Field>` applies to a field's row; and falls
|
|
614
583
|
// back to an invitation where a scalar shows nothing, because an empty body
|
|
615
584
|
// is a surface to write on and an empty control is a value not yet given.
|
|
616
|
-
// Asked only for a card that
|
|
617
|
-
// 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.
|
|
618
589
|
bodyGhost: hasBody
|
|
619
590
|
? resolveBodyGhost(
|
|
620
591
|
stringifyGhost(ghostDefault(rows.body ?? undefined)),
|
|
621
|
-
|
|
592
|
+
merged.bodyPlaceholder?.({ cardId: id, kind, isMain }),
|
|
622
593
|
merged.bodyGhost
|
|
623
594
|
)
|
|
624
595
|
: undefined
|
|
@@ -670,64 +641,93 @@
|
|
|
670
641
|
|
|
671
642
|
// ── Public entry points ─────────────────────────────────────────────────────
|
|
672
643
|
/**
|
|
673
|
-
* Resolve a preview
|
|
644
|
+
* Resolve a preview {@link Landing} to a mounted leaf and land in it.
|
|
674
645
|
*
|
|
675
|
-
* Async because the reveal has to
|
|
646
|
+
* Async because the reveal has to render before the landing: a collapsed group
|
|
676
647
|
* is `inert`, which swallows a focus silently, so a caret placed in the same
|
|
677
648
|
* tick as the reveal would go nowhere and report nothing. The consumer's
|
|
678
|
-
* `
|
|
649
|
+
* `onPick` ignores the promise: awaiting it is for a caller that wants to
|
|
679
650
|
* observe where the caret went.
|
|
680
651
|
*
|
|
681
|
-
* A destroy lands
|
|
652
|
+
* A destroy lands inside this one: `leaf` is looked up before the flush and
|
|
682
653
|
* dispatched into after it, and a PM view destroyed in that window throws on the
|
|
683
|
-
* dispatch. A consumer's `
|
|
684
|
-
*
|
|
654
|
+
* dispatch. A consumer's `onPick` outlives the surface it points at too, so the
|
|
655
|
+
* span is asked on the way in as well as after the await.
|
|
685
656
|
*/
|
|
686
|
-
export async function setCaret(
|
|
687
|
-
const
|
|
688
|
-
if (!
|
|
657
|
+
export async function setCaret(at: Landing): Promise<void> {
|
|
658
|
+
const found = await revealLeaf(at.field);
|
|
659
|
+
if (!found) return missed(`no mounted field at ${at.field}`, at.field);
|
|
689
660
|
// A `'segment'` hit landed on origin-less ink (list markers, a code fence's
|
|
690
|
-
// interior): `pos` is the segment
|
|
691
|
-
// (HitGranularity), so
|
|
692
|
-
//
|
|
693
|
-
//
|
|
694
|
-
|
|
695
|
-
|
|
661
|
+
// interior): `pos` is the segment start, not a cluster-exact caret
|
|
662
|
+
// (HitGranularity), so it is dropped rather than snapped to a spot the click
|
|
663
|
+
// did not resolve. `'cluster'` (and an absent granularity: the backend did not
|
|
664
|
+
// report it, treat as exact) places the caret. An absent `pos` is the placement
|
|
665
|
+
// rung and reaches the same floor.
|
|
666
|
+
land(found, at.granularity === 'segment' ? undefined : at.pos);
|
|
696
667
|
// The arrival cue. Unconditional, unlike the preview side's change-guarded
|
|
697
668
|
// bloom: a preview click is one discrete act, and its commonest target is the
|
|
698
|
-
// leaf
|
|
669
|
+
// leaf already focused (where landing a caret changes nothing on screen) or
|
|
699
670
|
// one off-screen, where the browser's focus-scroll moves the page and leaves
|
|
700
671
|
// the caret to be hunted for in a long form.
|
|
701
|
-
bloomInside(
|
|
672
|
+
bloomInside(found.control.el);
|
|
702
673
|
}
|
|
703
|
-
/** The active leaf's controller: the formatting popover's observation seam.
|
|
674
|
+
/** The active leaf's controller: the formatting popover's observation seam.
|
|
675
|
+
* `undefined` for a focused form control, which holds no marks to toggle. */
|
|
704
676
|
export function getActiveLeaf(): FieldController | undefined {
|
|
705
677
|
if (!activeAddr) return undefined;
|
|
706
678
|
const card = activeAddr.card != null ? activeCardId : undefined;
|
|
707
|
-
return leaves.
|
|
679
|
+
return leaves.prose(fieldKeyToString({ card, field: activeAddr.field }));
|
|
708
680
|
}
|
|
709
681
|
|
|
710
682
|
/**
|
|
711
|
-
* Reveal the
|
|
712
|
-
*
|
|
713
|
-
* to zero height and sits inside an `inert`
|
|
714
|
-
* so a caret placed in the same tick as the
|
|
715
|
-
* nothing. Exactly one card holds the key; the rest
|
|
683
|
+
* Reveal the field at `field` and hand back its landing handle plus the target it
|
|
684
|
+
* resolved to, once the reveal has rendered. The shared half of the two landing
|
|
685
|
+
* verbs: a collapsed group is clipped to zero height and sits inside an `inert`
|
|
686
|
+
* panel, which swallows a focus silently, so a caret placed in the same tick as the
|
|
687
|
+
* reveal goes nowhere and reports nothing. Exactly one card holds the key; the rest
|
|
688
|
+
* do nothing.
|
|
689
|
+
*
|
|
690
|
+
* The target comes back with the handle because the caller needs its key to ask for
|
|
691
|
+
* the codec lane (`leaves.prose`), and minting it twice would be two parses of one
|
|
692
|
+
* path. The reveal itself is the key's: a group holds the field, and an element
|
|
693
|
+
* address opens the same group its array sits in.
|
|
716
694
|
*
|
|
717
|
-
* A destroy lands
|
|
695
|
+
* A destroy lands inside this: the field is looked up before the flush and
|
|
718
696
|
* dispatched into after it, and a PM view destroyed in that window throws on the
|
|
719
697
|
* dispatch. A consumer's call outlives the surface it points at too, so the span is
|
|
720
698
|
* asked on the way in as well as after the await.
|
|
721
699
|
*/
|
|
722
|
-
async function revealLeaf(field: DocPath): Promise<
|
|
700
|
+
async function revealLeaf(field: DocPath): Promise<Landed | undefined> {
|
|
723
701
|
if (!span.alive) return undefined;
|
|
724
|
-
const
|
|
725
|
-
const
|
|
726
|
-
if (!
|
|
727
|
-
mainCard?.revealLeaf(key
|
|
728
|
-
for (const card of cardRefs) card?.revealLeaf(key
|
|
702
|
+
const target = leafTargetFor(field);
|
|
703
|
+
const control = target && leaves.control(target.key);
|
|
704
|
+
if (!target || !control) return undefined;
|
|
705
|
+
mainCard?.revealLeaf(target.key);
|
|
706
|
+
for (const card of cardRefs) card?.revealLeaf(target.key);
|
|
729
707
|
if (!(await span.resumes(tick()))) return undefined;
|
|
730
|
-
return
|
|
708
|
+
return { ...target, control };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Put the caret in a revealed target, at the finest grain it can take.
|
|
713
|
+
*
|
|
714
|
+
* - An element address focuses that row, with no caret inside it: an array element
|
|
715
|
+
* is no `createField` leaf and its handle is a bare focus.
|
|
716
|
+
* - A prose leaf with a `pos` takes the caret at that USV offset.
|
|
717
|
+
* - Everything else takes the focus alone: the offset is a position in rendered
|
|
718
|
+
* content and a form control has no coordinate to spend it in (an
|
|
719
|
+
* `<input type="number">` refuses a selection outright), which is also the whole
|
|
720
|
+
* of what a click on plate-placed ink can mean.
|
|
721
|
+
*/
|
|
722
|
+
function land(found: Landed, pos: number | undefined): void {
|
|
723
|
+
if (found.element != null && found.control.focusElement) {
|
|
724
|
+
return found.control.focusElement(found.element);
|
|
725
|
+
}
|
|
726
|
+
if (pos != null) {
|
|
727
|
+
const prose = leaves.prose(found.key);
|
|
728
|
+
if (prose) return prose.setCaret(pos);
|
|
729
|
+
}
|
|
730
|
+
found.control.focus();
|
|
731
731
|
}
|
|
732
732
|
|
|
733
733
|
// ── The verbs, as instance exports ──────────────────────────────────────────
|
|
@@ -737,16 +737,25 @@
|
|
|
737
737
|
// They speak the public vocabulary — a `DocPath` for a place, a `CardId` for a
|
|
738
738
|
// card — so a host drives them with what the hooks handed it.
|
|
739
739
|
//
|
|
740
|
-
// A target the surface does not hold is a
|
|
741
|
-
// `dev`:
|
|
742
|
-
//
|
|
740
|
+
// A target the surface does not hold is a no-op that reports `target-unknown` at
|
|
741
|
+
// `dev`: a key from a previous session or a card already removed, or a path naming
|
|
742
|
+
// no declared field — an element path (`main.references.0`) whose array is
|
|
743
|
+
// undeclared or unmounted included, that being the only way one misses. `setCaret`
|
|
744
|
+
// reports it too, being the verb a preview click drives: a landing that resolved
|
|
745
|
+
// nothing is indistinguishable from one that landed, and a consumer wiring the
|
|
746
|
+
// bridge reads the difference off nothing else.
|
|
743
747
|
|
|
744
|
-
/**
|
|
748
|
+
/**
|
|
749
|
+
* Reveal and focus the field at `field`, without placing a caret inside it. Any
|
|
750
|
+
* mounted field: a prose leaf takes its view's focus, a form control takes the same
|
|
751
|
+
* handoff a click on its label does (`Field`, `leaves.ts`), and an element address
|
|
752
|
+
* takes its own row.
|
|
753
|
+
*/
|
|
745
754
|
export async function focusField(field: DocPath): Promise<void> {
|
|
746
|
-
const
|
|
747
|
-
if (!
|
|
748
|
-
|
|
749
|
-
bloomInside(
|
|
755
|
+
const found = await revealLeaf(field);
|
|
756
|
+
if (!found) return missed(`no mounted field at ${field}`, field);
|
|
757
|
+
land(found, undefined);
|
|
758
|
+
bloomInside(found.control.el);
|
|
750
759
|
}
|
|
751
760
|
/** Seed a card of `kind` and insert it at `at` (default: the end). Returns the new
|
|
752
761
|
* card's session key, or `undefined` when the quill seeds no card of that kind. */
|
|
@@ -777,19 +786,56 @@
|
|
|
777
786
|
reportError(onError, { code: 'target-unknown', severity: 'dev', message, path });
|
|
778
787
|
}
|
|
779
788
|
|
|
780
|
-
/**
|
|
781
|
-
*
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
789
|
+
/** What a `DocPath` resolves to in the mounted tree: a leaf key, and the array
|
|
790
|
+
* element within it when the address names one. */
|
|
791
|
+
interface LeafTarget {
|
|
792
|
+
key: string;
|
|
793
|
+
element?: number;
|
|
794
|
+
}
|
|
795
|
+
/** A resolved target with the handle the registry holds for it. */
|
|
796
|
+
type Landed = LeafTarget & { control: FieldControl };
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Map a landing's `field` (a canonical `DocPath`) to a mounted target: the
|
|
800
|
+
* `addrForFieldPath` route the diagnostics take, the absolute card index resolved
|
|
801
|
+
* to its live stable id, then the shared `fieldKeyToString` form.
|
|
802
|
+
*
|
|
803
|
+
* Two rungs, because the boundary mints addresses at a finer granularity than
|
|
804
|
+
* `Addr` can name: a `richtext[]` element surfaces as `main.references.0`, which
|
|
805
|
+
* the grammar reads as a field literally named `"0"`. The second rung reads that
|
|
806
|
+
* trailing segment as an index, under a field the schema declares an array — which
|
|
807
|
+
* is why the ladder is the editor's (VISUAL_EDITOR.md §Surface): the preview
|
|
808
|
+
* carries no schema, and truncating the address there is worse than guessing, since
|
|
809
|
+
* `pos` is an offset into the element's own content.
|
|
810
|
+
*/
|
|
811
|
+
function leafTargetFor(field: DocPath): LeafTarget | undefined {
|
|
812
|
+
const direct = addrForFieldPath(field);
|
|
813
|
+
if (direct) {
|
|
814
|
+
const resolved = resolveCardKey(direct, cardIds);
|
|
815
|
+
return resolved ? { key: fieldKeyToString(resolved) } : undefined;
|
|
816
|
+
}
|
|
817
|
+
const element = elementAddrForFieldPath(field);
|
|
818
|
+
if (!element || !isArrayField(element.field)) return undefined;
|
|
819
|
+
const resolved = resolveCardKey(element.field, cardIds);
|
|
820
|
+
return resolved ? { key: fieldKeyToString(resolved), element: element.index } : undefined;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/** Whether `addr` names a field this document renders as an array repeater: the
|
|
824
|
+
* guard the element rung stands on. Asked of `controlKind`, so what the ladder
|
|
825
|
+
* tests is the control the tree mounted — the one holding a `focusElement` — and
|
|
826
|
+
* not a second reading of the schema beside it. */
|
|
827
|
+
function isArrayField(addr: Addr): boolean {
|
|
828
|
+
if (addr.field == null) return false;
|
|
829
|
+
const kind = addr.card == null ? undefined : model.cards[addr.card]?.kind;
|
|
830
|
+
const schema = quill.schema;
|
|
831
|
+
const card = addr.card == null ? schema.main : kind ? schema.card_kinds?.[kind] : undefined;
|
|
832
|
+
const declared = card?.fields?.[addr.field];
|
|
833
|
+
return !!declared && controlKind(declared) === 'array';
|
|
788
834
|
}
|
|
789
835
|
</script>
|
|
790
836
|
|
|
791
837
|
<div class="qm-editor {className ?? ''}" {style} data-qm-root bind:this={rootEl}>
|
|
792
|
-
<!-- `main` and the tips card are
|
|
838
|
+
<!-- `main` and the tips card are one block in the stack: the tips card tucks under
|
|
793
839
|
`main`'s bottom corners, so the two share a seam rather than a gutter and the
|
|
794
840
|
wrapper is what holds them to it. -->
|
|
795
841
|
<div class="qm-primary">
|
|
@@ -803,12 +849,12 @@
|
|
|
803
849
|
isLast={true}
|
|
804
850
|
{kinds}
|
|
805
851
|
ops={opsFor('main', true)}
|
|
852
|
+
{enumDisallowed}
|
|
806
853
|
onFocus={handleFocus}
|
|
807
854
|
onCaretMove={handleCaret}
|
|
808
855
|
onChange={proseChanged}
|
|
809
856
|
{onError}
|
|
810
|
-
{
|
|
811
|
-
{unregister}
|
|
857
|
+
{leaves}
|
|
812
858
|
/>
|
|
813
859
|
|
|
814
860
|
<!-- The tips card: a fixed slot after `main`, ahead of the cards, so
|
|
@@ -821,7 +867,7 @@
|
|
|
821
867
|
</div>
|
|
822
868
|
|
|
823
869
|
{@render addAffordance(0)}
|
|
824
|
-
<!-- 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
|
|
825
871
|
strip below a card is the same strip wherever the card lands, so it rides along
|
|
826
872
|
rather than being slid across. It is also the shape `animate:` asks for, being the
|
|
827
873
|
keyed block's only child. -->
|
|
@@ -837,12 +883,12 @@
|
|
|
837
883
|
isLast={i === model.cards.length - 1}
|
|
838
884
|
{kinds}
|
|
839
885
|
ops={opsFor(c.id, false)}
|
|
886
|
+
{enumDisallowed}
|
|
840
887
|
onFocus={handleFocus}
|
|
841
888
|
onCaretMove={handleCaret}
|
|
842
889
|
onChange={proseChanged}
|
|
843
890
|
{onError}
|
|
844
|
-
{
|
|
845
|
-
{unregister}
|
|
891
|
+
{leaves}
|
|
846
892
|
/>
|
|
847
893
|
{@render addAffordance(i + 1)}
|
|
848
894
|
</div>
|
|
@@ -851,47 +897,46 @@
|
|
|
851
897
|
|
|
852
898
|
<FormatPopover {getActiveLeaf} />
|
|
853
899
|
|
|
854
|
-
<!-- Cards always render; the
|
|
900
|
+
<!-- Cards always render; the add affordance is gated on the schema declaring
|
|
855
901
|
`card_kinds`, since there is nothing to seed otherwise. A card already in the
|
|
856
902
|
document shows regardless of its kind: a kind with no schema (foreign, or a
|
|
857
903
|
schema with no `card_kinds` at all) degrades to a recovery shell inside <Card>
|
|
858
904
|
(retype + delete), never gated away, so its content is neither dropped nor
|
|
859
|
-
trapped. The gate lives
|
|
905
|
+
trapped. The gate lives here rather than at each call site: the strip is one
|
|
860
906
|
decision, and two copies of it drift into a stack with a gap at one end. -->
|
|
861
907
|
{#snippet addAffordance(atIndex: number)}
|
|
862
908
|
{#if kinds.length}
|
|
863
|
-
<!-- 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
|
|
864
910
|
rather than finds by position, so it alone states itself in words at rest. The
|
|
865
|
-
gaps between cards stay bare: the strip
|
|
911
|
+
gaps between cards stay bare: the strip is the gap, and the pill each fills on
|
|
866
912
|
hover draws what a label would state, the space the new card takes. -->
|
|
867
913
|
{@const marked = atIndex === model.cards.length}
|
|
868
914
|
<div class="qm-add-card" class:qm-add-card-marked={marked}>
|
|
869
|
-
<!-- Marked, the words
|
|
870
|
-
them; bare, the
|
|
871
|
-
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. -->
|
|
872
918
|
{#if kinds.length === 1}
|
|
873
919
|
<button
|
|
874
920
|
type="button"
|
|
875
|
-
class="qm-add-btn qm-add-affordance"
|
|
876
|
-
aria-label={marked ? undefined : merged.
|
|
877
|
-
onclick={() => addCard(atIndex, kinds[0])}
|
|
878
|
-
>{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
|
|
879
924
|
>
|
|
880
925
|
{:else}
|
|
881
|
-
<!-- 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
|
|
882
927
|
disclosure: it floats out of the stack, so raising it moves no card, and
|
|
883
928
|
it dismisses on pick, on Escape and on a click outside, none of which a
|
|
884
929
|
`<details>` does. The trigger is bits-ui's `<button>`, which is why the
|
|
885
930
|
rules below reach it through `:global`. -->
|
|
886
931
|
<DropdownMenu.Root>
|
|
887
932
|
<DropdownMenu.Trigger
|
|
888
|
-
class="qm-add-btn qm-add-affordance"
|
|
933
|
+
class="qm-add-btn qm-add-affordance qm-tap-floor"
|
|
889
934
|
aria-label={marked ? undefined : merged.addCard}
|
|
890
|
-
>{marked ? merged.
|
|
935
|
+
>{marked ? merged.addCard : ''}</DropdownMenu.Trigger
|
|
891
936
|
>
|
|
892
937
|
<DropdownMenu.Portal to={rootEl}>
|
|
893
938
|
<DropdownMenu.Content sideOffset={4}>
|
|
894
|
-
<!-- Portalled out of the row but
|
|
939
|
+
<!-- Portalled out of the row but into the stack's root, and carrying the
|
|
895
940
|
marker itself: floating is still a detached subtree to the
|
|
896
941
|
derivation, like FormatPopover and the enum listbox. -->
|
|
897
942
|
<div class="qm-menu-surface" data-qm-root>
|
|
@@ -911,33 +956,40 @@
|
|
|
911
956
|
|
|
912
957
|
<style>
|
|
913
958
|
/* The private scale lands via `data-qm-root` on the root element above: this is
|
|
914
|
-
a
|
|
959
|
+
a detached root, one of those core/theme.css applies the derivation to. The
|
|
915
960
|
root rule also carries the baseline font and colour, so nothing here restates
|
|
916
961
|
them. Nothing here mints; `check:style` enforces that. */
|
|
917
|
-
/* The
|
|
962
|
+
/* The column, not only the cards in it: the gutter the stack sits in and the tone
|
|
918
963
|
behind it are the surface's own, so a bare `<div>` is a mounting site and
|
|
919
964
|
nothing is owed before the editor looks right. Consumer CSS is unlayered and
|
|
920
965
|
beats all of it, which is how a host that wants the column back takes it.
|
|
921
|
-
`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. */
|
|
922
974
|
.qm-editor {
|
|
923
975
|
box-sizing: border-box;
|
|
924
976
|
display: flex;
|
|
925
977
|
flex-direction: column;
|
|
926
978
|
gap: var(--_qm-space-2);
|
|
927
|
-
padding: var(--_qm-space-
|
|
928
|
-
background: var(--_qm-surface);
|
|
979
|
+
padding: var(--_qm-space-4);
|
|
980
|
+
background: var(--_qm-surface-sunken);
|
|
929
981
|
color: var(--_qm-ink);
|
|
930
982
|
}
|
|
931
|
-
/* A fixed-height pane, which the stack is
|
|
983
|
+
/* A fixed-height pane, which the stack is not by default, and the four popovers
|
|
932
984
|
are the reason: each resolves its portal target with
|
|
933
985
|
`closest('[data-qm-root]')`, which is this element, so an `overflow` here
|
|
934
986
|
clips the menu a leaf raised. Mounted in a page that scrolls, nothing clips;
|
|
935
987
|
mounted in a pane that does, the host says so and takes the clipping it would
|
|
936
|
-
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
|
|
937
989
|
scroller because it is only meaningful under one: dead space below the last
|
|
938
990
|
card, so it can be read mid-pane rather than against the bottom edge.
|
|
939
991
|
|
|
940
|
-
The
|
|
992
|
+
The height is what makes the rest of the rule mean anything, and it is the
|
|
941
993
|
pane's rather than a number of ours: `overflow` on a box that grows to its
|
|
942
994
|
content never has anything to scroll, so the stack would run past the pane
|
|
943
995
|
and be clipped by whatever the host frames it with. Taking the pane's height
|
|
@@ -955,7 +1007,7 @@
|
|
|
955
1007
|
padding-block-end: var(--_qm-tail);
|
|
956
1008
|
}
|
|
957
1009
|
/* The stack's one gapless seam, which `TipsCard` draws and this holds the two
|
|
958
|
-
blocks to. `main` has to paint
|
|
1010
|
+
blocks to. `main` has to paint over the tip: a later sibling paints over an earlier
|
|
959
1011
|
one otherwise, and the tip's background would cover the corners it is there to
|
|
960
1012
|
fill. `isolation` keeps that z-index local, so one wrapper owns the ordering and no
|
|
961
1013
|
card below inherits an argument about it. The wrapper is a single flex item, so the
|
|
@@ -977,56 +1029,71 @@
|
|
|
977
1029
|
gap: var(--_qm-space-2);
|
|
978
1030
|
}
|
|
979
1031
|
/* The strip between two blocks, and the whole of it is the target: a gap is found
|
|
980
|
-
by
|
|
981
|
-
to aim at in the middle of it. It takes the editor's gap back on
|
|
982
|
-
it is not a control sitting in the gutter; it
|
|
983
|
-
cards is the trigger's own height
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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. */
|
|
990
1042
|
.qm-add-card {
|
|
991
1043
|
display: flex;
|
|
992
1044
|
margin-top: calc(var(--_qm-space-2) * -1);
|
|
993
1045
|
margin-bottom: calc(var(--_qm-space-2) * -1);
|
|
994
1046
|
}
|
|
995
1047
|
/* Unboxed, like every button, and not dashed: a
|
|
996
|
-
dashed edge is the
|
|
1048
|
+
dashed edge is the placeholder idiom ("nothing is here yet") which on a button
|
|
997
1049
|
reads as disabled or as a drop target. It stays honest in one place, the
|
|
998
1050
|
un-schemable card (`Card.svelte`), which is a state rather than a control. A bare
|
|
999
|
-
trigger rests invisible, so the
|
|
1051
|
+
trigger rests invisible, so the fill is the whole of what it shows: the pill fills
|
|
1000
1052
|
the strip because the strip is what was pressed, and the strip is the gap, so the
|
|
1001
1053
|
fill is the card-to-be drawn where it will land. Every gap is an equal entry point,
|
|
1002
1054
|
reached by the name rather than by a mark: a card goes anywhere in the stack, not
|
|
1003
1055
|
just after the last one.
|
|
1004
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
|
+
|
|
1005
1066
|
`:global`, because the multi-kind trigger is bits-ui's own element and a `class`
|
|
1006
1067
|
passed to a primitive is a plain string that never picks up the scoping hash:
|
|
1007
1068
|
the same seam the enum trigger is styled through. */
|
|
1008
1069
|
.qm-add-card :global(.qm-add-btn) {
|
|
1009
|
-
/* No inset of its own: the pill and the gap are the same rectangle, and the tap
|
|
1010
|
-
floor is the whole of the height, which is what the marked strip's words
|
|
1011
|
-
centre in. */
|
|
1012
1070
|
display: flex;
|
|
1013
1071
|
align-items: center;
|
|
1014
1072
|
justify-content: center;
|
|
1015
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);
|
|
1016
1080
|
}
|
|
1017
|
-
/*
|
|
1018
|
-
|
|
1019
|
-
(`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. */
|
|
1020
1083
|
.qm-add-card-marked :global(.qm-add-btn) {
|
|
1084
|
+
height: var(--_qm-tap-min);
|
|
1021
1085
|
font-size: var(--_qm-text-label);
|
|
1086
|
+
font-weight: var(--_qm-weight-mid);
|
|
1087
|
+
opacity: var(--_qm-opacity-idle);
|
|
1022
1088
|
}
|
|
1023
|
-
/*
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
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
|
|
1027
1092
|
the pill the menu hangs from. */
|
|
1093
|
+
.qm-add-card :global(.qm-add-btn:hover),
|
|
1028
1094
|
.qm-add-card :global(.qm-add-btn:focus-visible),
|
|
1029
1095
|
.qm-add-card :global(.qm-add-btn[data-state='open']) {
|
|
1030
|
-
|
|
1096
|
+
opacity: 1;
|
|
1097
|
+
background: var(--_qm-add-fill);
|
|
1031
1098
|
}
|
|
1032
1099
|
</style>
|