@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,108 @@
1
+ import { scanDoc } from './encode.js';
2
+ /** Build the position index for `doc` (walks it once via the shared scanner). */
3
+ export function buildLineIndex(doc) {
4
+ const s = scanDoc(doc);
5
+ const runs = s.runs;
6
+ let pmEndContent = 1;
7
+ let firstContentStart = 1;
8
+ if (runs.length) {
9
+ const first = runs[0];
10
+ firstContentStart = first.pmStart;
11
+ const last = runs[runs.length - 1];
12
+ pmEndContent = runEndPm(last);
13
+ }
14
+ return { runs, usvEnd: s.usvEnd, pmEndContent, firstContentStart };
15
+ }
16
+ /** PM position just past a run (its exclusive end). */
17
+ function runEndPm(run) {
18
+ if (run.kind === 'text')
19
+ return run.pmStart + run.s.length;
20
+ if (run.kind === 'nl')
21
+ return run.pmEnd;
22
+ return run.pmStart + 1;
23
+ }
24
+ const usvStartOf = (run) => run.usvStart;
25
+ const pmStartOf = (run) => run.pmStart;
26
+ /**
27
+ * Index of the last run whose `coord` is ≤ `pos`, or `-1` when `pos` precedes the
28
+ * first run. The walk emits runs in document order and every run owns at least one
29
+ * position in each space, so both coordinates increase strictly across the list and
30
+ * the lookup bisects. Bulk conversion is what wants it: `readAnchorsUsv` converts
31
+ * once per anchor on every commit, and a scan would make that O(anchors × runs).
32
+ */
33
+ function lastRunAtOrBefore(runs, pos, coord) {
34
+ let lo = 0;
35
+ let hi = runs.length - 1;
36
+ let found = -1;
37
+ while (lo <= hi) {
38
+ const mid = (lo + hi) >> 1;
39
+ if (coord(runs[mid]) <= pos) {
40
+ found = mid;
41
+ lo = mid + 1;
42
+ }
43
+ else {
44
+ hi = mid - 1;
45
+ }
46
+ }
47
+ return found;
48
+ }
49
+ /** USV content offset → PM position. */
50
+ export function usvToPM(index, usvPos) {
51
+ const usv = clamp(usvPos, 0, index.usvEnd);
52
+ if (usv >= index.usvEnd)
53
+ return index.usvEnd === 0 ? index.firstContentStart : index.pmEndContent;
54
+ // The runs TILE the USV space from 0 (every text the walk appends pushes its run
55
+ // first), so below `usvEnd` the run at or before `usv` is the run that owns it.
56
+ const run = index.runs[lastRunAtOrBefore(index.runs, usv, usvStartOf)];
57
+ if (run.kind === 'text')
58
+ return run.pmStart + utf16OfCodePoints(run.s, usv - run.usvStart);
59
+ return run.pmStart; // nl / atom: the run's single owned position
60
+ }
61
+ /** PM position → USV content offset. */
62
+ export function pmToUsv(index, pmPos) {
63
+ const i = lastRunAtOrBefore(index.runs, pmPos, pmStartOf);
64
+ if (i < 0)
65
+ return 0; // before the first run's PM position (or no runs at all)
66
+ const run = index.runs[i];
67
+ // PM coverage, unlike USV, has gaps no run owns (a rule's own token). A position
68
+ // in one is past every run that could answer it → the end of the content text.
69
+ if (pmPos >= runEndPm(run))
70
+ return index.usvEnd;
71
+ if (run.kind === 'text')
72
+ return run.usvStart + codePointsOfUtf16(run.s, pmPos - run.pmStart);
73
+ return run.usvStart;
74
+ }
75
+ // ── UTF-16 ↔ USV conversions within one text run ────────────────────────────
76
+ /** UTF-16 length of the first `n` code points of `s`. */
77
+ function utf16OfCodePoints(s, n) {
78
+ let cp = 0;
79
+ let i = 0;
80
+ while (cp < n && i < s.length) {
81
+ i += charUnitLen(s, i);
82
+ cp++;
83
+ }
84
+ return i;
85
+ }
86
+ /** Number of whole code points within the first `k` UTF-16 units of `s`. */
87
+ function codePointsOfUtf16(s, k) {
88
+ let cp = 0;
89
+ let i = 0;
90
+ while (i < k && i < s.length) {
91
+ i += charUnitLen(s, i);
92
+ cp++;
93
+ }
94
+ return cp;
95
+ }
96
+ /** UTF-16 unit length (1 or 2) of the code point starting at `i`. */
97
+ function charUnitLen(s, i) {
98
+ const code = s.charCodeAt(i);
99
+ if (code >= 0xd800 && code <= 0xdbff && i + 1 < s.length) {
100
+ const next = s.charCodeAt(i + 1);
101
+ if (next >= 0xdc00 && next <= 0xdfff)
102
+ return 2;
103
+ }
104
+ return 1;
105
+ }
106
+ function clamp(n, lo, hi) {
107
+ return n < lo ? lo : n > hi ? hi : n;
108
+ }
@@ -0,0 +1,458 @@
1
+ /* ProseMirror's own base stylesheet is imported beside this one (`codec/index.ts`):
2
+ the structural rules a view needs to render at all: `white-space:
3
+ break-spaces`, `li { position: relative }` for list markers, the
4
+ hidden-selection and separator-image guards. It is imported whole; the rules are
5
+ interdependent and which ones a given schema needs is not visible from here.
6
+
7
+ What it does NOT carry is a block reset, so without the rules below every leaf
8
+ inherits the UA `p { margin: 1em 0 }` (32px of margin in a box drawn to hold one
9
+ line), which is enough on its own to make an inline richtext field twice the height
10
+ of the input beside it. The reset lives here rather than per component because every
11
+ prose leaf in the package mounts through the codec (a field, the body, an array
12
+ element) and picks it up without restating it, and a leaf that carries the
13
+ control recipe (`visual/ProseField.svelte`) can then match `.qm-input` by
14
+ construction instead of by a tuned `min-height`. The source view is NOT one of
15
+ them: it is a `<pre>` holding `textContent` (`source/view.ts`), outside
16
+ `.ProseMirror` and so outside every rule in this file.
17
+
18
+ It also carries hard-coded VALUES that answer to no rung: a `#8cf` node-selection
19
+ outline, twice, and the `-32px` placing a selected list item's. Retinted and
20
+ re-measured here rather than in `theme.css` because a layered rule loses to that
21
+ unlayered stylesheet: equal specificity and a later import is what wins. */
22
+ .ProseMirror-selectednode {
23
+ outline: var(--_qm-ring-width) solid var(--_qm-accent);
24
+ }
25
+ /* Upstream's `-32px` is measured against the UA's 40px list indent, which the list
26
+ rule below replaces with a rung. The offset moves with it, or a selected item's
27
+ outline hangs into the margin by the difference. */
28
+ li.ProseMirror-selectednode::after {
29
+ border: var(--_qm-ring-width) solid var(--_qm-accent);
30
+ left: calc(var(--_qm-space-4) * -1);
31
+ }
32
+
33
+ /* The GAP CURSOR (`prosemirror-gapcursor`, imported beside this file): a caret
34
+ between two blocks, where the document holds no text position for an ordinary one.
35
+ It is what a press beside an island lands, so it is drawn in the surface's ink
36
+ rather than the upstream sheet's `black`, which is the `#8cf` defect again. */
37
+ .ProseMirror-gapcursor::after {
38
+ border-top-color: var(--_qm-ink);
39
+ }
40
+
41
+ /* Everything starts flush. Not a list of the block tags: the subtree is entirely
42
+ what the codec emits, a paragraph-only reset leaves headings and lists on UA
43
+ margins (two rhythms in one leaf), and an enumeration silently lags the schema:
44
+ `island_block` renders a bare `div` (`islands.ts`) and would match no such list. */
45
+ .ProseMirror * {
46
+ margin: 0;
47
+ }
48
+
49
+ /* One rhythm between sibling blocks, in place of the UA's collapsed pair. Scoped by
50
+ PARENT rather than by tag, which is what keeps it true as the schema grows: the
51
+ four containers below are every node whose content is `block+` (`schema.ts`), so
52
+ their children are blocks by definition, whatever kinds exist. `ul`/`ol` are
53
+ deliberately absent: a run of list items is tight, and the gap before a list is
54
+ the list's, not each item's. */
55
+ .ProseMirror > * + *,
56
+ .ProseMirror :is(blockquote, li, [data-qm-unknown-container]) > * + * {
57
+ margin-top: var(--_qm-space-4);
58
+ }
59
+
60
+ /* A blockquote's UA indent was its only mark, and the reset above takes it. The rule
61
+ and the inset carry it instead: the same hairline every surface reads, so a quote
62
+ reads as set apart without a fill or a second box. */
63
+ .ProseMirror blockquote {
64
+ padding-left: var(--_qm-space-3);
65
+ border-left: var(--_qm-border-width) solid var(--_qm-border);
66
+ }
67
+
68
+ /* ── The positive half ───────────────────────────────────────────────────────
69
+ What the reset leaves bare, and the whole of it: every mark and node `schema.ts`
70
+ emits, minus the four the UA already renders correctly (`strong`, `em`, `u`, `s`)
71
+ and the blockquote above.
72
+
73
+ Each rule answers to the REFERENCE QUILL rather than to a browser default or to a
74
+ web editor's chrome, because the leaf sits beside a live preview of the same
75
+ content. `usaf_memo` sets `raw` in a monospace face and adds nothing; it renders a
76
+ heading as a bold RUN-IN prepended to the following paragraph, with the level
77
+ never read; it gives a link no decoration at all. So a leaf shows that a span IS
78
+ code and a line IS a heading: never a prediction of the size paper will set it
79
+ at, which the package cannot know for a quill it has not seen.
80
+
81
+ Scoping is by TAG, and the schema does the rest: `h1`–`h6`, `hr` and the lists
82
+ cannot exist under the inline schema, so no rule for them can land on a `subject`
83
+ field however that field is mounted. The two MARKS are the exception: `code` and
84
+ `link` are in both schemas, and a `plaintext` leaf strips them at DECODE while
85
+ mounting a schema that still carries them, so a paste can put either in one. The
86
+ rules stand there too, and correctly: they say the leaf holds code, or a link,
87
+ which is exactly what it then holds.
88
+
89
+ `visual/TipsCard.svelte` restates the two mark rules by hand, over the same
90
+ `toDOM` output injected outside any view. */
91
+
92
+ /* Code is a FACE: no chip, no fill, no inset, no size step. The blockquote above
93
+ already argues the general form: a span reads as set apart without a fill or a
94
+ second box, on the one surface in the card with no edge of its own. A chip has
95
+ nowhere to come from in any case: the card is
96
+ `--_qm-surface-raised` and the only rung over it is `--_qm-surface-hover`, the
97
+ hover lane, so a chip at rest would read as a hovered button.
98
+
99
+ ONE selector for the mark and the block: a `code_block` renders `pre > code`
100
+ (`schema.ts`), so every character of it is inside a `code` element and the `pre`
101
+ needs no face of its own. `pre` takes no `overflow-x` either, because there is no
102
+ overflow: ProseMirror's own stylesheet sets `white-space: pre-wrap` there, so a
103
+ long line wraps. A scroller means overriding that to `pre` inside an editable
104
+ `code` textblock, which is an editing change, not a display rule. */
105
+ .ProseMirror code {
106
+ font-family: var(--_qm-font-mono);
107
+ }
108
+
109
+ /* A link takes the surface's ink (the UA's blue is the `#8cf` defect again) and a
110
+ DOTTED underline. Dotted rather than solid because `underline` is a mark in the
111
+ same schema, and a solid rule renders an authored `<u>` and a link identically in a
112
+ paragraph that can hold both. An underline at all, where the tips card's link takes
113
+ none, because this leaf is EDITABLE: the `href` is otherwise imperceptible, so
114
+ removing the rule loses an answer rather than ornament. No hue:
115
+ `--_qm-accent` already means focus, and a body of accent-coloured links collides
116
+ with the ring and with the correlation wash. */
117
+ .ProseMirror a {
118
+ color: inherit;
119
+ text-decoration: underline dotted;
120
+ }
121
+
122
+ /* The UA draws a rule as an inset border in the browser's own grey. It is the same
123
+ hairline every other surface reads instead. Spacing is already the sibling gap's,
124
+ above and below, so the rule carries none of its own. */
125
+ .ProseMirror hr {
126
+ border: none;
127
+ border-top: var(--_qm-border-width) solid var(--_qm-border);
128
+ }
129
+
130
+ /* The UA's 40px list indent is the one box the reset above does not take (it is
131
+ padding, not margin), and it is a length nothing chose. A rung replaces it. The
132
+ MARKERS stay the UA's: `disc` and `decimal` are already right, and restating them
133
+ buys nothing. Per-item spacing is untouched, which is the point of putting this on
134
+ the list rather than the item: the sibling-gap rule deliberately omits `ul`/`ol`,
135
+ and padding on the container cannot reintroduce what it withholds. */
136
+ .ProseMirror :is(ul, ol) {
137
+ padding-left: var(--_qm-space-4);
138
+ }
139
+
140
+ /* Six levels, one weight, no size step. The quill reads no level at all, so a size
141
+ ramp here would be the editor predicting a hierarchy the paper does not set; the
142
+ level stays in the model, which is where the codec round-trips it. It is also all
143
+ the ramp could carry: `--_qm-text-title` is one 1.125 step over the body, the
144
+ body sits on `--_qm-text-body` by decision, and leading alone is what separates
145
+ paper from chrome. Six levels do not fit in one step, and
146
+ minting five more is the drift the scale exists to prevent. */
147
+ .ProseMirror :is(h1, h2, h3, h4, h5, h6) {
148
+ font-size: inherit;
149
+ font-weight: var(--_qm-weight-label);
150
+ }
151
+
152
+ /* ── The table island ────────────────────────────────────────────────────────
153
+
154
+ The one construct in this file with CHROME in it. The rest of the stylesheet
155
+ states what a block is and lets paper set its size; a table island is edited
156
+ through controls that do not print, so it also has a recede ladder, a target
157
+ floor and a reveal. Those live here rather than in `visual/controls.css` for the
158
+ same reason the NodeView is vanilla: the island is inside the leaf, and the leaf
159
+ is the codec's. What it does NOT restate is the surface's dials: every value
160
+ below is a `--_qm-*` rung, so the island retints with everything else.
161
+
162
+ THE CHROME IS IN NO TRACK. Every control is absolutely positioned inside a real
163
+ data cell, hanging into a band the scroller carries as PADDING. So the grid holds
164
+ the box a paragraph would, the accessibility tree is the data's shape, and a
165
+ handle still lines up with its line by CONSTRUCTION rather than by a measured
166
+ overlay: it is inside the cell it names. */
167
+
168
+ /* The band, and the whole of what it costs. `padding` opens it; the island's
169
+ negative `margin` spends the card's own inset buying it back, so the grid sits one
170
+ inset from the prose edge instead of a tap target plus one.
171
+
172
+ `margin-top: 0` overrides the sibling-gap rule rather than going negative: the band
173
+ ABSORBS the gap. A negative top margin would hang the band over the block above,
174
+ and an `opacity: 0` control is still hit-tested, so it would take that block's last
175
+ presses. */
176
+ .qm-table-island {
177
+ margin-top: 0;
178
+ margin-left: calc(var(--_qm-space-3) * -1);
179
+ }
180
+ .qm-table-scroller {
181
+ /* The one horizontally scrolling box in a leaf: a table is as wide as its
182
+ columns, and a wide one must not push the card's own layout. `overflow-y` is
183
+ `hidden` rather than left alone, because a non-`visible` axis computes the other
184
+ to `auto` and a table has nothing to scroll vertically. */
185
+ position: relative;
186
+ overflow-x: auto;
187
+ overflow-y: hidden;
188
+ padding: var(--_qm-tap-min) 0 0 var(--_qm-tap-min);
189
+ width: max-content;
190
+ max-width: 100%;
191
+ }
192
+ .qm-table {
193
+ border-collapse: separate;
194
+ border-spacing: 0;
195
+ border: var(--_qm-border-width) solid var(--_qm-border);
196
+ border-radius: var(--_qm-radius-inner);
197
+ }
198
+ /* Two tiers of rule, because a table draws two different things. The FRAME and the
199
+ header's rule are structure and keep the full hairline; the lines between cells are
200
+ rhythm and drop to the faint rung, so the grid reads as one object with divisions
201
+ rather than as a stack of boxes. `separate` with zero spacing rather than
202
+ `collapse`: a collapsed border cannot be rounded, and the two tiers would fight
203
+ over which one wins an edge. */
204
+ .qm-table-cell {
205
+ position: relative;
206
+ border-right: var(--_qm-border-width) solid var(--_qm-border-faint);
207
+ border-bottom: var(--_qm-border-width) solid var(--_qm-border-faint);
208
+ padding: var(--_qm-space) var(--_qm-space-2);
209
+ vertical-align: top;
210
+ font-weight: inherit;
211
+ text-align: inherit;
212
+ }
213
+ .qm-table tr > .qm-table-cell:last-child {
214
+ border-right: none;
215
+ }
216
+ .qm-table tbody tr:last-child > .qm-table-cell {
217
+ border-bottom: none;
218
+ }
219
+ /* The header row is the ONE weight distinction the island draws: `header` is a
220
+ separate field in the model rather than a flag on a row, so the surface says so.
221
+ It takes a tone with it, which is the same elevation move the cards make: a rung,
222
+ never a shadow. The rest of a table's type is paper's. */
223
+ .qm-table-header-row .qm-table-cell {
224
+ font-weight: var(--_qm-weight-label);
225
+ background: var(--_qm-surface-raised);
226
+ border-bottom: var(--_qm-border-width) solid var(--_qm-border);
227
+ }
228
+ /* A cell holds a whole nested view. Its measure floor keeps an empty table from
229
+ collapsing to a row of hairlines with nothing to aim at, and the caret is the
230
+ focus indicator here exactly as it is in the leaf: no ring.
231
+
232
+ The HEIGHT floor is one line box, named as size times leading so both factors move
233
+ with the type ramp (`.qm-prose-block` states the same expression for three). An
234
+ empty row is then the height it will be once typed, so the first keystroke changes
235
+ no geometry. */
236
+ .qm-table-cell-host {
237
+ min-width: 4rem;
238
+ min-height: calc(var(--_qm-text-body) * var(--_qm-leading-body));
239
+ }
240
+ .qm-table-cell-host .ProseMirror {
241
+ outline: none;
242
+ }
243
+ /* A selected island takes the node outline every leaf block does
244
+ (`.ProseMirror-selectednode`, above), and that is the state the corner and Escape
245
+ leave a line FOR: the caret is out, the island is what is selected, and Backspace
246
+ deletes it. A caret inside a cell is not that state and does not paint it. */
247
+
248
+ /* A SELECTED LINE, washed across its cells: what Backspace deletes and Alt+arrow
249
+ moves. The hover rung, because it makes a hover's claim (this line, the one the next
250
+ command is about) held rather than passing. */
251
+ .qm-table-cell.qm-table-line {
252
+ background: var(--_qm-surface-hover);
253
+ }
254
+
255
+ /* Every control in the band is a flat centred button hung out of flow; what differs
256
+ is where it hangs and what it draws. */
257
+ :is(.qm-table-handle, .qm-table-seam, .qm-table-align-item) {
258
+ display: flex;
259
+ align-items: center;
260
+ justify-content: center;
261
+ border: none;
262
+ background: none;
263
+ cursor: pointer;
264
+ }
265
+ :is(.qm-table-seam, .qm-table-align-item) svg {
266
+ width: var(--_qm-glyph-control);
267
+ height: var(--_qm-glyph-control);
268
+ }
269
+
270
+ /* ONE control per line, and it is a bar rather than a glyph: what a handle acts on
271
+ is the row or column it sits against, and position says that better than an icon
272
+ would.
273
+
274
+ The TARGET is the band and the MARK is drawn small inside it, which is the rule
275
+ the tips card's dots already state: the handle spans its line on one axis and the
276
+ tap minimum on the other, and the bar in the middle of it is 3px. A handle sized to
277
+ the bar would be a 3px target on a control a pointer has to find. */
278
+ .qm-table-handle {
279
+ position: absolute;
280
+ padding: var(--_qm-space-half);
281
+ /* The pointer owns this gesture, not the page's scroll: a handle drags. */
282
+ touch-action: none;
283
+ }
284
+ .qm-table-handle[data-axis='column'] {
285
+ inset-inline: 0;
286
+ bottom: 100%;
287
+ height: var(--_qm-tap-min);
288
+ }
289
+ .qm-table-handle[data-axis='row'] {
290
+ inset-block: 0;
291
+ right: 100%;
292
+ width: var(--_qm-tap-min);
293
+ }
294
+ .qm-table-handle-bar {
295
+ display: block;
296
+ border-radius: var(--_qm-radius-pill);
297
+ background: var(--_qm-border);
298
+ }
299
+ .qm-table-handle[data-axis='column'] .qm-table-handle-bar {
300
+ width: 100%;
301
+ height: 0.1875rem;
302
+ }
303
+ .qm-table-handle[data-axis='row'] .qm-table-handle-bar {
304
+ width: 0.1875rem;
305
+ height: 100%;
306
+ min-height: 0.75rem;
307
+ }
308
+ /* A selected handle carries the line's ink, so the bar and the wash say the same
309
+ thing at both ends of the gesture. */
310
+ .qm-table-handle[aria-pressed='true'] .qm-table-handle-bar {
311
+ background: var(--_qm-ink-label);
312
+ }
313
+
314
+ /* The corner is the ISLAND's handle: the grid origin, where the two bands converge,
315
+ and the one gesture that selects the whole table. Its mark is a square because what
316
+ it acts on is the rectangle and not a line of it. */
317
+ .qm-table-corner-handle {
318
+ right: 100%;
319
+ bottom: 100%;
320
+ width: var(--_qm-tap-min);
321
+ height: var(--_qm-tap-min);
322
+ }
323
+ .qm-table-corner-mark {
324
+ display: block;
325
+ width: 0.4375rem;
326
+ height: 0.4375rem;
327
+ border-radius: var(--_qm-radius-pill);
328
+ background: var(--_qm-border);
329
+ }
330
+
331
+ /* A SEAM: the boundary between two lines, and the whole of how a table grows. One per
332
+ boundary including the leading and trailing ones, so an append and an interior
333
+ insert are one gesture at different places.
334
+
335
+ It sits OVER the handles in the band, and it is narrow, so the two share the band
336
+ without a dead zone: the handle owns the line's length and the seam owns its ends. */
337
+ .qm-table-seam {
338
+ position: absolute;
339
+ z-index: 1;
340
+ width: var(--_qm-tap-min);
341
+ height: var(--_qm-tap-min);
342
+ border-radius: var(--_qm-radius-pill);
343
+ color: var(--_qm-accent);
344
+ }
345
+ .qm-table-seam[data-axis='column'] {
346
+ bottom: 100%;
347
+ left: calc(var(--_qm-tap-min) / -2);
348
+ }
349
+ /* The TRAILING seams sit flush inside their edge rather than centred on it: the last
350
+ boundary is the grid's own edge, and a seam centred on it would hang half outside
351
+ the scroller, where the padding does not reach and the clip does, making the append
352
+ affordance the one control that could not be pressed. */
353
+ .qm-table-seam[data-axis='column'][data-side='trail'] {
354
+ left: auto;
355
+ right: 0;
356
+ }
357
+ .qm-table-seam[data-axis='row'] {
358
+ right: 100%;
359
+ top: calc(var(--_qm-tap-min) / -2);
360
+ }
361
+ .qm-table-seam[data-axis='row'][data-side='trail'] {
362
+ top: auto;
363
+ bottom: 0;
364
+ }
365
+
366
+ /* The alignment cluster: the one op with no gesture, so the one surface the chrome
367
+ still raises. It rides the column's SELECTION rather than a hover, so it costs
368
+ nothing at rest, and it sits in the band beside the handle rather than portalled:
369
+ four glyphs in a row is not a list to read. */
370
+ .qm-table-align {
371
+ position: absolute;
372
+ z-index: 2;
373
+ display: flex;
374
+ bottom: 100%;
375
+ left: 0;
376
+ height: var(--_qm-tap-min);
377
+ background: var(--_qm-surface-raised);
378
+ border: var(--_qm-border-width) solid var(--_qm-border);
379
+ border-radius: var(--_qm-radius-pill);
380
+ }
381
+ /* The LAST column anchors right instead: a cluster is wider than a narrow column, and
382
+ one hung off the right edge would be the only chrome the scroller could clip. */
383
+ .qm-table-cell:last-child .qm-table-align {
384
+ left: auto;
385
+ right: 0;
386
+ }
387
+ .qm-table-align-item {
388
+ width: var(--_qm-tap-min);
389
+ height: 100%;
390
+ padding: 0;
391
+ border-radius: var(--_qm-radius-pill);
392
+ color: var(--_qm-ink-label);
393
+ }
394
+ .qm-table-align-item:hover {
395
+ background: var(--_qm-surface-hover);
396
+ color: var(--_qm-ink);
397
+ }
398
+ .qm-table-align-item[aria-pressed='true'] {
399
+ color: var(--_qm-accent);
400
+ }
401
+
402
+ /* Pointer OR focus, the card stack's rule unchanged: opacity rather than absence, so
403
+ the controls stay in the tab order, and `:focus-within` on the ISLAND, so a caret
404
+ in a cell holds the reveal as a focused control does.
405
+
406
+ The reveal is per LINE: the island's hover arms the band, and `qm-hot` (the line the
407
+ pointer is actually over) is what reaches the idle rung, so a hover answers about
408
+ one line rather than about every one at once. */
409
+ .qm-table-handle,
410
+ .qm-table-seam {
411
+ opacity: 0;
412
+ transition: opacity var(--_qm-duration-fast) var(--_qm-ease-reverse);
413
+ }
414
+ .qm-table-island:is(:hover, :focus-within)
415
+ :is(.qm-table-handle.qm-hot, .qm-table-corner-handle, .qm-table-seam) {
416
+ opacity: var(--_qm-opacity-idle);
417
+ }
418
+ .qm-table-handle:hover,
419
+ .qm-table-handle:focus-visible,
420
+ .qm-table-handle[aria-pressed='true'],
421
+ .qm-table-seam:hover,
422
+ .qm-table-seam:focus-visible {
423
+ opacity: 1;
424
+ }
425
+
426
+ /* The ring, on the chrome and nowhere else in this file. A prose leaf's focus
427
+ indicator is its CARET and a cell's is too, so the outline is dropped there; a
428
+ handle is a button, and a button that takes focus to hold a selection has to show
429
+ it. Declared here rather than reached for as `.qm-focus-ring`: that recipe is the
430
+ visual tier's stylesheet, and the island is inside the leaf, which is the codec's. */
431
+ .qm-table-handle:focus-visible,
432
+ .qm-table-seam:focus-visible,
433
+ .qm-table-align-item:focus-visible {
434
+ outline: var(--_qm-ring-focus);
435
+ outline-offset: var(--_qm-ring-offset);
436
+ }
437
+
438
+ /* A DRAG in flight. The lifted line reads as picked up by TONE, never a shadow:
439
+ elevation is a surface rung here, which says the same thing at both poles. The drop
440
+ rule is the accent, the surface's one saturated ink, spent where a change is about
441
+ to land. */
442
+ .qm-table-dragging .qm-table-cell.qm-table-lifted {
443
+ background: var(--_qm-surface-raised);
444
+ opacity: var(--_qm-opacity-muted);
445
+ }
446
+ .qm-table-drop {
447
+ position: absolute;
448
+ z-index: 3;
449
+ background: var(--_qm-accent);
450
+ border-radius: var(--_qm-radius-pill);
451
+ pointer-events: none;
452
+ }
453
+ .qm-table-drop[data-axis='row'] {
454
+ height: var(--_qm-ring-width);
455
+ }
456
+ .qm-table-drop[data-axis='column'] {
457
+ width: var(--_qm-ring-width);
458
+ }
@@ -0,0 +1,26 @@
1
+ import type { Content } from '@quillmark/wasm';
2
+ /** Tracks the last content the codec knows for a leaf, to distinguish foreign edits. */
3
+ export interface Reconciler {
4
+ /** Should `current` (a fresh read of the leaf) trigger a re-hydrate? */
5
+ shouldRehydrate(current: Content): boolean;
6
+ /** Record `rt` as the codec's known state (after its own edit or a re-hydrate). */
7
+ commit(rt: Content): void;
8
+ /** The last known content. Seeded at construction and only ever replaced, so it
9
+ * is never absent: the field reads it directly rather than through a cast. */
10
+ readonly last: Content;
11
+ }
12
+ /** A reconciler seeded with the leaf's initial content. */
13
+ export declare function createReconciler(initial: Content): Reconciler;
14
+ /**
15
+ * Canonical content equality: a structural deep-equal, key-order-insensitive so a
16
+ * WASM read (`doc.get`) and a codec projection (`pmToContent`) compare by value.
17
+ * This is the per-field twin of `Document.equals`.
18
+ */
19
+ export declare function contentEqual(a: Content, b: Content): boolean;
20
+ /**
21
+ * The same structural equality over any boundary value: what the island diff
22
+ * compares two `ContentIsland` entries with (`encode.ts`). Key order is the reason
23
+ * it is not `JSON.stringify`: an island's opaque `props` crosses from a WASM read
24
+ * on one side and a PM node attribute on the other.
25
+ */
26
+ export declare function valueEqual(a: unknown, b: unknown): boolean;
@@ -0,0 +1,64 @@
1
+ /** A reconciler seeded with the leaf's initial content. */
2
+ export function createReconciler(initial) {
3
+ let known = initial;
4
+ return {
5
+ shouldRehydrate(current) {
6
+ return !contentEqual(current, known);
7
+ },
8
+ commit(rt) {
9
+ known = rt;
10
+ },
11
+ get last() {
12
+ return known;
13
+ }
14
+ };
15
+ }
16
+ /**
17
+ * Canonical content equality: a structural deep-equal, key-order-insensitive so a
18
+ * WASM read (`doc.get`) and a codec projection (`pmToContent`) compare by value.
19
+ * This is the per-field twin of `Document.equals`.
20
+ */
21
+ export function contentEqual(a, b) {
22
+ return deepEqual(a, b);
23
+ }
24
+ /**
25
+ * The same structural equality over any boundary value: what the island diff
26
+ * compares two `ContentIsland` entries with (`encode.ts`). Key order is the reason
27
+ * it is not `JSON.stringify`: an island's opaque `props` crosses from a WASM read
28
+ * on one side and a PM node attribute on the other.
29
+ */
30
+ export function valueEqual(a, b) {
31
+ return deepEqual(a, b);
32
+ }
33
+ function deepEqual(a, b) {
34
+ if (a === b)
35
+ return true;
36
+ if (typeof a !== typeof b)
37
+ return false;
38
+ if (a === null || b === null)
39
+ return a === b;
40
+ if (Array.isArray(a) || Array.isArray(b)) {
41
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
42
+ return false;
43
+ for (let i = 0; i < a.length; i++)
44
+ if (!deepEqual(a[i], b[i]))
45
+ return false;
46
+ return true;
47
+ }
48
+ if (typeof a === 'object' && typeof b === 'object') {
49
+ const ao = a;
50
+ const bo = b;
51
+ const ak = Object.keys(ao);
52
+ const bk = Object.keys(bo);
53
+ if (ak.length !== bk.length)
54
+ return false;
55
+ for (const k of ak) {
56
+ if (!Object.prototype.hasOwnProperty.call(bo, k))
57
+ return false;
58
+ if (!deepEqual(ao[k], bo[k]))
59
+ return false;
60
+ }
61
+ return true;
62
+ }
63
+ return false;
64
+ }
@@ -0,0 +1,13 @@
1
+ import { Schema } from 'prosemirror-model';
2
+ /** The full field schema: every block kind, container, mark, and island. */
3
+ export declare const blockSchema: Schema<string, string>;
4
+ /**
5
+ * The constrained inline schema: one paragraph, no block splitting, no
6
+ * containers, no islands. `doc: "paragraph"` (exactly one child) is what makes an
7
+ * Enter a no-op at the model level: there is no second block for a split to land.
8
+ * Marks are present; `plaintext` fields strip them at decode and mount no
9
+ * mark input (a schema-level distinction the field draws, per CODEC §Inline mode).
10
+ */
11
+ export declare const inlineSchema: Schema<"text" | "doc" | "paragraph", string>;
12
+ /** True for the constrained inline schema (no block containers); decode branches on it. */
13
+ export declare function isInlineSchema(schema: Schema): boolean;