@quillmark/svelte 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/core/address.d.ts +17 -10
  2. package/dist/core/address.js +26 -12
  3. package/dist/core/bloom.js +4 -3
  4. package/dist/core/codec/decode.js +21 -15
  5. package/dist/core/codec/encode.js +1 -1
  6. package/dist/core/codec/islands.d.ts +5 -4
  7. package/dist/core/codec/islands.js +23 -38
  8. package/dist/core/codec/slash.js +0 -0
  9. package/dist/core/rungs.d.ts +8 -0
  10. package/dist/core/rungs.js +24 -0
  11. package/dist/core/theme.css +10 -0
  12. package/dist/visual/ArrayField.svelte +596 -167
  13. package/dist/visual/ArrayField.svelte.d.ts +14 -1
  14. package/dist/visual/BooleanField.svelte +10 -7
  15. package/dist/visual/Card.svelte +3 -22
  16. package/dist/visual/Field.svelte +69 -29
  17. package/dist/visual/MatrixField.svelte +352 -0
  18. package/dist/visual/MatrixField.svelte.d.ts +36 -0
  19. package/dist/visual/ObjectField.svelte +351 -133
  20. package/dist/visual/ObjectField.svelte.d.ts +17 -2
  21. package/dist/visual/VariantField.svelte +55 -2
  22. package/dist/visual/VariantField.svelte.d.ts +10 -1
  23. package/dist/visual/VisualEditorInner.svelte +53 -49
  24. package/dist/visual/controls.css +31 -0
  25. package/dist/visual/diagnostics.d.ts +26 -2
  26. package/dist/visual/diagnostics.js +52 -5
  27. package/dist/visual/hold.js +2 -2
  28. package/dist/visual/icons/nodes.d.ts +3 -0
  29. package/dist/visual/icons/nodes.js +2 -1
  30. package/dist/visual/leaves.d.ts +25 -12
  31. package/dist/visual/motion.d.ts +14 -5
  32. package/dist/visual/motion.js +13 -11
  33. package/dist/visual/strings.d.ts +19 -9
  34. package/dist/visual/strings.js +6 -2
  35. package/dist/visual/structure.d.ts +94 -9
  36. package/dist/visual/structure.js +229 -12
  37. package/package.json +2 -2
@@ -1,8 +1,17 @@
1
1
  import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
2
+ import { type ArrayLayout } from './structure.js';
3
+ import { type DeepDiagnostic } from './diagnostics.js';
4
+ import type { LandingBox } from './leaves.js';
2
5
  import './controls.css';
3
6
  interface Props {
4
7
  value: unknown[] | undefined;
5
8
  items: QuillFieldSchema | undefined;
9
+ /** How the elements draw: the record list, or the table an `array<object>` of
10
+ * short cells asks for (`arrayLayout`). */
11
+ layout?: ArrayLayout;
12
+ /** The schema's `max:`, past which the add chip disables and Enter stops
13
+ * inserting; `undefined` is no cap. */
14
+ max?: number;
6
15
  /** Accessible-name prefix for the element controls (`label` + 1-based index). */
7
16
  label?: string;
8
17
  /** No-default field → a persistent required `*` on the label. */
@@ -28,11 +37,15 @@ interface Props {
28
37
  * control has spliced in and whose commit has yet to land, or was refused. */
29
38
  contentAt: (path: PathStep[]) => Content | undefined;
30
39
  onCommit: (arr: unknown[]) => void;
40
+ /** The diagnostics routed into this array, each with its steps still to walk: one
41
+ * naming a row draws under that row — under its head while it is collapsed, under
42
+ * the cell once it is open — and one naming no row draws at the list's foot. */
43
+ diagnostics?: readonly DeepDiagnostic[];
31
44
  }
32
45
  declare const ArrayField: import("svelte").Component<Props, {
33
46
  focus: () => void;
34
47
  washBox: () => HTMLElement | undefined;
35
- focusElement: (k: number, pos?: number) => HTMLElement | undefined;
48
+ focusPath: (steps: readonly PathStep[], pos?: number) => LandingBox;
36
49
  }, "">;
37
50
  type ArrayField = ReturnType<typeof ArrayField>;
38
51
  export default ArrayField;
@@ -36,9 +36,12 @@
36
36
  const local = syncedLocal(() => value ?? fallback ?? false);
37
37
  </script>
38
38
 
39
- <span class="qm-switch-wrap">
39
+ <!-- Not `.qm-switch`: that name is the preset's pane band, whose narrow-viewport rule
40
+ stands every child of it at `--qmh-tap`, so a track wearing it grows a thumb three times
41
+ its own height over the labels around it (`preset/recipes.css`, held by `check:style`). -->
42
+ <span class="qm-toggle-wrap">
40
43
  <Switch.Root
41
- class="qm-switch qm-focus-ring qm-tap-floor"
44
+ class="qm-toggle qm-focus-ring qm-tap-floor"
42
45
  checked={local.value}
43
46
  {id}
44
47
  aria-label={id ? undefined : label}
@@ -48,7 +51,7 @@
48
51
  onCommit(v);
49
52
  }}
