@quillmark/svelte 0.23.0 → 0.25.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 (68) hide show
  1. package/README.md +7 -8
  2. package/THEMING.md +1 -1
  3. package/dist/core/codec/blocks.d.ts +23 -6
  4. package/dist/core/codec/blocks.js +163 -38
  5. package/dist/core/codec/code.js +3 -8
  6. package/dist/core/codec/decode.d.ts +2 -1
  7. package/dist/core/codec/decode.js +19 -11
  8. package/dist/core/codec/field.d.ts +6 -3
  9. package/dist/core/codec/field.js +12 -4
  10. package/dist/core/codec/index.d.ts +2 -0
  11. package/dist/core/codec/index.js +4 -0
  12. package/dist/core/codec/inputrules.js +40 -10
  13. package/dist/core/codec/islands.d.ts +4 -1
  14. package/dist/core/codec/islands.js +43 -16
  15. package/dist/core/codec/keymap.js +19 -1
  16. package/dist/core/codec/line-paste.d.ts +7 -0
  17. package/dist/core/codec/line-paste.js +36 -0
  18. package/dist/core/codec/list-move.d.ts +6 -0
  19. package/dist/core/codec/list-move.js +30 -0
  20. package/dist/core/codec/list-paste.d.ts +2 -0
  21. package/dist/core/codec/list-paste.js +142 -0
  22. package/dist/core/codec/list-toggle.d.ts +4 -0
  23. package/dist/core/codec/list-toggle.js +170 -0
  24. package/dist/core/codec/lists.d.ts +22 -15
  25. package/dist/core/codec/lists.js +404 -97
  26. package/dist/core/codec/markdown-paste.d.ts +13 -3
  27. package/dist/core/codec/markdown-paste.js +70 -6
  28. package/dist/core/codec/nesting.d.ts +5 -1
  29. package/dist/core/codec/nesting.js +1 -1
  30. package/dist/core/codec/prose.css +36 -20
  31. package/dist/core/codec/slash.d.ts +2 -1
  32. package/dist/core/codec/slash.js +0 -0
  33. package/dist/core/codec/touching.d.ts +12 -0
  34. package/dist/core/codec/touching.js +33 -0
  35. package/dist/core/codec/undo.d.ts +5 -0
  36. package/dist/core/codec/undo.js +21 -0
  37. package/dist/index.js +1 -1
  38. package/dist/preview/Preview.svelte +88 -13
  39. package/dist/preview/Preview.svelte.d.ts +2 -2
  40. package/dist/preview/bridge.js +8 -2
  41. package/dist/preview/controller.d.ts +8 -4
  42. package/dist/preview/controller.js +10 -2
  43. package/dist/preview/index.js +1 -1
  44. package/dist/preview/paint.d.ts +6 -2
  45. package/dist/preview/paint.js +67 -56
  46. package/dist/preview/strings.d.ts +2 -0
  47. package/dist/preview/strings.js +4 -2
  48. package/dist/preview/viewport.d.ts +10 -0
  49. package/dist/preview/viewport.js +271 -0
  50. package/dist/preview/zoom.d.ts +41 -0
  51. package/dist/preview/zoom.js +52 -0
  52. package/dist/visual/ArrayField.svelte +24 -4
  53. package/dist/visual/Card.svelte +5 -4
  54. package/dist/visual/DateField.svelte +26 -13
  55. package/dist/visual/Field.svelte +2 -2
  56. package/dist/visual/FormatPopover.svelte +41 -2
  57. package/dist/visual/ObjectField.svelte +4 -7
  58. package/dist/visual/ObjectField.svelte.d.ts +2 -2
  59. package/dist/visual/ProseValue.svelte +35 -3
  60. package/dist/visual/ProseValue.svelte.d.ts +7 -0
  61. package/dist/visual/VisualEditorInner.svelte +5 -11
  62. package/dist/visual/icons/nodes.d.ts +26 -0
  63. package/dist/visual/icons/nodes.js +16 -0
  64. package/dist/visual/strings.d.ts +6 -4
  65. package/dist/visual/strings.js +9 -3
  66. package/dist/visual/structure.d.ts +22 -10
  67. package/dist/visual/structure.js +35 -11
  68. package/package.json +2 -2
@@ -1,5 +1,6 @@
1
1
  <!--
2
- One card block, its fields grouped by `ui.group`/`ui.compact`. `main` is headerless
2
+ One card block, its top-level objects (or legacy `ui.group`s) drawn as panels and its
3
+ fields packed by `ui.compact`. `main` is headerless
3
4
  and takes no controls. Every prose leaf takes a parent-built live address, so a card
4
5
  reorder re-targets its commits without a remount.
