@quillmark/svelte 0.3.2 → 0.4.1

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.
@@ -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)}
@@ -185,9 +185,13 @@
185
185
  }
186
186
  </script>
187
187
 
188
+ <!-- `data-qm-card` names the island for a floating surface that has to stay inside one
189
+ ({@link FieldHint}). A surface portals to `[data-qm-root]`, so the card is not its
190
+ ancestor and nothing else marks the box its guidance belongs in. -->
188
191
  <section
189
192
  bind:this={el}
190
193
  class="qm-card"
194
+ data-qm-card
191
195
  class:qm-main={card.isMain}
192
196
  class:qm-unschemable={card.unschemable}
193
197
  >
@@ -662,7 +666,7 @@
662
666
  color: var(--_qm-ink-label);
663
667
  }
664
668
  .qm-recovery-muted {
665
- color: var(--_qm-ink-ghost);
669
+ color: var(--_qm-ink-label);
666
670
  }
667
671
  .qm-recovery-retype {
668
672
  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}
@@ -46,6 +46,13 @@
46
46
  * along with the editor's subtree. `undefined` falls back to bits-ui's default. */
47
47
  const portalTarget = $derived(triggerEl?.closest<HTMLElement>('[data-qm-root]') ?? undefined);
48
48
 
49
+ /** What the surface stays inside: the field's own card and the root it portals into.
50
+ * floating-ui intersects the two, so a flip answers to whichever is tighter. Empty
51
+ * falls back to the primitive's default, the surface's own clipping ancestors. */
52
+ const bounds = $derived(
53
+ [triggerEl?.closest<HTMLElement>('[data-qm-card]'), portalTarget].filter((el) => el != null)
54
+ );
55
+
49
56
  /** Placement, re-measured on each opening: the surface rides the label rung beside
50
57
  * the glyph, or stands over the field. */
51
58
  let beside = $state(false);
@@ -132,11 +139,18 @@
132
139
  neighbour's control instead, which reaching for it dismisses. -->
133
140
  <!-- `align="center"` for the rung, `"start"` over the field: centred, a surface taller
134
141
  than the glyph's line box would hang half its height onto the field's own control. -->
142
+ <!-- Both are preferences, and the card is what refuses them: its first field has no room
143
+ above its label inside one, so standing over that field puts the surface on the ground
144
+ above the card, reading as the neighbour's rather than as this field's. Refused, the
145
+ placement reverses and the surface goes under the field, over the control it describes:
146
+ the overlap the fork above spends a probe to avoid, and the only one left where there is
147
+ nothing above to stand over. -->
135
148
 
136
149
  <Popover.Root bind:open>
137
150
  <Popover.Portal to={portalTarget}>
138
151
  <Popover.Content
139
152
  customAnchor={triggerEl ?? null}
153
+ collisionBoundary={bounds}
140
154
  side={beside ? 'right' : 'top'}
141
155
  align={beside ? 'center' : 'start'}
142
156
  sideOffset={6}
@@ -186,8 +200,8 @@
186
200
  It takes no fill either: the ink step below says hover, and the surface itself
187
201
  opens on it. */
188
202
  .qm-field-hint {
189
- /* The marker recedes to the label's ghost tone; the surface carries the text. */
190
- color: var(--_qm-ink-ghost);
203
+ /* The marker recedes to the label rung; the surface carries the text. */
204
+ color: var(--_qm-ink-label);
191
205
  /* Not the family's `pointer`: the glyph raises guidance, it does not act. */
192
206
  cursor: help;
193
207
  padding: 0;
@@ -65,6 +65,7 @@
65
65
  import Icon from './icons/Icon.svelte';
66
66
  import type { IconName } from './icons/nodes.js';
67
67
  import {
68
+ hasMarks,
68
69
  rangeAnchor,
69
70
  type FieldController,
70
71
  type LeafViews,
@@ -140,7 +141,17 @@
140
141
  // Escape out of a table cell lands.
141
142
  const selection = view?.state.selection;
142
143
  const formattable = selection instanceof TextSelection && !selection.empty;
143
- if (!view || !view.hasFocus() || !formattable || !hasText(view)) {
144
+ // …in a leaf that has formatting at all. A `plaintext` leaf declares no mark
145
+ // types (`plaintextSchema`), so this surface withholds itself over one entirely,
146
+ // the way it never rises over the text input a `string` field draws: the value is
147
+ // literal text, and a mark on it is a value the boundary refuses to coerce back.
148
+ if (
149
+ !view ||
150
+ !view.hasFocus() ||
151
+ !formattable ||
152
+ !hasMarks(view.state.schema) ||
153
+ !hasText(view)
154
+ ) {
144
155
  open = false;
145
156
  linkPromptOpen = false;
146
157
  return;
@@ -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 })
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. */