50
53
  >
51
- <Switch.Thumb class="qm-switch-thumb" />
54
+ <Switch.Thumb class="qm-toggle-thumb" />
52
55
  </Switch.Root>
53
56
  </span>
54
57
 
@@ -60,7 +63,7 @@
60
63
  2.5.8's floor is half again that. The `<label for>` beside it forwards a press but
61
64
  stands at the label rung, so it is no taller: without the floor no region on the
62
65
  surface reaches this control at the size every other control on it holds. */
63
- .qm-switch-wrap :global(.qm-switch) {
66
+ .qm-toggle-wrap :global(.qm-toggle) {
64
67
  display: inline-flex;
65
68
  align-items: center;
66
69
  width: 1.75rem;
@@ -75,21 +78,21 @@
75
78
  cursor: pointer;
76
79
  transition: background var(--_qm-duration-fast) var(--_qm-ease-reverse);
77
80
  }
78
- .qm-switch-wrap :global(.qm-switch[data-state='checked']) {
81
+ .qm-toggle-wrap :global(.qm-toggle[data-state='checked']) {
79
82
  background: var(--_qm-accent);
80
83
  }
81
84
  /* The focus ring rides `.qm-focus-ring` on the switch (controls.css). */
82
85
  /* The thumb reads on tone alone: the base surface over the track's `hover` rung
83
86
  unchecked, and over `--_qm-accent` checked. No edge of its own: a hairline around
84
87
  a 12px pill is a second box inside the track's. */
85
- .qm-switch-wrap :global(.qm-switch-thumb) {
88
+ .qm-toggle-wrap :global(.qm-toggle-thumb) {
86
89
  width: 0.75rem;
87
90
  height: 0.75rem;
88
91
  border-radius: var(--_qm-radius-pill);
89
92
  background: var(--_qm-surface);
90
93
  transition: translate var(--_qm-duration-fast) var(--_qm-ease-reverse);
91
94
  }
92
- .qm-switch-wrap :global(.qm-switch-thumb[data-state='checked']) {
95
+ .qm-toggle-wrap :global(.qm-toggle-thumb[data-state='checked']) {
93
96
  translate: 0.75rem;
94
97
  }
95
98
  </style>
@@ -332,7 +332,7 @@
332
332
  </section>
333
333
 
334
334
  {#snippet sectionFields(fields: FieldModel[])}
335
- <div class="qm-fields">
335
+ <div class="qm-fields qm-tracks">
336
336
  {#each placeFields(fields) as { field: f, span } (f.name)}
337
337
  <Field
338
338
  field={f}
@@ -584,33 +584,14 @@
584
584
  .qm-group-panel-inner {
585
585
  container-type: inline-size;
586
586
  }
587
- /* Capacity is the container's, not JavaScript's: nothing measures, so there is no
588
- observer to loop and no re-packing to restructure the DOM under a prose leaf.
589
-
590
- It steps 1 → 2 → 4, skipping 3: each rung is the width at which a track still clears
591
- the comfortable field minimum, and an even capacity is what lets the half a subform
592
- of one takes (`--cols-half`, `ObjectField`) land on a track boundary. A field ends on
593
- its track's own edge, so a rung is `n * 220px + gaps`. */
587
+ /* The count is the ladder's (`.qm-tracks`, controls.css), stepped by the container
588
+ above; what this block owns is the grid the count fills. */
594
589
  .qm-fields {
595
- --cols: 1;
596
- --cols-half: 1;
597
590
  display: grid;
598
591
  grid-template-columns: repeat(var(--cols), 1fr);
599
592
  column-gap: var(--_qm-space-2);
600
593
  row-gap: var(--_qm-space-3);
601
594
  }
602
- @container (min-width: 28rem) {
603
- .qm-fields {
604
- --cols: 2;
605
- --cols-half: 1;
606
- }
607
- }
608
- @container (min-width: 57rem) {
609
- .qm-fields {
610
- --cols: 4;
611
- --cols-half: 2;
612
- }
613
- }
614
595
  /* The nesting vertical, one `--_qm-nest` in from the card's edge with the panel's
615
596
  fields the same rung off it (ARCHITECTURE §"A plane is a tone"). `--_qm-border` and
616
597
  not `--_qm-accent`: a card's edge and the verticals inside it are one stroke at three
@@ -1,8 +1,10 @@
1
1
  <!--
2
2
  Type dispatch (VISUAL_EDITOR §"Structure mirrors the schema"). Given one projected
3
3
  {@link FieldModel} and its live value, render the label + the control the type
4
- maps to. Array controls own their label (paired with the add affordance in
5
- {@link ArrayField}); other types render the label here. Prose leaves take a
4
+ maps to. Array and matrix controls own their label (paired with the add affordance in
5
+ {@link ArrayField}, with the held count in {@link MatrixField}); other types render
6
+ the label here. A container control also takes the walk a nested address makes into
7
+ it ({@link FieldControl.focusPath}) and the diagnostics anchored inside it. Prose leaves take a
6
8
  parent-built live `addr` (its `card` a getter over the stable-id→index map) so
7
9
  a reorder re-targets without a remount; scalars, arrays, and objects commit
8
10
  their value up through `onCommitScalar`, which the parent lowers to the typed
@@ -24,9 +26,16 @@
24
26
  ResolvedField
25
27
  } from '@quillmark/wasm';
26
28
  import type { EditorErrorHandler } from '../core/errors.js';
27
- import type { LeafRegistry } from './leaves.js';
29
+ import type { LandingBox, LeafRegistry } from './leaves.js';
28
30
  import type { FieldModel, FieldSpan } from './structure.js';
29
- import { VARIANT_DISCRIMINANT, ghostDefault, stringifyGhost } from './structure.js';
31
+ import {
32
+ VARIANT_DISCRIMINANT,
33
+ arrayLayout,
34
+ ghostDefault,
35
+ isContainer,
36
+ stringifyGhost
37
+ } from './structure.js';
38
+ import { deepen } from './diagnostics.js';
30
39
  import type { FieldDomIds } from './domid.js';
31
40
  import ProseField from './ProseField.svelte';
32
41
  import TextField from './TextField.svelte';
@@ -37,6 +46,7 @@
37
46
  import ArrayField from './ArrayField.svelte';
38
47
  import ObjectField from './ObjectField.svelte';
39
48
  import VariantField from './VariantField.svelte';
49
+ import MatrixField from './MatrixField.svelte';
40
50
  import DiagnosticList from './DiagnosticList.svelte';
41
51
  import FieldLabel from './FieldLabel.svelte';
42
52
 
@@ -123,6 +133,13 @@
123
133
  // reference must vanish with it: `aria-describedby` pointing at nothing describes
124
134
  // nothing, and silently.
125
135
  const describedBy = $derived(field.description ? domIds.description : undefined);
136
+ // Two controls own their label row, the add chip and the held count sharing it.
137
+ const ownsLabel = $derived(field.control === 'array' || field.control === 'matrix');
138
+
139
+ // A container walks its diagnostics down to the cell each names and draws the rest
140
+ // at its own foot (`diagnostics.ts`); every other control draws them here, under
141
+ // itself, there being nothing inside it for a path to name.
142
+ const deep = $derived(deepen(diagnostics));
126
143
 
127
144
  /** The boundary's nested content read with this field's address already bound: an
128
145
  * array's elements, an object's properties, a variant's cells, and a subform's cells
@@ -145,28 +162,29 @@
145
162
  // restores a selection, a date field lands on its first segment, an array lands on
146
163
  // its first element or on the add affordance that is all an empty one has, an
147
164
  // object on its first property.
165
+ /** What a container control exports: its focus, and the walk into it. */
166
+ type Inside = {
167
+ focus: () => void;
168
+ focusPath: (steps: readonly PathStep[], pos?: number) => LandingBox;
169
+ washBox?: () => HTMLElement | undefined;
170
+ };
148
171
  let proseEl = $state<{ focus: () => void } | undefined>();
149
172
  let dateEl = $state<{ focus: () => void } | undefined>();
150
- let arrayEl = $state<
151
- | {
152
- focus: () => void;
153
- focusElement: (k: number, pos?: number) => HTMLElement | undefined;
154
- washBox: () => HTMLElement | undefined;
155
- }
156
- | undefined
157
- >();
158
- let objectEl = $state<{ focus: () => void } | undefined>();
173
+ let arrayEl = $state<Inside | undefined>();
174
+ let objectEl = $state<Inside | undefined>();
175
+ let variantEl = $state<Inside | undefined>();
176
+ let matrixEl = $state<Inside | undefined>();
177
+ const inside = (): Inside | undefined => arrayEl ?? objectEl ?? variantEl ?? matrixEl;
159
178
  function focusControl(): void {
160
- const owner = proseEl ?? dateEl ?? arrayEl ?? objectEl;
179
+ const owner = proseEl ?? dateEl ?? inside();
161
180
  if (owner) return owner.focus();
162
181
  document.getElementById(domIds.control)?.focus();
163
182
  }
164
- /** The array's per-ELEMENT landing, for the addresses the preview mints under an
165
- * array field (`leaves.ts`); read at the call, so it tracks the mounted repeater.
166
- * The row it settled in comes back with it, the wash being the landing's own
167
- * granularity. */
168
- function focusElement(k: number, pos?: number): HTMLElement | undefined {
169
- return arrayEl?.focusElement(k, pos);
183
+ /** The landing at depth, for the addresses the boundary mints under a container
184
+ * (`leaves.ts`); read at the call, so it tracks the mounted control. The innermost
185
+ * row it settled in comes back with it, the wash being the landing's own granularity. */
186
+ function focusPath(steps: readonly PathStep[], pos?: number): LandingBox {
187
+ return inside()?.focusPath(steps, pos);
170
188
  }
171
189
  // Only where `for` cannot reach; the labelable four are the browser's own, and a
172
190
  // second handler over them would be a focus the label already placed.
@@ -176,17 +194,17 @@
176
194
  * This field's landing handle. The wrapper is the bloom host rather than the
177
195
  * control: `bloomInside` appends an inset child and an `<input>` holds none. The
178
196
  * label is outside it, and stays out of the wash: an arrival marks where the caret
179
- * landed, which is the control. The one control that owns its label is the array, so
180
- * that one names its own box (`ArrayField.washBox`), read at the bloom the way
181
- * `focusElement` is read at the call.
197
+ * landed, which is the control. A control that owns its label (`ownsLabel`: the array
198
+ * and the matrix) holds that row inside itself, so it names its own box
199
+ * (`washBox`) and the bloom reads that in the wrapper's place.
182
200
  *
183
201
  * A prose leaf is absent here — it registers its own controller from inside
184
202
  * `ProseField`, carrying the codec seam this handle has no half of — and reactive
185
203
  * rather than mount-once, because a retype can swap the control under a leaf key
186
204
  * that does not remount.
187
205
  *
188
- * An array carries the per-element lane as well; no other control has elements for
189
- * an address to name. `el` is the field-wide box, which an element landing does not
206
+ * A container carries the walk into it as well; no other control has an inside for
207
+ * an address to name. `el` is the field-wide box, which a landing at depth does not
190
208
  * wash: that lane hands back its own row (`leaves.ts`).
191
209
  */
192
210
  let controlEl = $state<HTMLElement | undefined>();
@@ -197,9 +215,9 @@
197
215
  const wrapper = controlEl;
198
216
  registry.registerControl(key, {
199
217
  focus: focusControl,
200
- focusElement: field.control === 'array' ? focusElement : undefined,
218
+ focusPath: isContainer(field.control) ? focusPath : undefined,
201
219
  get el() {
202
- return arrayEl?.washBox() ?? wrapper;
220
+ return inside()?.washBox?.() ?? wrapper;
203
221
  }
204
222
  });
205
223
  return () => registry.unregisterControl(key);
@@ -217,7 +235,7 @@
217
235
  </script>
218
236
 
219
237
  <div class="qm-field" class:cell={span === 'cell'}>
220
- {#if field.control !== 'array'}
238
+ {#if !ownsLabel}
221
239
  <FieldLabel
222
240
  label={field.label}
223
241
  controlId={labelable ? domIds.control : undefined}
@@ -265,6 +283,7 @@
265
283
  />
266
284
  {:else if field.control === 'variant'}
267
285
  <VariantField
286
+ bind:this={variantEl}
268
287
  value={value as Record<string, unknown> | undefined}
269
288
  schema={field.schema}
270
289
  {ghostMember}
@@ -276,6 +295,7 @@
276
295
  onCommit={onCommitScalar}
277
296
  {optionAllowed}
278
297
  {enumDisallowed}
298
+ diagnostics={deep}
279
299
  />
280
300
  {:else if field.control === 'number'}
281
301
  <NumberField
@@ -308,6 +328,8 @@
308
328
  bind:this={arrayEl}
309
329
  value={value as unknown[] | undefined}
310
330
  items={field.schema.items}
331
+ layout={arrayLayout(field.schema)}
332
+ max={field.schema.max}
311
333
  label={field.label}
312
334
  required={field.required}
313
335
  description={field.description}
@@ -316,6 +338,7 @@
316
338
  idBase={domIds.control}
317
339
  {contentAt}
318
340
  onCommit={onCommitScalar}
341
+ diagnostics={deep}
319
342
  />
320
343
  {:else if field.control === 'object'}
321
344
  <ObjectField
@@ -328,6 +351,21 @@
328
351
  {describedBy}
329
352
  {contentAt}
330
353
  onCommit={onCommitScalar}
354
+ diagnostics={deep}
355
+ />
356
+ {:else if field.control === 'matrix'}
357
+ <MatrixField
358
+ bind:this={matrixEl}
359
+ value={value as Record<string, unknown> | undefined}
360
+ schema={field.schema}
361
+ label={field.label}
362
+ description={field.description}
363
+ labelId={domIds.label}
364
+ descriptionId={domIds.description}
365
+ idBase={domIds.control}
366
+ {contentAt}
367
+ onCommit={onCommitScalar}
368
+ diagnostics={deep}
331
369
  />
332
370
  {:else}
333
371
  <TextField
@@ -340,7 +378,9 @@
340
378
  {/if}
341
379
  </div>
342
380
 
343
- <DiagnosticList {diagnostics} />
381
+ {#if !isContainer(field.control)}
382
+ <DiagnosticList {diagnostics} />
383
+ {/if}
344
384
  </div>
345
385
  </div>
346
386