5
6
  -->
@@ -273,9 +274,9 @@
273
274
  {@const headIds = section.head ? ops.domIds(section.head.name) : undefined}
274
275
  {@const headHint = section.head?.description}
275
276
  <div class={['qm-group', { 'qm-open': isOpen }]}>
276
- <!-- A flat field alone in its section is named by the header, which
277
- takes its label id and parks its description: the field draws no
278
- label of its own to say the section's name twice. -->
277
+ <!-- A panel's object, or an object alone in its section, is named by the
278
+ header, which takes its label id and parks its description: the field
279
+ draws no label of its own to say the section's name twice. -->
279
280
  <div class="qm-group-head">
280
281
  <button
281
282
  type="button"
@@ -1,7 +1,7 @@
1
1
  <!--
2
2
  A `date` (or `datetime`) field → a styled segmented date field on bits-ui. The
3
- stored value is a string, `YYYY-MM-DD` or `today`; a cleared control commits as the
4
- focus leaves it, the blank `""` over a `default:`, which stays blank, and `undefined`
3
+ stored value is a string, `YYYY-MM-DD` or `today`; a cleared control commits as its
4
+ last segment empties, the blank `""` over a `default:`, which stays blank, and `undefined`
5
5
  (the unset rung) where nothing is declared, the parent removing the field. The value-object a date field lowers to is a render-time
6
6
  concern: the editor only sees the stored string.
7
7
 
@@ -48,12 +48,14 @@
48
48
  A segment edit that leaves a segment empty empties the primitive's value, which is
49
49
  held rather than committed while the focus stays inside: the first Backspace through
50
50
  a date writes nothing unset, and the empty segment shows its hint, not the default's
51
- digits beside the ones still standing. Leaving settles it: blank where every segment
52
- is empty, otherwise the stored date back. A field storing no date puts its segments
53
- back by remounting the primitive, which re-reads an unchanged empty value as nothing
54
- to sync.
51
+ digits beside the ones still standing. The keystroke that empties the last segment
52
+ writes the blank at once, so the render follows the clear without waiting on the
53
+ focus. Leaving with a segment filled puts the stored date back; a field storing no
54
+ date puts its segments back by remounting the primitive, which re-reads an unchanged
55
+ empty value as nothing to sync.
55
56
  -->
56
57
  <script lang="ts">
58
+ import { flushSync } from 'svelte';
57
59
  import { DateField as BitsDateField } from 'bits-ui';
58
60
  import { getLocalTimeZone, parseDate, today, type DateValue } from '@internationalized/date';
59
61
  import Icon from './icons/Icon.svelte';
@@ -164,18 +166,28 @@
164
166
  function enter(e: FocusEvent): void {
165
167
  if (!within(e) && value == null && fallbackDate) seat = true;
166
168
  }
169
+ const filled = (): boolean =>
170
+ [...(wrapEl?.querySelectorAll('[data-segment]') ?? [])].some(
171
+ (s) => s.getAttribute('data-segment') !== 'literal' && !s.hasAttribute('data-ghosted')
172
+ );
173
+ /** Every segment empty: the blank, written at once. */
174
+ function clear(): void {
175
+ const blank = fallbackDate ? '' : undefined;
176
+ if (value !== blank) onCommit(blank);
177
+ }
167
178
  function leave(e: FocusEvent): void {
168
179
  if (within(e)) return;
169
180
  seat = false;
170
181
  if (!held) return;
171
182
  held = false;
172
- const filled = [...(wrapEl?.querySelectorAll('[data-segment]') ?? [])].some(
173
- (s) => s.getAttribute('data-segment') !== 'literal' && !s.hasAttribute('data-ghosted')
174
- );
175
- const blank = fallbackDate ? '' : undefined;
176
- if (!filled) {
177
- if (value !== blank) onCommit(blank);
178
- } else if (!value) generation++;
183
+ if (!filled()) clear();
184
+ else if (!value) generation++;
185
+ }
186
+ /** The keystroke that empties the last segment, after the primitive has drawn it. */
187
+ function settle(): void {
188
+ if (!held) return;
189
+ flushSync();
190
+ if (!filled()) clear();
179
191
  }
180
192
 
181
193
  // What one segment prints, and which rung it takes.
@@ -249,6 +261,7 @@
249
261
  aria-describedby={describedBy}
250
262
  data-ghosted={ghost ? '' : undefined}
251
263
  data-default={seated || follows ? '' : undefined}
264
+ onkeydown={settle}
252
265
  >
