@quillmark/svelte 0.0.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 (143) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +58 -0
  3. package/README.md +241 -0
  4. package/THEMING.md +153 -0
  5. package/dist/core/address.d.ts +56 -0
  6. package/dist/core/address.js +95 -0
  7. package/dist/core/bloom.d.ts +37 -0
  8. package/dist/core/bloom.js +116 -0
  9. package/dist/core/codec/anchor.d.ts +19 -0
  10. package/dist/core/codec/anchor.js +36 -0
  11. package/dist/core/codec/code.d.ts +12 -0
  12. package/dist/core/codec/code.js +118 -0
  13. package/dist/core/codec/decode.d.ts +24 -0
  14. package/dist/core/codec/decode.js +270 -0
  15. package/dist/core/codec/encode.d.ts +71 -0
  16. package/dist/core/codec/encode.js +480 -0
  17. package/dist/core/codec/field.d.ts +171 -0
  18. package/dist/core/codec/field.js +493 -0
  19. package/dist/core/codec/index.d.ts +22 -0
  20. package/dist/core/codec/index.js +43 -0
  21. package/dist/core/codec/inputrules.d.ts +7 -0
  22. package/dist/core/codec/inputrules.js +168 -0
  23. package/dist/core/codec/islands.d.ts +36 -0
  24. package/dist/core/codec/islands.js +82 -0
  25. package/dist/core/codec/keymap.d.ts +13 -0
  26. package/dist/core/codec/keymap.js +29 -0
  27. package/dist/core/codec/lists.d.ts +22 -0
  28. package/dist/core/codec/lists.js +104 -0
  29. package/dist/core/codec/marks.d.ts +37 -0
  30. package/dist/core/codec/marks.js +74 -0
  31. package/dist/core/codec/positions.d.ts +17 -0
  32. package/dist/core/codec/positions.js +108 -0
  33. package/dist/core/codec/prose.css +458 -0
  34. package/dist/core/codec/reconcile.d.ts +26 -0
  35. package/dist/core/codec/reconcile.js +64 -0
  36. package/dist/core/codec/schema.d.ts +13 -0
  37. package/dist/core/codec/schema.js +155 -0
  38. package/dist/core/codec/slash.d.ts +79 -0
  39. package/dist/core/codec/slash.js +0 -0
  40. package/dist/core/codec/table-view.d.ts +50 -0
  41. package/dist/core/codec/table-view.js +928 -0
  42. package/dist/core/codec/table.d.ts +85 -0
  43. package/dist/core/codec/table.js +224 -0
  44. package/dist/core/errors.d.ts +73 -0
  45. package/dist/core/errors.js +24 -0
  46. package/dist/core/index.d.ts +5 -0
  47. package/dist/core/index.js +19 -0
  48. package/dist/core/lifecycle.d.ts +7 -0
  49. package/dist/core/lifecycle.js +21 -0
  50. package/dist/core/rebind.svelte.d.ts +18 -0
  51. package/dist/core/rebind.svelte.js +44 -0
  52. package/dist/core/teardown.d.ts +20 -0
  53. package/dist/core/teardown.js +37 -0
  54. package/dist/core/theme.css +276 -0
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.js +9 -0
  57. package/dist/preset/index.css +10 -0
  58. package/dist/preset/recipes.css +188 -0
  59. package/dist/preset/scale.css +97 -0
  60. package/dist/preview/Preview.svelte +118 -0
  61. package/dist/preview/Preview.svelte.d.ts +39 -0
  62. package/dist/preview/bridge.d.ts +13 -0
  63. package/dist/preview/bridge.js +88 -0
  64. package/dist/preview/controller.d.ts +40 -0
  65. package/dist/preview/controller.js +141 -0
  66. package/dist/preview/geometry.d.ts +37 -0
  67. package/dist/preview/geometry.js +45 -0
  68. package/dist/preview/index.d.ts +5 -0
  69. package/dist/preview/index.js +14 -0
  70. package/dist/preview/overlay.d.ts +10 -0
  71. package/dist/preview/overlay.js +82 -0
  72. package/dist/preview/paint.d.ts +17 -0
  73. package/dist/preview/paint.js +262 -0
  74. package/dist/preview/strings.d.ts +14 -0
  75. package/dist/preview/strings.js +22 -0
  76. package/dist/visual/ArrayField.svelte +362 -0
  77. package/dist/visual/ArrayField.svelte.d.ts +24 -0
  78. package/dist/visual/BooleanField.svelte +88 -0
  79. package/dist/visual/BooleanField.svelte.d.ts +20 -0
  80. package/dist/visual/Card.svelte +849 -0
  81. package/dist/visual/Card.svelte.d.ts +43 -0
  82. package/dist/visual/CardControls.svelte +92 -0
  83. package/dist/visual/CardControls.svelte.d.ts +11 -0
  84. package/dist/visual/DateField.svelte +203 -0
  85. package/dist/visual/DateField.svelte.d.ts +28 -0
  86. package/dist/visual/DiagnosticList.svelte +50 -0
  87. package/dist/visual/DiagnosticList.svelte.d.ts +7 -0
  88. package/dist/visual/EnumField.svelte +169 -0
  89. package/dist/visual/EnumField.svelte.d.ts +25 -0
  90. package/dist/visual/Field.svelte +270 -0
  91. package/dist/visual/Field.svelte.d.ts +37 -0
  92. package/dist/visual/FieldHint.svelte +185 -0
  93. package/dist/visual/FieldHint.svelte.d.ts +13 -0
  94. package/dist/visual/FieldLabel.svelte +141 -0
  95. package/dist/visual/FieldLabel.svelte.d.ts +19 -0
  96. package/dist/visual/FormatPopover.svelte +396 -0
  97. package/dist/visual/FormatPopover.svelte.d.ts +9 -0
  98. package/dist/visual/NumberField.svelte +72 -0
  99. package/dist/visual/NumberField.svelte.d.ts +19 -0
  100. package/dist/visual/ObjectField.svelte +131 -0
  101. package/dist/visual/ObjectField.svelte.d.ts +17 -0
  102. package/dist/visual/ProseArrayElement.svelte +98 -0
  103. package/dist/visual/ProseArrayElement.svelte.d.ts +19 -0
  104. package/dist/visual/ProseField.svelte +213 -0
  105. package/dist/visual/ProseField.svelte.d.ts +50 -0
  106. package/dist/visual/SlashMenu.svelte +126 -0
  107. package/dist/visual/SlashMenu.svelte.d.ts +14 -0
  108. package/dist/visual/TextField.svelte +63 -0
  109. package/dist/visual/TextField.svelte.d.ts +23 -0
  110. package/dist/visual/TipsCard.svelte +191 -0
  111. package/dist/visual/TipsCard.svelte.d.ts +13 -0
  112. package/dist/visual/VisualEditor.svelte +96 -0
  113. package/dist/visual/VisualEditor.svelte.d.ts +14 -0
  114. package/dist/visual/VisualEditorInner.svelte +1032 -0
  115. package/dist/visual/VisualEditorInner.svelte.d.ts +17 -0
  116. package/dist/visual/controls.css +272 -0
  117. package/dist/visual/diagnostics.d.ts +52 -0
  118. package/dist/visual/diagnostics.js +83 -0
  119. package/dist/visual/domid.d.ts +21 -0
  120. package/dist/visual/domid.js +36 -0
  121. package/dist/visual/ext.d.ts +19 -0
  122. package/dist/visual/ext.js +29 -0
  123. package/dist/visual/icons/Icon.svelte +39 -0
  124. package/dist/visual/icons/Icon.svelte.d.ts +12 -0
  125. package/dist/visual/icons/nodes.d.ts +103 -0
  126. package/dist/visual/icons/nodes.js +55 -0
  127. package/dist/visual/index.d.ts +9 -0
  128. package/dist/visual/index.js +24 -0
  129. package/dist/visual/motion.d.ts +18 -0
  130. package/dist/visual/motion.js +53 -0
  131. package/dist/visual/props.d.ts +77 -0
  132. package/dist/visual/props.js +1 -0
  133. package/dist/visual/signals.d.ts +59 -0
  134. package/dist/visual/signals.js +1 -0
  135. package/dist/visual/strings.d.ts +123 -0
  136. package/dist/visual/strings.js +80 -0
  137. package/dist/visual/structure.d.ts +224 -0
  138. package/dist/visual/structure.js +270 -0
  139. package/dist/visual/synced.svelte.d.ts +15 -0
  140. package/dist/visual/synced.svelte.js +34 -0
  141. package/dist/visual/tips.d.ts +24 -0
  142. package/dist/visual/tips.js +50 -0
  143. package/package.json +102 -0
