@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,17 @@
1
+ import { type DocPath } from '../core/address.js';
2
+ import type { ContentHit } from '@quillmark/wasm';
3
+ import type { VisualEditorProps } from './props.js';
4
+ import type { CardId } from './signals.js';
5
+ import type { FieldController } from '../core/codec/index.js';
6
+ import './controls.css';
7
+ declare const VisualEditorInner: import("svelte").Component<VisualEditorProps, {
8
+ setCaret: (hit: ContentHit) => Promise<void>;
9
+ getActiveLeaf: () => FieldController | undefined;
10
+ focusField: (field: DocPath) => Promise<void>;
11
+ insertCard: (kind: string, at?: number) => CardId | undefined;
12
+ removeCard: (cardId: CardId) => void;
13
+ moveCard: (cardId: CardId, dir: -1 | 1) => void;
14
+ setKind: (cardId: CardId, kind: string) => void;
15
+ }, "">;
16
+ type VisualEditorInner = ReturnType<typeof VisualEditorInner>;
17
+ export default VisualEditorInner;
@@ -0,0 +1,272 @@
1
+ /* The control recipes more than one component draws: the focus ring, the box a
2
+ typed value sits in, and the square icon button. They live here rather than per
3
+ component because Svelte scopes a `<style>` block to the component that owns the
4
+ markup: a component that carries a shared class without also carrying its rule
5
+ ships unstyled, and nothing raises it.
6
+
7
+ NOT in `core/theme.css`: that file is the DERIVATION (public dials in, the
8
+ private `--_qm-*` scale out) and it mints values. This file mints none; every
9
+ declaration reads a rung, exactly as a component's own block must; `check:style`
10
+ holds that over `.css` too.
11
+
12
+ `@layer qm.chrome` is the second layer `theme.css` declares. Layered rules lose
13
+ to unlayered ones, which is the point twice over: a consumer's stylesheet beats
14
+ the chrome without `!important`, and a component's own scoped block still
15
+ overrides its shared base: `.qm-card-delete` recolours a `.qm-icon-btn`, and
16
+ `.qm-json` adds the monospace face over `.qm-input`, with no specificity fight. */
17
+
18
+ @layer qm.chrome {
19
+ /* The themed focus ring in place of the raw UA outline, as a
20
+ RULE rather than a pair of rungs each control assembles. `--_qm-ring-focus`
21
+ fixes the VALUE; which properties draw a ring, and on what state, is the other
22
+ half of the decision, and `check:style` sees a minted literal but never a
23
+ divergent rule. A control opts in by carrying the marker class.
24
+
25
+ Two selectors, because focus lands in two places. Usually on the control
26
+ itself; but the date field's focus lives on a SEGMENT, so it rings the field
27
+ around it: a per-segment ring flickers across the field as the caret walks
28
+ it. That is the only distinction, and this pair is the whole of it. */
29
+ .qm-focus-ring:focus-visible,
30
+ .qm-focus-ring-within:focus-within {
31
+ outline: var(--_qm-ring-focus);
32
+ outline-offset: var(--_qm-ring-offset);
33
+ }
34
+
35
+ /* THE TYPE HALF, for both recipe families at once. A UA control inherits neither
36
+ family nor size, so a control or a button that declares none renders in the UA
37
+ face at the UA size: `--qm-font`, a documented dial, stopping short of the
38
+ package's own surfaces with nothing raised, since an absence mints no literal
39
+ for `check:style`. `font: inherit` first, then the BODY rung off the ramp:
40
+ inheriting the size lands the surface on the host page's body size, a rung the
41
+ scale never minted and one a consumer moves by restyling their page rather than
42
+ by turning `--qm-font-size`.
43
+
44
+ ONE rule over both families rather than the same two declarations in each,
45
+ because the drift a copy invites here is a box and the button beside it reading
46
+ DIFFERENT rungs, which no gate can see: `check:style`'s type axis holds that
47
+ each side reads a rung, never that both read the same one.
48
+
49
+ The list is wider than either recipe's chrome: the body leaf withholds the box
50
+ and still reads the ramp, so `.qm-prose` is
51
+ here and not below.
52
+
53
+ `font` is a shorthand carrying `line-height`, so each family's leading is
54
+ declared in the rules BELOW and would be dropped in silence above them. */
55
+ .qm-control-box,
56
+ .qm-input,
57
+ .qm-prose,
58
+ .qm-icon-btn,
59
+ .qm-add-affordance {
60
+ font: inherit;
61
+ font-size: var(--_qm-text-body);
62
+ }
63
+
64
+ /* A typed value's box (every scalar control and every prose leaf): the text and
65
+ numeric inputs, the array's JSON textarea, the enum trigger, the date field, and
66
+ the inline richtext leaves. ONE rule, because a
67
+ leaf that agrees with the input beside it "by construction" has to draw the same
68
+ declarations, not a copy of them: five hand-kept copies agree until one is
69
+ edited, and nothing raises it (no literal is minted, so `check:style` stays
70
+ green while a control renders subtly the wrong height).
71
+
72
+ LEADING sits on the wide list rather than on the box's own, because a box's
73
+ height is a padding rung plus a line box plus two hairlines: a leaf and the
74
+ input beside it agree on height exactly as long as they agree on leading, which
75
+ is the whole of what "by construction" buys.
76
+
77
+ COLOUR is the trap one property over: `font` does not carry it either, so an
78
+ input or trigger left off this line falls to the UA's own text colour: a second
79
+ black sitting beside the card's ink rather than the rung it renders on.
80
+ `caret-color` is pinned to `currentColor` rather than left at its `auto`
81
+ initial, which resolves the same way: the prose leaf's caret IS its focus
82
+ indicator, the outline dropped in its favour (`ProseField`), so it tracks
83
+ this rule's own ink deliberately. */
84
+ .qm-control-box,
85
+ .qm-input,
86
+ .qm-prose {
87
+ line-height: var(--_qm-leading-body);
88
+ color: var(--_qm-ink);
89
+ caret-color: currentColor;
90
+ }
91
+ .qm-control-box,
92
+ .qm-input {
93
+ padding: var(--_qm-space) var(--_qm-space-2);
94
+ border: var(--_qm-border-width) solid var(--_qm-border);
95
+ border-radius: var(--_qm-radius-inner);
96
+ background: var(--_qm-surface);
97
+ }
98
+ /* What a form control adds over the box: it does not fill its row on its own. */
99
+ .qm-input {
100
+ width: 100%;
101
+ box-sizing: border-box;
102
+ }
103
+ /* The native placeholder is the ghost rung, not the UA's own grey: a text or
104
+ number control's placeholder is the resolved `default:` (Field's `defaultStr`)
105
+ and reads as shown-never-written the same way the prose leaf's empty-state
106
+ ghost does (`ProseField`). */
107
+ .qm-input::placeholder {
108
+ color: var(--_qm-ink-ghost);
109
+ }
110
+
111
+ /* The second recipe, and it is not the box above: a field is a box because you
112
+ type into it, a button is not a box because you press it. No resting border and
113
+ no resting fill; the
114
+ surface arrives on hover, off the rung the enum listbox already highlights with.
115
+ Deliberately no `color`: the callers disagree on purpose (the card delete is
116
+ `--_qm-danger`, the array remove inherits its row's ink), and a base colour here
117
+ would flatten that distinction rather than share a recipe.
118
+
119
+ `--_qm-tap-min` is WCAG 2.5.8's floor, read rather than re-expressed per
120
+ control: `min-*` is outside every `check:style` axis, so a second expression of
121
+ the same threshold has nothing watching it. `box-sizing` is what makes it a BOX
122
+ floor: a `<button>` takes border-box from the UA sheet and a plain element does
123
+ not, so a control on this recipe that is not a button clears the threshold on its
124
+ CONTENT box and stands a padding taller than the button beside it. Declared here,
125
+ the floor means the same height whatever element carries it; and a caller that
126
+ stretches one of these to its row gets a width the padding sits inside.
127
+
128
+ Both button families are named here rather than opting in through a marker: a
129
+ glyph button and an add affordance are not SOMETIMES flat, so a class they could
130
+ be built without is a class one of them will eventually miss.
131
+
132
+ The type half is the shared rule above, which both families are named in; what
133
+ diverges is the LEADING. Where the box's height is a line box, a button's is
134
+ `--_qm-tap-min`: a label is one line and a glyph is not text at all, so the line
135
+ box collapses onto its content (`1`) and the floor decides, rather than two
136
+ height sources competing. That is the same structural `1` a glyph row takes,
137
+ which is why it is here and not on one family, and it is what keeps a button off
138
+ the host page's leading the way the size rung keeps it off the host page's body
139
+ size. A caller whose button carries a LABEL restates the rung it wants from its
140
+ own block: the tips foot takes the meta rung, which is that surface's decision,
141
+ not the family's. */
142
+ .qm-icon-btn,
143
+ .qm-add-affordance {
144
+ line-height: 1;
145
+ box-sizing: border-box;
146
+ min-width: var(--_qm-tap-min);
147
+ min-height: var(--_qm-tap-min);
148
+ border: none;
149
+ background: transparent;
150
+ border-radius: var(--_qm-radius-inner);
151
+ cursor: pointer;
152
+ transition: background-color var(--_qm-duration-fast) var(--_qm-ease-reverse);
153
+ }
154
+ .qm-icon-btn:hover:not(:disabled),
155
+ .qm-add-affordance:hover:not(:disabled) {
156
+ background: var(--_qm-surface-hover);
157
+ }
158
+ .qm-icon-btn:disabled,
159
+ .qm-add-affordance:disabled {
160
+ opacity: var(--_qm-opacity-idle);
161
+ cursor: default;
162
+ }
163
+
164
+ /* A glyph button: card reorder/delete, array remove, the field label's guidance
165
+ marker, the tips card's foot. */
166
+ .qm-icon-btn {
167
+ display: inline-flex;
168
+ align-items: center;
169
+ justify-content: center;
170
+ gap: var(--_qm-space-half);
171
+ padding: var(--_qm-space-half) var(--_qm-space);
172
+ }
173
+
174
+ /* An add affordance: the between-card trigger and the array foot's. A pill rather
175
+ than the inner radius, because it FILLS on hover instead of tinting ink: both rest
176
+ unfilled on `--_qm-opacity-idle`, so a hover shifting ink alone would have little
177
+ to shift. The recede ladder itself stays with each caller: it is driven by their
178
+ own hover/focus state. */
179
+ .qm-add-affordance {
180
+ border-radius: var(--_qm-radius-pill);
181
+ color: var(--_qm-ink-label);
182
+ transition:
183
+ opacity var(--_qm-duration-fast) var(--_qm-ease-reverse),
184
+ background-color var(--_qm-duration-fast) var(--_qm-ease-reverse);
185
+ }
186
+ .qm-add-affordance:hover {
187
+ color: var(--_qm-ink);
188
+ }
189
+
190
+ /* A floating list of choices: the enum's listbox and the card stack's kind menu.
191
+ Both portal into the nearest `[data-qm-root]`, and both are PICKED FROM, so the
192
+ surface (its edge, its inset, its ink) and the item (its pad, its radius, the
193
+ rung a highlight fills with) are one recipe rather than two that agree until one
194
+ is edited. What a caller adds over it is its own: the enum sizes itself off its
195
+ trigger and weights the stored value.
196
+
197
+ `[data-highlighted]` is bits-ui's mark and it is the ONLY hover lane: an item
198
+ under the pointer and an item under the keyboard cursor are one state to a menu,
199
+ so the primitive marks both and a `:hover` rule beside it would paint a second
200
+ highlight the keyboard cannot reach. */
201
+ .qm-menu-surface {
202
+ padding: var(--_qm-space-half);
203
+ border: var(--_qm-border-width) solid var(--_qm-border);
204
+ border-radius: var(--_qm-radius);
205
+ background: var(--_qm-surface);
206
+ font-size: var(--_qm-text-body);
207
+ color: var(--_qm-ink);
208
+ }
209
+ .qm-menu-item {
210
+ padding: var(--_qm-space) var(--_qm-space-2);
211
+ border-radius: var(--_qm-radius-inner);
212
+ cursor: pointer;
213
+ user-select: none;
214
+ }
215
+ .qm-menu-item[data-highlighted] {
216
+ background: var(--_qm-surface-hover);
217
+ }
218
+
219
+ /* A floating surface that is READ rather than picked from: the selection popover
220
+ and the field label's guidance. The theme surface mixed toward transparent
221
+ behind a backdrop blur, so the page reads faintly through it and the surface
222
+ rung stays the mix base, which is what keeps `--qm-bg` tinting it. The lists
223
+ above stay opaque instead: a row of choices reads through a blur worse than a
224
+ row of glyphs does.
225
+
226
+ The raise and the dismissal are the recipe's too, one existence↔absence trip
227
+ taken twice, which is why the class goes on the PRESENCE
228
+ node (bits-ui's own content element, reached through its `child` snippet)
229
+ rather than on a pill inside it: the node's animations are what the primitive
230
+ waits on before unmounting, and an animation one level in is a fade it never
231
+ sees. Both frames are the inner box's own; floating-ui's outer wrapper, whose
232
+ transform is doing the positioning, is untouched. What a caller adds over this
233
+ is its own inset, its measure, and its type.
234
+
235
+ `data-ending-style` is the primitive's mark for the leaving half. The ink is
236
+ not all that leaves with it: a surface fading out is not a thing to click or to
237
+ read. That half is the caller's `inert` rather than a rule here, because the
238
+ primitive styles the same node inline with `pointer-events: auto`, which no
239
+ layered rule outranks; the attribute settles it without a cascade fight and
240
+ takes the leaving surface out of the tab order and the a11y tree with it. */
241
+ .qm-popover-surface {
242
+ background: var(--_qm-surface-popover);
243
+ backdrop-filter: blur(var(--_qm-blur));
244
+ -webkit-backdrop-filter: blur(var(--_qm-blur));
245
+ border: var(--_qm-border-width) solid var(--_qm-border);
246
+ border-radius: var(--_qm-radius);
247
+ animation: qm-pop-in var(--_qm-duration-fast) var(--_qm-ease-arrive);
248
+ }
249
+ .qm-popover-surface[data-ending-style] {
250
+ animation: qm-pop-out var(--_qm-duration-fast) var(--_qm-ease-depart);
251
+ }
252
+ @keyframes qm-pop-in {
253
+ from {
254
+ opacity: 0;
255
+ transform: scale(0.94);
256
+ }
257
+ to {
258
+ opacity: 1;
259
+ transform: none;
260
+ }
261
+ }
262
+ @keyframes qm-pop-out {
263
+ from {
264
+ opacity: 1;
265
+ transform: none;
266
+ }
267
+ to {
268
+ opacity: 0;
269
+ transform: scale(0.94);
270
+ }
271
+ }
272
+ }
@@ -0,0 +1,52 @@
1
+ import type { Diagnostic } from '@quillmark/wasm';
2
+ /** A field's routing address. `card` is `undefined` for the main card; a
3
+ * composable card slot is a stable session id (the editor's own bookkeeping)
4
+ * once resolved, or an ABSOLUTE document-array index straight off a parsed
5
+ * `DocPath` (resolve via {@link resolveCardKey} before merging with id-keyed
6
+ * sources). `field` `undefined` addresses the body (the field-less leaf). */
7
+ export interface FieldKey {
8
+ card?: string | number;
9
+ field?: string;
10
+ }
11
+ /** Canonical string key for a `FieldKey`: the routing `Map`'s key space. */
12
+ export declare function fieldKeyToString(k: FieldKey): string;
13
+ /**
14
+ * Route a canonical `DocPath` (a `Diagnostic.path`, or a `ContentHit` /
15
+ * `FieldRegion` field address; one grammar) to a `FieldKey`, `card` the ABSOLUTE
16
+ * document-array index. `/core`'s `addrForFieldPath` IS this walk — an `Addr` is a
17
+ * positional `FieldKey` — so routing reads the one public inverse rather than a
18
+ * second copy of the grammar that drifts from it. `undefined` for a path naming no
19
+ * single commit address (a nested / array-element path, a field-rooted one, a
20
+ * malformed one).
21
+ */
22
+ export declare function parsePath(path: string): FieldKey | undefined;
23
+ /**
24
+ * Resolve a positional `FieldKey` (an ABSOLUTE document-array index, straight off
25
+ * {@link parsePath}) to the editor's stable-id keying using the LIVE `cardIds`
26
+ * array: re-resolved fresh on every call (never cached), the same "resolve only
27
+ * at the point of use" discipline `cardIndexOf` applies to writes (VISUAL_EDITOR
28
+ * §"The address is the spine"). A key already id-keyed (`card` a string) or main
29
+ * (`card` undefined) passes through unchanged. `undefined` when the index is out
30
+ * of the current card array: the diagnostic is dropped rather than mis-routed.
31
+ */
32
+ export declare function resolveCardKey(key: FieldKey, cardIds: readonly string[]): FieldKey | undefined;
33
+ /** One diagnostic paired with the `FieldKey` it targets. */
34
+ export interface RoutedDiagnostic {
35
+ key: FieldKey;
36
+ diagnostic: Diagnostic;
37
+ }
38
+ /**
39
+ * Route a path-keyed producer's raw `Diagnostic[]` (validate / external) to the
40
+ * editor's stable-id keying: the ONE door producers #1/#3 take, `parsePath` and
41
+ * `resolveCardKey` in a single pass. An entry drops rather than mis-routes when it
42
+ * carries no `path`, when `parsePath` cannot place it (a nested / array-element
43
+ * address), or when its absolute card index is out of the live `cardIds`.
44
+ */
45
+ export declare function routeAndResolve(diagnostics: Diagnostic[] | undefined, cardIds: readonly string[]): RoutedDiagnostic[];
46
+ /**
47
+ * Merge N routed groups into `Map<fieldKeyToString(key), Diagnostic[]>`.
48
+ * Dedupes an identical `(severity, message)` pair landing on the same key from
49
+ * more than one group (e.g. the same warning present in both `validate()` and
50
+ * an external feed); within a key, errors sort before warnings.
51
+ */
52
+ export declare function mergeDiagnostics(...groups: RoutedDiagnostic[][]): Map<string, Diagnostic[]>;
@@ -0,0 +1,83 @@
1
+ import { addrForFieldPath } from '../core/address.js';
2
+ /** Canonical string key for a `FieldKey`: the routing `Map`'s key space. */
3
+ export function fieldKeyToString(k) {
4
+ return `${k.card ?? 'main'}:${k.field ?? '$body'}`;
5
+ }
6
+ /**
7
+ * Route a canonical `DocPath` (a `Diagnostic.path`, or a `ContentHit` /
8
+ * `FieldRegion` field address; one grammar) to a `FieldKey`, `card` the ABSOLUTE
9
+ * document-array index. `/core`'s `addrForFieldPath` IS this walk — an `Addr` is a
10
+ * positional `FieldKey` — so routing reads the one public inverse rather than a
11
+ * second copy of the grammar that drifts from it. `undefined` for a path naming no
12
+ * single commit address (a nested / array-element path, a field-rooted one, a
13
+ * malformed one).
14
+ */
15
+ export function parsePath(path) {
16
+ return addrForFieldPath(path);
17
+ }
18
+ /**
19
+ * Resolve a positional `FieldKey` (an ABSOLUTE document-array index, straight off
20
+ * {@link parsePath}) to the editor's stable-id keying using the LIVE `cardIds`
21
+ * array: re-resolved fresh on every call (never cached), the same "resolve only
22
+ * at the point of use" discipline `cardIndexOf` applies to writes (VISUAL_EDITOR
23
+ * §"The address is the spine"). A key already id-keyed (`card` a string) or main
24
+ * (`card` undefined) passes through unchanged. `undefined` when the index is out
25
+ * of the current card array: the diagnostic is dropped rather than mis-routed.
26
+ */
27
+ export function resolveCardKey(key, cardIds) {
28
+ if (typeof key.card !== 'number')
29
+ return key;
30
+ const id = key.card >= 0 && key.card < cardIds.length ? cardIds[key.card] : undefined;
31
+ return id != null ? { card: id, field: key.field } : undefined;
32
+ }
33
+ /**
34
+ * Route a path-keyed producer's raw `Diagnostic[]` (validate / external) to the
35
+ * editor's stable-id keying: the ONE door producers #1/#3 take, `parsePath` and
36
+ * `resolveCardKey` in a single pass. An entry drops rather than mis-routes when it
37
+ * carries no `path`, when `parsePath` cannot place it (a nested / array-element
38
+ * address), or when its absolute card index is out of the live `cardIds`.
39
+ */
40
+ export function routeAndResolve(diagnostics, cardIds) {
41
+ const out = [];
42
+ for (const d of diagnostics ?? []) {
43
+ if (!d.path)
44
+ continue;
45
+ const key = parsePath(d.path);
46
+ const resolved = key && resolveCardKey(key, cardIds);
47
+ if (resolved)
48
+ out.push({ key: resolved, diagnostic: d });
49
+ }
50
+ return out;
51
+ }
52
+ /** Severity ordering: errors sort before warnings within a field's list (never dropped, just ranked: VISUAL_EDITOR §Diagnostics "nothing gates" extends to nothing hides). */
53
+ const SEVERITY_RANK = { error: 0, warning: 1 };
54
+ /**
55
+ * Merge N routed groups into `Map<fieldKeyToString(key), Diagnostic[]>`.
56
+ * Dedupes an identical `(severity, message)` pair landing on the same key from
57
+ * more than one group (e.g. the same warning present in both `validate()` and
58
+ * an external feed); within a key, errors sort before warnings.
59
+ */
60
+ export function mergeDiagnostics(...groups) {
61
+ const byKey = new Map();
62
+ const seen = new Set();
63
+ for (const group of groups) {
64
+ for (const { key, diagnostic } of group) {
65
+ const k = fieldKeyToString(key);
66
+ // Collision-proof key: JSON-encode the triple so arbitrary `message` text
67
+ // can never merge two distinct triples into one (a real diagnostic dropped).
68
+ const dedupeKey = JSON.stringify([k, diagnostic.severity, diagnostic.message]);
69
+ if (seen.has(dedupeKey))
70
+ continue;
71
+ seen.add(dedupeKey);
72
+ const arr = byKey.get(k);
73
+ if (arr)
74
+ arr.push(diagnostic);
75
+ else
76
+ byKey.set(k, [diagnostic]);
77
+ }
78
+ }
79
+ for (const arr of byKey.values()) {
80
+ arr.sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]);
81
+ }
82
+ return byKey;
83
+ }
@@ -0,0 +1,21 @@
1
+ /** The three names one field spends in the DOM. */
2
+ export interface FieldDomIds {
3
+ /** The control's own id: a `<label for>` target where the control is labelable. */
4
+ control: string;
5
+ /** The label's own id: the `aria-labelledby` target where `for` cannot reach. */
6
+ label: string;
7
+ /** Where the `description` text is parked for `aria-describedby`. */
8
+ description: string;
9
+ }
10
+ export declare function fieldDomIds(uid: string, leafKey: string): FieldDomIds;
11
+ /**
12
+ * A group panel's own id: what its header's `aria-controls` names, and the only
13
+ * name in the DOM that is a card's rather than a field's. `card` follows the leaf
14
+ * key's convention (`undefined` is the main card), so a panel and the fields
15
+ * inside it carry the same card token.
16
+ *
17
+ * The `-g-` infix cannot land on a field id: a field's is the escaped
18
+ * `card:field`, whose separator escapes to `-3a-`, and the two differ at exactly
19
+ * that position for every card token the id space mints.
20
+ */
21
+ export declare function groupPanelId(uid: string, card: string | undefined, group: string): string;
@@ -0,0 +1,36 @@
1
+ // A field's DOM ids. The DOM needs three names per field (the control a `<label
2
+ // for>` points at, the label an `aria-labelledby` control points back to, and the
3
+ // parked description an `aria-describedby` reads) and they are derived from the
4
+ // leaf key so the DOM shares the ONE per-field identity space the registry and
5
+ // diagnostics already collapse to (`fieldKeyToString`), rather than minting a
6
+ // second.
7
+ //
8
+ // `uid` is the editor instance's own (`$props.id()`, stable across SSR and
9
+ // hydration). The leaf-key space is unique per EDITOR, not per document: two
10
+ // `VisualEditor`s on a page both hold `main:subject`, and a duplicate `id` makes
11
+ // `for` resolve to whichever mounted first.
12
+ /**
13
+ * `id` syntax admits far less than a leaf key spends (`main:subject`, `$body`).
14
+ * The escape is INJECTIVE (`-` introduces it, so `-` escapes itself), which a
15
+ * plain replace-with-dash is not: `a:b` and `a-b` would collapse onto one id.
16
+ */
17
+ function escapeId(key) {
18
+ return key.replace(/[^A-Za-z0-9_]/g, (c) => `-${c.codePointAt(0).toString(16)}-`);
19
+ }
20
+ export function fieldDomIds(uid, leafKey) {
21
+ const base = `qm-${uid}-${escapeId(leafKey)}`;
22
+ return { control: base, label: `${base}-label`, description: `${base}-desc` };
23
+ }
24
+ /**
25
+ * A group panel's own id: what its header's `aria-controls` names, and the only
26
+ * name in the DOM that is a card's rather than a field's. `card` follows the leaf
27
+ * key's convention (`undefined` is the main card), so a panel and the fields
28
+ * inside it carry the same card token.
29
+ *
30
+ * The `-g-` infix cannot land on a field id: a field's is the escaped
31
+ * `card:field`, whose separator escapes to `-3a-`, and the two differ at exactly
32
+ * that position for every card token the id space mints.
33
+ */
34
+ export function groupPanelId(uid, card, group) {
35
+ return `qm-${uid}-${escapeId(card ?? 'main')}-g-${escapeId(group)}`;
36
+ }
@@ -0,0 +1,19 @@
1
+ import type { Document, CardAddr } from '@quillmark/wasm';
2
+ /**
3
+ * Merge `patch` into `$ext.editor` on the card `addr` targets (absent `card` =
4
+ * main). A key whose patch value is `undefined` is DROPPED; every other key in the
5
+ * namespace is carried through untouched.
6
+ *
7
+ * **The namespace is the write unit.** `storeExtNamespace` replaces the namespace
8
+ * it targets (preserving sibling namespaces, but not sibling keys) so a writer
9
+ * that stores only its own key silently destroys the others, and
10
+ * `removeExtNamespace` destroys all of them. `tips` and `title` are siblings here,
11
+ * so a namespace-replacing dismissal would wipe every renamed card's title. Routing
12
+ * every writer through this function is what makes that unexpressible rather than
13
+ * merely documented: key N+1 inherits the merge instead of re-deriving it.
14
+ *
15
+ * The drop is an explicit `delete`, not a stored `undefined`: whether a JS
16
+ * `undefined` survives the wasm-bindgen crossing is not a property worth depending
17
+ * on.
18
+ */
19
+ export declare function patchEditorExt(doc: Document, addr: CardAddr, patch: Record<string, unknown>): void;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Merge `patch` into `$ext.editor` on the card `addr` targets (absent `card` =
3
+ * main). A key whose patch value is `undefined` is DROPPED; every other key in the
4
+ * namespace is carried through untouched.
5
+ *
6
+ * **The namespace is the write unit.** `storeExtNamespace` replaces the namespace
7
+ * it targets (preserving sibling namespaces, but not sibling keys) so a writer
8
+ * that stores only its own key silently destroys the others, and
9
+ * `removeExtNamespace` destroys all of them. `tips` and `title` are siblings here,
10
+ * so a namespace-replacing dismissal would wipe every renamed card's title. Routing
11
+ * every writer through this function is what makes that unexpressible rather than
12
+ * merely documented: key N+1 inherits the merge instead of re-deriving it.
13
+ *
14
+ * The drop is an explicit `delete`, not a stored `undefined`: whether a JS
15
+ * `undefined` survives the wasm-bindgen crossing is not a property worth depending
16
+ * on.
17
+ */
18
+ export function patchEditorExt(doc, addr, patch) {
19
+ // `getExtNamespace` reads just this namespace rather than serializing the whole
20
+ // card to fish out one `$ext` slot.
21
+ const next = { ...(doc.getExtNamespace(addr, 'editor') ?? {}) };
22
+ for (const [key, value] of Object.entries(patch)) {
23
+ if (value === undefined)
24
+ delete next[key];
25
+ else
26
+ next[key] = value;
27
+ }
28
+ doc.storeExtNamespace(addr, 'editor', next);
29
+ }
@@ -0,0 +1,39 @@
1
+ <script lang="ts">
2
+ // The one glyph renderer: a name off the closed set in `nodes.ts`, drawn against
3
+ // the shared frame. Stroke, fill and cap are the frame rather than props — a glyph
4
+ // that strokes differently from its neighbours is a decision no call site has, and
5
+ // `currentColor` is what puts the hue on the button's `color`, where the surface's
6
+ // hover and open transitions already reach it.
7
+ import { ICONS, type IconName } from './nodes.js';
8
+ import type { SVGAttributes } from 'svelte/elements';
9
+
10
+ interface Props extends Omit<SVGAttributes<SVGSVGElement>, 'name'> {
11
+ name: IconName;
12
+ /** The frame's edge, in px. The card controls pass none and take the default,
13
+ * then size the glyph from `--_qm-glyph-control` in CSS, where the header's
14
+ * right hang is measured against the same number. */
15
+ size?: number;
16
+ }
17
+ let { name, size = 24, ...rest }: Props = $props();
18
+ </script>
19
+
20
+ <!-- `aria-hidden` ahead of the spread, so it is the default rather than the rule: every
21
+ glyph in this package sits inside a button that carries the name, and a call site
22
+ that has one to say overrides it. -->
23
+ <svg
24
+ xmlns="http://www.w3.org/2000/svg"
25
+ width={size}
26
+ height={size}
27
+ viewBox="0 0 24 24"
28
+ fill="none"
29
+ stroke="currentColor"
30
+ stroke-width="2"
31
+ stroke-linecap="round"
32
+ stroke-linejoin="round"
33
+ aria-hidden="true"
34
+ {...rest}
35
+ >
36
+ {#each ICONS[name] as [tag, attrs]}
37
+ <svelte:element this={tag} {...attrs} />
38
+ {/each}
39
+ </svg>
@@ -0,0 +1,12 @@
1
+ import { type IconName } from './nodes.js';
2
+ import type { SVGAttributes } from 'svelte/elements';
3
+ interface Props extends Omit<SVGAttributes<SVGSVGElement>, 'name'> {
4
+ name: IconName;
5
+ /** The frame's edge, in px. The card controls pass none and take the default,
6
+ * then size the glyph from `--_qm-glyph-control` in CSS, where the header's
7
+ * right hang is measured against the same number. */
8
+ size?: number;
9
+ }
10
+ declare const Icon: import("svelte").Component<Props, {}, "">;
11
+ type Icon = ReturnType<typeof Icon>;
12
+ export default Icon;