253
266
  {#snippet children({ segments })}
254
267
  <!-- Keyed by index: `part` repeats (the `literal` separators between
@@ -33,7 +33,6 @@
33
33
  VARIANT_DISCRIMINANT,
34
34
  arrayLayout,
35
35
  baseType,
36
- flatObject,
37
36
  ghostDefault,
38
37
  isContainer,
39
38
  optionalCell,
@@ -174,7 +173,8 @@
174
173
  );
175
174
  // Two controls own their label row, the add chip and the held count sharing it.
176
175
  const ownsLabel = $derived(field.control === 'array' || field.control === 'matrix');
177
- const flat = $derived(flatObject(field.schema));
176
+ // A headed object is a panel's, and the panel is its frame.
177
+ const flat = $derived(headed);
178
178
 
179
179
  // A container walks its diagnostics down to the cell each names and draws the rest
180
180
  // at its own foot (`diagnostics.ts`); every other control draws them here, under
@@ -12,7 +12,10 @@
12
12
  (`ProseValue`). This component never touches the content. The keymap mirror
13
13
  (Mod-b/i/u) lives in the codec's keymap (field.ts); this is the pointer
14
14
  affordance for all six marks. Mod-K is this surface's own key, the prompt it raises
15
- being this surface's: a document `keydown` the leaf's keymap left unclaimed.
15
+ being this surface's: a document `keydown` the leaf's keymap left unclaimed. Over a
16
+ leaf that holds lists, two more buttons number or bullet every selected line, or take
17
+ the marker off where every one carries it: the codec's `toggleList`, whose keys
18
+ (Mod-Shift-7/8) the leaf's keymap binds.
16
19
 
17
20
  Selection observation. The view's `dispatchTransaction` is the leaf's
18
21
  (read-only), so there is no hook to observe transactions from outside.
@@ -72,7 +75,7 @@
72
75
  import { Popover } from 'bits-ui';
73
76
  import Icon from './icons/Icon.svelte';
74
77
  import type { IconName } from './icons/nodes.js';
75
- import { hasMarks, rangeAnchor, type RangeAnchor } from '../core/codec/index.js';
78
+ import { hasMarks, rangeAnchor, toggleList, type RangeAnchor } from '../core/codec/index.js';
76
79
  import type { ActiveProse } from './leaves.js';
77
80
  import { clearLink, hrefInSelection, normalizeHref, setLink } from './links.js';
78
81
  import { spacePx } from '../core/rungs.js';
@@ -96,7 +99,15 @@
96
99
  { name: 'link', icon: 'link', title: t.strings.formatLink }
97
100
  ]);
98
101
 
102
+ const LISTS: { ordered: boolean; icon: IconName; title: string }[] = $derived([
103
+ { ordered: true, icon: 'list-ordered', title: t.strings.formatNumberedList },
104
+ { ordered: false, icon: 'list', title: t.strings.formatBulletedList }
105
+ ]);
106
+
99
107
  let open = $state(false);
108
+ /** Whether the leaf under the selection holds lists: a body does, a one-line field
109
+ * and a table cell do not. */
110
+ let listable = $state(false);
100
111
  /** A pointer is down outside this surface: the selection is still being made. */
101
112
  let pressed = false;
102
113
  let linkPromptOpen = $state(false);
@@ -161,6 +172,7 @@
161
172
  return;
162
173
  }
163
174
  portalTarget = view.dom.closest<HTMLElement>('[data-qm-root]') ?? undefined;
175
+ listable = !!view.state.schema.nodes.list_item;
164
176
  const { from, to } = view.state.selection;
165
177
  anchor = rangeAnchor(view, from, to);
166
178
  open = true;
@@ -268,6 +280,14 @@
268
280
  sync();
269
281
  }
270
282
 
283
+ function toggleLines(ordered: boolean): void {
284
+ const view = activeLeafView();
285
+ if (!view) return;
286
+ toggleList(ordered)(view.state, view.dispatch);
287
+ view.focus();
288
+ sync();
289
+ }
290
+
271
291
  /** Every exit from the prompt: lower it, run `cmd` if there is one, and hand the
272
292
  * leaf back its focus and the surface its buttons. */