@@ -0,0 +1,85 @@
1
+ import type { Content, TableCell, TableProps } from '@quillmark/wasm';
2
+ import type { Node as PMNode } from 'prosemirror-model';
3
+ /** One column's alignment, read off the boundary rather than restated. */
4
+ export type TableAlign = TableProps['aligns'][number];
5
+ /** The alignments a column can take: the four the content declares, `none` first
6
+ * because it is what a column arrives at and what a set is cleared back to. */
7
+ export declare const ALIGNS: readonly TableAlign[];
8
+ /** The zero cell: empty text, no marks. */
9
+ export declare function emptyCell(): TableCell;
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.
15
+ */
16
+ export declare function rowCount(props: TableProps): number;
17
+ /** The cells of row `r` in that space. */
18
+ export declare function rowCells(props: TableProps, r: number): TableCell[];
19
+ /** The cell at `(row, column)`, or the zero cell where the rectangle does not reach. */
20
+ export declare function cellAt(props: TableProps, r: number, c: number): TableCell;
21
+ /** The table's column count: the one number `header`, every row and `aligns` share. */
22
+ export declare function columnCount(props: TableProps): number;
23
+ /**
24
+ * The rectangularizing constructor: one column count across `header`, every row and
25
+ * `aligns`, padding with empty cells and `none`. The column count is the widest
26
+ * thing present, so a caller that appended to one axis alone still gets a rectangle
27
+ * back, and a table is never narrower than one column.
28
+ */
29
+ export declare function normalizeTable(props: TableProps): TableProps;
30
+ /** A fresh table: a header plus `bodyRows` empty rows, every column unaligned.
31
+ * Growth is cheap (Tab at the last cell appends a row), so the default is small. */
32
+ export declare function newTable(cols?: number, bodyRows?: number): TableProps;
33
+ /** `props` with the cell at `(r, c)` replaced. */
34
+ export declare function withCell(props: TableProps, r: number, c: number, cell: TableCell): TableProps;
35
+ /** A new empty body row below row `r` (row 0, the header, opens the first body row). */
36
+ 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. */
39
+ export declare function deleteRow(props: TableProps, r: number): TableProps;
40
+ /** A new empty column after column `c`. */
41
+ export declare function insertColumn(props: TableProps, c: number): TableProps;
42
+ /** Drop column `c`; a no-op at one column (a table has at least one). */
43
+ 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. */
48
+ export declare function moveRow(props: TableProps, r: number, by: number): TableProps;
49
+ /** Column `c` moved by `by` places, clamped. `aligns` travels with the column: an
50
+ * alignment is the column's property, so a move that left it behind would retint the
51
+ * column that took the index. */
52
+ 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;
57
+ /** Set column `c`'s alignment: the one table capability the content round-trips
58
+ * today and nothing in the editor could reach. */
59
+ export declare function setAlign(props: TableProps, c: number, align: TableAlign): TableProps;
60
+ /**
61
+ * 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
63
+ * text: the cell-local coordinate space is a `Content`'s coordinate space with
64
+ * one line in it.
65
+ */
66
+ export declare function cellContent(cell: TableCell): Content;
67
+ /**
68
+ * The cell a nested PM doc projects, carrying `prior`'s identity anchors through.
69
+ *
70
+ * Anchors inside a cell are **preserved verbatim, never minted**: they are not in
71
+ * the field plugin's coordinate space (the field's position map holds one `atom`
72
+ * run for the whole table), so nothing here can mint one and the popover withholds
73
+ * its `anchor` button while the caret is in a cell. Preserving them still means
74
+ * rebasing: a cell edit is a splice like any other, so each held anchor maps through
75
+ * the cell's own text delta by the rule `applyChange` uses on a field's
76
+ * (start-assoc `after`), and a mark the splice deleted through lands where the
77
+ * splice left it rather than pointing past the text.
78
+ */
79
+ export declare function cellFromDoc(doc: PMNode, prior: TableCell): TableCell;
80
+ /** Whether two cells are the same value: what tells an own edit (the projection the
81
+ * view just produced) from an external one (an undo, a re-hydrate) without a flag. */
82
+ export declare function cellEqual(a: TableCell, b: TableCell): boolean;
83
+ /** Whether two tables have the same rectangle and alignment: the change that forces
84
+ * a rebuild rather than a per-cell reseed. */
85
+ export declare function shapeEqual(a: TableProps, b: TableProps): boolean;
@@ -0,0 +1,224 @@
1
+ // The table island's model: the cell codec (a `TableCell` ↔ one inline-schema PM
2
+ // doc) and the rectangularizing constructors every row and column op goes through
3
+ // (CODEC §Islands, §"The table island").
4
+ //
5
+ // `TableProps` normalizes to ONE column count shared by `header`, every row, and
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
8
+ // §Reconciliation), so an op that emitted a ragged table would leave the store
9
+ // rectangularized and PM ragged with no error channel and no repair. Every
10
+ // constructor here therefore ends in {@link normalizeTable}, and a caller reaches
11
+ // the shape only through them.
12
+ //
13
+ // A cell is its own content unit: `marks` are USV offsets into that cell's `text`,
14
+ // not into `Content.text`. That is the second coordinate space, and the inline mode
15
+ // the codec already runs (one paragraph, no containers, no islands) is exactly its
16
+ // shape, so a cell decodes and projects through the same machinery a
17
+ // `richtext(inline)` field does.
18
+ import { isAnchorMark, mapPos } from '@quillmark/wasm';
19
+ import { contentEdit, pmToContent } from './encode.js';
20
+ /** The alignments a column can take: the four the content declares, `none` first
21
+ * because it is what a column arrives at and what a set is cleared back to. */
22
+ export const ALIGNS = ['none', 'left', 'center', 'right'];
23
+ /** The zero cell: empty text, no marks. */
24
+ export function emptyCell() {
25
+ return { text: '', marks: [] };
26
+ }
27
+ /**
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.
32
+ */
33
+ export function rowCount(props) {
34
+ return props.rows.length + 1;
35
+ }
36
+ /** The cells of row `r` in that space. */
37
+ export function rowCells(props, r) {
38
+ return r === 0 ? props.header : (props.rows[r - 1] ?? []);
39
+ }
40
+ /** The cell at `(row, column)`, or the zero cell where the rectangle does not reach. */
41
+ export function cellAt(props, r, c) {
42
+ return rowCells(props, r)[c] ?? emptyCell();
43
+ }
44
+ /** The table's column count: the one number `header`, every row and `aligns` share. */
45
+ export function columnCount(props) {
46
+ return props.aligns.length;
47
+ }
48
+ /**
49
+ * The rectangularizing constructor: one column count across `header`, every row and
50
+ * `aligns`, padding with empty cells and `none`. The column count is the widest
51
+ * thing present, so a caller that appended to one axis alone still gets a rectangle
52
+ * back, and a table is never narrower than one column.
53
+ */
54
+ export function normalizeTable(props) {
55
+ const cols = Math.max(1, props.header.length, props.aligns.length, ...props.rows.map((row) => row.length));
56
+ const fit = (cells) => Array.from({ length: cols }, (_, c) => cells[c] ?? emptyCell());
57
+ return {
58
+ header: fit(props.header),
59
+ rows: props.rows.map(fit),
60
+ aligns: Array.from({ length: cols }, (_, c) => props.aligns[c] ?? 'none')
61
+ };
62
+ }
63
+ /** A fresh table: a header plus `bodyRows` empty rows, every column unaligned.
64
+ * Growth is cheap (Tab at the last cell appends a row), so the default is small. */
65
+ export function newTable(cols = 3, bodyRows = 2) {
66
+ return normalizeTable({
67
+ header: Array.from({ length: cols }, emptyCell),
68
+ rows: Array.from({ length: bodyRows }, () => Array.from({ length: cols }, emptyCell)),
69
+ aligns: Array.from({ length: cols }, () => 'none')
70
+ });
71
+ }
72
+ /** `props` with the cell at `(r, c)` replaced. */
73
+ export function withCell(props, r, c, cell) {
74
+ const replace = (cells) => cells.map((old, i) => (i === c ? cell : old));
75
+ return normalizeTable({
76
+ header: r === 0 ? replace(props.header) : props.header,
77
+ rows: props.rows.map((row, i) => (i === r - 1 ? replace(row) : row)),
78
+ aligns: props.aligns
79
+ });
80
+ }
81
+ /** A new empty body row below row `r` (row 0, the header, opens the first body row). */
82
+ export function insertRow(props, r) {
83
+ const at = Math.max(0, Math.min(r, props.rows.length));
84
+ const fresh = Array.from({ length: columnCount(props) }, emptyCell);
85
+ return normalizeTable({
86
+ ...props,
87
+ rows: [...props.rows.slice(0, at), fresh, ...props.rows.slice(at)]
88
+ });
89
+ }
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. */
92
+ export function deleteRow(props, r) {
93
+ if (r === 0)
94
+ return normalizeTable(props);
95
+ return normalizeTable({ ...props, rows: props.rows.filter((_, i) => i !== r - 1) });
96
+ }
97
+ /** A new empty column after column `c`. */
98
+ export function insertColumn(props, c) {
99
+ const at = Math.max(0, Math.min(c + 1, columnCount(props)));
100
+ const splice = (cells) => [...cells.slice(0, at), emptyCell(), ...cells.slice(at)];
101
+ return normalizeTable({
102
+ header: splice(props.header),
103
+ rows: props.rows.map(splice),
104
+ aligns: [...props.aligns.slice(0, at), 'none', ...props.aligns.slice(at)]
105
+ });
106
+ }
107
+ /** Drop column `c`; a no-op at one column (a table has at least one). */
108
+ export function deleteColumn(props, c) {
109
+ if (columnCount(props) <= 1)
110
+ return normalizeTable(props);
111
+ const drop = (xs) => xs.filter((_, i) => i !== c);
112
+ return normalizeTable({
113
+ header: drop(props.header),
114
+ rows: props.rows.map(drop),
115
+ aligns: drop(props.aligns)
116
+ });
117
+ }
118
+ /** One element moved within an array, out of place. The whole of what both move ops
119
+ * do to their axis, which is why they can only disagree about the index. */
120
+ function move(xs, from, to) {
121
+ const next = [...xs];
122
+ const [moved] = next.splice(from, 1);
123
+ next.splice(to, 0, moved);
124
+ return next;
125
+ }
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. */
130
+ 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)
134
+ return normalizeTable(props);
135
+ return normalizeTable({ ...props, rows: move(props.rows, from, to) });
136
+ }
137
+ /** Column `c` moved by `by` places, clamped. `aligns` travels with the column: an
138
+ * alignment is the column's property, so a move that left it behind would retint the
139
+ * column that took the index. */
140
+ export function moveColumn(props, c, by) {
141
+ const to = Math.max(0, Math.min(c + by, columnCount(props) - 1));
142
+ if (c === to || c < 0 || c >= columnCount(props))
143
+ return normalizeTable(props);
144
+ const shift = (xs) => move(xs, c, to);
145
+ return normalizeTable({
146
+ header: shift(props.header),
147
+ rows: props.rows.map(shift),
148
+ aligns: shift(props.aligns)
149
+ });
150
+ }
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));
156
+ return normalizeTable({
157
+ header: blank(props.header),
158
+ rows: props.rows.map(blank),
159
+ aligns: props.aligns
160
+ });
161
+ }
162
+ /** Set column `c`'s alignment: the one table capability the content round-trips
163
+ * today and nothing in the editor could reach. */
164
+ export function setAlign(props, c, align) {
165
+ return normalizeTable({
166
+ ...props,
167
+ aligns: props.aligns.map((a, i) => (i === c ? align : a))
168
+ });
169
+ }
170
+ // ── The cell codec ──────────────────────────────────────────────────────────
171
+ /**
172
+ * 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
174
+ * text: the cell-local coordinate space is a `Content`'s coordinate space with
175
+ * one line in it.
176
+ */
177
+ export function cellContent(cell) {
178
+ return {
179
+ text: cell.text,
180
+ lines: [{ containers: [], kind: 'para' }],
181
+ marks: cell.marks,
182
+ islands: []
183
+ };
184
+ }
185
+ /**
186
+ * The cell a nested PM doc projects, carrying `prior`'s identity anchors through.
187
+ *
188
+ * Anchors inside a cell are **preserved verbatim, never minted**: they are not in
189
+ * the field plugin's coordinate space (the field's position map holds one `atom`
190
+ * run for the whole table), so nothing here can mint one and the popover withholds
191
+ * its `anchor` button while the caret is in a cell. Preserving them still means
192
+ * rebasing: a cell edit is a splice like any other, so each held anchor maps through
193
+ * the cell's own text delta by the rule `applyChange` uses on a field's
194
+ * (start-assoc `after`), and a mark the splice deleted through lands where the
195
+ * splice left it rather than pointing past the text.
196
+ */
197
+ export function cellFromDoc(doc, prior) {
198
+ const projected = pmToContent(doc);
199
+ const text = projected.text;
200
+ const anchors = prior.marks.filter(isAnchorMark);
201
+ if (!anchors.length)
202
+ return { text, marks: projected.marks };
203
+ const delta = contentEdit(cellContent(prior), cellContent({ text, marks: [] })).delta;
204
+ const rebased = anchors.map((a) => {
205
+ const pos = delta ? mapPos(delta, a.start, 'after') : a.start;
206
+ return { ...a, start: pos, end: pos };
207
+ });
208
+ return {
209
+ text,
210
+ marks: [...projected.marks, ...rebased].sort((a, b) => a.start - b.start || a.end - b.end)
211
+ };
212
+ }
213
+ /** Whether two cells are the same value: what tells an own edit (the projection the
214
+ * view just produced) from an external one (an undo, a re-hydrate) without a flag. */
215
+ export function cellEqual(a, b) {
216
+ return a.text === b.text && JSON.stringify(a.marks) === JSON.stringify(b.marks);
217
+ }
218
+ /** Whether two tables have the same rectangle and alignment: the change that forces
219
+ * a rebuild rather than a per-cell reseed. */
220
+ export function shapeEqual(a, b) {
221
+ return (a.rows.length === b.rows.length &&
222
+ a.aligns.length === b.aligns.length &&
223
+ a.aligns.every((x, i) => x === b.aligns[i]));
224
+ }
@@ -0,0 +1,73 @@
1
+ import type { DocPath } from './address.js';
2
+ /**
3
+ * What failed. Stable strings rather than an enum: a consumer switches on them,
4
+ * and a new surface adds a code without moving the ones an app already handles.
5
+ */
6
+ export type EditorErrorCode =
7
+ /** A scalar/array/object write the boundary refused. Also pinned as a
8
+ * diagnostic on its own field; editing continues, the value is unwritten. */
9
+ 'commit-refused'
10
+ /** A prose commit whose op path the boundary refused: the leaf overwrote
11
+ * the whole projection instead, so the store is correct and this field's
12
+ * identity anchors were paid. */
13
+ | 'commit-fallback'
14
+ /** The overwrite fallback ALSO failed: the optimistic PM state stands and the
15
+ * store is stale for this field. The one code here that leaves damage. */
16
+ | 'commit-lost'
17
+ /** A card operation (add, move, remove, retype) that threw. The document is
18
+ * unchanged; the boundary's mutators are transactional. */
19
+ | 'card-op-failed'
20
+ /** `quill.validate` threw: this derive contributes no validation diagnostics. */
21
+ | 'validate-failed'
22
+ /** `quill.resolve` threw: ghosted `default:`s fall back to none for this derive. */
23
+ | 'resolve-failed'
24
+ /** A page paint the backend refused. The preview shows its error message state. */
25
+ | 'paint-failed'
26
+ /** A tip's markdown did not render: the tip shows as literal text. */
27
+ | 'tip-render-failed'
28
+ /**
29
+ * A prop a surface binds ONCE was swapped in place, and the surface neither
30
+ * re-keyed on it nor observed it. The mounted surface still names the previous
31
+ * handle. `dev` throughout: the fix is a remount at the call site, so this is
32
+ * aimed at whoever wired it and never at a running app's telemetry.
33
+ */
34
+ | 'rebind-ignored'
35
+ /**
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.
41
+ */
42
+ | 'target-unknown';
43
+ /** A failure a surface recovered from. */
44
+ export interface EditorError {
45
+ code: EditorErrorCode;
46
+ /**
47
+ * `error` is a runtime failure an app routes to telemetry; `dev` is a contract
48
+ * violation aimed at whoever is building against the package. Without the
49
+ * split every consumer's sink filters the dev codes by hand.
50
+ */
51
+ severity: 'error' | 'dev';
52
+ /** English, for a log. Not for display: nothing here has a place on screen. */
53
+ message: string;
54
+ /** Whatever was thrown, unwrapped: a `QuillmarkError` arrives intact. */
55
+ cause?: unknown;
56
+ /** The field the failure belongs to, where it has one. */
57
+ path?: DocPath;
58
+ /** The page a paint failed on. */
59
+ page?: number;
60
+ }
61
+ /** The `onError` hook every surface takes. */
62
+ export type EditorErrorHandler = (err: EditorError) => void;
63
+ /**
64
+ * Report `err`, or log it when nothing is listening: a failure the package
65
+ * recovered from is worth seeing in a console.
66
+ *
67
+ * A throwing handler is the consumer's bug and must not become the package's: it
68
+ * is caught, because every call site sits on a recovery path where a second throw
69
+ * would undo the recovery.
70
+ */
71
+ export declare function reportError(handler: EditorErrorHandler | undefined, err: EditorError): void;
72
+ /** A thrown value's message, for the `message` field. */
73
+ export declare function errorMessage(e: unknown): string;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Report `err`, or log it when nothing is listening: a failure the package
3
+ * recovered from is worth seeing in a console.
4
+ *
5
+ * A throwing handler is the consumer's bug and must not become the package's: it
6
+ * is caught, because every call site sits on a recovery path where a second throw
7
+ * would undo the recovery.
8
+ */
9
+ export function reportError(handler, err) {
10
+ if (!handler) {
11
+ console.error(`[quillmark/ui] ${err.code}: ${err.message}`, err.cause);
12
+ return;
13
+ }
14
+ try {
15
+ handler(err);
16
+ }
17
+ catch (e) {
18
+ console.error(`[quillmark/ui] onError handler threw while reporting ${err.code}`, e);
19
+ }
20
+ }
21
+ /** A thrown value's message, for the `message` field. */
22
+ export function errorMessage(e) {
23
+ return e instanceof Error ? e.message : String(e);
24
+ }
@@ -0,0 +1,5 @@
1
+ import './theme.css';
2
+ export { init } from './lifecycle.js';
3
+ export type { DocPath, Place } from './address.js';
4
+ export { fieldPathForAddr, addrForFieldPath } from './address.js';
5
+ export type { EditorError, EditorErrorCode, EditorErrorHandler } from './errors.js';
@@ -0,0 +1,19 @@
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).
6
+ import './theme.css';
7
+ // `@quillmark/svelte/core`: what this package OWNS at the substrate seam.
8
+ //
9
+ // The `@quillmark/wasm` API is not re-exported: import it straight from the peer
10
+ // dependency, the single source of truth (quiver states the same rule). The
11
+ // handles cross this package's surfaces untouched (no wrapper types, per
12
+ // DOCUMENT_MODEL §What the editor owns), so the boundary needs no second door
13
+ // here. What earns a line is what more than one surface speaks and no other
14
+ // package declares: the address vocabulary and the error channel, plus the WASM
15
+ // init gate.
16
+ export { init } from './lifecycle.js';
17
+ // The hop between that vocabulary and the `Addr` the document verbs take, both
18
+ // directions: a host handed a path by a hook and holding a mutator needs it.
19
+ export { fieldPathForAddr, addrForFieldPath } from './address.js';
@@ -0,0 +1,7 @@
1
+ /**
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.
6
+ */
7
+ export declare function init(): Promise<void>;
@@ -0,0 +1,21 @@
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.
5
+ import { init as initWasm } from '@quillmark/wasm';
6
+ let started;
7
+ /**
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.
12
+ */
13
+ export function init() {
14
+ if (started)
15
+ return started;
16
+ started = initWasm().catch((err) => {
17
+ started = undefined;
18
+ throw err;
19
+ });
20
+ return started;
21
+ }
@@ -0,0 +1,18 @@
1
+ import { type EditorErrorHandler } from './errors.js';
2
+ /** The once-bound props by name, `onError` among them. */
3
+ export type Bound = Record<string, unknown> & {
4
+ onError?: EditorErrorHandler;
5
+ };
6
+ /**
7
+ * 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.`).
10
+ *
11
+ * Mounted identities are recorded on the effect's FIRST run, the flush the surface
12
+ * binds in; every later comparison reads them through the same `snapshot`.
13
+ *
14
+ * The report goes to the SNAPSHOT's `onError`: every other error these surfaces
15
+ * report reaches the handler they bound, and a swapped `onError` is itself one of
16
+ * the reports.
17
+ */
18
+ export declare function guardRebind(snapshot: () => Bound, remount: string): void;
@@ -0,0 +1,44 @@
1
+ // The remount contract, enforced. `Preview` binds its props ONCE, in `onMount`, and
2
+ // observes nothing after: the vanilla core below it owns scroll position, mounted
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.
5
+ //
6
+ // A guard over ONE prop of a set is worse than none: a consumer who learns the
7
+ // surface complains when it ignores a prop reads silence on the rest as reactivity.
8
+ // The snapshot is every once-bound prop, and one report names whichever went stale.
9
+ import { reportError } from './errors.js';
10
+ /**
11
+ * 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.`).
14
+ *
15
+ * Mounted identities are recorded on the effect's FIRST run, the flush the surface
16
+ * binds in; every later comparison reads them through the same `snapshot`.
17
+ *
18
+ * The report goes to the SNAPSHOT's `onError`: every other error these surfaces
19
+ * report reaches the handler they bound, and a swapped `onError` is itself one of
20
+ * the reports.
21
+ */
22
+ export function guardRebind(snapshot, remount) {
23
+ let mounted;
24
+ let reported = false;
25
+ $effect(() => {
26
+ const live = snapshot();
27
+ if (mounted === undefined) {
28
+ mounted = live;
29
+ return;
30
+ }
31
+ if (reported)
32
+ return;
33
+ const bound = mounted;
34
+ const stale = Object.keys(bound).filter((k) => live[k] !== bound[k]);
35
+ if (stale.length === 0)
36
+ return;
37
+ 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
+ });
43
+ });
44
+ }
@@ -0,0 +1,20 @@
1
+ /** A surface's lifespan: alive from construction until {@link Lifespan.end}. */
2
+ export interface Lifespan {
3
+ /** False once the span has ended. */
4
+ readonly alive: boolean;
5
+ /**
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
8
+ * where the order lives. Registering on an ended span runs the canceller at
9
+ * once: there is no later run to hold it for.
10
+ */
11
+ onEnd(cancel: () => void): void;
12
+ /**
13
+ * Await `pending`, then report whether the surface survived the window. `false`
14
+ * is a continuation to drop: what it captured before the await is gone.
15
+ */
16
+ resumes(pending: Promise<unknown>): Promise<boolean>;
17
+ /** Run the cancellers once and end the span. Idempotent. */
18
+ end(): void;
19
+ }
20
+ export declare function createLifespan(): Lifespan;
@@ -0,0 +1,37 @@
1
+ // The teardown order every surface holds: unregister, cancel, then free.
2
+ //
3
+ // An order rather than a list, because work is deferred: a surface that frees
4
+ // first leaves a scheduled continuation holding what nobody owns, and one that
5
+ // cancels first but stays registered still takes the next event.
6
+ //
7
+ // `Lifespan` carries it. A surface builds one, registers its cancellers in the
8
+ // order they must run, and ends the span once; continuations that cross an await
9
+ // ask `resumes()` rather than a flag they captured before it.
10
+ export function createLifespan() {
11
+ let alive = true;
12
+ const cancels = [];
13
+ return {
14
+ get alive() {
15
+ return alive;
16
+ },
17
+ onEnd(cancel) {
18
+ if (!alive)
19
+ return void cancel();
20
+ cancels.push(cancel);
21
+ },
22
+ async resumes(pending) {
23
+ await pending;
24
+ return alive;
25
+ },
26
+ end() {
27
+ if (!alive)
28
+ return;
29
+ // Dead BEFORE the cancellers run: one of them may synchronously reach
30
+ // something that asks, and a span mid-teardown is not alive.
31
+ alive = false;
32
+ for (const cancel of cancels)
33
+ cancel();
34
+ cancels.length = 0;
35
+ }
36
+ };
37
+ }