@quillmark/svelte 0.1.1 → 0.3.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 (124) hide show
  1. package/README.md +16 -12
  2. package/THEMING.md +48 -25
  3. package/dist/core/address.d.ts +42 -7
  4. package/dist/core/address.js +49 -21
  5. package/dist/core/bloom.d.ts +5 -5
  6. package/dist/core/bloom.js +8 -8
  7. package/dist/core/codec/anchor.d.ts +1 -1
  8. package/dist/core/codec/anchor.js +1 -1
  9. package/dist/core/codec/atoms.d.ts +10 -0
  10. package/dist/core/codec/atoms.js +78 -0
  11. package/dist/core/codec/clipboard.d.ts +59 -0
  12. package/dist/core/codec/clipboard.js +202 -0
  13. package/dist/core/codec/code.d.ts +1 -1
  14. package/dist/core/codec/code.js +4 -4
  15. package/dist/core/codec/decode.d.ts +1 -1
  16. package/dist/core/codec/decode.js +8 -8
  17. package/dist/core/codec/encode.d.ts +4 -4
  18. package/dist/core/codec/encode.js +14 -11
  19. package/dist/core/codec/field.d.ts +22 -27
  20. package/dist/core/codec/field.js +33 -32
  21. package/dist/core/codec/index.js +5 -8
  22. package/dist/core/codec/inputrules.js +56 -18
  23. package/dist/core/codec/islands.d.ts +11 -7
  24. package/dist/core/codec/islands.js +16 -10
  25. package/dist/core/codec/keymap.d.ts +5 -6
  26. package/dist/core/codec/keymap.js +8 -7
  27. package/dist/core/codec/lists.d.ts +3 -3
  28. package/dist/core/codec/lists.js +32 -13
  29. package/dist/core/codec/marks.d.ts +1 -1
  30. package/dist/core/codec/marks.js +1 -1
  31. package/dist/core/codec/positions.js +1 -1
  32. package/dist/core/codec/prose.css +266 -228
  33. package/dist/core/codec/schema.js +4 -4
  34. package/dist/core/codec/slash.d.ts +39 -41
  35. package/dist/core/codec/slash.js +0 -0
  36. package/dist/core/codec/table-view.d.ts +12 -16
  37. package/dist/core/codec/table-view.js +488 -292
  38. package/dist/core/codec/table.d.ts +42 -15
  39. package/dist/core/codec/table.js +73 -27
  40. package/dist/core/errors.d.ts +17 -7
  41. package/dist/core/index.d.ts +1 -1
  42. package/dist/core/index.js +6 -5
  43. package/dist/core/lifecycle.d.ts +21 -5
  44. package/dist/core/lifecycle.js +34 -8
  45. package/dist/core/rebind.svelte.d.ts +18 -4
  46. package/dist/core/rebind.svelte.js +30 -12
  47. package/dist/core/teardown.d.ts +1 -1
  48. package/dist/core/teardown.js +1 -1
  49. package/dist/core/theme.css +173 -58
  50. package/dist/preset/index.css +1 -1
  51. package/dist/preset/recipes.css +79 -30
  52. package/dist/preset/scale.css +40 -28
  53. package/dist/preview/Preview.svelte +22 -19
  54. package/dist/preview/Preview.svelte.d.ts +8 -8
  55. package/dist/preview/bridge.d.ts +6 -4
  56. package/dist/preview/bridge.js +19 -9
  57. package/dist/preview/controller.d.ts +13 -6
  58. package/dist/preview/controller.js +8 -5
  59. package/dist/preview/geometry.d.ts +17 -2
  60. package/dist/preview/geometry.js +21 -1
  61. package/dist/preview/index.d.ts +1 -0
  62. package/dist/preview/index.js +9 -2
  63. package/dist/preview/overlay.d.ts +2 -1
  64. package/dist/preview/overlay.js +26 -11
  65. package/dist/preview/paint.js +19 -17
  66. package/dist/visual/ArrayField.svelte +103 -83
  67. package/dist/visual/ArrayField.svelte.d.ts +5 -4
  68. package/dist/visual/BooleanField.svelte +8 -6
  69. package/dist/visual/BooleanField.svelte.d.ts +2 -2
  70. package/dist/visual/Card.svelte +161 -333
  71. package/dist/visual/Card.svelte.d.ts +5 -4
  72. package/dist/visual/CardControls.svelte +1 -1
  73. package/dist/visual/DateField.svelte +19 -19
  74. package/dist/visual/DateField.svelte.d.ts +3 -3
  75. package/dist/visual/DiagnosticList.svelte +2 -2
  76. package/dist/visual/EnumField.svelte +48 -28
  77. package/dist/visual/EnumField.svelte.d.ts +5 -4
  78. package/dist/visual/Field.svelte +207 -132
  79. package/dist/visual/Field.svelte.d.ts +11 -7
  80. package/dist/visual/FieldHint.svelte +79 -48
  81. package/dist/visual/FieldHint.svelte.d.ts +1 -1
  82. package/dist/visual/FieldLabel.svelte +8 -8
  83. package/dist/visual/FormatPopover.svelte +82 -35
  84. package/dist/visual/NumberField.svelte +5 -5
  85. package/dist/visual/NumberField.svelte.d.ts +1 -1
  86. package/dist/visual/ObjectField.svelte +28 -9
  87. package/dist/visual/ObjectField.svelte.d.ts +4 -2
  88. package/dist/visual/ProseArrayElement.svelte +12 -19
  89. package/dist/visual/ProseArrayElement.svelte.d.ts +1 -2
  90. package/dist/visual/ProseField.svelte +62 -85
  91. package/dist/visual/ProseField.svelte.d.ts +14 -17
  92. package/dist/visual/SlashMenu.svelte +42 -16
  93. package/dist/visual/TextField.svelte +5 -5
  94. package/dist/visual/TextField.svelte.d.ts +1 -1
  95. package/dist/visual/TipsCard.svelte +43 -28
  96. package/dist/visual/VisualEditor.svelte +18 -19
  97. package/dist/visual/VisualEditor.svelte.d.ts +2 -3
  98. package/dist/visual/VisualEditorInner.svelte +251 -184
  99. package/dist/visual/VisualEditorInner.svelte.d.ts +2 -3
  100. package/dist/visual/controls.css +85 -38
  101. package/dist/visual/diagnostics.d.ts +13 -21
  102. package/dist/visual/diagnostics.js +7 -19
  103. package/dist/visual/domid.js +3 -3
  104. package/dist/visual/ext.d.ts +1 -1
  105. package/dist/visual/ext.js +1 -1
  106. package/dist/visual/icons/nodes.d.ts +3 -0
  107. package/dist/visual/icons/nodes.js +2 -1
  108. package/dist/visual/index.d.ts +1 -0
  109. package/dist/visual/index.js +10 -3
  110. package/dist/visual/leaves.d.ts +47 -0
  111. package/dist/visual/leaves.js +31 -0
  112. package/dist/visual/links.d.ts +24 -0
  113. package/dist/visual/links.js +67 -0
  114. package/dist/visual/props.d.ts +33 -12
  115. package/dist/visual/signals.d.ts +6 -6
  116. package/dist/visual/strings.d.ts +14 -19
  117. package/dist/visual/strings.js +5 -7
  118. package/dist/visual/structure.d.ts +24 -17
  119. package/dist/visual/structure.js +8 -8
  120. package/dist/visual/synced.svelte.d.ts +1 -1
  121. package/dist/visual/synced.svelte.js +4 -4
  122. package/dist/visual/tips.d.ts +4 -2
  123. package/dist/visual/tips.js +7 -4
  124. package/package.json +2 -2
