@quillmark/svelte 0.3.2 → 0.4.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.
package/README.md CHANGED
@@ -200,7 +200,7 @@ onPick: (at) => visualEditor.setCaret(at);
200
200
  onCaretMove: preview.focusPosition;
201
201
  ```
202
202
 
203
- A pick carries a caret where the compile tracks the content under the point, and the field alone where it tracks only the placement — a scalar the plate prints without tracking. `setCaret` takes both: an absent `pos` reveals and focuses the field, which is the whole of what a click on plate-placed ink can mean. The address may name an array element (`main.references.0`), which lands on that row.
203
+ A pick carries a caret where the compile tracks the content under the point, and the field alone where it tracks only the placement — a scalar the plate prints without tracking. `setCaret` takes both: an absent `pos` reveals and focuses the field, which is the whole of what a click on plate-placed ink can mean. The address may name an array element (`main.keywords[0]`), which lands on that row.
204
204
 
205
205
  A consumer holding an `Addr` of its own maps it with `fieldPathForAddr` (from `@quillmark/svelte/core`). The playground app's split-pane route is the full reference shell: one session, both bridge directions, the preview following edits, and diagnostics routed inline.
206
206
 
package/THEMING.md CHANGED
@@ -54,8 +54,8 @@ The poles take a colour, not `transparent` or `inherit`: the raised surfaces, th
54
54
  | `--qm-bg` | `#fff` / `#272729` | Base surface: a card, and the preview's sheet. Fields, the popover and the column behind the cards each step off it. |
55
55
  | `--qm-fg` | `#1c1c1c` / `#d7dadc` | Base ink. Body text, labels, and borders step off it. |
56
56
  | `--qm-accent` | `#71717a` / `#a1a1aa` | Focus rings, active marks, the preview's active field box. The surface spends no hue of its own, so this is the one it wears. |
57
- | `--qm-danger` | `#c5221f` | Error diagnostics, the required marker, the delete glyph. |
58
- | `--qm-warning` | `#b25000` | Warning diagnostics. |
57
+ | `--qm-danger` | `#c5221f` / `#ff9c94` | Error diagnostics, the required marker, the delete glyph. |
58
+ | `--qm-warning` | `#b25000` / `#e0b44c` | Warning diagnostics. |
59
59
  | `--qm-font` | `ui-sans-serif, system-ui, sans-serif` | The editor surface's font family. Controls and buttons take it too, in place of the UA face. |
60
60
  | `--qm-font-mono` | `ui-monospace, monospace` | The monospace face: the JSON array control, the tips card. |
61
61
  | `--qm-font-size` | `0.875rem` | Chrome text: every control's size, the anchor the ramp derives up (title) and down (label/meta) from, and the anchor a document body sits one small step over. |
@@ -80,7 +80,7 @@ Declared before our sheet it fixes the order outright; declared after, `qm` is a
80
80
 
81
81
  Ink steps `fg → bg` and the borders and hover fills step `bg → fg`, mixed in **oklab**, so inverting the two poles inverts all of them together.
82
82
 
83
- **Planes are the exception**, and deliberately: the raised plane is the brighter one in both schemes, which is what makes a card read as a card rather than as a differently-tinted page. So the column under the cards sinks from `--qm-bg` toward shadow and a floating surface rises from it toward light, and neither reads the ink pole. Each direction takes a step sized to the scheme it is in, which is why pinning a dark palette while your `color-scheme` still says light leaves the column barely off its cards — the one case §"Make it yours" tells you to pin the scheme alongside the poles.
83
+ **Planes are the exception**, and deliberately: the raised plane is the brighter one in both schemes, which is what makes a card read as a card rather than as a differently-tinted page. So the column under the cards sinks from `--qm-bg` toward shadow and a floating surface rises from it toward light, and neither reads the ink pole. How far each steps is a function of the card's own lightness — a dark card has to spend more before the plane above it reads as lifted — so a palette pinned at neither pole takes a proportionate step without being told which scheme it is in.
84
84
 
85
85
  Which way the poles default is **your `color-scheme`**, not the operating system's. Declare the scheme your app is in and the surface lands on it, along with native chrome, which reads the same property:
86
86
 
@@ -106,7 +106,11 @@ Setting the poles yourself wins over the default in both schemes, so a fixed pal
106
106
  }
107
107
  ```
108
108
 
109
- A palette **pinned against** your own scheme is the one case needing a second line: the surfaces follow your dials, but native chrome follows `color-scheme`, so pin it with them (`.my-editor { color-scheme: dark }`). Skip that line and what the browser paints — the selection highlight, the scrollbar, the spinner — stays on whichever scheme the host inherits.
109
+ That cuts both ways for the status hues: `--qm-danger` and `--qm-warning` each default to a pair calibrated against the card at both poles, and one pinned value replaces both arms, so a red measured against white carries into the dark scheme unmeasured. Pin them under your own scheme rule (`.dark { --qm-danger: … }`) where the colour your app means by error is itself scheme-aware.
110
+
111
+ A palette **pinned against** your own scheme still wants a second line, for a smaller reason than it used to be: the planes read your card's lightness, so the surface itself lands right either way, but the poles' own defaults, the status hues and native chrome all still read `color-scheme`. Pin it with them (`.my-editor { color-scheme: dark }`). Skip that line and what the browser paints — the selection highlight, the scrollbar, the spinner — stays on whichever scheme the host inherits, and a status hue you left to us is measured against the wrong card.
112
+
113
+ Pinning **one mount** rather than a page is the `style` prop, which lands on the same root: `<Preview style="color-scheme: dark" />`. There is no `scheme` prop, and deliberately — an inline declaration of ours would be the one thing your own CSS could not beat.
110
114
 
111
115
  No JS runs and no media query: the derivation resolves through the cascade at paint time, so there is no `dark` prop and no mode toggle. The consumer's palette decides.
112
116
 
@@ -121,7 +125,7 @@ import '@quillmark/svelte/preset';
121
125
  ```html
122
126
  <body class="qm-page">
123
127
  <p class="qm-label">quill</p>
