@quillmark/svelte 0.16.0 → 0.18.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 (54) hide show
  1. package/README.md +4 -2
  2. package/dist/core/codec/index.d.ts +0 -3
  3. package/dist/core/rebind.svelte.js +5 -8
  4. package/dist/core/theme.css +12 -1
  5. package/dist/preset/index.d.ts +1 -0
  6. package/dist/preview/Preview.svelte +14 -19
  7. package/dist/preview/Preview.svelte.d.ts +3 -3
  8. package/dist/preview/index.d.ts +0 -1
  9. package/dist/visual/ArrayField.svelte +44 -41
  10. package/dist/visual/ArrayField.svelte.d.ts +0 -1
  11. package/dist/visual/BooleanField.svelte +16 -14
  12. package/dist/visual/BooleanField.svelte.d.ts +0 -1
  13. package/dist/visual/Card.svelte +65 -35
  14. package/dist/visual/CardControls.svelte.d.ts +0 -1
  15. package/dist/visual/DateField.svelte +14 -17
  16. package/dist/visual/DateField.svelte.d.ts +0 -1
  17. package/dist/visual/EnumField.svelte +9 -12
  18. package/dist/visual/EnumField.svelte.d.ts +0 -1
  19. package/dist/visual/Field.svelte +17 -10
  20. package/dist/visual/Field.svelte.d.ts +3 -0
  21. package/dist/visual/FieldHint.svelte.d.ts +0 -1
  22. package/dist/visual/FieldLabel.svelte +1 -2
  23. package/dist/visual/FormatPopover.svelte +22 -27
  24. package/dist/visual/FormatPopover.svelte.d.ts +0 -1
  25. package/dist/visual/MatrixAdd.svelte +210 -0
  26. package/dist/visual/MatrixAdd.svelte.d.ts +15 -0
  27. package/dist/visual/MatrixField.svelte +329 -84
  28. package/dist/visual/MatrixField.svelte.d.ts +8 -6
  29. package/dist/visual/NumberField.svelte +6 -10
  30. package/dist/visual/NumberField.svelte.d.ts +0 -1
  31. package/dist/visual/ObjectField.svelte +30 -17
  32. package/dist/visual/ObjectField.svelte.d.ts +3 -1
  33. package/dist/visual/ProseField.svelte +42 -41
  34. package/dist/visual/ProseField.svelte.d.ts +0 -1
  35. package/dist/visual/ProseValue.svelte +56 -56
  36. package/dist/visual/ProseValue.svelte.d.ts +0 -1
  37. package/dist/visual/SlashMenu.svelte +5 -15
  38. package/dist/visual/SlashMenu.svelte.d.ts +0 -1
  39. package/dist/visual/TextField.svelte +5 -9
  40. package/dist/visual/TextField.svelte.d.ts +0 -1
  41. package/dist/visual/TipsCard.svelte +10 -11
  42. package/dist/visual/TipsCard.svelte.d.ts +0 -1
  43. package/dist/visual/VisualEditorInner.svelte +21 -14
  44. package/dist/visual/VisualEditorInner.svelte.d.ts +0 -1
  45. package/dist/visual/checklists.d.ts +82 -0
  46. package/dist/visual/checklists.js +171 -0
  47. package/dist/visual/index.d.ts +0 -1
  48. package/dist/visual/strings.d.ts +15 -1
  49. package/dist/visual/strings.js +6 -0
  50. package/dist/visual/structure.d.ts +63 -31
  51. package/dist/visual/structure.js +122 -44
  52. package/package.json +20 -16
  53. package/dist/visual/synced.svelte.d.ts +0 -15
  54. package/dist/visual/synced.svelte.js +0 -34
@@ -11,6 +11,11 @@ import { core } from '../core/lifecycle.js';
11
11
  export function isContainer(kind) {
12
12
  return kind === 'array' || kind === 'object' || kind === 'variant' || kind === 'matrix';
13
13
  }