@@ -8,14 +8,18 @@ export declare const ALIGNS: readonly TableAlign[];
8
8
  /** The zero cell: empty text, no marks. */
9
9
  export declare function emptyCell(): TableCell;
10
10
  /**
11
- * The row index space the chrome and the ops share: **0 is the header**, 1…n the
12
- * body rows. A table always has a header (`header` is a separate field, and a
13
- * header-less table is not expressible), so the header is a row that never
14
- * deletes rather than a toggle.
11
+ * The row index space the chrome and the ops share: **0 is the header**, 1…n the body
12
+ * rows. The split is how a table is stored, not a rank the ops answer to: a row op
13
+ * works {@link allRows} and whichever row lands at index 0 is the header afterwards,
14
+ * so index 0 moves and deletes like any other. The one row the model refuses is the
15
+ * last, there being no table under it.
15
16
  */
16
17
  export declare function rowCount(props: TableProps): number;
17
18
  /** The cells of row `r` in that space. */
18
19
  export declare function rowCells(props: TableProps, r: number): TableCell[];
20
+ /** Whether a row holds no text, which is what tells a row on offer from one taken up
21
+ * (`table-view.ts` §`step`). Text alone: a mark over none is nothing a writer can see. */
22
+ export declare function rowEmpty(props: TableProps, r: number): boolean;
19
23
  /** The cell at `(row, column)`, or the zero cell where the rectangle does not reach. */
20
24
  export declare function cellAt(props: TableProps, r: number, c: number): TableCell;
21
25
  /** The table's column count: the one number `header`, every row and `aligns` share. */
@@ -27,6 +31,13 @@ export declare function columnCount(props: TableProps): number;
27
31
  * back, and a table is never narrower than one column.
28
32
  */
29
33
  export declare function normalizeTable(props: TableProps): TableProps;
34
+ /**
35
+ * The rows as one list, header first: the space a row op works in. `header` is a
36
+ * separate field because a table always has one and `header: []` is not a table — a
37
+ * storage shape, not a rank a row op has to know about. Reading it as one list is what
38
+ * lets move and delete treat every row alike, and {@link fromRows} splits it back.
39
+ */
40
+ export declare function allRows(props: TableProps): TableCell[][];
30
41
  /** A fresh table: a header plus `bodyRows` empty rows, every column unaligned.
31
42
  * Growth is cheap (Tab at the last cell appends a row), so the default is small. */
