@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.
@@ -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
- // one slot per field key, replaced on each failed `commitScalar`, cleared on
136
- // the next successful one for that field. Id-keyed (not positional) so an
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
- * `cardId` is passed rather than derived from `at`: the sites below hold the id
156
- * already, and the removal has no address to derive one from. `at` is an `Addr` for
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
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
- /** Resolve a stable card id to its current content index, or -1 if gone: read
170
- * at the mutation boundary, never cached (VISUAL_EDITOR §"The address is the spine"). */
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
- * (a focus, a prose commit): `'main'` for a main address, else the live id at
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). Prose leaves
278
- // commit themselves via the codec (applyChange) and do not pass through here.
279
- // A scalar control commits `undefined` for a cleared entry: the unset lane
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`, whose
283
- // `diagnostics[0]` carries a `code` and a canonical `path` (e.g.
284
- // `edit::field_coercion_failed` at `main.font_size`, or `edit::unknown_field`). The
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
287
- // parsed from the positional path; while surfacing the thrown diagnostic
288
- // verbatim (its `code`/`message`) as the payload (VISUAL_EDITOR §Diagnostics,
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 (a cleared scalar control,
299
- // VISUAL_EDITOR §"Structure mirrors the schema"): remove the field so the
300
- // engine's authored › `default:` › zero-fill resolve renders the
301
- // default, rather than baking a snapshot the schema can't track
302
- // (canon SCHEMAS.md: the engine never persists a default; nor do we).
303
- // Removal writes no value, so there is nothing for a schema to conform:
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
- // `center` for an insert: the new card is the subject, and centring it shows
376
- // the neighbours it landed between.
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` for a reorder: the card was already in view and only needs to stay
416
- // there, so a card that never left the viewport does not move it at all.
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
- // Drop any commit-error diagnostics id-keyed to the now-gone card: id-keying
429
- // (VISUAL_EDITOR §"The address is the spine") avoids mis-attributing them to
430
- // whichever card next takes this position, but an orphaned entry would
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 no address left, and the surviving cards'
437
- // addresses all shifted. The stack changed, not a leaf. The id still names
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
- * Clear the tips channel: the dismissal write, and the only write
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
- // Merge all three producers: validate() + external (both positional `.path`,
515
- // resolved to the live stable-id keying) + local commit errors (already
516
- // id-keyed). Precedence is errors-before-warnings within a field's list
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: the
582
- // same text-ghost projection `<Field>` applies to a field's row; and falls
583
- // back to an invitation where a scalar shows nothing, because an empty body
584
- // is a surface to write on and an empty control is a value not yet given.
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.
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; // allocate once
603
- const cards = doc.cards; // allocate once
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 DOCUMENT-level, not a property of the main card: they
621
- // hang off `main`'s `$ext` because that is where a document-scoped `$ext`
622
- // lives, and the model says so at the root rather than making every card
623
- // carry a field one card renders.
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
- // The arrival cue. Unconditional, unlike the preview side's change-guarded
668
- // bloom: a preview click is one discrete act, and its commonest target is the
669
- // leaf already focused (where landing a caret changes nothing on screen) or
670
- // one off-screen, where the browser's focus-scroll moves the page and leaves
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. 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.
643
+ * path.
694
644
  *
695
- * A destroy lands inside this: the field is looked up before the flush and
696
- * dispatched into after it, and a PM view destroyed in that window throws on the
697
- * dispatch. A consumer's call outlives the surface it points at too, so the span is
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
- * - 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.
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`: a
735
- // host toolbar, command palette or shortcut wants the door the card header gets,
736
- // and every one of them reports through `onChange` exactly as the click does.
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 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.
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
- * 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
- */
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
- /** Move a card one slot. The step the reorder control takes, and the one the
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` (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.
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
- /** 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
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
- <!-- The tips card: a fixed slot after `main`, ahead of the cards, so
861
- document-level guidance reads as document-level and never displaces a field.
862
- Absent when the channel is empty; which is what dismissal makes it, so the
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
- <!-- Cards always render; the add affordance is gated on the schema declaring
901
- `card_kinds`, since there is nothing to seed otherwise. A card already in the
902
- document shows regardless of its kind: a kind with no schema (foreign, or a
903
- schema with no `card_kinds` at all) degrades to a recovery shell inside <Card>
904
- (retype + delete), never gated away, so its content is neither dropped nor
905
- trapped. The gate lives here rather than at each call site: the strip is one
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
- /* The private scale lands via `data-qm-root` on the root element above: this is
959
- a detached root, one of those core/theme.css applies the derivation to. The
960
- root rule also carries the baseline font and colour, so nothing here restates
961
- them. Nothing here mints; `check:style` enforces that. */
962
- /* The column, not only the cards in it: the gutter the stack sits in and the tone
963
- behind it are the surface's own, so a bare `<div>` is a mounting site and
964
- nothing is owed before the editor looks right. Consumer CSS is unlayered and
965
- beats all of it, which is how a host that wants the column back takes it.
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-4);
908
+ padding: var(--_qm-space-5);
980
909
  background: var(--_qm-surface-sunken);
981
910
  color: var(--_qm-ink);
982
911
  }
983
- /* A fixed-height pane, which the stack is not by default, and the four popovers
984
- are the reason: each resolves its portal target with
985
- `closest('[data-qm-root]')`, which is this element, so an `overflow` here
986
- clips the menu a leaf raised. Mounted in a page that scrolls, nothing clips;
987
- mounted in a pane that does, the host says so and takes the clipping it would
988
- have had from its own scrolling frame either way. The tail rides with the
989
- scroller because it is only meaningful under one: dead space below the last
990
- card, so it can be read mid-pane rather than against the bottom edge.
991
-
992
- The height is what makes the rest of the rule mean anything, and it is the
993
- pane's rather than a number of ours: `overflow` on a box that grows to its
994
- content never has anything to scroll, so the stack would run past the pane
995
- and be clipped by whatever the host frames it with. Taking the pane's height
996
- is the whole of what `.qm-pane` claims (THEMING §"Drop it in": the surface
997
- becomes its own scroll container), and it resolves to `auto` — today's page
998
- behaviour, so a class on a mounting site with no height of its own changes
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
- 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. */
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
- dashed edge is the placeholder idiom ("nothing is here yet") which on a button
1049
- reads as disabled or as a drop target. It stays honest in one place, the
1050
- un-schemable card (`Card.svelte`), which is a state rather than a control. A bare
1051
- trigger rests invisible, so the fill is the whole of what it shows: the pill fills
1052
- the strip because the strip is what was pressed, and the strip is the gap, so the
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
  }]];