14
+ /** `ui.layout: flat` on a typed dictionary: no frame of its own, its properties
15
+ * standing at the depth of the fields around it rather than a rung in. */
16
+ export function flatObject(schema) {
17
+ return controlKind(schema) === 'object' && schema.ui?.layout === 'flat';
18
+ }
14
19
  /**
15
20
  * A card's resolved rows keyed by field name: the provenance channel parallel to
16
21
  * {@link CardModel.values}. `resolve` returns rows in declaration order; the
@@ -40,6 +45,18 @@ export function resolvedByCardIndex(resolved) {
40
45
  export function ghostDefault(row) {
41
46
  return row?.source === 'default' ? row.value : undefined;
42
47
  }
48
+ /** A stored `boolean` cell as the render reads it: a boolean, `true` or `false` in any
49
+ * case, or a number by whether it is non-zero; undefined where the render refuses the
50
+ * value. The engine's coercion, restated, so the suite holds it to `reader.resolve()`. */
51
+ export function renderedBoolean(value) {
52
+ if (typeof value === 'boolean')
53
+ return value;
54
+ if (typeof value === 'number')
55
+ return value !== 0;
56
+ if (typeof value === 'string' && /^(true|false)$/i.test(value))
57
+ return value.toLowerCase() === 'true';
58
+ return undefined;
59
+ }
43
60
  /** A ghost value's string form, or undefined for null/object (only text ghosts
44
61
  * render a placeholder). A scalar's resolved default reads through it; a content
45
62
  * field's resolves as `Content`, which a prose leaf reads by its text instead
@@ -142,10 +159,10 @@ export function controlKind(f) {
142
159
  * crosses inside untyped container data with no type to read it off, so a literal here
143
160
  * would be a second spelling of a reserved key. No variant may declare it. */
144
161
  export const VARIANT_DISCRIMINANT = VARIANT_DISCRIMINANT_KEY;
145
- /** The tick cell of a matrix member, the second reserved key beside the discriminant.
146
- * The boundary synthesizes it on every member and exports no constant for it, so this
147
- * is where the package spells it once (canon `SCHEMAS.md` §Matrix). */
148
- export const MATRIX_HELD = 'held';
162
+ /** The label key of an open matrix's added item, the one cell a roster member never
163
+ * stores. The boundary exports no constant for it, so the package spells it once
164
+ * (canon `SCHEMAS.md` §Matrix). */
165
+ export const MATRIX_TITLE = 'title';
149
166
  /** The roster's members, in declaration order. Own keys only: a schema map is indexed
150
167
  * by a document-supplied id, and `'toString' in members` is true of every roster. */