32
43
  export declare function newTable(cols?: number, bodyRows?: number): TableProps;
@@ -34,32 +45,48 @@ export declare function newTable(cols?: number, bodyRows?: number): TableProps;
34
45
  export declare function withCell(props: TableProps, r: number, c: number, cell: TableCell): TableProps;
35
46
  /** A new empty body row below row `r` (row 0, the header, opens the first body row). */
36
47
  export declare function insertRow(props: TableProps, r: number): TableProps;
37
- /** Drop body row `r`. The header (`r === 0`) has no delete: `header: []` is not a
38
- * table, so the asymmetry is the model's rather than a guard's. */
48
+ /** Drop row `r`, the header included: the row left at index 0 is the header, which is
49
+ * how a table whose first row went still has one. A no-op at the last row, that being
50
+ * the removal of the table rather than of a row, and the island's own to answer. */
39
51
  export declare function deleteRow(props: TableProps, r: number): TableProps;
40
52
  /** A new empty column after column `c`. */
41
53
  export declare function insertColumn(props: TableProps, c: number): TableProps;
42
54
  /** Drop column `c`; a no-op at one column (a table has at least one). */
43
55
  export declare function deleteColumn(props: TableProps, c: number): TableProps;
44
- /** Body row `r` moved by `by` places, clamped: a drag that leaves the rectangle is a
45
- * no-op rather than an error, since the drop target is geometry and geometry runs
46
- * past the last row. The header is refused because it has no destination: it is a
47
- * separate field, not row 0 of one array. */
56
+ /** Row `r` moved by `by` places, clamped: a drag that leaves the rectangle is a no-op
57
+ * rather than an error, since the drop target is geometry and geometry runs past the
58
+ * last row. The header travels like any other row, and so does the row that displaces
59
+ * it: being the header is holding index 0, which a move is free to hand over. */
48
60
  export declare function moveRow(props: TableProps, r: number, by: number): TableProps;
49
61
  /** Column `c` moved by `by` places, clamped. `aligns` travels with the column: an
50
62
  * alignment is the column's property, so a move that left it behind would retint the
51
63
  * column that took the index. */
52
64
  export declare function moveColumn(props: TableProps, c: number, by: number): TableProps;
53
- /** Column `c` with every cell emptied, alignment kept: what a delete gesture means at
54
- * the LAST column, which the model keeps. There is no row analogue: the header is the
55
- * only row a delete is refused for, and it carries no handle to select it from. */
56
- export declare function clearColumn(props: TableProps, c: number): TableProps;
65
+ /**
66
+ * Every cell in the inclusive rectangle emptied, alignment kept: what a delete gesture
67
+ * means over a block of cells that covers no whole rank. A rectangle spanning one axis
68
+ * covers ranks and deletes them, and one spanning both is the table itself, so what
69
+ * reaches here is a proper sub-rectangle and the shape it leaves is the shape it found.
70
+ *
71
+ * Alignment is the column's rather than the cells', so it survives a clear that spans one.
72
+ */
73
+ export declare function clearCells(props: TableProps, r0: number, c0: number, r1: number, c1: number): TableProps;
74
+ /**
75
+ * A rectangle written in at `(r, c)`, the table growing to hold whatever runs past its
76
+ * edges: what a paste into a cell means (`clipboard.ts`). The alternative is clipping
77
+ * to the rectangle that is there, which silently drops the tail of what the author
78
+ * copied; growth drops nothing and every added rank is one the ops already remove.
79
+ *
80
+ * `aligns` is the table's own throughout: a column keeps its alignment under a paste,
81
+ * and a column the paste added arrives `none` like any other fresh one.
82
+ */
83
+ export declare function pasteCells(props: TableProps, r: number, c: number, block: TableCell[][]): TableProps;
57
84
  /** Set column `c`'s alignment: the one table capability the content round-trips
58
85
  * today and nothing in the editor could reach. */
59
86
  export declare function setAlign(props: TableProps, c: number, align: TableAlign): TableProps;
60
87
  /**
61
88
  * A cell as a one-line `Content`: what `decode` takes under the inline schema. The
62
- * cell's marks ARE that content's marks, because both are offsets into the same
89
+ * cell's marks are that content's marks, because both are offsets into the same
63
90
  * text: the cell-local coordinate space is a `Content`'s coordinate space with
64
91
  * one line in it.
65
92
  */
@@ -2,9 +2,9 @@
2
2
  // doc) and the rectangularizing constructors every row and column op goes through
3
3
  // (CODEC §Islands, §"The table island").
4
4
  //
5
- // `TableProps` normalizes to ONE column count shared by `header`, every row, and
5
+ // `TableProps` normalizes to one column count shared by `header`, every row, and
6
6
  // `aligns`, so a ragged table is not a state the content can hold. The editor keeps
7
- // its optimistic PM and re-hydrates only on an EXTERNAL change (CODEC
7
+ // its optimistic PM and re-hydrates only on an external change (CODEC
8
8
  // §Reconciliation), so an op that emitted a ragged table would leave the store
9
9
  // rectangularized and PM ragged with no error channel and no repair. Every