273
293
  function closePrompt(cmd?: Command): void {
@@ -421,6 +441,19 @@
421
441
  onclick={() => toggle(m.name)}><Icon name={m.icon} /></button
422
442
  >
423
443
  {/each}
444
+ {#if listable}
445
+ <span class="qm-format-divider" aria-hidden="true"></span>
446
+ {#each LISTS as l (l.icon)}
447
+ <button
448
+ type="button"
449
+ class="qm-icon-btn qm-mark-btn qm-focus-ring"
450
+ title={l.title}
451
+ aria-label={l.title}
452
+ onmousedown={keepFocus}
453
+ onclick={() => toggleLines(l.ordered)}><Icon name={l.icon} /></button
454
+ >
455
+ {/each}
456
+ {/if}
424
457
  </div>
425
458
  {/if}
426
459
  </div>
@@ -452,6 +485,12 @@
452
485
 
453
486
  No state for a mark the selection already carries: a run may carry one over part of
454
487
  itself, which a two-state button has no third face for. */
488
+ .qm-format-divider {
489
+ align-self: stretch;
490
+ width: 1px;
491
+ margin: var(--_qm-space-half) 0;
492
+ background: var(--_qm-rule);
493
+ }
455
494
  .qm-mark-btn {
456
495
  padding: var(--_qm-space) var(--_qm-space-2);
457
496
  color: var(--_qm-ink);
@@ -30,9 +30,8 @@
30
30
  member's id. So a cell reads at whatever depth it sits, which nothing here knows.
31
31
 
32
32
  `bare` is the table's row: the same cells with no labels and no figure of their own,
33
- laid onto the tracks the table's header names ({@link ArrayField}). `flat` is
34
- `ui.layout: flat`: the cells keep their labels and lose the figure, standing at the
35
- depth of whatever holds the subform.
33
+ laid onto the tracks the table's header names ({@link ArrayField}). `flat` is a
34
+ panel's object: the cells keep their labels and lose the figure, the panel being it.
36
35
  -->
37
36
  <script lang="ts">
38
37
  import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
@@ -42,7 +41,6 @@
42
41
  controlKind,
43
42
  declaredContent,
44
43
  fieldModels,
45
- flatObject,
46
44
  humanize,
47
45
  isContainer,
48
46
  literalText,
@@ -103,8 +101,8 @@
103
101
  /** A table's row: the cells alone, on the tracks the table's header names, with
104
102
  * no labels and no vertical of their own. */
105
103
  bare?: boolean;
106
- /** `ui.layout: flat`: no vertical and no rung, the properties at the depth of the
107
- * list the subform stands in. */
104
+ /** A panel's object: no vertical and no rung, the properties at the depth of the
105
+ * panel's field list. */
108
106
  flat?: boolean;
109
107
  /** A card-level object, or one nested in it: its properties are placed as a
110
108
  * card's fields are (`placeFields`), so moving a field into a group leaves its row
@@ -426,7 +424,6 @@
426
424
  bind:this={nestedEls[key]}
427
425
  value={obj[key] as Record<string, unknown> | undefined}
428
426
  properties={sub.properties}
429
- flat={flatObject(sub)}
430
427
  {placed}
431
428
  label={ids ? undefined : fallbackName(key, sub)}
432
429
  idBase={ids?.control}
@@ -32,8 +32,8 @@ interface Props {
32
32
  /** A table's row: the cells alone, on the tracks the table's header names, with
33
33
  * no labels and no vertical of their own. */
34
34
  bare?: boolean;
35
- /** `ui.layout: flat`: no vertical and no rung, the properties at the depth of the
36
- * list the subform stands in. */
35
+ /** A panel's object: no vertical and no rung, the properties at the depth of the
36
+ * panel's field list. */
37
37
  flat?: boolean;
38
38
  /** A card-level object, or one nested in it: its properties are placed as a
39
39
  * card's fields are (`placeFields`), so moving a field into a group leaves its row
@@ -31,7 +31,9 @@
31
31
  declaring `multiline: true` takes the full row ({@link ObjectField}), a `richtext` one
32
32
  on the block schema and a `text` one on the plain schema, paragraphs and hard breaks.
33
33
  An array element is one textblock whatever it declares, since Enter there is the
34
- repeater's. There is no slash menu at either width, and no island view.
34
+ repeater's, and a paste of several lines into one is a row per line, as Enter would
35
+ open them, up to the array's `max:`, the last row taking what is left joined
36
+ (`lines`). There is no slash menu at either width, and no island view.
35
37
 
36
38
  A leaf over content it cannot edit is held (`fitsLeaf`): a narrowed one over anything
37
39
  but one plain paragraph, a plain one over anything but plain lines, where the decode
@@ -43,6 +45,7 @@
43
45
  -->
44
46
  <script lang="ts">
45
47
  import { untrack } from 'svelte';
48
+ import { Slice, type Node as PMNode } from 'prosemirror-model';
46
49
  import { EditorState, Selection } from 'prosemirror-state';
47
50
  import { EditorView } from 'prosemirror-view';
48
51
  import {
@@ -52,7 +55,9 @@
52
55
  emptyContent,
53
56
  fitsLeaf,
54
57
  heldAttributes,
58
+ joinLines,
55
59
  leafSchema,
60
+ pastedLines,
56
61
  pmToContent,
57
62
  proseAttributes,
58
63
  proseLeafPlugins,
@@ -103,6 +108,10 @@
103
108
  /** The declared `max:`, a character count no edit takes the leaf past. */
104
109
  cap?: TextCap;
105
110
  onChange: (rt: Content) => void;
111
+ /** Where a paste's lines past the first go: rows of their own below this one, at
112
+ * most `room()` of them, the last taking the rest joined. The row's own edit comes
113
+ * up beside them, the two one commit. Absent, the leaf joins them into its line. */
114
+ lines?: { room: () => number; onLines: (rt: Content, below: Content[]) => void };
106
115
  /** Raw keydown, for a container whose own keys run through this leaf: the
107
116
  * array repeater's Enter/Backspace (`ArrayField`). Fires before the view's own
108
117
  * keymap, so the state it reads is the one this keystroke has yet to change. */
@@ -120,6 +129,7 @@
120
129
  placeholder,
121
130
  cap,
122
131
  onChange,
132
+ lines,
123
133
  onKey
124
134
  }: Props = $props();
125
135
 
@@ -177,6 +187,14 @@
177
187
  let seen = value;
178
188
  let defaulted = stored === undefined && !!fallback;
179
189
  const reconciler = createReconciler(shown(stored));
190
+ // The rows a paste's later lines become, held from the parse to the edit that
191
+ // lands the first line.
192
+ let spill: Content[] | undefined;
193
+ const row = (group: PMNode[]): Content => {
194
+ const { schema } = mounted.state;
195
+ const line = schema.nodes.paragraph.create(null, joinLines(group, schema));
196
+ return pmToContent(schema.node('doc', null, [line]));
197
+ };
180
198
  held = !fitsLeaf(reconciler.last, leaf);
181
199
  // Which of `aria-label` / `aria-labelledby` wins is the codec's one answer
182
200
  // (`proseAttributes`), so a cell carrying a label element and a row carrying
@@ -208,14 +226,28 @@
208
226
  const named = held ? heldNamed() : plainNamed();
209
227
  return defaulted ? { ...named, 'data-default': '' } : named;
210
228
  },
229
+ transformPasted: (slice) => {
230
+ spill = undefined;
231
+ if (!lines || held) return slice;
232
+ const pasted = pastedLines(slice);
233
+ const room = Math.min(pasted.length - 1, lines.room());
234
+ if (room < 1) return slice;
235
+ const below = pasted.slice(1);
236
+ spill = [...below.slice(0, room - 1).map((l) => row([l])), row(below.slice(room - 1))];
237
+ return new Slice(pasted[0]!.content, 0, 0);
238
+ },
211
239
  dispatchTransaction(tr) {
240
+ const event = tr.getMeta('uiEvent');
241
+ const below = event === 'paste' || event === 'drop' ? spill : undefined;
242
+ spill = undefined;
212
243
  const next = mounted.state.apply(tr);
213
244
  if (next === mounted.state) return;
214
245
  const edit = tr.docChanged && !held;
215
246
  if (edit) defaulted = false;
216
247
  mounted.updateState(next);
217
- if (!edit) return;
218
- onChange(pmToContent(next.doc));
248
+ if (!edit && !below) return;
249
+ if (below && lines) lines.onLines(pmToContent(next.doc), below);
250
+ else onChange(pmToContent(next.doc));
219
251
  // The commit as the store took it, so its echo through the re-derive is no move.
220
252
  seen = value;
221
253
  reconciler.commit(shown(content()));
@@ -35,6 +35,13 @@ interface Props {
35
35
  /** The declared `max:`, a character count no edit takes the leaf past. */
36
36
  cap?: TextCap;
37
37
  onChange: (rt: Content) => void;
38
+ /** Where a paste's lines past the first go: rows of their own below this one, at
39
+ * most `room()` of them, the last taking the rest joined. The row's own edit comes
40
+ * up beside them, the two one commit. Absent, the leaf joins them into its line. */
41
+ lines?: {
42
+ room: () => number;
43
+ onLines: (rt: Content, below: Content[]) => void;
44
+ };
38
45
  /** Raw keydown, for a container whose own keys run through this leaf: the
39
46
  * array repeater's Enter/Backspace (`ArrayField`). Fires before the view's own
40
47
  * keymap, so the state it reads is the one this keystroke has yet to change. */
@@ -54,10 +54,7 @@
54
54
  isContainer,
55
55
  shortCell,
56
56
  schemaAt,
57
- fieldModels,
58
- groupOrder,
59
- groupSections,
60
- groupLabel,
57
+ cardSections,
61
58
  cardTitle,
62
59
  kindTitle,
63
60
  fieldValues,
@@ -325,9 +322,9 @@
325
322
  try {
326
323
  if (value === undefined) {
327
324
  // The unset rung (VISUAL_EDITOR §"The commitment ladder"): removing the field
328
- // leaves the engine's authored › `default:` › blank-fill resolve to render the
325
+ // leaves Quillmark's authored › `default:` › blank-fill resolve to render the
329
326
  // default, where writing one bakes a snapshot the schema cannot track (canon
330
- // SCHEMAS.md: the engine never writes a default; the editor writes one only
327
+ // SCHEMAS.md: Quillmark never writes a default; the editor writes one only
331
328
  // as the edit that takes it). Removal writes no value, so there is
332
329
  // nothing for a schema to conform and the lane is the quill-free one.
333
330
  if (isMain) {
@@ -632,14 +629,11 @@
632
629
  payloadItems: readonly PayloadItem[];
633
630
  ext?: Record<string, unknown>;
634
631
  },
635
- cardSchema: Parameters<typeof fieldModels>[0] | undefined,
632
+ cardSchema: Parameters<typeof cardSections>[0] | undefined,
636
633
  rows: ResolvedCardRows
637
634
  ): CardModel {
638
635
  const values = fieldValues(card.payloadItems);
639
- const fields = cardSchema ? fieldModels(cardSchema) : [];
640
- const sections = cardSchema
641
- ? groupSections(fields, groupOrder(cardSchema), (g) => groupLabel(cardSchema, g))
642
- : [];
636
+ const sections = cardSchema ? cardSections(cardSchema) : [];
643
637
  const extEditor = card.ext?.editor as { title?: string } | undefined;
644
638
  const hasBody = bodyEnabled(cardSchema);
645
639
  return {
@@ -82,6 +82,32 @@ export declare const ICONS: {
82
82
  }], readonly ["path", {
83
83
  readonly d: "m8 6-6 6 6 6";
84
84
  }]];
85
+ readonly list: readonly [readonly ["path", {
86
+ readonly d: "M3 5h.01";
87
+ }], readonly ["path", {
88
+ readonly d: "M3 12h.01";
89
+ }], readonly ["path", {
90
+ readonly d: "M3 19h.01";
91
+ }], readonly ["path", {
92
+ readonly d: "M8 5h13";
93
+ }], readonly ["path", {
94
+ readonly d: "M8 12h13";
95
+ }], readonly ["path", {
96
+ readonly d: "M8 19h13";
97
+ }]];
98
+ readonly 'list-ordered': readonly [readonly ["path", {
99
+ readonly d: "M11 5h10";
100
+ }], readonly ["path", {
101
+ readonly d: "M11 12h10";
102
+ }], readonly ["path", {
103
+ readonly d: "M11 19h10";
104
+ }], readonly ["path", {
105
+ readonly d: "M4 4h1v5";
106
+ }], readonly ["path", {
107
+ readonly d: "M4 9h2";
108
+ }], readonly ["path", {
109
+ readonly d: "M6.5 20H3.4c0-1 2.6-1.925 2.6-3.5a1.5 1.5 0 0 0-2.6-1.02";
110
+ }]];
85
111
  readonly link: readonly [readonly ["path", {
86
112
  readonly d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71";
87
113
  }], readonly ["path", {
@@ -41,6 +41,22 @@ export const ICONS = {
41
41
  ['path', { d: 'm16 18 6-6-6-6' }],
42
42
  ['path', { d: 'm8 6-6 6 6 6' }]
43
43
  ],
44
+ list: [
45
+ ['path', { d: 'M3 5h.01' }],
46
+ ['path', { d: 'M3 12h.01' }],
47
+ ['path', { d: 'M3 19h.01' }],
48
+ ['path', { d: 'M8 5h13' }],
49
+ ['path', { d: 'M8 12h13' }],
50
+ ['path', { d: 'M8 19h13' }]
51
+ ],
52
+ 'list-ordered': [
53
+ ['path', { d: 'M11 5h10' }],
54
+ ['path', { d: 'M11 12h10' }],
55
+ ['path', { d: 'M11 19h10' }],
56
+ ['path', { d: 'M4 4h1v5' }],
57
+ ['path', { d: 'M4 9h2' }],
58
+ ['path', { d: 'M6.5 20H3.4c0-1 2.6-1.925 2.6-3.5a1.5 1.5 0 0 0-2.6-1.02' }]
59
+ ],
44
60
  link: [
45
61
  ['path', { d: 'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71' }],
46
62
  ['path', { d: 'M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71' }]
@@ -79,6 +79,8 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
79
79
  formatStrikethrough: string;
80
80
  formatCode: string;
81
81
  formatLink: string;
82
+ formatNumberedList: string;
83
+ formatBulletedList: string;
82
84
  /** The anchor button's accessible name. The popover withholds the button, so
83
85
  * nothing draws this or the title below. */
84
86
  formatAnchor: string;
@@ -122,7 +124,7 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
122
124
  *
123
125
  * - **`validation::enum_violation`, `type_mismatch`, `format_violation`**: the
124
126
  * constraint re-words from the quill's schema at `path`, the offending value from
125
- * `args` (`value` / `sourceToken` / `actual`, the engine testifying to what it
127
+ * `args` (`value` / `sourceToken` / `actual`, Quillmark testifying to what it
126
128
  * saw) and never from the document at `path`: validation runs post-coercion, so
127
129
  * the validator read a value the document does not hold and a sentence built from
128
130
  * `path` names a spelling the user never typed.
@@ -130,7 +132,7 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
130
132
  * refused value being in neither document (unchanged on throw) nor schema.
131
133
  * - **`parse::yaml_error_with_location`, `invalid_structure`**: does not re-word. No
132
134
  * `path`; `args` names the block (`blockIndex`) and `location` places the failure in
133
- * the document's own coordinates, engine prose riding under no key: the parser's own
135
+ * the document's own coordinates, Quillmark prose riding under no key: the parser's own
134
136
  * text, its column and its caret snippet exist only inside `message`.
135
137
  * - **`LiveSession.warnings`**: does not re-word. Backend text, an external feed.
136
138
  *
@@ -145,8 +147,8 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
145
147
  export type FormatDiagnostic = (d: Diagnostic) => string | undefined;
146
148
  /**
147
149
  * A `Mod` chord as the writer's platform prints it: `⌘B` on Apple's, `Ctrl+B`
148
- * elsewhere. The platform test is the one `prosemirror-keymap` resolves `Mod` by, so a
149
- * tooltip names the chord the keymap binds.
150
+ * elsewhere, and `Shift-7` as `⇧⌘7` or `Ctrl+Shift+7`. The platform test is the one
151
+ * `prosemirror-keymap` resolves `Mod` by, so a tooltip names the chord the keymap binds.
150
152
  */
151
153
  export declare function chord(key: string, platform?: string): string;
152
154
  /** The package's English. Every key, so the merge below is total. */
@@ -21,11 +21,15 @@ import { getContext, setContext } from 'svelte';
21
21
  import { DEFAULT_SLASH_STRINGS, DEFAULT_TABLE_STRINGS } from '../core/codec/index.js';
22
22
  /**
23
23
  * A `Mod` chord as the writer's platform prints it: `⌘B` on Apple's, `Ctrl+B`
24
- * elsewhere. The platform test is the one `prosemirror-keymap` resolves `Mod` by, so a
25
- * tooltip names the chord the keymap binds.
24
+ * elsewhere, and `Shift-7` as `⇧⌘7` or `Ctrl+Shift+7`. The platform test is the one
25
+ * `prosemirror-keymap` resolves `Mod` by, so a tooltip names the chord the keymap binds.
26
26
  */
27
27
  export function chord(key, platform = typeof navigator === 'undefined' ? '' : navigator.platform) {
28
- return /Mac|iP(hone|[oa]d)/.test(platform) ? `⌘${key}` : `Ctrl+${key}`;
28
+ const shift = key.startsWith('Shift-');
29
+ const bare = shift ? key.slice('Shift-'.length) : key;
30
+ if (/Mac|iP(hone|[oa]d)/.test(platform))
31
+ return `${shift ? '⇧' : ''}⌘${bare}`;
32
+ return `Ctrl+${shift ? 'Shift+' : ''}${bare}`;
29
33
  }
30
34
  /** The package's English. Every key, so the merge below is total. */
31
35
  export const DEFAULT_VISUAL_STRINGS = {
@@ -59,6 +63,8 @@ export const DEFAULT_VISUAL_STRINGS = {
59
63
  formatStrikethrough: 'Strikethrough',
60
64
  formatCode: 'Code',
61
65
  formatLink: `Link (${chord('K')})`,
66
+ formatNumberedList: `Numbered list (${chord('Shift-7')})`,
67
+ formatBulletedList: `Bulleted list (${chord('Shift-8')})`,
62
68
  formatAnchor: 'Anchor',
63
69
  formatAnchorTitle: 'Anchor — an identity handle over the selection',
64
70
  linkPlaceholder: 'https://…',
@@ -26,18 +26,17 @@ export interface FieldModel {
26
26
  inline: boolean;
27
27
  plaintext: boolean;
28
28
  }
29
- /** A `ui.group` section: its label and the fields (declaration order) inside it. */
29
+ /** A panel section, a top-level `object`'s or a legacy `ui.group`'s: its label and the
30
+ * fields (declaration order) inside it. */
30
31
  export interface GroupSection {
31
32
  group: string | undefined;
32
33
  label: string;
33
34
  fields: FieldModel[];
34
- /** The field the section's header names in place of a label of its own: the one
35
- * field of a group section, where that field is {@link flatObject}. */
35
+ /** The object the section's header names in place of a label of its own: the
36
+ * object a panel is, or a group section's one field where that field is an
37
+ * `object`. Its properties draw flat, the panel being their frame. */
36
38
  head?: FieldModel;
37
39
  }
38
- /** `ui.layout: flat` on a typed dictionary: no frame of its own, its properties
39
- * standing at the depth of the fields around it rather than a rung in. */
40
- export declare function flatObject(schema: QuillFieldSchema): boolean;
41
40
  /**
42
41
  * One card instance projected for rendering: the schema × payload join result
43
42
  * the VisualEditor's `$derived` produces and hands to `<Card>`. Positional
@@ -112,7 +111,7 @@ export declare function resolvedByCardIndex(resolved: Resolved | undefined): Map
112
111
  export declare function ghostDefault(row: ResolvedField | undefined): unknown;
113
112
  /** A stored `boolean` cell as the render reads it: a boolean, `true` or `false` in any
114
113
  * case, or a number by whether it is non-zero; undefined where the render refuses the
115
- * value. The engine's coercion, restated, so the suite holds it to `reader.resolve()`. */
114
+ * value. Quillmark's coercion, restated, so the suite holds it to `reader.resolve()`. */
116
115
  export declare function renderedBoolean(value: unknown): boolean | undefined;
117
116
  /** A ghost value's string form, or undefined for null/object (only text ghosts
118
117
  * render a placeholder). A scalar's resolved default reads through it; a content
@@ -207,9 +206,9 @@ export declare function matrixMembers(roster: Record<string, string> | undefined
207
206
  export declare function memberValue(map: Record<string, unknown> | undefined, id: string): unknown;
208
207
  /**
209
208
  * Whether a stored member is held: by being present. A mapping is held whatever it
210
- * carries, `{}` included, and a bare scalar is the tick itself, read as the engine's
209
+ * carries, `{}` included, and a bare scalar is the tick itself, read as Quillmark's
211
210
  * coercion reads it: `false`, `0`, `"false"` and `null` unheld, every other present
212
- * scalar held, so a spelling the engine would refuse still draws the tick it claims.
211
+ * scalar held, so a spelling Quillmark would refuse still draws the tick it claims.
213
212
  */
214
213
  export declare function matrixHeld(stored: unknown): boolean;
215
214
  /** A member's columns: its mapping without the reserved keys, `{}` for a bare tick or an
@@ -245,7 +244,7 @@ export declare function matrixDeclares(matrix: QuillFieldSchema, id: string): bo
245
244
  /**
246
245
  * The added items a stored map holds, sorted by id as the plate orders them: on an
247
246
  * open matrix, each key off the roster spelled as an id whose mapping carries a
248
- * `title`. Any other key off the roster is the engine's refusal, drawn nowhere and
247
+ * `title`. Any other key off the roster is Quillmark's refusal, drawn nowhere and
249
248
  * ridden through.
250
249
  */
251
250
  export declare function addedItems(matrix: QuillFieldSchema, map: Record<string, unknown> | undefined): MatrixMember[];
@@ -329,6 +328,19 @@ export declare function groupLabel(cardSchema: QuillCardSchema, group: string):
329
328
  * the card schema, which falls back to {@link humanize} itself.
330
329
  */
331
330
  export declare function groupSections(fields: FieldModel[], order: string[], labelFor: (group: string) => string): GroupSection[];
331
+ /**
332
+ * Object-panel sections (VISUAL_EDITOR §"Structure mirrors the schema"): every
333
+ * top-level `object` is a panel of its own, headed by the object, in declaration
334
+ * order; everything else stands in one unframed section above them. The section id
335
+ * is the field name, which a card's keys hold unique.
336
+ */
337
+ export declare function objectSections(fields: FieldModel[]): GroupSection[];
338
+ /**
339
+ * A card's sections: its ungrouped fields' {@link objectSections}, then each legacy
340
+ * `ui.group` a panel in registry order, the order the loader settles the fields in.
341
+ * A panel named as a group is keyed off the group's id space, which `.` never joins.
342
+ */
343
+ export declare function cardSections(cardSchema: QuillCardSchema): GroupSection[];
332
344
  /**
333
345
  * The group section a card's accordion opens on first mount: the first in
334
346
  * order, or `null` when the card declares none. Ungrouped fields render outside