151
168
  export function matrixMembers(roster) {
@@ -157,17 +174,14 @@ export function memberValue(map, id) {
157
174
  return map && Object.hasOwn(map, id) ? map[id] : undefined;
158
175
  }
159
176
  /**
160
- * Whether a stored member is held. A bare scalar is the tick and a mapping is the member
161
- * object, whose `held` cell resolves as every absent cell does, to its `default:` of
162
- * `false` (canon `SCHEMAS.md` §Matrix): an absent key is unheld, a bare `true` is held,
163
- * and a member object is held only where its `held` cell says so. The spellings the
164
- * engine coerces to false — `false`, `0`, `"false"`, `null` — read false here; every other
165
- * present scalar reads held, so a document the engine would refuse still draws the tick
166
- * its spelling claims.
177
+ * Whether a stored member is held: by being present. A mapping is held whatever it
178
+ * carries, `{}` included, and a bare scalar is the tick itself, read as the engine's
179
+ * coercion reads it: `false`, `0`, `"false"` and `null` unheld, every other present
180
+ * scalar held, so a spelling the engine would refuse still draws the tick it claims.
167
181
  */
168
182
  export function matrixHeld(stored) {
169
183
  if (typeof stored === 'object' && stored !== null)
170
- return (Object.hasOwn(stored, MATRIX_HELD) && tickOf(stored[MATRIX_HELD]));
184
+ return true;
171
185
  return tickOf(stored);
172
186
  }
173
187
  /** A tick's scalar reading: absent and the engine's false spellings unheld, the rest held. */
@@ -182,20 +196,29 @@ function tickOf(v) {
182
196
  return v !== 'false';
183
197
  return true;
184
198
  }
185
- /** A member's columns: the member object with its `held` cell taken out, `{}` for the
186
- * bare and absent spellings. What the columns subform takes as its value. */
199
+ /** The keys no column may take (`quill::matrix_reserved_column`): an added item's
200
+ * label, and the `held` a member never stores. */
201
+ const MATRIX_RESERVED = [MATRIX_TITLE, 'held'];
202
+ /** A member's columns: its mapping without the reserved keys, `{}` for a bare tick or an
203
+ * absent member. What the columns subform takes as its value, so an edit writes back
204
+ * no `held` a document stored (`validation::held_stored`). */
187
205
  export function matrixColumns(stored) {
188
206
  if (typeof stored !== 'object' || stored === null || Array.isArray(stored))
189
207
  return {};
190
208
  const rest = { ...stored };
191
- delete rest[MATRIX_HELD];
209
+ for (const key of MATRIX_RESERVED)
210
+ delete rest[key];
192
211
  return rest;
193
212
  }
213
+ /** What a matrix holding nothing commits: the unset field where unset prints nothing
214
+ * held, and the authored `{}` where it would print a `default:` or `none` instead. */
215
+ export function matrixEmpty(matrix) {
216
+ return matrix.default !== undefined || optionalCell(matrix) ? {} : undefined;
217
+ }
194
218
  /**
195
219
  * The map after one member moves: `next` written under `id`, or the key dropped where
196
220
  * `next` is `undefined`; a map left holding nothing is an unset field. Every other
197
- * member rides through in the spelling the document had, so a stored `cyber_200: true`
198
- * stays `true` while its neighbour is edited.
221
+ * member rides through in the spelling the document had.
199
222
  */
200
223
  export function commitMember(map, id, next) {
201
224
  const out = { ...(map ?? {}) };
@@ -206,33 +229,86 @@ export function commitMember(map, id, next) {
206
229
  return Object.keys(out).length ? out : undefined;
207
230
  }
208
231
  /**
209
- * The member a tick or an edit writes. Held with columns is the member object with an
210
- * explicit `held: true`; unheld keeps the columns under `held: false`, so tick, type,
211
- * untick, retick loses nothing; unheld with no columns is `undefined`, the key dropped.
212
- * Which columns are blank is the engine's rule and not restated here: a column control
213
- * that clears drops its key (`ObjectField`), so a member emptied by hand arrives with no
214
- * columns and only then leaves the map.
232
+ * The held member a tick or an edit writes: its columns, the bare tick `true` where
233
+ * it answers none, and an added item's `title` beside them. Unticking is removal
234
+ * ({@link commitMember} with `undefined`), never a write.
215
235
  */
216
- export function memberWrite(held, columns) {
217
- if (!held && Object.keys(columns).length === 0)
218
- return undefined;
219
- return { [MATRIX_HELD]: held, ...columns };
236
+ export function memberWrite(columns, title) {
237
+ if (title !== undefined)
238
+ return { [MATRIX_TITLE]: title, ...columns };
239
+ return Object.keys(columns).length ? columns : true;
220
240
  }
221
- /** The `object` a matrix member desugars to, for the schema walk: `held` beside the
222
- * declared columns. Composed here because the boundary serializes the roster alone. */
223
- export function matrixMemberSchema(matrix) {
224
- return {
225
- type: 'object',
226
- properties: {
227
- [MATRIX_HELD]: { type: 'boolean', default: false },
228
- ...(matrix.properties ?? {})
229
- }
230
- };
241
+ /** Whether `id` is spelled as a matrix id: snake_case, the rule the roster's ids and
242
+ * an added item's key share. */
243
+ export function matrixId(id) {
244
+ return /^[a-z][a-z0-9_]*$/.test(id);
231
245
  }
232
246
  /** Whether `id` is on the roster. */
233
- export function matrixDeclares(matrix, id) {
247
+ export function onRoster(matrix, id) {
234
248
  return matrix.members != null && Object.hasOwn(matrix.members, id);
235
249
  }
250
+ /**
251
+ * Whether a step under the matrix names a member, judged on the schema alone as an
252
+ * index step is: a roster id, or on an open matrix any key spelled as an id, the added
253
+ * item it would be. So an unheld member's address resolves.
254
+ */
255
+ export function matrixDeclares(matrix, id) {
256
+ return onRoster(matrix, id) || (!!matrix.open && matrixId(id));
257
+ }
258
+ /**
259
+ * The added items a stored map holds, sorted by id as the plate orders them: on an
260
+ * open matrix, each key off the roster spelled as an id whose mapping carries a
261
+ * `title`. Any other key off the roster is the engine's refusal, drawn nowhere and
262
+ * ridden through.
263
+ */
264
+ export function addedItems(matrix, map) {
265
+ if (!matrix.open || !map)
266
+ return [];
267
+ const out = [];
268
+ for (const id of Object.keys(map)) {
269
+ if (onRoster(matrix, id) || !matrixId(id))
270
+ continue;
271
+ const stored = map[id];
272
+ if (typeof stored !== 'object' || stored === null || Array.isArray(stored))
273
+ continue;
274
+ const title = stored[MATRIX_TITLE];
275
+ if (title == null)
276
+ continue;
277
+ out.push({ id, title: String(title) });
278
+ }
279
+ return out.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
280
+ }
281
+ /**
282
+ * The id an item added under `title` takes: the title folded to snake_case, prefixed
283
+ * where it opens on no letter, and suffixed past every id `taken` names, the roster's
284
+ * and the document's alike. The id is the wire's and never drawn, so a later retitle
285
+ * leaves it where it is.
286
+ */
287
+ export function addedId(title, taken) {
288
+ const folded = title
289
+ .normalize('NFKD')
290
+ .replace(/[\u0300-\u036f]/g, '')
291
+ .toLowerCase()
292
+ .replace(/[^a-z0-9]+/g, '_')
293
+ .replace(/^_+|_+$/g, '');
294
+ const base = /^[a-z]/.test(folded) ? folded : `item${folded ? `_${folded}` : ''}`;
295
+ if (!taken(base))
296
+ return base;
297
+ let n = 2;
298
+ while (taken(`${base}_${n}`))
299
+ n++;
300
+ return `${base}_${n}`;
301
+ }
302
+ /** The `object` a matrix member is, for the schema walk: the declared columns, and an
303
+ * added item's `title` beside them. Composed here because the boundary serializes the
304
+ * roster and the columns alone. */
305
+ export function matrixMemberSchema(matrix, id) {
306
+ const columns = matrix.properties ?? {};
307
+ return {
308
+ type: 'object',
309
+ properties: onRoster(matrix, id) ? columns : { [MATRIX_TITLE]: { type: 'string' }, ...columns }
310
+ };
311
+ }
236
312
  // ── The schema walk ──────────────────────────────────────────────────────────
237
313
  /**
238
314
  * The schema at `steps` under `field`, or `undefined` where the schema declares no such
@@ -265,7 +341,7 @@ function stepInto(schema, step) {
265
341
  : undefined;
266
342
  case 'matrix':
267
343
  return typeof step === 'string' && matrixDeclares(schema, step)
268
- ? matrixMemberSchema(schema)
344
+ ? matrixMemberSchema(schema, step)
269
345
  : undefined;
270
346
  case 'variant': {
271
347
  if (typeof step !== 'string')
@@ -318,7 +394,7 @@ export function rowSummary(items, row) {
318
394
  * The layout an array takes: `'table'` where the array's own `ui.layout` asks for it
319
395
  * and every cell of the row is short (a block prose cell declines it, at load width and
320
396
  * every width after), else `'list'`. That every column is a leaf is the loader's
321
- * contract (`quill::table_column_not_flat`); whether a leaf fits a cell is the surface's
397
+ * contract (`quill::table_column_not_leaf`); whether a leaf fits a cell is the surface's
322
398
  * answer (canon `SCHEMAS.md`): a table composes by position, so a row that would stack
323
399
  * inside a cell is not one.
324
400
  */
@@ -362,9 +438,10 @@ export function commitDiscriminant(value, member) {
362
438
  /**
363
439
  * Whether the schema obliges a cell: `default:`'s absence, which is the whole of the
364
440
  * obligation (DOCUMENT_MODEL). Exempt are the cells whose absence of one says nothing:
365
- * a typed dictionary and a matrix, since a namespace declares no `default:` at all and
366
- * `validate` anchors obligation on the leaves under it; and an optional cell, which can
367
- * declare none, its unanswered render being `none`.
441
+ * a typed dictionary, since a namespace declares no `default:` at all and `validate`
442
+ * anchors obligation on the leaves under it; a matrix, whose blank is nothing held, a
443
+ * whole answer; and an optional cell, which can declare none, its unanswered render
444
+ * being `none`.
368
445
  */
369
446
  export function obliged(schema) {
370
447
  return (schema.type !== 'object' &&
@@ -431,7 +508,8 @@ export function groupSections(fields, order, labelFor) {
431
508
  for (const g of order) {
432
509
  const fs = fields.filter((f) => f.group === g);
433
510
  if (fs.length) {
434
- sections.push({ group: g, label: labelFor(g), fields: fs });
511
+ const head = fs.length === 1 && flatObject(fs[0].schema) ? fs[0] : undefined;
512
+ sections.push({ group: g, label: labelFor(g), fields: fs, head });
435
513
  emitted.add(g);
436
514
  }
437
515
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/svelte",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Editor, live-preview and source surfaces over a Quillmark WASM session.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -48,7 +48,10 @@
48
48
  "svelte": "./dist/visual/index.js",
49
49
  "default": "./dist/visual/index.js"
50
50
  },
51
- "./preset": "./dist/preset/index.css"
51
+ "./preset": {
52
+ "types": "./dist/preset/index.d.ts",
53
+ "default": "./dist/preset/index.css"
54
+ }
52
55
  },
53
56
  "files": [
54
57
  "dist",
@@ -57,21 +60,22 @@
57
60
  ],
58
61
  "sideEffects": true,
59
62
  "scripts": {
60
- "build": "svelte-package",
63
+ "build": "svelte-package && node scripts/strip-dts-css.mjs",
61
64
  "prepare": "npm run build",
62
65
  "prepack": "rm -rf dist && npm run build",
63
66
  "check:pack": "publint",
64
67
  "test": "vitest run",
65
68
  "test:watch": "vitest",
66
- "check": "svelte-check --tsconfig ./tsconfig.json"
69
+ "check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings"
67
70
  },
68
71
  "peerDependencies": {
69
- "@quillmark/wasm": ">=0.118.0-0",
70
- "svelte": "^5.0.0"
72
+ "@quillmark/wasm": ">=0.122.0-0",
73
+ "svelte": "^5.33.0"
71
74
  },
72
75
  "dependencies": {
73
76
  "@internationalized/date": "^3.12.2",
74
- "bits-ui": "^2.0.0",
77
+ "bits-ui": "^2.19.0",
78
+ "esm-env": "^1.2.2",
75
79
  "prosemirror-commands": "1.7.1",
76
80
  "prosemirror-gapcursor": "1.3.2",
77
81
  "prosemirror-history": "1.5.0",
@@ -84,15 +88,15 @@
84
88
  "prosemirror-view": "1.42.1"
85
89
  },
86
90
  "devDependencies": {
87
- "@sveltejs/package": "^2.5.0",
88
- "@sveltejs/vite-plugin-svelte": "^6.0.0",
89
- "@types/node": "^26.1.1",
90
- "jsdom": "^29.1.1",
91
- "publint": "^0.3.0",
92
- "svelte": "^5.0.0",
93
- "svelte-check": "^4.1.0",
94
- "typescript": "^5.7.0",
95
- "vitest": "^4.1.10"
91
+ "@sveltejs/package": "^3.0.0",
92
+ "@sveltejs/vite-plugin-svelte": "^7.3.0",
93
+ "@types/node": "^24.19.1",
94
+ "jsdom": "^30.1.0",
95
+ "publint": "^0.3.25",
96
+ "svelte": "^5.54.0",
97
+ "svelte-check": "^4.7.6",
98
+ "typescript": "^6.0.0",
99
+ "vitest": "^5.0.0"
96
100
  },
97
101
  "publishConfig": {
98
102
  "access": "public"
@@ -1,15 +0,0 @@
1
- /** A settable local synced to an external projection. Bind `.value` in the
2
- * template; assign `.value` on input. */
3
- export interface SyncedLocal<T> {
4
- value: T;
5
- }
6
- /**
7
- * Seed a local from `project` and reconcile only an external change into it.
8
- * `project` reads the reactive source (a prop) and maps it to the local's type:
9
- * ` => value ?? ''`, ` => (value != null ? String(value) : '')`, etc. The
10
- * seed is `untrack`ed (the ongoing sync is the `$effect`, not the initializer),
11
- * and each reconcile is `untrack`ed so an own-edit write never re-triggers it.
12
- * Reassignment through the `value` setter is a plain local write; only a changed
13
- * `project` result flows the other way.
14
- */
15
- export declare function syncedLocal<T>(project: () => T): SyncedLocal<T>;
@@ -1,34 +0,0 @@
1
- // The controlled-local reconcile shared by the scalar controls (TextField /
2
- // NumberField / DateField / BooleanField / EnumField). Each seeds a local from a
3
- // projection of its `value` prop and reconciles only an external change back into
4
- // the local: own edits stay `untrack`ed, so typing never re-runs the sync and
5
- // resets the caret. One copy for all five, rather than a character-identical block
6
- // in each (VISUAL_EDITOR §Surface).
7
- import { untrack } from 'svelte';
8
- /**
9
- * Seed a local from `project` and reconcile only an external change into it.
10
- * `project` reads the reactive source (a prop) and maps it to the local's type:
11
- * ` => value ?? ''`, ` => (value != null ? String(value) : '')`, etc. The
12
- * seed is `untrack`ed (the ongoing sync is the `$effect`, not the initializer),
13
- * and each reconcile is `untrack`ed so an own-edit write never re-triggers it.
14
- * Reassignment through the `value` setter is a plain local write; only a changed
15
- * `project` result flows the other way.
16
- */
17
- export function syncedLocal(project) {
18
- let local = $state(untrack(project));
19
- $effect(() => {
20
- const incoming = project();
21
- untrack(() => {
22
- if (incoming !== local)
23
- local = incoming;
24
- });
25
- });
26
- return {
27
- get value() {
28
- return local;
29
- },
30
- set value(v) {
31
- local = v;
32
- }
33
- };
34
- }