10
10
  // constructor here therefore ends in {@link normalizeTable}, and a caller reaches
@@ -15,7 +15,8 @@
15
15
  // the codec already runs (one paragraph, no containers, no islands) is exactly its
16
16
  // shape, so a cell decodes and projects through the same machinery a
17
17
  // `richtext(inline)` field does.
18
- import { isAnchorMark, mapPos } from '@quillmark/wasm';
18
+ import { isAnchorMark } from '@quillmark/wasm';
19
+ import { core } from '../lifecycle.js';
19
20
  import { contentEdit, pmToContent } from './encode.js';
20
21
  /** The alignments a column can take: the four the content declares, `none` first
21
22
  * because it is what a column arrives at and what a set is cleared back to. */
@@ -25,10 +26,11 @@ export function emptyCell() {
25
26
  return { text: '', marks: [] };
26
27
  }
27
28
  /**
28
- * The row index space the chrome and the ops share: **0 is the header**, 1…n the
29
- * body rows. A table always has a header (`header` is a separate field, and a
30
- * header-less table is not expressible), so the header is a row that never
31
- * deletes rather than a toggle.
29
+ * The row index space the chrome and the ops share: **0 is the header**, 1…n the body
30
+ * rows. The split is how a table is stored, not a rank the ops answer to: a row op
31
+ * works {@link allRows} and whichever row lands at index 0 is the header afterwards,
32
+ * so index 0 moves and deletes like any other. The one row the model refuses is the
33
+ * last, there being no table under it.
32
34
  */