124
- <pre class="qm-readout">usaf_memo@0.2.0</pre>
128
+ <pre class="qm-readout">specimen@1.0.0</pre>
125
129
  </body>
126
130
  ```
127
131
 
@@ -198,4 +202,4 @@ The derived surface scale (surface / border / ink rungs, the blur radius, the po
198
202
 
199
203
  ## Requirements
200
204
 
201
- `@layer`, `color-mix()` and `light-dark()`: Baseline since 2022, 2023 and 2024 respectively. `@property` is the one feature the surface does not require: where it is missing, an invalid length dial collapses the surface rather than falling back.
205
+ `@layer`, `color-mix()`, `light-dark()` and relative colour syntax (`oklab(from …)`): Baseline since 2022, 2023, 2024 and 2024 respectively — the last is what lets a plane step by the card's own lightness. `@property` is the one feature the surface does not require: where it is missing, an invalid length dial collapses the surface rather than falling back.
@@ -63,7 +63,7 @@ export declare function cardPath(index: number, kinds: readonly string[]): DocPa
63
63
  /**
64
64
  * The inverse: a canonical `DocPath` back to the `Addr` the document verbs take, or
65
65
  * `undefined` for a path that names no single commit address — a nested or
66
- * array-element path (`main.references.0`, which {@link elementAddrForFieldPath}
66
+ * array-element path (`main.keywords[0]`, which {@link elementAddrForFieldPath}
67
67
  * takes instead), a field-rooted one, or a malformed one. A bare card and a `.body`
68
68
  * terminal both land on the field-less `{card: i}` the body leaf answers to.
69
69
  *
@@ -72,16 +72,10 @@ export declare function cardPath(index: number, kinds: readonly string[]): DocPa
72
72
  */
73
73
  export declare function addrForFieldPath(path: DocPath): Addr | undefined;
74
74
  /**
75
- * An array element's address, split. `main.references.0` and `main.references[0]`
76
- * both land here; anything {@link addrForFieldPath} can name, and anything whose
77
- * trailing segment is not an index, does not.
78
- *
79
- * Both spellings, because the boundary mints one and formats the other: `regions()`
80
- * and `positionAt` emit `main.references.0`, which `parseDocPath` reads as a field
81
- * literally named `"0"`, while `formatDocPath` spells the index segment
82
- * `main.references[0]`. A trailing all-digits field name is therefore an index that
83
- * lost its brackets — but only under a field the schema declares an array, the
84
- * caller's guard to apply: this module holds the grammar and no schema.
75
+ * An array element's address, split: `main.keywords[0]` lands here, and anything
76
+ * {@link addrForFieldPath} can name — or whose trailing segment is not an index —
77
+ * does not. `regions()`, `positionAt` and `formatDocPath` all spell the index
78
+ * segment bracketed, so there is one spelling to read and none to bridge.
85
79
  */
86
80
  export declare function elementAddrForFieldPath(path: DocPath): ElementAddr | undefined;
87
81
  /** An array element's address: the array's `Addr`, and which element of it. */
@@ -37,7 +37,7 @@ export function cardPath(index, kinds) {
37
37
  /**
38
38
  * The inverse: a canonical `DocPath` back to the `Addr` the document verbs take, or
39
39
  * `undefined` for a path that names no single commit address — a nested or
40
- * array-element path (`main.references.0`, which {@link elementAddrForFieldPath}
40
+ * array-element path (`main.keywords[0]`, which {@link elementAddrForFieldPath}
41
41
  * takes instead), a field-rooted one, or a malformed one. A bare card and a `.body`
42
42
  * terminal both land on the field-less `{card: i}` the body leaf answers to.
43
43
  *
@@ -49,32 +49,21 @@ export function addrForFieldPath(path) {
49
49
  return segs && addrForSegs(segs);
50
50
  }
51
51
  /**
52
- * An array element's address, split. `main.references.0` and `main.references[0]`
53
- * both land here; anything {@link addrForFieldPath} can name, and anything whose
54
- * trailing segment is not an index, does not.
55
- *
56
- * Both spellings, because the boundary mints one and formats the other: `regions()`
57
- * and `positionAt` emit `main.references.0`, which `parseDocPath` reads as a field
58
- * literally named `"0"`, while `formatDocPath` spells the index segment
59
- * `main.references[0]`. A trailing all-digits field name is therefore an index that
60
- * lost its brackets — but only under a field the schema declares an array, the
61
- * caller's guard to apply: this module holds the grammar and no schema.
52
+ * An array element's address, split: `main.keywords[0]` lands here, and anything
53
+ * {@link addrForFieldPath} can name — or whose trailing segment is not an index —
54
+ * does not. `regions()`, `positionAt` and `formatDocPath` all spell the index
55
+ * segment bracketed, so there is one spelling to read and none to bridge.
62
56
  */
63
57
  export function elementAddrForFieldPath(path) {
64
58
  const segs = segsOf(path);
65
59
  if (!segs)
66
60
  return undefined;
67
61
  const last = segs[segs.length - 1];
68
- const index = last?.seg === 'index'
69
- ? last.index
70
- : last?.seg === 'field' && /^\d+$/.test(last.name)
71
- ? Number(last.name)
72
- : undefined;
73
- if (index == null)
62
+ if (last?.seg !== 'index')
74
63
  return undefined;
75
64
  const field = addrForSegs(segs.slice(0, -1));
76
65
  // A body has no elements: the parent must name a field.
77
- return field?.field != null ? { field, index } : undefined;
66
+ return field?.field != null ? { field, index: last.index } : undefined;
78
67
  }
79
68
  /** `parseDocPath`, with a malformed path as `undefined` rather than a throw. The gate
80
69
  * is read outside the try: an uninitialized core is not a malformed path, and would
@@ -44,11 +44,10 @@ function markInputRule(regexp, markType, delimLen) {
44
44
  * `heading` being wrapped becomes a `paragraph` first.
45
45
  *
46
46
  * `list_item` is `block+`, so `list_item > heading` is *representable* and a bare
47
- * wrap mints it (a shape no quill renders): the reference quill derives an item's
48
- * numbering and indent from the container path and typesets the item's blocks as
49
- * body paragraphs (`usaf_memo`'s `render-body`), where a heading resolves to
50
- * nothing. This is the wrap-side route into that shape; the `# ` rule guards the
51
- * other, in {@link markdownInputRules}.
47
+ * wrap mints it — a shape markdown has no spelling for, so nothing downstream is
48
+ * obliged to typeset it and the gesture that made it cannot be read back. This is
49
+ * the wrap-side route into it; the `# ` rule guards the other, in
50
+ * {@link markdownInputRules}.
52
51
  *
