@quillmark/svelte 0.3.0 → 0.3.2
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/NOTICE +6 -6
- package/README.md +1 -1
- package/THEMING.md +1 -1
- package/dist/core/bloom.d.ts +1 -30
- package/dist/core/bloom.js +13 -45
- package/dist/core/codec/anchor.d.ts +5 -5
- package/dist/core/codec/anchor.js +37 -13
- package/dist/core/codec/prose.css +5 -6
- package/dist/core/theme.css +39 -19
- package/dist/preset/scale.css +4 -4
- package/dist/preview/Preview.svelte +6 -18
- package/dist/preview/Preview.svelte.d.ts +2 -3
- package/dist/preview/bridge.js +2 -2
- package/dist/preview/controller.d.ts +8 -10
- package/dist/preview/controller.js +16 -41
- package/dist/preview/geometry.d.ts +7 -9
- package/dist/preview/geometry.js +7 -9
- package/dist/preview/index.js +1 -1
- package/dist/preview/paint.d.ts +1 -1
- package/dist/preview/paint.js +5 -5
- package/dist/visual/Card.svelte +40 -35
- package/dist/visual/Card.svelte.d.ts +0 -1
- package/dist/visual/FormatPopover.svelte +58 -37
- package/dist/visual/ProseField.svelte +22 -20
- package/dist/visual/ProseField.svelte.d.ts +1 -2
- package/dist/visual/TipsCard.svelte +56 -53
- package/dist/visual/VisualEditorInner.svelte +126 -206
- package/dist/visual/icons/nodes.d.ts +16 -0
- package/dist/visual/icons/nodes.js +11 -0
- package/package.json +2 -2
- package/dist/preview/overlay.d.ts +0 -11
- package/dist/preview/overlay.js +0 -97
|
@@ -9,19 +9,6 @@
|
|
|
9
9
|
mounted against. That is what makes them plain state rather than something
|
|
10
10
|
threaded through a generation token.
|
|
11
11
|
|
|
12
|
-
It owns:
|
|
13
|
-
• structure: the schema × payload join, re-derived from the live `Document`;
|
|
14
|
-
• stable card identity: a session-id array reordered in lockstep with the
|
|
15
|
-
content, resolved to an index only at the mutation boundary;
|
|
16
|
-
• commit routing: prose leaves lower to `applyChange` (in the codec); scalars/
|
|
17
|
-
arrays/objects go through the typed `writer`; structure through the mutators;
|
|
18
|
-
• focus + the bridge outputs (`onActiveLeafChange`, `onCaretMove`) and the
|
|
19
|
-
`setCaret(at)` entry wired to the preview;
|
|
20
|
-
• the one formatting popover (`FormatPopover`, mounted once, observing the
|
|
21
|
-
active leaf via `getActiveLeaf`) and diagnostics routing (`diagnostics.ts`:
|
|
22
|
-
quill.validate + local commit errors + the external `diagnostics` prop,
|
|
23
|
-
merged into `diagByKey` and threaded to each `<Field>`/card body).
|
|
24
|
-
|
|
25
12
|
Reactivity across the WASM handle. The `Document` is opaque to Svelte, so a
|
|
26
13
|
`revision` counter is bumped after every scalar/structure mutation and the card
|
|
27
14
|
tree is `$derived` by re-reading `doc.main`/`doc.cards`/`quill.schema`. Prose
|
|
@@ -131,10 +118,9 @@
|
|
|
131
118
|
// svelte-ignore state_referenced_locally
|
|
132
119
|
let cardIds = $state<string[]>(seq.take(doc.cardCount));
|
|
133
120
|
|
|
134
|
-
// Local commit-error diagnostics (VISUAL_EDITOR §Diagnostics, producer #2)
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
// error stays pinned to its field across a card reorder.
|
|
121
|
+
// Local commit-error diagnostics (VISUAL_EDITOR §Diagnostics, producer #2),
|
|
122
|
+
// id-keyed rather than positional so an error stays pinned to its field across a
|
|
123
|
+
// card reorder.
|
|
138
124
|
let commitErrors = $state(new Map<string, RoutedDiagnostic>());
|
|
139
125
|
/** Edit the commit-error map copy-on-write: a `$state` Map is not deeply
|
|
140
126
|
* reactive, so a mutation in place re-derives nothing. */
|
|
@@ -152,13 +138,9 @@
|
|
|
152
138
|
* `proseChanged`, which does not bump the revision (bumping it would re-derive
|
|
153
139
|
* and remount every leaf, costing the caret on every keystroke).
|
|
154
140
|
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* anything inside it; either way the path is minted here, after the bump, because
|
|
159
|
-
* an op's own index is only addressable against the tree the bump re-derived (an
|
|
160
|
-
* insert's card does not exist in the previous one, and a retype's kind is the
|
|
161
|
-
* previous kind).
|
|
141
|
+
* The path is minted here, after the bump, because an op's own index is only
|
|
142
|
+
* addressable against the tree the bump re-derived: an insert's card does not exist
|
|
143
|
+
* in the previous one, and a retype's kind is the previous kind.
|
|
162
144
|
*/
|
|
163
145
|
function bump(source: ChangeSource, cardId?: CardId, at?: Addr | number): void {
|
|
164
146
|
revision++;
|
|
@@ -166,15 +148,13 @@
|
|
|
166
148
|
at == null ? undefined : typeof at === 'number' ? cardPath(at, liveKinds()) : pathFor(at);
|
|
167
149
|
onChange?.({ source, cardId, path });
|
|
168
150
|
}
|
|
169
|
-
/**
|
|
170
|
-
*
|
|
151
|
+
/** Read at the mutation boundary, never cached (VISUAL_EDITOR §"The address is the
|
|
152
|
+
* spine"). */
|
|
171
153
|
function cardIndexOf(id: string): number {
|
|
172
154
|
return cardIds.indexOf(id);
|
|
173
155
|
}
|
|
174
|
-
/** The inverse, for the two signals that arrive as an address and nothing else
|
|
175
|
-
*
|
|
176
|
-
* that index. Resolved at the emit site off the same array the mutation boundary
|
|
177
|
-
* reads, never cached. */
|
|
156
|
+
/** 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. */
|
|
178
158
|
function cardIdOf(addr: Addr): CardId {
|
|
179
159
|
return addr.card == null ? 'main' : cardIds[addr.card];
|
|
180
160
|
}
|
|
@@ -274,35 +254,28 @@
|
|
|
274
254
|
}
|
|
275
255
|
|
|
276
256
|
// ── Commit routing ──────────────────────────────────────────────────────────
|
|
277
|
-
// Scalars / arrays / objects → the typed writer (schema-checked)
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
// below (`doc.removeField`), not a write.
|
|
257
|
+
// Scalars / arrays / objects → the typed writer (schema-checked); a cleared entry
|
|
258
|
+
// arrives as `undefined` and takes the unset lane instead. Prose leaves commit
|
|
259
|
+
// themselves via the codec (applyChange) and do not pass through here.
|
|
281
260
|
//
|
|
282
|
-
// A bad value makes `writer.set` throw a `QuillmarkError
|
|
283
|
-
//
|
|
284
|
-
// `
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
// producer #2). It is stashed in `commitErrors`; a subsequent successful commit
|
|
290
|
-
// for the same field clears it. Nothing here gates: the value is not written
|
|
291
|
-
// (the document is unchanged on throw, per the boundary's own transactional
|
|
292
|
-
// contract) and editing continues.
|
|
261
|
+
// A bad value makes `writer.set` throw a `QuillmarkError` whose `diagnostics[0]`
|
|
262
|
+
// carries a `code` and a canonical `path` (`edit::field_coercion_failed` at
|
|
263
|
+
// `main.font_size`, `edit::unknown_field`). That diagnostic is surfaced verbatim,
|
|
264
|
+
// under a key minted from the address being committed rather than parsed from the
|
|
265
|
+
// positional path, so it survives a later card reorder. Nothing gates: the document
|
|
266
|
+
// is unchanged on throw, per the boundary's transactional contract, and editing
|
|
267
|
+
// continues.
|
|
293
268
|
function commitScalar(id: string, isMain: boolean, name: string, value: unknown): void {
|
|
294
269
|
const key: FieldKey = { card: isMain ? undefined : id, field: name };
|
|
295
270
|
const keyStr = fieldKeyToString(key);
|
|
296
271
|
try {
|
|
297
272
|
if (value === undefined) {
|
|
298
|
-
// The unset rung of the commitment ladder (
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
// it goes through the quill-free `doc.removeField` (bare string = main
|
|
305
|
-
// `{ field }`; `{ card, field }` for a card), not the typed writer.
|
|
273
|
+
// The unset rung of the commitment ladder (VISUAL_EDITOR §"Structure mirrors
|
|
274
|
+
// the schema"): removing the field leaves the engine's authored › `default:`
|
|
275
|
+
// › zero-fill resolve to render the default, where writing one bakes a
|
|
276
|
+
// snapshot the schema cannot track (canon SCHEMAS.md: the engine never
|
|
277
|
+
// persists a default; nor do we). Removal writes no value, so there is
|
|
278
|
+
// nothing for a schema to conform and the lane is the quill-free one.
|
|
306
279
|
if (isMain) {
|
|
307
280
|
doc.removeField(name);
|
|
308
281
|
} else {
|
|
@@ -349,8 +322,6 @@
|
|
|
349
322
|
* Coalesced on a single pending id: two quick adds resolve their `tick()` in
|
|
350
323
|
* order, the earlier one sees a newer pending id and drops out, so one smooth
|
|
351
324
|
* scroll runs to the last card rather than two fighting over the same scroller.
|
|
352
|
-
* The two guards below answer different questions: the span, whether this surface
|
|
353
|
-
* is still there; the pending id, whether this continuation is still the current one.
|
|
354
325
|
*/
|
|
355
326
|
let pendingScrollId: string | null = null;
|
|
356
327
|
async function scrollCardIntoView(id: string, block: ScrollLogicalPosition): Promise<void> {
|
|
@@ -372,8 +343,8 @@
|
|
|
372
343
|
const id = seq.next();
|
|
373
344
|
cardIds = [...cardIds.slice(0, atIndex), id, ...cardIds.slice(atIndex)];
|
|
374
345
|
bump('structure', id, atIndex);
|
|
375
|
-
//
|
|
376
|
-
//
|
|
346
|
+
// Centred: the new card is the subject, and the neighbours it landed between
|
|
347
|
+
// are what say where it went.
|
|
377
348
|
void scrollCardIntoView(id, 'center');
|
|
378
349
|
return id;
|
|
379
350
|
} catch (e) {
|
|
@@ -412,8 +383,8 @@
|
|
|
412
383
|
w.splice(to, 0, x);
|
|
413
384
|
cardIds = w;
|
|
414
385
|
bump('structure', id, to);
|
|
415
|
-
// `nearest
|
|
416
|
-
//
|
|
386
|
+
// `nearest`: the card was already in view and only needs to stay there, so one
|
|
387
|
+
// that never left the viewport moves it not at all.
|
|
417
388
|
void scrollCardIntoView(id, 'nearest');
|
|
418
389
|
}
|
|
419
390
|
function removeCardById(id: string): void {
|
|
@@ -425,18 +396,15 @@
|
|
|
425
396
|
activeCardId = undefined;
|
|
426
397
|
activeAddr = undefined;
|
|
427
398
|
}
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
// otherwise sit in the map forever (ids are never reused).
|
|
399
|
+
// Ids are never reused, so an entry keyed to the gone card is unreachable rather
|
|
400
|
+
// than mis-attributed to whichever card takes this position: it needs dropping
|
|
401
|
+
// because nothing else will ever clear it.
|
|
432
402
|
if ([...commitErrors.keys()].some((k) => k.startsWith(`${id}:`)))
|
|
433
403
|
editCommitErrors((m) => {
|
|
434
404
|
for (const k of [...m.keys()]) if (k.startsWith(`${id}:`)) m.delete(k);
|
|
435
405
|
});
|
|
436
|
-
// No addr: the removed card has
|
|
437
|
-
//
|
|
438
|
-
// which card went, which is the one thing a host keying on it needs, and the
|
|
439
|
-
// only handle the removal leaves it.
|
|
406
|
+
// No addr: the removed card has none left and every survivor's shifted. The
|
|
407
|
+
// stack changed, not a leaf, and the id is the only handle the removal leaves.
|
|
440
408
|
bump('structure', id);
|
|
441
409
|
}
|
|
442
410
|
function retypeCardById(id: string, kind: string): void {
|
|
@@ -460,11 +428,8 @@
|
|
|
460
428
|
patchEditorExt(doc, { card: i }, { title });
|
|
461
429
|
bump('structure', id, i);
|
|
462
430
|
}
|
|
463
|
-
/**
|
|
464
|
-
*
|
|
465
|
-
* tips make. `undefined` drops the key while `title` and any later sibling ride
|
|
466
|
-
* through; `ext.ts` holds why that matters.
|
|
467
|
-
*/
|
|
431
|
+
/** The only write tips make. `undefined` drops the key while `title` and any later
|
|
432
|
+
* sibling ride through; `ext.ts` holds why that matters. */
|
|
468
433
|
function dismissTips(): void {
|
|
469
434
|
patchEditorExt(doc, MAIN_CARD_ADDR, { tips: undefined });
|
|
470
435
|
// Document-level chrome, not a leaf's: no addr, as for a card removal, and no
|
|
@@ -511,11 +476,9 @@
|
|
|
511
476
|
}
|
|
512
477
|
});
|
|
513
478
|
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
// (mergeDiagnostics sorts; nothing is dropped: diagnostics never gate, so
|
|
518
|
-
// nothing here hides one either).
|
|
479
|
+
// All three producers. The two positional ones resolve their `.path` to the live
|
|
480
|
+
// stable-id keying; commit errors are id-keyed already. Nothing is dropped:
|
|
481
|
+
// diagnostics never gate, so nothing here hides one either.
|
|
519
482
|
const diagByKey = $derived.by(() => {
|
|
520
483
|
const fromValidate = routeAndResolve(validation, cardIds);
|
|
521
484
|
const fromExternal = routeAndResolve(diagnostics, cardIds);
|
|
@@ -537,8 +500,6 @@
|
|
|
537
500
|
retype: (kind: string) => retypeCardById(id, kind),
|
|
538
501
|
rename: (title: string) => renameCardById(id, title),
|
|
539
502
|
diagFor: (field?: string) => diagByKey.get(leafKey(field)),
|
|
540
|
-
// Bind the consumer policy hook to this field's resolved addr;
|
|
541
|
-
// no hook → every option allowed.
|
|
542
503
|
enumAllowed: (field: string, value: string) =>
|
|
543
504
|
enumOptionAllowed?.(makeAddr(id, isMain, field), value) ?? true
|
|
544
505
|
};
|
|
@@ -569,7 +530,6 @@
|
|
|
569
530
|
id,
|
|
570
531
|
isMain,
|
|
571
532
|
kind,
|
|
572
|
-
// No schema for this kind → a recovery shell, not a field list.
|
|
573
533
|
// `main` always resolves `schema.main`, so it is never unschemable.
|
|
574
534
|
unschemable: !isMain && !cardSchema,
|
|
575
535
|
titleOverride: extEditor?.title ?? '',
|
|
@@ -578,14 +538,11 @@
|
|
|
578
538
|
provenance: provenanceMap(rows.fields),
|
|
579
539
|
sections,
|
|
580
540
|
hasBody,
|
|
581
|
-
// The body ghosts its resolved `default:` exactly as a scalar does
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
// is
|
|
585
|
-
//
|
|
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.
|
|
541
|
+
// The body ghosts its resolved `default:` exactly as a scalar does, and falls
|
|
542
|
+
// back to an invitation where a scalar shows nothing: an empty body is a
|
|
543
|
+
// surface to write on where an empty control is a value not yet given. The
|
|
544
|
+
// hook is pure by contract, so its answer is kept nowhere — a ghost holds
|
|
545
|
+
// still across a re-derive because the function does.
|
|
589
546
|
bodyGhost: hasBody
|
|
590
547
|
? resolveBodyGhost(
|
|
591
548
|
stringifyGhost(ghostDefault(rows.body ?? undefined)),
|
|
@@ -599,8 +556,8 @@
|
|
|
599
556
|
const model = $derived.by(() => {
|
|
600
557
|
revision; // re-derive on every mutation
|
|
601
558
|
const schema = quill.schema;
|
|
602
|
-
const main = doc.main;
|
|
603
|
-
const cards = doc.cards;
|
|
559
|
+
const main = doc.main;
|
|
560
|
+
const cards = doc.cards;
|
|
604
561
|
// The provenance channel (FIELD_PROVENANCE): one whole-doc resolve per
|
|
605
562
|
// derive, feeding the ghosted `default:` only. Guarded: provenance is
|
|
606
563
|
// chrome, so a resolve failure degrades to no ghosts, never a blank form.
|
|
@@ -617,10 +574,10 @@
|
|
|
617
574
|
}
|
|
618
575
|
const byCard = resolvedByCardIndex(resolved);
|
|
619
576
|
return {
|
|
620
|
-
// Tips are
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
//
|
|
577
|
+
// Tips are document-level, not a property of the main card: they hang off
|
|
578
|
+
// `main`'s `$ext` because that is where a document-scoped `$ext` lives, and
|
|
579
|
+
// the model says so at the root rather than making every card carry a field
|
|
580
|
+
// one card renders.
|
|
624
581
|
tips: tipsChannel((main.ext?.editor as { tips?: unknown } | undefined)?.tips),
|
|
625
582
|
main: buildCard('main', true, 'main', main, schema.main, {
|
|
626
583
|
fields: resolved?.main.fields ?? [],
|
|
@@ -648,11 +605,6 @@
|
|
|
648
605
|
* tick as the reveal would go nowhere and report nothing. The consumer's
|
|
649
606
|
* `onPick` ignores the promise: awaiting it is for a caller that wants to
|
|
650
607
|
* observe where the caret went.
|
|
651
|
-
*
|
|
652
|
-
* A destroy lands inside this one: `leaf` is looked up before the flush and
|
|
653
|
-
* dispatched into after it, and a PM view destroyed in that window throws on the
|
|
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.
|
|
656
608
|
*/
|
|
657
609
|
export async function setCaret(at: Landing): Promise<void> {
|
|
658
610
|
const found = await revealLeaf(at.field);
|
|
@@ -664,11 +616,10 @@
|
|
|
664
616
|
// report it, treat as exact) places the caret. An absent `pos` is the placement
|
|
665
617
|
// rung and reaches the same floor.
|
|
666
618
|
land(found, at.granularity === 'segment' ? undefined : at.pos);
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
//
|
|
671
|
-
// the caret to be hunted for in a long form.
|
|
619
|
+
// Unconditional: a preview click is one discrete act, and its commonest target
|
|
620
|
+
// is the leaf already focused (where landing a caret changes nothing on screen)
|
|
621
|
+
// or one off-screen, where the browser's focus-scroll moves the page and leaves
|
|
622
|
+
// the caret to be hunted for.
|
|
672
623
|
bloomInside(found.control.el);
|
|
673
624
|
}
|
|
674
625
|
/** The active leaf's controller: the formatting popover's observation seam.
|
|
@@ -689,13 +640,11 @@
|
|
|
689
640
|
*
|
|
690
641
|
* The target comes back with the handle because the caller needs its key to ask for
|
|
691
642
|
* the codec lane (`leaves.prose`), and minting it twice would be two parses of one
|
|
692
|
-
* path.
|
|
693
|
-
* address opens the same group its array sits in.
|
|
643
|
+
* path.
|
|
694
644
|
*
|
|
695
|
-
* A destroy lands inside
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
* asked on the way in as well as after the await.
|
|
645
|
+
* A destroy lands inside the flush, and a PM view destroyed in that window throws on
|
|
646
|
+
* the dispatch that follows. A consumer's call outlives the surface it points at
|
|
647
|
+
* too, so the span is asked on the way in as well as after the await.
|
|
699
648
|
*/
|
|
700
649
|
async function revealLeaf(field: DocPath): Promise<Landed | undefined> {
|
|
701
650
|
if (!span.alive) return undefined;
|
|
@@ -709,15 +658,11 @@
|
|
|
709
658
|
}
|
|
710
659
|
|
|
711
660
|
/**
|
|
712
|
-
* Put the caret in a revealed target, at the finest grain it can take
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
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.
|
|
661
|
+
* Put the caret in a revealed target, at the finest grain it can take: a USV offset
|
|
662
|
+
* in a prose leaf, a bare focus everywhere else. An array element is no
|
|
663
|
+
* `createField` leaf, and a form control has no coordinate to spend an offset in (an
|
|
664
|
+
* `<input type="number">` refuses a selection outright), which is also the whole of
|
|
665
|
+
* what a click on plate-placed ink can mean.
|
|
721
666
|
*/
|
|
722
667
|
function land(found: Landed, pos: number | undefined): void {
|
|
723
668
|
if (found.element != null && found.control.focusElement) {
|
|
@@ -731,26 +676,18 @@
|
|
|
731
676
|
}
|
|
732
677
|
|
|
733
678
|
// ── The verbs, as instance exports ──────────────────────────────────────────
|
|
734
|
-
// The same functions the card's own chrome calls, reached through `bind:this
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
// They speak the public vocabulary — a `DocPath` for a place, a `CardId` for a
|
|
738
|
-
// card — so a host drives them with what the hooks handed it.
|
|
679
|
+
// The same functions the card's own chrome calls, reached through `bind:this`, and
|
|
680
|
+
// they speak the public vocabulary — a `DocPath` for a place, a `CardId` for a card
|
|
681
|
+
// — so a host drives them with what the hooks handed it.
|
|
739
682
|
//
|
|
740
683
|
// A target the surface does not hold is a no-op that reports `target-unknown` at
|
|
741
|
-
// `dev`: a key from a previous session
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
// nothing is indistinguishable from one that landed, and a consumer wiring the
|
|
746
|
-
// bridge reads the difference off nothing else.
|
|
684
|
+
// `dev`: a key from a previous session, a card already removed, a path naming no
|
|
685
|
+
// declared field. `setCaret` reports it too, being the verb a preview click drives:
|
|
686
|
+
// a landing that resolved nothing is indistinguishable from one that landed, and a
|
|
687
|
+
// consumer wiring the bridge reads the difference off nothing else.
|
|
747
688
|
|
|
748
|
-
/**
|
|
749
|
-
*
|
|
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
|
-
*/
|
|
689
|
+
/** Reveal and focus the field at `field`, placing no caret inside it: a form control
|
|
690
|
+
* takes the same handoff a click on its label does (`Field`, `leaves.ts`). */
|
|
754
691
|
export async function focusField(field: DocPath): Promise<void> {
|
|
755
692
|
const found = await revealLeaf(field);
|
|
756
693
|
if (!found) return missed(`no mounted field at ${field}`, field);
|
|
@@ -766,8 +703,7 @@
|
|
|
766
703
|
if (!holds(cardId)) return;
|
|
767
704
|
removeCardById(cardId);
|
|
768
705
|
}
|
|
769
|
-
/**
|
|
770
|
-
* surface animates; at either edge it is a no-op. */
|
|
706
|
+
/** One slot: the step the reorder control takes, and the one the surface animates. */
|
|
771
707
|
export function moveCard(cardId: CardId, dir: -1 | 1): void {
|
|
772
708
|
if (!holds(cardId)) return;
|
|
773
709
|
moveCardById(cardId, dir);
|
|
@@ -796,9 +732,8 @@
|
|
|
796
732
|
type Landed = LeafTarget & { control: FieldControl };
|
|
797
733
|
|
|
798
734
|
/**
|
|
799
|
-
* Map a landing's `field`
|
|
800
|
-
*
|
|
801
|
-
* to its live stable id, then the shared `fieldKeyToString` form.
|
|
735
|
+
* Map a landing's `field` to a mounted target, by the `addrForFieldPath` route the
|
|
736
|
+
* diagnostics take.
|
|
802
737
|
*
|
|
803
738
|
* Two rungs, because the boundary mints addresses at a finer granularity than
|
|
804
739
|
* `Addr` can name: a `richtext[]` element surfaces as `main.references.0`, which
|
|
@@ -820,8 +755,7 @@
|
|
|
820
755
|
return resolved ? { key: fieldKeyToString(resolved), element: element.index } : undefined;
|
|
821
756
|
}
|
|
822
757
|
|
|
823
|
-
/**
|
|
824
|
-
* guard the element rung stands on. Asked of `controlKind`, so what the ladder
|
|
758
|
+
/** The guard the element rung stands on. Asked of `controlKind`, so what the ladder
|
|
825
759
|
* tests is the control the tree mounted — the one holding a `focusElement` — and
|
|
826
760
|
* not a second reading of the schema beside it. */
|
|
827
761
|
function isArrayField(addr: Addr): boolean {
|
|
@@ -844,7 +778,6 @@
|
|
|
844
778
|
card={model.main}
|
|
845
779
|
{doc}
|
|
846
780
|
{quill}
|
|
847
|
-
index={-1}
|
|
848
781
|
isFirst={true}
|
|
849
782
|
isLast={true}
|
|
850
783
|
{kinds}
|
|
@@ -857,10 +790,9 @@
|
|
|
857
790
|
{leaves}
|
|
858
791
|
/>
|
|
859
792
|
|
|
860
|
-
<!--
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
card leaves for good (VISUAL_EDITOR §"Card operations"). -->
|
|
793
|
+
<!-- A fixed slot after `main`, ahead of the cards, so document-level guidance
|
|
794
|
+
reads as document-level and never displaces a field. Dismissal empties the
|
|
795
|
+
channel, so the card leaves for good (VISUAL_EDITOR §"Card operations"). -->
|
|
864
796
|
{#if model.tips.length}
|
|
865
797
|
<TipsCard tips={model.tips} onDismiss={dismissTips} {onError} />
|
|
866
798
|
{/if}
|
|
@@ -878,7 +810,6 @@
|
|
|
878
810
|
card={c}
|
|
879
811
|
{doc}
|
|
880
812
|
{quill}
|
|
881
|
-
index={i}
|
|
882
813
|
isFirst={i === 0}
|
|
883
814
|
isLast={i === model.cards.length - 1}
|
|
884
815
|
{kinds}
|
|
@@ -897,13 +828,12 @@
|
|
|
897
828
|
|
|
898
829
|
<FormatPopover {getActiveLeaf} />
|
|
899
830
|
|
|
900
|
-
<!--
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
decision, and two copies of it drift into a stack with a gap at one end. -->
|
|
831
|
+
<!-- The add affordance is gated on the schema declaring `card_kinds`, there being
|
|
832
|
+
nothing to seed otherwise. Cards render regardless: a kind with no schema degrades to
|
|
833
|
+
a recovery shell inside <Card> (retype + delete), never gated away, so its content is
|
|
834
|
+
neither dropped nor trapped. The gate lives here rather than at each call site: the
|
|
835
|
+
strip is one decision, and two copies of it drift into a stack with a gap at one
|
|
836
|
+
end. -->
|
|
907
837
|
{#snippet addAffordance(atIndex: number)}
|
|
908
838
|
{#if kinds.length}
|
|
909
839
|
<!-- The strip past the last card is the append point, the one a reader looks for
|
|
@@ -955,15 +885,14 @@
|
|
|
955
885
|
{/snippet}
|
|
956
886
|
|
|
957
887
|
<style>
|
|
958
|
-
/*
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
`border-box`, so a height from the caller is the height this draws.
|
|
888
|
+
/* A detached root, carrying `data-qm-root` for the derivation core/theme.css applies
|
|
889
|
+
to one — the baseline font and colour included, so nothing here restates them.
|
|
890
|
+
|
|
891
|
+
The column, not only the cards in it: the gutter the stack sits in and the tone
|
|
892
|
+
behind it are the surface's own, so a bare `<div>` is a mounting site and nothing
|
|
893
|
+
is owed before the editor looks right. Consumer CSS is unlayered and beats all of
|
|
894
|
+
it, which is how a host that wants the column back takes it. `border-box`, so a
|
|
895
|
+
height from the caller is the height this draws.
|
|
967
896
|
|
|
968
897
|
The sunken rung, which is what makes a card an island: the cards are the base
|
|
969
898
|
plane and this is the one they float on, so the gutter reads as ground rather
|
|
@@ -976,30 +905,26 @@
|
|
|
976
905
|
display: flex;
|
|
977
906
|
flex-direction: column;
|
|
978
907
|
gap: var(--_qm-space-2);
|
|
979
|
-
padding: var(--_qm-space-
|
|
908
|
+
padding: var(--_qm-space-5);
|
|
980
909
|
background: var(--_qm-surface-sunken);
|
|
981
910
|
color: var(--_qm-ink);
|
|
982
911
|
}
|
|
983
|
-
/*
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
nothing. The cards below do not shrink to fit it: a flex item's automatic
|
|
1000
|
-
minimum floors each at its own height, so the stack overflows and scrolls
|
|
1001
|
-
rather than squashing. `.qm-preview` states the same thing for the same
|
|
1002
|
-
reason, unconditionally, having no page form to keep. */
|
|
912
|
+
/* Opt-in, and the four popovers are the reason: each resolves its portal target with
|
|
913
|
+
`closest('[data-qm-root]')`, which is this element, so an `overflow` here clips the
|
|
914
|
+
menu a leaf raised. Mounted in a page that scrolls, nothing clips; mounted in a
|
|
915
|
+
pane that does, the host says so and takes the clipping it would have had from its
|
|
916
|
+
own scrolling frame either way. The tail rides with the scroller because it is only
|
|
917
|
+
meaningful under one: dead space below the last card, so it can be read mid-pane
|
|
918
|
+
rather than against the bottom edge.
|
|
919
|
+
|
|
920
|
+
The height is what makes the rest of the rule mean anything, and it is the pane's
|
|
921
|
+
rather than a number of ours: `overflow` on a box that grows to its content never
|
|
922
|
+
has anything to scroll, so the stack would run past the pane and be clipped by
|
|
923
|
+
whatever the host frames it with. Taking the pane's height is the whole of what
|
|
924
|
+
`.qm-pane` claims (THEMING §"Drop it in"), and it resolves to `auto` on a mounting
|
|
925
|
+
site with no height of its own, which is the page form kept. The cards do not
|
|
926
|
+
shrink to fit: a flex item's automatic minimum floors each at its own height, so
|
|
927
|
+
the stack overflows and scrolls rather than squashing. */
|
|
1003
928
|
.qm-editor.qm-pane {
|
|
1004
929
|
height: 100%;
|
|
1005
930
|
overflow: auto;
|
|
@@ -1028,31 +953,26 @@
|
|
|
1028
953
|
flex-direction: column;
|
|
1029
954
|
gap: var(--_qm-space-2);
|
|
1030
955
|
}
|
|
1031
|
-
/* The strip between two blocks, and the whole of it is the target: a gap is found
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
every card from the next under a quill declaring no kinds, where the affordance
|
|
1041
|
-
does not render at all. */
|
|
956
|
+
/* The strip between two blocks, and the whole of it is the target: a gap is found by
|
|
957
|
+
position, so reveal and hit region are the full-bleed row rather than a word to aim
|
|
958
|
+
at in the middle of it. Taking the editor's gap back on both sides is what makes it
|
|
959
|
+
the gutter rather than a control sitting in one, and the pill it fills on hover is
|
|
960
|
+
edge to edge the space the new card opens into. No band is held back as
|
|
961
|
+
miss-tolerance: gutter that reads as the trigger and inserts nothing unsays what the
|
|
962
|
+
fill claims. Absorbed rather than removed, because `gap` is also what separates the
|
|
963
|
+
one seam no strip sits in: every card from the next under a quill declaring no
|
|
964
|
+
kinds, where the affordance does not render at all. */
|
|
1042
965
|
.qm-add-card {
|
|
1043
966
|
display: flex;
|
|
1044
967
|
margin-top: calc(var(--_qm-space-2) * -1);
|
|
1045
968
|
margin-bottom: calc(var(--_qm-space-2) * -1);
|
|
1046
969
|
}
|
|
1047
|
-
/* Unboxed, like every button, and not dashed: a
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
the strip
|
|
1053
|
-
fill is the card-to-be drawn where it will land. Every gap is an equal entry point,
|
|
1054
|
-
reached by the name rather than by a mark: a card goes anywhere in the stack, not
|
|
1055
|
-
just after the last one.
|
|
970
|
+
/* Unboxed, like every button, and not dashed: a dashed edge is the placeholder idiom
|
|
971
|
+
("nothing is here yet"), which on a button reads as disabled or as a drop target. It
|
|
972
|
+
stays honest in one place, the un-schemable card (`Card.svelte`), which is a state
|
|
973
|
+
rather than a control. A bare trigger rests invisible, so the fill is the whole of
|
|
974
|
+
what it shows: the pill fills the strip because the strip is what was pressed, and
|
|
975
|
+
the strip is the gap, so the fill is the card-to-be drawn where it will land.
|
|
1056
976
|
|
|
1057
977
|
No inset of its own: the pill and the gap are the same rectangle, and a bare strip is
|
|
1058
978
|
a band of gutter, so its height is a rhythm rung. The press floor the box gives up is
|
|
@@ -4,6 +4,11 @@ export declare const ICONS: {
|
|
|
4
4
|
readonly 'chevron-right': readonly [readonly ["path", {
|
|
5
5
|
readonly d: "m9 18 6-6-6-6";
|
|
6
6
|
}]];
|
|
7
|
+
readonly 'arrow-right': readonly [readonly ["path", {
|
|
8
|
+
readonly d: "M5 12h14";
|
|
9
|
+
}], readonly ["path", {
|
|
10
|
+
readonly d: "m12 5 7 7-7 7";
|
|
11
|
+
}]];
|
|
7
12
|
readonly 'chevron-down': readonly [readonly ["path", {
|
|
8
13
|
readonly d: "m6 9 6 6 6-6";
|
|
9
14
|
}]];
|
|
@@ -97,6 +102,17 @@ export declare const ICONS: {
|
|
|
97
102
|
}], readonly ["path", {
|
|
98
103
|
readonly d: "m6 6 12 12";
|
|
99
104
|
}]];
|
|
105
|
+
readonly 'trash-2': readonly [readonly ["path", {
|
|
106
|
+
readonly d: "M10 11v6";
|
|
107
|
+
}], readonly ["path", {
|
|
108
|
+
readonly d: "M14 11v6";
|
|
109
|
+
}], readonly ["path", {
|
|
110
|
+
readonly d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6";
|
|
111
|
+
}], readonly ["path", {
|
|
112
|
+
readonly d: "M3 6h18";
|
|
113
|
+
}], readonly ["path", {
|
|
114
|
+
readonly d: "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2";
|
|
115
|
+
}]];
|
|
100
116
|
readonly minus: readonly [readonly ["path", {
|
|
101
117
|
readonly d: "M5 12h14";
|
|
102
118
|
}]];
|