33
35
  export function rowCount(props) {
34
36
  return props.rows.length + 1;
@@ -37,6 +39,11 @@ export function rowCount(props) {
37
39
  export function rowCells(props, r) {
38
40
  return r === 0 ? props.header : (props.rows[r - 1] ?? []);
39
41
  }
42
+ /** Whether a row holds no text, which is what tells a row on offer from one taken up
43
+ * (`table-view.ts` §`step`). Text alone: a mark over none is nothing a writer can see. */
44
+ export function rowEmpty(props, r) {
45
+ return rowCells(props, r).every((cell) => cell.text === '');
46
+ }
40
47
  /** The cell at `(row, column)`, or the zero cell where the rectangle does not reach. */
41
48
  export function cellAt(props, r, c) {
42
49
  return rowCells(props, r)[c] ?? emptyCell();
@@ -60,6 +67,22 @@ export function normalizeTable(props) {
60
67
  aligns: Array.from({ length: cols }, (_, c) => props.aligns[c] ?? 'none')
61
68
  };
62
69
  }
70
+ /**
71
+ * The rows as one list, header first: the space a row op works in. `header` is a
72
+ * separate field because a table always has one and `header: []` is not a table — a
73
+ * storage shape, not a rank a row op has to know about. Reading it as one list is what
74
+ * lets move and delete treat every row alike, and {@link fromRows} splits it back.
75
+ */
76
+ export function allRows(props) {
77
+ return [props.header, ...props.rows];
78
+ }
79
+ /** A flat list back into the stored shape: whatever sits at index 0 is the header, by
80
+ * position rather than by having been one. No caller passes an empty list — every op
81
+ * that could reach one refuses the last row first. */
82
+ function fromRows(rows, aligns) {
83
+ const [header = [], ...rest] = rows;
84
+ return normalizeTable({ header, rows: rest, aligns });
85
+ }
63
86
  /** A fresh table: a header plus `bodyRows` empty rows, every column unaligned.
64
87
  * Growth is cheap (Tab at the last cell appends a row), so the default is small. */
65
88
  export function newTable(cols = 3, bodyRows = 2) {
@@ -87,12 +110,14 @@ export function insertRow(props, r) {
87
110
  rows: [...props.rows.slice(0, at), fresh, ...props.rows.slice(at)]
88
111
  });
89
112
  }
90
- /** Drop body row `r`. The header (`r === 0`) has no delete: `header: []` is not a
91
- * table, so the asymmetry is the model's rather than a guard's. */
113
+ /** Drop row `r`, the header included: the row left at index 0 is the header, which is
114
+ * how a table whose first row went still has one. A no-op at the last row, that being
115
+ * the removal of the table rather than of a row, and the island's own to answer. */
92
116
  export function deleteRow(props, r) {
93
- if (r === 0)
117
+ const rows = allRows(props);
118
+ if (rows.length <= 1 || r < 0 || r >= rows.length)
94
119
  return normalizeTable(props);
95
- return normalizeTable({ ...props, rows: props.rows.filter((_, i) => i !== r - 1) });
120
+ return fromRows(rows.filter((_, i) => i !== r), props.aligns);
96
121
  }
97
122
  /** A new empty column after column `c`. */
98
123
  export function insertColumn(props, c) {
@@ -123,16 +148,16 @@ function move(xs, from, to) {
123
148
  next.splice(to, 0, moved);
124
149
  return next;
125
150
  }
126
- /** Body row `r` moved by `by` places, clamped: a drag that leaves the rectangle is a
127
- * no-op rather than an error, since the drop target is geometry and geometry runs
128
- * past the last row. The header is refused because it has no destination: it is a
129
- * separate field, not row 0 of one array. */
151
+ /** Row `r` moved by `by` places, clamped: a drag that leaves the rectangle is a no-op
152
+ * rather than an error, since the drop target is geometry and geometry runs past the
153
+ * last row. The header travels like any other row, and so does the row that displaces
154
+ * it: being the header is holding index 0, which a move is free to hand over. */
130
155
  export function moveRow(props, r, by) {
131
- const from = r - 1;
132
- const to = Math.max(0, Math.min(from + by, props.rows.length - 1));
133
- if (r === 0 || from === to || from >= props.rows.length)
156
+ const rows = allRows(props);
157
+ const to = Math.max(0, Math.min(r + by, rows.length - 1));
158
+ if (r === to || r < 0 || r >= rows.length)
134
159
  return normalizeTable(props);
135
- return normalizeTable({ ...props, rows: move(props.rows, from, to) });
160
+ return fromRows(move(rows, r, to), props.aligns);
136
161
  }
137
162
  /** Column `c` moved by `by` places, clamped. `aligns` travels with the column: an
138
163
  * alignment is the column's property, so a move that left it behind would retint the
@@ -148,17 +173,37 @@ export function moveColumn(props, c, by) {
148
173
  aligns: shift(props.aligns)
149
174
  });
150
175
  }
151
- /** Column `c` with every cell emptied, alignment kept: what a delete gesture means at
152
- * the LAST column, which the model keeps. There is no row analogue: the header is the
153
- * only row a delete is refused for, and it carries no handle to select it from. */
154
- export function clearColumn(props, c) {
155
- const blank = (cells) => cells.map((cell, i) => (i === c ? emptyCell() : cell));
176
+ /**
177
+ * Every cell in the inclusive rectangle emptied, alignment kept: what a delete gesture
178
+ * means over a block of cells that covers no whole rank. A rectangle spanning one axis
179
+ * covers ranks and deletes them, and one spanning both is the table itself, so what
180
+ * reaches here is a proper sub-rectangle and the shape it leaves is the shape it found.
181
+ *
182
+ * Alignment is the column's rather than the cells', so it survives a clear that spans one.
183
+ */
184
+ export function clearCells(props, r0, c0, r1, c1) {
185
+ const blank = (cells, r) => cells.map((cell, c) => (r >= r0 && r <= r1 && c >= c0 && c <= c1 ? emptyCell() : cell));
156
186
  return normalizeTable({
157
- header: blank(props.header),
158
- rows: props.rows.map(blank),
187
+ header: blank(props.header, 0),
188
+ rows: props.rows.map((row, i) => blank(row, i + 1)),
159
189
  aligns: props.aligns
160
190
  });
161
191
  }
192
+ /**
193
+ * A rectangle written in at `(r, c)`, the table growing to hold whatever runs past its
194
+ * edges: what a paste into a cell means (`clipboard.ts`). The alternative is clipping
195
+ * to the rectangle that is there, which silently drops the tail of what the author
196
+ * copied; growth drops nothing and every added rank is one the ops already remove.
197
+ *
198
+ * `aligns` is the table's own throughout: a column keeps its alignment under a paste,
199
+ * and a column the paste added arrives `none` like any other fresh one.
200
+ */
201
+ export function pasteCells(props, r, c, block) {
202
+ const rows = allRows(props);
203
+ const height = Math.max(rows.length, r + block.length);
204
+ const width = Math.max(columnCount(props), ...block.map((row) => c + row.length));
205
+ return fromRows(Array.from({ length: height }, (_, i) => Array.from({ length: width }, (_, j) => block[i - r]?.[j - c] ?? rows[i]?.[j] ?? emptyCell())), props.aligns);
206
+ }
162
207
  /** Set column `c`'s alignment: the one table capability the content round-trips
163
208
  * today and nothing in the editor could reach. */
164
209
  export function setAlign(props, c, align) {
@@ -170,7 +215,7 @@ export function setAlign(props, c, align) {
170
215
  // ── The cell codec ──────────────────────────────────────────────────────────
171
216
  /**
172
217
  * A cell as a one-line `Content`: what `decode` takes under the inline schema. The
173
- * cell's marks ARE that content's marks, because both are offsets into the same
218
+ * cell's marks are that content's marks, because both are offsets into the same
174
219
  * text: the cell-local coordinate space is a `Content`'s coordinate space with
175
220
  * one line in it.
176
221
  */
@@ -201,6 +246,7 @@ export function cellFromDoc(doc, prior) {
201
246
  if (!anchors.length)
202
247
  return { text, marks: projected.marks };
203
248
  const delta = contentEdit(cellContent(prior), cellContent({ text, marks: [] })).delta;
249
+ const { mapPos } = core();
204
250
  const rebased = anchors.map((a) => {
205
251
  const pos = delta ? mapPos(delta, a.start, 'after') : a.start;
206
252
  return { ...a, start: pos, end: pos };
@@ -11,7 +11,7 @@ export type EditorErrorCode =
11
11
  * the whole projection instead, so the store is correct and this field's
12
12
  * identity anchors were paid. */
13
13
  | 'commit-fallback'
14
- /** The overwrite fallback ALSO failed: the optimistic PM state stands and the
14
+ /** The overwrite fallback also failed: the optimistic PM state stands and the
15
15
  * store is stale for this field. The one code here that leaves damage. */
16
16
  | 'commit-lost'
17
17
  /** A card operation (add, move, remove, retype) that threw. The document is
@@ -26,18 +26,28 @@ export type EditorErrorCode =
26
26
  /** A tip's markdown did not render: the tip shows as literal text. */
27
27
  | 'tip-render-failed'
28
28
  /**
29
- * A prop a surface binds ONCE was swapped in place, and the surface neither
29
+ * A prop a surface binds once was swapped in place, and the surface neither
30
30
  * re-keyed on it nor observed it. The mounted surface still names the previous
31
31
  * handle. `dev` throughout: the fix is a remount at the call site, so this is
32
32
  * aimed at whoever wired it and never at a running app's telemetry.
33
+ *
34
+ * The one code that is also thrown, in a dev build, once reported: a swapped
35
+ * `onError` is itself a rebind, so the handler this arrives at may be the one the
36
+ * consumer just replaced (`rebind.svelte.ts`).
33
37
  */
34
38
  | 'rebind-ignored'
35
39
  /**
36
- * An instance verb named a card or a field the surface does not hold: a `cardId`
37
- * from a previous session or an already-removed card, a `DocPath` naming no
38
- * mounted leaf. The call is a no-op and the document is untouched. `dev`: the
39
- * editor's own chrome cannot mint a bad target, so this only ever reports a host
40
- * driving the verbs from outside.
40
+ * A landing named a card or a field the surface does not hold: a `cardId` from a
41
+ * previous session or an already-removed card, or a `DocPath` naming no declared
42
+ * field — an array element path included, where the array itself is undeclared or
43
+ * unmounted. The call is a no-op and the document is untouched.
44
+ *
45
+ * `dev`, because the fix is at the call site: a host driving the instance verbs
46
+ * from outside, or a preview pick the editor has no field to route to. Every
47
+ * mounted field is a target, prose leaf and form control alike, and an array's
48
+ * elements are targets through it, so this is not how a scalar or an element
49
+ * reports. The preview never reports it at all: a field it places nothing for is
50
+ * `scrollToField` returning `false`.
41
51
  */
42
52
  | 'target-unknown';
43
53
  /** A failure a surface recovered from. */
@@ -1,5 +1,5 @@
1
1
  import './theme.css';
2
2
  export { init } from './lifecycle.js';
3
- export type { DocPath, Place } from './address.js';
3
+ export type { DocPath, Landing, Place } from './address.js';
4
4
  export { fieldPathForAddr, addrForFieldPath } from './address.js';
5
5
  export type { EditorError, EditorErrorCode, EditorErrorHandler } from './errors.js';
@@ -1,10 +1,11 @@
1
1
  // The theme derivation: the public dials → the private `--_qm-*` scale, applied
2
- // to every `data-qm-root` element. Imported HERE because `core/` is the one module
3
- // both `preview/` and `visual/` already pull, so a consumer cannot forget it and
4
- // the derivation is minted once rather than re-declared per detached root
5
- // (THEMING.md).
2
+ // to every `data-qm-root` element (THEMING.md). Imported at every barrel a
3
+ // consumer can enter through — here, `/visual`, `/preview` — because a subpath is
4
+ // what a consumer gets, and a surface reaching a module inside `core/` reaches
5
+ // nothing this file imports. One sheet however many barrels name it: the
6
+ // derivation is minted once rather than re-declared per detached root.
6
7
  import './theme.css';
7
- // `@quillmark/svelte/core`: what this package OWNS at the substrate seam.
8
+ // `@quillmark/svelte/core`: what this package owns at the substrate seam.
8
9
  //
9
10
  // The `@quillmark/wasm` API is not re-exported: import it straight from the peer
10
11
  // dependency, the single source of truth (quiver states the same rule). The
@@ -1,7 +1,23 @@
1
+ import { type CoreSurface } from '@quillmark/wasm';
1
2
  /**
2
- * Instantiate the core WASM build. Await once before any other boundary verb,
3
- * from any surface's setup path; the promise is memoized, so concurrent callers
4
- * share one instantiation and repeat calls are free. A failed init clears the
5
- * memo, leaving a retry possible.
3
+ * Instantiate the core WASM build and resolve to its surface. Await once before
4
+ * any other boundary verb, from any surface's setup path; the promise is memoized,
5
+ * so concurrent callers share one instantiation and repeat calls are free. A failed
6
+ * init clears the memo, leaving a retry possible.
7
+ *
8
+ * The surface is the artifact's own, handed back unchanged: `const { Quill, Document }
9
+ * = await init()` is the whole of a host's access to the classes, and this package
10
+ * re-declares none of it.
6
11
  */
7
- export declare function init(): Promise<void>;
12
+ export declare function init(): Promise<CoreSurface>;
13
+ /**
14
+ * The surface synchronously, for this package's pure verbs: the `DocPath` formatter
15
+ * on the public API and the codec inside a ProseMirror transaction, neither of which
16
+ * can await. Package-internal, and read at the call rather than destructured at
17
+ * module scope, so no module's import graph carries an ordering constraint.
18
+ *
19
+ * Reaching one before {@link init} resolves throws, naming the fix. This is the seam
20
+ * that turns an awaited surface back into sync verbs, so it is the one place the
21
+ * precondition the gate made structural has to be stated again.
22
+ */
23
+ export declare function core(): CoreSurface;
@@ -1,21 +1,47 @@
1
1
  // `@quillmark/wasm` ships wasm-bindgen's web target: the core build is
2
- // instantiated by an explicit `await init()`, and every export throws
3
- // `runtime::not_initialized` until it resolves. Instantiation carries the panic
4
- // hook on its start section, so a Rust panic reaches JS as a stack trace.
2
+ // instantiated by an explicit `await init()`, which resolves to the classes and
3
+ // free functions — the artifact exports none of them statically, so awaiting is
4
+ // the only way to hold one. Instantiation carries the panic hook on its start
5
+ // section, so a Rust panic reaches JS as a stack trace.
5
6
  import { init as initWasm } from '@quillmark/wasm';
6
7
  let started;
8
+ /** The resolved surface, for the sync verbs below that cannot await one. */
9
+ let latched;
7
10
  /**
8
- * Instantiate the core WASM build. Await once before any other boundary verb,
9
- * from any surface's setup path; the promise is memoized, so concurrent callers
10
- * share one instantiation and repeat calls are free. A failed init clears the
11
- * memo, leaving a retry possible.
11
+ * Instantiate the core WASM build and resolve to its surface. Await once before
12
+ * any other boundary verb, from any surface's setup path; the promise is memoized,
13
+ * so concurrent callers share one instantiation and repeat calls are free. A failed
14
+ * init clears the memo, leaving a retry possible.
15
+ *
16
+ * The surface is the artifact's own, handed back unchanged: `const { Quill, Document }
17
+ * = await init()` is the whole of a host's access to the classes, and this package
18
+ * re-declares none of it.
12
19
  */
13
20
  export function init() {
14
21
  if (started)
15
22
  return started;
16
- started = initWasm().catch((err) => {
23
+ started = initWasm().then((surface) => {
24
+ latched = surface;
25
+ return surface;
26
+ }, (err) => {
17
27
  started = undefined;
18
28
  throw err;
19
29
  });
20
30
  return started;
21
31
  }
32
+ /**
33
+ * The surface synchronously, for this package's pure verbs: the `DocPath` formatter
34
+ * on the public API and the codec inside a ProseMirror transaction, neither of which
35
+ * can await. Package-internal, and read at the call rather than destructured at
36
+ * module scope, so no module's import graph carries an ordering constraint.
37
+ *
38
+ * Reaching one before {@link init} resolves throws, naming the fix. This is the seam
39
+ * that turns an awaited surface back into sync verbs, so it is the one place the
40
+ * precondition the gate made structural has to be stated again.
41
+ */
42
+ export function core() {
43
+ if (!latched) {
44
+ throw new Error('@quillmark/svelte: the WASM core is not initialized. Await `init()` from `@quillmark/svelte/core` before mounting a surface or calling a boundary verb.');
45
+ }
46
+ return latched;
47
+ }
@@ -3,15 +3,29 @@ import { type EditorErrorHandler } from './errors.js';
3
3
  export type Bound = Record<string, unknown> & {
4
4
  onError?: EditorErrorHandler;
5
5
  };
6
+ /**
7
+ * Report the violation, then throw it in a dev build. Both surfaces raise it here; the
8
+ * guards themselves stay separate, since the preview snapshots its props once and the
9
+ * editor re-reads its pair on every re-key.
10
+ *
11
+ * The report alone reaches whoever bound `onError` at mount, and a consumer swapping
12
+ * that handler beside the handle it belongs to is the case this exists for: they are
13
+ * told through the sink they just abandoned. A throw needs no handler to arrive.
14
+ *
15
+ * It costs the surface nothing: an effect that throws neither unmounts its tree nor
16
+ * stops the ones beside it, so the surface goes on serving what it bound, which is the
17
+ * very state being reported.
18
+ */
19
+ export declare function reportRebindIgnored(onError: EditorErrorHandler | undefined, message: string): void;
6
20
  /**
7
21
  * Report `rebind-ignored` once, at `dev`, when any prop `snapshot` names is swapped
8
- * in place. `remount` closes the message with the surface's own remount spelling
9
- * (`Remount the preview ({#key session}) to rebind.`).
22
+ * in place, and in a dev build throw it after. `remount` closes the message with the
23
+ * surface's own remount spelling (`Remount the preview ({#key session}) to rebind.`).
10
24
  *
11
- * Mounted identities are recorded on the effect's FIRST run, the flush the surface
25
+ * Mounted identities are recorded on the effect's first run, the flush the surface
12
26
  * binds in; every later comparison reads them through the same `snapshot`.
13
27
  *
14
- * The report goes to the SNAPSHOT's `onError`: every other error these surfaces
28
+ * The report goes to the snapshot's `onError`: every other error these surfaces
15
29
  * report reaches the handler they bound, and a swapped `onError` is itself one of
16
30
  * the reports.
17
31
  */
@@ -1,21 +1,43 @@
1
- // The remount contract, enforced. `Preview` binds its props ONCE, in `onMount`, and
1
+ // The remount contract, enforced. `Preview` binds its props once, in `onMount`, and
2
2
  // observes nothing after: the vanilla core below it owns scroll position, mounted
3
3
  // slots and observer sets that a remount would discard on every apply. So a prop
4
- // swapped in place is reported, not dropped in silence.
4
+ // swapped in place is reported, not dropped in silence, and thrown in a dev build.
5
5
  //
6
- // A guard over ONE prop of a set is worse than none: a consumer who learns the
6
+ // A guard over one prop of a set is worse than none: a consumer who learns the
7
7
  // surface complains when it ignores a prop reads silence on the rest as reactivity.
8
8
  // The snapshot is every once-bound prop, and one report names whichever went stale.
9
9
  import { reportError } from './errors.js';
10
+ // True in a dev build whose bundler injects `import.meta.env`. Cast, since the package
11
+ // declares no bundler's ambient types; false where nothing injects it, so a toolchain
12
+ // that does not answer gets the report alone.
13
+ const DEV = import.meta.env?.DEV === true;
14
+ /**
15
+ * Report the violation, then throw it in a dev build. Both surfaces raise it here; the
16
+ * guards themselves stay separate, since the preview snapshots its props once and the
17
+ * editor re-reads its pair on every re-key.
18
+ *
19
+ * The report alone reaches whoever bound `onError` at mount, and a consumer swapping
20
+ * that handler beside the handle it belongs to is the case this exists for: they are
21
+ * told through the sink they just abandoned. A throw needs no handler to arrive.
22
+ *
23
+ * It costs the surface nothing: an effect that throws neither unmounts its tree nor
24
+ * stops the ones beside it, so the surface goes on serving what it bound, which is the
25
+ * very state being reported.
26
+ */
27
+ export function reportRebindIgnored(onError, message) {
28
+ reportError(onError, { code: 'rebind-ignored', severity: 'dev', message });
29
+ if (DEV)
30
+ throw new Error(`[quillmark/ui] rebind-ignored: ${message}`);
31
+ }
10
32
  /**
11
33
  * Report `rebind-ignored` once, at `dev`, when any prop `snapshot` names is swapped
12
- * in place. `remount` closes the message with the surface's own remount spelling
13
- * (`Remount the preview ({#key session}) to rebind.`).
34
+ * in place, and in a dev build throw it after. `remount` closes the message with the
35
+ * surface's own remount spelling (`Remount the preview ({#key session}) to rebind.`).
14
36
  *
15
- * Mounted identities are recorded on the effect's FIRST run, the flush the surface
37
+ * Mounted identities are recorded on the effect's first run, the flush the surface
16
38
  * binds in; every later comparison reads them through the same `snapshot`.
17
39
  *
18
- * The report goes to the SNAPSHOT's `onError`: every other error these surfaces
40
+ * The report goes to the snapshot's `onError`: every other error these surfaces
19
41
  * report reaches the handler they bound, and a swapped `onError` is itself one of
20
42
  * the reports.
21
43
  */
@@ -35,10 +57,6 @@ export function guardRebind(snapshot, remount) {
35
57
  if (stale.length === 0)
36
58
  return;
37
59
  reported = true;
38
- reportError(bound.onError, {
39
- code: 'rebind-ignored',
40
- severity: 'dev',
41
- message: `${stale.join(', ')} swapped in place; the mounted surface still holds what it bound at mount. ${remount}`
42
- });
60
+ reportRebindIgnored(bound.onError, `${stale.join(', ')} swapped in place; the mounted surface still holds what it bound at mount. ${remount}`);
43
61
  });
44
62
  }
@@ -4,7 +4,7 @@ export interface Lifespan {
4
4
  readonly alive: boolean;
5
5
  /**
6
6
  * Register work to undo at the end: a registry entry to drop, a timer or frame
7
- * to cancel, a handle to free. Cancellers run in REGISTRATION order, which is
7
+ * to cancel, a handle to free. Cancellers run in registration order, which is
8
8
  * where the order lives. Registering on an ended span runs the canceller at
9
9
  * once: there is no later run to hold it for.
10
10
  */
@@ -26,7 +26,7 @@ export function createLifespan() {
26
26
  end() {
27
27
  if (!alive)
28
28
  return;
29
- // Dead BEFORE the cancellers run: one of them may synchronously reach
29
+ // Dead before the cancellers run: one of them may synchronously reach
30
30
  // something that asks, and a span mid-teardown is not alive.
31
31
  alive = false;
32
32
  for (const cancel of cancels)