53
52
  * `item` is the second guard, and it is about the gesture rather than the shape: the
54
53
  * rule declines at the start of an item's own first block ({@link openingAnItem}).
@@ -84,16 +84,17 @@ li.ProseMirror-selectednode::after {
84
84
  emits, minus the four the UA already renders correctly (`strong`, `em`, `u`, `s`)
85
85
  and the blockquote above.
86
86
 
87
- Each rule answers to the reference quill rather than to a browser default or to a
87
+ Each rule answers to the leaf's own job rather than to a browser default or to a
88
88
  web editor's chrome, because the leaf sits beside a live preview of the same
89
- content. `usaf_memo` sets `raw` in a monospace face and adds nothing; it renders a
90
- heading as a bold run-in prepended to the following paragraph, with the level
91
- never read; it gives a link no decoration at all. So a leaf shows that a span is
92
- code and a line is a heading: never a prediction of the size paper will set it
93
- at, which the package cannot know for a quill it has not seen.
89
+ content. A leaf shows *that* a span is code and a line is a heading: never a
90
+ prediction of the size paper will set it at, which the package cannot know for a
91
+ quill it has not seen. The reference quill is the case in point — it sets `raw` in
92
+ the face it bundles, scales a heading by its level and colours it, and gives a link
93
+ no decoration at all — and mirroring any of that here would be a guess about the
94
+ next quill.
94
95
 
95
96
  Scoping is by tag, and the schema does the rest: `h1`–`h6`, `hr` and the lists
96
- cannot exist under the inline schema, so no rule for them can land on a `subject`
97
+ cannot exist under the inline schema, so no rule for them can land on an inline
97
98
  field however that field is mounted. The two marks are the exception: `code` and
98
99
  `link` are in both schemas, and a `plaintext` leaf strips them at decode while
99
100
  mounting a schema that still carries them, so a paste can put either in one. The
@@ -89,8 +89,16 @@
89
89
  /* Neutral: the surface spends no hue on a state it can draw in grey. The pair
90
90
  clears WCAG 2.4.11's 3:1 against the card at both poles, at 4.8:1 and 5.8:1. */
91
91
  --_qm-accent: var(--qm-accent, light-dark(#71717a, #a1a1aa));
92
- --_qm-danger: var(--qm-danger, #c5221f);
93
- --_qm-warning: var(--qm-warning, #b25000);
92
+ /* The two hues the surface does spend, and it spends them on a state. Each pole
93
+ is its own figure rather than one ink shown twice: a hue calibrated against
94
+ white reads as a stain on a dark card. All four clear WCAG 1.4.3's 4.5:1
95
+ against the card, which is the bar the label rung asks for and the one the
96
+ glyphs drawn in these hues clear on the way past 1.4.11's 3:1 — 5.8:1 and
97
+ 5.2:1 light, 7.4:1 and 7.7:1 dark. The figures are the host scale's
98
+ (`preset/scale.css`), so a page's status line and a card's diagnostic carry
99
+ one colour meaning error and one meaning warning. */
100
+ --_qm-danger: var(--qm-danger, light-dark(#c5221f, #ff9c94));
101
+ --_qm-warning: var(--qm-warning, light-dark(#b25000, #e0b44c));
94
102
  --_qm-font: var(--qm-font, ui-sans-serif, system-ui, sans-serif);
95
103
  --_qm-font-mono: var(--qm-font-mono, ui-monospace, monospace);
96
104
 
@@ -118,46 +126,45 @@
118
126
  a channel of each other, which is where a card with little light left to
119
127
  give up leaves them.
120
128
 
121
- Each takes two arms, because one figure cannot serve both poles: a dark
122
- surface has to spend more of what little it has before the card above it
123
- reads as lifted. Pinning a dark palette under a light `color-scheme` takes
124
- the light arm with it
125
- (THEMING §"The surface follows your colour scheme"), which is a step you
126
- can barely see, so a host that pins declares the scheme with it.
129
+ How far each sinks is a function of the card's own L, not of the scheme the
130
+ host declared: a dark surface has to spend more of what little it has
131
+ before the card above it reads as lifted, so the two calibrated distances
132
+ — 0.0214 off white, 0.0553 off the dark default — interpolate linearly
133
+ between. A card pinned at neither pole takes a proportionate step whatever
134
+ `color-scheme` says, which is the half of the derivation that reads a
135
+ colour rather than a declaration.
127
136
 
128
- The ground steps by subtraction where every other rung mixes: a mix toward
129
- black is a ratio, so it scales what it takes and pulls the result toward
130
- neutral, and a ground is a fixed distance that keeps the pole's hue. Its
131
- arms are 0.0214 of oklab L in light and 0.0553 in dark, with a/b terms
132
- carrying a cool cast a mix toward black would strip, so the default lands
133
- on `#f7f8f9` and `#1a1a1b` exactly and a turned `--qm-bg` tints the plane
134
- under it.
135
-
136
- `raised` is a no-op at the light pole, where the card is already white
137
- and there is nowhere further to go, so a floating surface separates on
138
- its hairline in light and on the step in dark. */
137
+ Both sink by subtraction where every other rung mixes: a mix toward black
138
+ is a ratio, so it scales what it takes and pulls the result toward
139
+ neutral, and a ground is a fixed distance. `a b` pass through, so the
140
+ plane keeps the pole's hue and a turned `--qm-bg` tints it. The defaults
141
+ land within a channel of `#f7f8f9` and `#1a1a1b`. Below L ≈ 0.065 the
142
+ ground clamps to black, which is where a near-black card leaves no room
143
+ for a ground of its own. */
139
144
  --_qm-surface: var(--_qm-bg);
140
- --_qm-surface-sunken: light-dark(
141
- oklab(from var(--_qm-bg) calc(l - 0.0214) calc(a - 0.00065) calc(b - 0.00158)),
142
- oklab(from var(--_qm-bg) calc(l - 0.0553) calc(a - 0.00048) calc(b + 0.00165))
143
- );
144
- --_qm-surface-well: light-dark(
145
- color-mix(in oklab, var(--_qm-bg), #000 4%),
146
- color-mix(in oklab, var(--_qm-bg), #000 20%)
147
- );
145
+ --_qm-surface-sunken: oklab(from var(--_qm-bg) calc(l - (0.0214 + 0.0467 * (1 - l))) a b);
146
+ --_qm-surface-well: oklab(from var(--_qm-bg) calc(l - (0.04 + 0.0202 * (1 - l))) a b);
147
+ /* `raised` is the same ramp spelled as a mix, its step 0.08 × (1 − l): the light
148
+ arm is zero, a white card having nowhere further to go, so a floating surface
149
+ separates on its hairline and `--_qm-lift` there and the step joins them in
150
+ dark. */
148
151
  --_qm-surface-raised: color-mix(in oklab, var(--_qm-bg), #fff 8%);
149
152
  --_qm-surface-hover: color-mix(in oklab, var(--_qm-bg), var(--_qm-fg) 8%);
150
153
  --_qm-border: color-mix(in oklab, var(--_qm-bg), var(--_qm-fg) 17%);
151
154
  /* A rule that separates without structuring: the lines between a table's cells,
152
- where the frame around them is the structure. Half the border's mix rather
153
- than a surface rung of its own, because it is a stroke and reads against the
154
- surface behind it, not a fill stacked on one. */
155
- --_qm-border-faint: color-mix(in oklab, var(--_qm-bg), var(--_qm-fg) 9%);
156
- /* A fourth step between `label` and `ghost` is a number with no fourth kind of
157
- text under it. */
155
+ where the frame around them is the structure. Half the border, and read off
156
+ it rather than restated as a percentage of its own, so the two hold their
157
+ relationship at any mix: a stroke against the surface behind it, not a fill
158
+ stacked on one. */
159
+ --_qm-border-faint: color-mix(in oklab, var(--_qm-bg), var(--_qm-border) 50%);
160
+ /* Two rungs, and there is no third: a step below `label` lands under 3:1 on the
161
+ well a field draws it against, and what would take it — a placeholder, an
162
+ unset value, the resolved `default:` — is content a reader has to read
163
+ (WCAG 1.4.3), not decoration. So tone carries hierarchy alone, and a leaf
164
+ saying shown-never-written says it against the `ink` a written value takes,
165
+ in italic where it has no written neighbour to be read against. */
158
166
  --_qm-ink: var(--_qm-fg);
159
167
  --_qm-ink-label: color-mix(in oklab, var(--_qm-fg), var(--_qm-bg) 35%);
160
- --_qm-ink-ghost: color-mix(in oklab, var(--_qm-fg), var(--_qm-bg) 60%);
161
168
  /* The arrival wash (`core/bloom.ts`). Its alpha is a peak, not a tint: the
162
169
  wash animates from and back to zero, never held, so it reads over the leaf it
163
170
  lands on while the leaf rests clean. */
@@ -208,16 +215,21 @@
208
215
  --_qm-tips-font: 'Courier Prime', 'Courier New', Courier, ui-monospace, monospace;
209
216
 
210
217
  /* ── Ratios and effects ──────────────────────────────────────────────────
211
- No palette fixes these, so they are their own rungs. There is no shadow
212
- rung: elevation is a tone rung and a hairline, which say the same thing at
213
- both poles, where a shadow's offset states a light source the poles do not
214
- carry. `check:style` forbids the property outright,
215
- here as well as over the surfaces, so the absence is gated rather than
216
- remembered. */
218
+ No palette fixes these, so they are their own rungs. */
217
219
  --_qm-blur: 8px;
218
220
  /* The popover's translucency: the raised surface mixed toward transparent, so
219
221
  the page reads faintly through the pill. */
220
222
  --_qm-surface-popover: color-mix(in oklab, var(--_qm-surface-raised) 82%, transparent);
223
+ /* What a surface with no plane under it lifts on. A tone rung cannot carry that at
224
+ both poles: `raised` is a no-op against a white card (§Colour), so in light the
225
+ hairline is otherwise the whole separation over the body prose these float on.
226
+ Offset-free is the whole of it: an offset states where the light is and no pole
227
+ carries that, where a halo states occlusion. Toward black at both poles for the
228
+ same reason — a crevice is darker than what surrounds it in a dark room too — so
229
+ the cue means one thing whichever way the scale runs. Spent only on what portals;
230
+ `check:style` holds the offsets at zero here and the rung as the only shadow
231
+ anywhere. */
232
+ --_qm-lift: 0 0 12px color-mix(in oklab, #000 16%, transparent);
221
233
  /* The recede ladder: a hover-revealed or disabled affordance at rest, and a
222
234
  muted one. Opacity recedes toward whatever is behind, so these invert
223
235
  correctly given the element's own colour is a rung. */
@@ -183,7 +183,7 @@ html:has(.qm-workspace) {
183
183
  /* ── Type ──────────────────────────────────────────────────────────────────── */
184
184
 
185
185
  /* A run of chrome type: a section's label, a panel's caption, the name over a readout.
186
- Uppercase and tracked, which is what keeps a run this small legible at the ghost
186
+ Uppercase and tracked, which is what keeps a run this small legible at the meta
187
187
  rung. */
188
188
  .qm-label {
189
189
  font-family: var(--qmh-font-mono);
@@ -191,7 +191,7 @@ html:has(.qm-workspace) {
191
191
  line-height: var(--qmh-leading-tight);
192
192
  letter-spacing: var(--qmh-track-label);
193
193
  text-transform: uppercase;
194
- color: var(--qmh-ghost);
194
+ color: var(--qmh-ink-meta);
195
195
  margin: 0;
196
196
  }
197
197
 
@@ -232,7 +232,7 @@ pre.qm-readout {
232
232
  line-height: var(--qmh-leading-tight);
233
233
  letter-spacing: var(--qmh-track-label);
234
234
  text-transform: uppercase;
235
- color: var(--qmh-ghost);
235
+ color: var(--qmh-ink-meta);
236
236
  margin: 0;
237
237
  }
238
238
 
@@ -1,12 +1,12 @@
1
1
  /* The host'S derivation: the same public dials in, a closed `--qmh-*` scale for the
2
2
  page around the surfaces out. `core/theme.css` is the same shape one level down
3
3
  (dials in, `--_qm-*` out), and the two are calibrated against each other: the
4
- figures here are the figures there (the ground step's two arms, 8% hover, 17%
5
- border, 45% and 60% ink, the ring's width) and the dial means the same plane in
6
- both, so a host plate and a card on it land on the same tone at both poles and a
7
- host page meets an editor's own column exactly. The well under the ground has no counterpart here: a
8
- host's own controls are boxed and carry a hairline, so nothing on a page needs the
9
- step an edgeless field does.
4
+ figures here are the figures there (the ground step's ramp, 8% hover, 17%
5
+ border, 45% ink, the ring's width, the status hues at both poles) and the
6
+ dial means the same plane in both, so a host plate and a card on it land on the same
7
+ tone at both poles and a host page meets an editor's own column exactly. The well
8
+ under the ground has no counterpart here: a host's own controls are boxed and carry a
9
+ hairline, so nothing on a page needs the step an edgeless field does.
10
10
 
11
11
  Two scales rather than one because they answer to different roots. `--_qm-*` is
12
12
  declared on each mounted root, out of reach from an ancestor, so a surface holds its
@@ -26,30 +26,32 @@
26
26
  /* ── Colour ──────────────────────────────────────────────────────────────── */
27
27
  /* The dial is the plate, exactly as it is the card one level down: a plate beside
28
28
  a mounted editor is the same plane as a card in it, so both read the pole and
29
- the page sinks from it by the same two arms. That is what puts a host page and
29
+ the page sinks from it by the same ramp. That is what puts a host page and
30
30
  an editor's own column on one tone rather than a plane apart. Planes step by
31
31
  light and everything else by ink, which is the package's own split
32
- (ARCHITECTURE §Styling); the figures are its figures. */
32
+ (ARCHITECTURE §Styling); the figures are its figures. `Canvas` flips with the
33
+ declared scheme on its own, so the ramp reads whichever plane it resolved to
34
+ rather than trusting an arm to match. */
33
35
  --qmh-surface: var(--qm-bg, Canvas);
34
- --qmh-page: light-dark(
35
- oklab(from var(--qmh-surface) calc(l - 0.0214) calc(a - 0.00065) calc(b - 0.00158)),
36
- oklab(from var(--qmh-surface) calc(l - 0.0553) calc(a - 0.00048) calc(b + 0.00165))
37
- );
36
+ --qmh-page: oklab(from var(--qmh-surface) calc(l - (0.0214 + 0.0467 * (1 - l))) a b);
38
37
  --qmh-ink: var(--qm-fg, CanvasText);
38
+ /* One step off the ink and no second: a page's chrome type is small, tracked and
39
+ uppercase, so a rung below this reads under 4.5:1 against the plate and the
40
+ label and status lines that would take it are text (WCAG 1.4.3). */
39
41
  --qmh-ink-meta: color-mix(in oklab, var(--qmh-ink), var(--qmh-surface) 45%);
40
- --qmh-ghost: color-mix(in oklab, var(--qmh-ink), var(--qmh-surface) 60%);
41
42
  --qmh-surface-hover: color-mix(in oklab, var(--qmh-surface), var(--qmh-ink) 8%);
42
43
  --qmh-border: color-mix(in oklab, var(--qmh-surface), var(--qmh-ink) 17%);
43
44
  /* The edge that says "engaged" where the hairline says "edge": a pressed switch,
44
45
  the underline under a link. */
45
46
  --qmh-border-strong: color-mix(in oklab, var(--qmh-surface), var(--qmh-ink) 40%);
46
47
 
47
- /* The two hues a host spends, and it spends them on a message. Both read the dial,
48
- so a page carries one colour meaning error and one meaning warning rather than a
49
- second pair beside the surfaces'. The dark half is a lift the dials do not carry:
50
- a hue calibrated against white reads as a stain on a dark page. */
51
- --qmh-alert: light-dark(var(--qm-danger, #c5221f), #ff9c94);
52
- --qmh-warn: light-dark(var(--qm-warning, #b25000), #e0b44c);
48
+ /* The two hues a host spends, and it spends them on a message. The figures are the
49
+ surfaces' own, so a page carries one colour meaning error and one meaning warning
50
+ rather than a second pair beside the surfaces'. Each pole is its own figure: a hue
51
+ calibrated against white reads as a stain on a dark page. The dial sits in front
52
+ of the pair, as every pole's does, so a host that turns one owns both schemes. */
53
+ --qmh-alert: var(--qm-danger, light-dark(#c5221f, #ff9c94));
54
+ --qmh-warn: var(--qm-warning, light-dark(#b25000, #e0b44c));
53
55
 
54
56
  /* ── Rhythm ──────────────────────────────────────────────────────────────── */
55
57
  /* One base and its multiples, the same four steps the surfaces derive. A host
@@ -22,9 +22,10 @@ export interface PdfPoint {
22
22
  * its content and for a `richtext[]` element; runtime.d.ts says what such a field's
23
23
  * box is, a single `regions()` rect. The third rung is that sentence one granularity
24
24
  * down — an array's ink is its elements' — and is a fallback, not a union, so an
25
- * address with rects of its own never also draws its children's. The `.` keeps the
26
- * prefix a path boundary: `main.references` matches `main.references.0`, not
27
- * `main.references_note`.
25
+ * address with rects of its own never also draws its children's. The boundary
26
+ * character (`.` for a nested field, `[` for an array element) keeps the prefix a
27
+ * path boundary: `main.keywords` matches `main.keywords[0]`, not
28
+ * `main.keywords_note`.
28
29
  */
29
30
  export declare function boxesForField(field: string, boxes: readonly FieldRegion[], regions: readonly FieldRegion[]): readonly FieldRegion[];
30
31
  /**
@@ -7,15 +7,18 @@
7
7
  * its content and for a `richtext[]` element; runtime.d.ts says what such a field's
8
8
  * box is, a single `regions()` rect. The third rung is that sentence one granularity
9
9
  * down — an array's ink is its elements' — and is a fallback, not a union, so an
10
- * address with rects of its own never also draws its children's. The `.` keeps the
11
- * prefix a path boundary: `main.references` matches `main.references.0`, not
12
- * `main.references_note`.
10
+ * address with rects of its own never also draws its children's. The boundary
11
+ * character (`.` for a nested field, `[` for an array element) keeps the prefix a
12
+ * path boundary: `main.keywords` matches `main.keywords[0]`, not
13
+ * `main.keywords_note`.
13
14
  */
14
15
  export function boxesForField(field, boxes, regions) {
15
16
  if (boxes.length)
16
17
  return boxes;
17
18
  const own = regions.filter((r) => r.field === field);
18
- return own.length ? own : regions.filter((r) => r.field.startsWith(`${field}.`));
19
+ if (own.length)
20
+ return own;
21
+ return regions.filter((r) => r.field.startsWith(`${field}.`) || r.field.startsWith(`${field}[`));
19
22
  }
20
23
  /**
21
24
  * Forward: a PDF-pt rect -> CSS % of the page box. Percent positioning tracks
@@ -2,7 +2,7 @@
2
2
  An `array` field → an add/remove repeater. Elements commit by value: every
3
3
  edit / add / remove rebuilds the whole array and hands it to the parent's typed
4
4
  `writer.set(field, wholeArray)` (arrays are not op-addressed). Element control
5
- by `items.type`: `string` → text input, `richtext` → a prose element
5
+ by `items.type`: `string` / `plaintext` → text input, `richtext` → a prose element
6
6
  ({@link ProseArrayElement}), `object` → a minimal JSON editor, keeping the prior
7
7
  element value on an entry that does not parse. The add affordance sits in the
8
8
  label header row (space-between with the field label); {@link Field} skips its
@@ -59,10 +59,16 @@
59
59
  let { value, items, label, required, description, labelId, descriptionId, onCommit }: Props =
60
60
  $props();
61
61
 
62
- // The element control is the item schema's own; an array declaring no `items`
63
- // has text elements.
64
- const control = $derived(items ? controlKind(items) : 'text');
65
- const plaintext = $derived(items?.type === 'plaintext');
62
+ // The element control is the item schema's own, with one departure: a `plaintext`
63
+ // element is a text input where the scalar field of that type is a prose leaf. A
64
+ // `plaintext` value rests as its literal string (canon SCHEMAS.md §"Content fields
65
+ // rest per codec"), and the read that decodes a resting value to `Content` addresses
66
+ // a field, never an element (`getContent`; borb-sh/quillmark#1243), so a string is
67
+ // what the row is handed and a string is what it commits. The prose element stays
68
+ // the `richtext` arm, whose elements rest as `Content` and have one.
69
+ //
70
+ // An array declaring no `items` has text elements.
71
+ const control = $derived(items && items.type !== 'plaintext' ? controlKind(items) : 'text');
66
72
  const arr = $derived((value ?? []) as unknown[]);
67
73
 
68
74
  // Parallel stable ids, one per element, kept in lockstep with the data below.
@@ -243,7 +249,6 @@
243
249
  <ProseArrayElement
244
250
  bind:this={els[id]}
245
251
  value={(arr[k] ?? emptyElement()) as Content}
246
- {plaintext}
247
252
  label={label != null ? `${label} ${k + 1}` : undefined}
248
253
  onChange={(rt) => commitElement(k, rt)}
249
254
  onKey={(e) => onElementKey(e, k)}
@@ -662,7 +662,7 @@
662
662
  color: var(--_qm-ink-label);
663
663
  }
664
664
  .qm-recovery-muted {
665
- color: var(--_qm-ink-ghost);
665
+ color: var(--_qm-ink-label);
666
666
  }
667
667
  .qm-recovery-retype {
668
668
  display: inline-flex;
@@ -198,6 +198,6 @@
198
198
  never written, either way. The marker is the component's own (see the snippet):
199
199
  the date primitive emits no placeholder attribute of its own. */
200
200
  .qm-date-wrap :global(.qm-date-segment[data-ghosted]) {
201
- color: var(--_qm-ink-ghost);
201
+ color: var(--_qm-ink-label);
202
202
  }
203
203
  </style>
@@ -1,6 +1,6 @@
1
1
  <!--
2
- A `string`+`enum` (or `type: 'enum'`) field → a styled listbox over `enum ??
3
- values`, on bits-ui. When nothing is authored the list shows a distinct unset
2
+ A `type: enum` field → a styled listbox over its `values`, on bits-ui. When
3
+ nothing is authored the list shows a distinct unset
4
4
  sentinel that ghosts the `default:` (muted, shown-never-written), distinguishable
5
5
  from an authored pick and re-selectable; so re-picking the default fires a
6
6
  change. The sentinel commits nothing; any real pick: including the
@@ -164,7 +164,7 @@
164
164
  /* Shown-never-written: the closed control reads muted while unset, matching the
165
165
  ghosted placeholder the text/number controls show. */
166
166
  .qm-select-wrap :global(.qm-select[data-ghosted]) {
167
- color: var(--_qm-ink-ghost);
167
+ color: var(--_qm-ink-label);
168
168
  }
169
169
  /* The open list is `.qm-menu-surface` and its rows `.qm-menu-item` (controls.css):
170
170
  the lift, the inset and the highlight are the shared menu recipe. What a listbox
@@ -184,6 +184,6 @@
184
184
  cursor: default;
185
185
  }
186
186
  .qm-select-ghost {
187
- color: var(--_qm-ink-ghost);
187
+ color: var(--_qm-ink-label);
188
188
  }
189
189
  </style>
@@ -18,7 +18,7 @@
18
18
  import type { EditorErrorHandler } from '../core/errors.js';
19
19
  import type { LeafRegistry } from './leaves.js';
20
20
  import type { FieldModel, FieldSpan } from './structure.js';
21
- import { enumValues, ghostDefault, stringifyGhost } from './structure.js';
21
+ import { ghostDefault, stringifyGhost } from './structure.js';
22
22
  import type { FieldDomIds } from './domid.js';
23
23
  import ProseField from './ProseField.svelte';
24
24
  import TextField from './TextField.svelte';
@@ -207,7 +207,7 @@
207
207
  {:else if field.control === 'enum'}
208
208
  <EnumField
209
209
  value={value as string | undefined}
210
- values={enumValues(field.schema) ?? []}
210
+ values={field.schema.values ?? []}
211
211
  fallback={ghost as string | undefined}
212
212
  id={domIds.control}
213
213
  {describedBy}
@@ -186,8 +186,8 @@
186
186
  It takes no fill either: the ink step below says hover, and the surface itself
187
187
  opens on it. */
188
188
  .qm-field-hint {
189
- /* The marker recedes to the label's ghost tone; the surface carries the text. */
190
- color: var(--_qm-ink-ghost);
189
+ /* The marker recedes to the label rung; the surface carries the text. */
190
+ color: var(--_qm-ink-label);
191
191
  /* Not the family's `pointer`: the glyph raises guidance, it does not act. */
192
192
  cursor: help;
193
193
  padding: 0;
@@ -11,7 +11,7 @@
11
11
  -->
12
12
  <script lang="ts">
13
13
  import type { QuillFieldSchema } from '@quillmark/wasm';
14
- import { controlKind, enumValues, humanize } from './structure.js';
14
+ import { controlKind, humanize } from './structure.js';
15
15
  import TextField from './TextField.svelte';
16
16
  import EnumField from './EnumField.svelte';
17
17
  import NumberField from './NumberField.svelte';
@@ -79,7 +79,7 @@
79
79
  <EnumField
80
80
  label={propLabel}
81
81
  value={obj[key] as string | undefined}
82
- values={enumValues(sub) ?? []}
82
+ values={sub.values ?? []}
83
83
  fallback={sub.default as string | undefined}
84
84
  onCommit={(v) => commitProp(key, v)}
85
85
  />
@@ -144,7 +144,7 @@
144
144
  }
145
145
  .qm-unsupported {
146
146
  font-size: var(--_qm-text-body);
147
- color: var(--_qm-ink-ghost);
147
+ color: var(--_qm-ink-label);
148
148
  font-style: italic;
149
149
  }
150
150
  </style>
@@ -1,7 +1,7 @@
1
1
  <!--
2
- One element of an array-of-`richtext` field (fixture `references`). Array
2
+ One element of an array-of-`richtext` field (fixture `keywords`). Array
3
3
  elements are not `applyChange`-addressable: `Addr.field` is a flat name, so
4
- `references.0` has no op address. So
4
+ `keywords[0]` has no op address. So
5
5
  this is not a `createField` leaf: it mounts a minimal PM view over the codec's
6
6
  decode/encode + inline schema, and on every edit hands the re-encoded
7
7
  `Content` up to the parent {@link ArrayField}, which commits the whole array
@@ -19,7 +19,6 @@
19
19
 
20
20
  interface Props {
21
21
  value: Content;
22
- plaintext?: boolean;
23
22
  /** Accessible name for the editable region (the array has no per-element label). */
24
23
  label?: string;
25
24
  onChange: (rt: Content) => void;
@@ -28,7 +27,7 @@
28
27
  * keymap, so the state it reads is the one this keystroke has yet to change. */
29
28
  onKey?: (e: KeyboardEvent) => void;
30
29
  }
31
- let { value, plaintext, label, onChange, onKey }: Props = $props();
30
+ let { value, label, onChange, onKey }: Props = $props();
32
31
 
33
32
  let containerEl: HTMLDivElement | undefined = $state();
34
33
  let view: EditorView | undefined;
@@ -45,10 +44,10 @@
45
44
  // plugin stack a `createField` leaf mounts (shared `proseLeafPlugins`), minus
46
45
  // the anchor-position plugin (element anchors are dropped on the array's value
47
46
  // write, per the header). Its own `dispatchTransaction` hands `Content` up.
48
- const pmDoc = decode(value, inlineSchema, { plaintext: !!plaintext });
47
+ const pmDoc = decode(value, inlineSchema);
49
48
  const state = EditorState.create({
50
49
  doc: pmDoc,
51
- plugins: proseLeafPlugins(inlineSchema, { inline: true, plaintext: !!plaintext })
50
+ plugins: proseLeafPlugins(inlineSchema, { inline: true, plaintext: false })
52
51
  });
53
52
  const mounted = new EditorView(containerEl, {
54
53
  state,
@@ -2,7 +2,6 @@ import './controls.css';
2
2
  import type { Content } from '@quillmark/wasm';
3
3
  interface Props {
4
4
  value: Content;
5
- plaintext?: boolean;
6
5
  /** Accessible name for the editable region (the array has no per-element label). */
7
6
  label?: string;
8
7
  onChange: (rt: Content) => void;
@@ -179,11 +179,13 @@
179
179
  .qm-prose :global(.ProseMirror) {
180
180
  outline: none;
181
181
  }
182
- /* Empty-leaf ghost, at the scalar ghost rung: a node decoration's data attr, so it
183
- stays out of the document; `float`/`height: 0` keep it from displacing the caret. */
182
+ /* Empty-leaf ghost, at the rung every scalar's placeholder takes: a node decoration's
183
+ data attr, so it stays out of the document; `float`/`height: 0` keep it from
184
+ displacing the caret. The italic is what a leaf with no written neighbour has
185
+ instead of the step to `ink`. */
184
186
  .qm-prose :global(.ProseMirror .qm-prose-placeholder::before) {
185
187
  content: attr(data-placeholder);
186
- color: var(--_qm-ink-ghost);
188
+ color: var(--_qm-ink-label);
187
189
  font-style: italic;
188
190
  float: left;
189
191
  height: 0;
@@ -736,12 +736,11 @@
736
736
  * diagnostics take.
737
737
  *
738
738
  * Two rungs, because the boundary mints addresses at a finer granularity than
739
- * `Addr` can name: a `richtext[]` element surfaces as `main.references.0`, which
740
- * the grammar reads as a field literally named `"0"`. The second rung reads that
741
- * trailing segment as an index, under a field the schema declares an array — which
742
- * is why the ladder is the editor's (VISUAL_EDITOR.md §Surface): the preview
743
- * carries no schema, and truncating the address there is worse than guessing, since
744
- * `pos` is an offset into the element's own content.
739
+ * `Addr` can name: a `richtext[]` element surfaces as `main.keywords[0]`. The
740
+ * second rung reads that trailing index segment under a field the schema declares
741
+ * an array — which is why the ladder is the editor's (VISUAL_EDITOR.md §Surface):
742
+ * the preview carries no schema, and truncating the address there is worse than
743
+ * guessing, since `pos` is an offset into the element's own content.
745
744
  */
746
745
  function leafTargetFor(field: DocPath): LeafTarget | undefined {
747
746
  const direct = addrForFieldPath(field);
@@ -106,12 +106,13 @@
106
106
  width: 100%;
107
107
  box-sizing: border-box;
108
108
  }
109
- /* The native placeholder is the ghost rung, not the UA's own grey: a text or
110
- number control's placeholder is the resolved `default:` (Field's `defaultStr`)
111
- and reads as shown-never-written the same way the prose leaf's empty-state
112
- ghost does (`ProseField`). */
109
+ /* The native placeholder is the label rung, not the UA's own grey: a text or
110
+ number control's placeholder is the resolved `default:` (Field's `defaultStr`),
111
+ which is content and reads at a content's contrast. What says shown-never-written
112
+ is the step from the `ink` a written value takes, the same step the prose leaf's
113
+ empty state reads (`ProseField`). */
113
114
  .qm-input::placeholder {
114
- color: var(--_qm-ink-ghost);
115
+ color: var(--_qm-ink-label);
115
116
  }
116
117
 
117
118
  /* The second recipe, and it is not the box above: a field is a box because you
@@ -242,13 +243,15 @@
242
243
 
243
244
  A floating surface is the one place a hairline is still load-bearing: it hangs
244
245
  over content the surface did not choose, so there is no plane under it for a
245
- tone to step off (ARCHITECTURE §Styling). It takes the raised rung as well, which
246
- is a real step in dark and nothing in light, where the edge is the whole of it. */
246
+ tone to step off (ARCHITECTURE §Styling). The edge and `--_qm-lift` carry it
247
+ there, and the raised rung joins them in dark, where a tone still has somewhere
248
+ to go. */
247
249
  .qm-menu-surface {
248
250
  padding: var(--_qm-space-half);
249
251
  border: var(--_qm-border-width) solid var(--_qm-border);
250
252
  border-radius: var(--_qm-radius);
251
253
  background: var(--_qm-surface-raised);
254
+ box-shadow: var(--_qm-lift);
252
255
  font-size: var(--_qm-text-body);
253
256
  color: var(--_qm-ink);
254
257
  }
@@ -291,6 +294,7 @@
291
294
  -webkit-backdrop-filter: blur(var(--_qm-blur));
292
295
  border: var(--_qm-border-width) solid var(--_qm-border);
293
296
  border-radius: var(--_qm-radius);
297
+ box-shadow: var(--_qm-lift);
294
298
  animation: qm-pop-in var(--_qm-duration-fast) var(--_qm-ease-reverse);
295
299
  }
296
300
  .qm-popover-surface[data-ending-style] {
@@ -150,8 +150,6 @@ export declare function resolveBodyGhost(resolvedDefault: string | undefined, cu
150
150
  * An array's element control is this over `items`: a missing `items` is a text
151
151
  * element. */
152
152
  export declare function controlKind(f: QuillFieldSchema): ControlKind;
153
- /** The closed value domain of an enum/enum-string field, or undefined. */
154
- export declare function enumValues(f: QuillFieldSchema): string[] | undefined;
155
153
  /** `foo_bar` → `Foo bar`: the label fallback when a field declares no `ui.title`. */
156
154
  export declare function humanize(name: string): string;
157
155
  /** Project a card schema's `fields` map (declaration = key order) into models. */
@@ -57,9 +57,7 @@ export function controlKind(f) {
57
57
  case 'enum':
58
58
  return 'enum';
59
59
  case 'string':
60
- // Either spelling of a closed domain (the `enum` modifier or `values`)
61
- // promotes a string to a select; a bare string is a text input.
62
- return enumValues(f) ? 'enum' : 'text';
60
+ return 'text';
63
61
  case 'number':
64
62
  case 'integer':
65
63
  return 'number';
@@ -76,10 +74,6 @@ export function controlKind(f) {
76
74
  return 'text';
77
75
  }
78
76
  }
79
- /** The closed value domain of an enum/enum-string field, or undefined. */
80
- export function enumValues(f) {
81
- return f.enum ?? f.values;
82
- }
83
77
  /** `foo_bar` → `Foo bar`: the label fallback when a field declares no `ui.title`. */
84
78
  export function humanize(name) {
85
79
  const spaced = name.replace(/_/g, ' ').trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/svelte",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Editor, live-preview and source surfaces over a Quillmark WASM session.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -66,7 +66,7 @@
66
66
  "check": "svelte-check --tsconfig ./tsconfig.json"
67
67
  },
68
68
  "peerDependencies": {
69
- "@quillmark/wasm": ">=0.103.0-0",
69
+ "@quillmark/wasm": ">=0.104.0-0",
70
70
  "svelte": "^5.0.0"
71
71
  },
72
72
  "dependencies": {