@hueest/xray 0.5.0 → 0.7.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/dist/index.d.ts CHANGED
@@ -1,39 +1,34 @@
1
1
  import { Plugin } from "vite";
2
-
3
2
  //#region src/diagnostics.d.ts
4
3
  /**
5
- * Dev-only capture diagnostics: a small, serializable record of why a Plate may
6
- * be incomplete or lower-fidelity, plus the one shared formatter both the
7
- * browser console (`client.ts`) and the Vite server output (`index.ts`) print.
4
+ * Dev-only capture diagnostics: a small, serializable record of why a Plate may be incomplete or
5
+ * lower-fidelity, plus the one shared formatter both the browser console (`client.ts`) and the Vite
6
+ * server output (`index.ts`) print.
8
7
  *
9
- * Motivation: the serializer silently swallows a handful of capture omissions
10
- * (an unreadable cross-origin stylesheet, skipped pseudo-elements, off-screen
11
- * bones dropped at the fold, an over-the-limit subtree). Early adopters asking "why does this
12
- * skeleton look wrong?" had to read the serializer source to find out. These
13
- * diagnostics surface those omissions as data the capture process returns, so a
14
- * caller can log them — without the core performing any console side effects.
8
+ * Motivation: the serializer silently swallows a handful of capture omissions (an unreadable
9
+ * cross-origin stylesheet, skipped pseudo-elements, off-screen bones dropped at the fold, an
10
+ * over-the-limit subtree). Without these records, "why does this skeleton look wrong?" has no
11
+ * answer short of reading the serializer source. Diagnostics surface those omissions as data the
12
+ * capture returns, so a caller can log them; the core performs no console side effects.
15
13
  *
16
- * Strictly DEV-ONLY (see the plan's decisions and ADR 0008): this type is NOT a
17
- * public package export and MUST NEVER reach the persisted Plate JSON, the
18
- * `MergedPlate`, or the production adapter. It rides on the TRANSIENT
19
- * `ViewIR.diagnostics` field only, which `captureView` populates and the
20
- * dev client strips before posting (so committed `plates/<name>.json` stays
21
- * byte-identical to a capture that produced no diagnostics).
14
+ * Strictly DEV-ONLY (ADR 0024, ADR 0008): this type is NOT a public package export and MUST NEVER
15
+ * reach the persisted Plate JSON, the `GatedPlate`, or the production adapter. It rides on the
16
+ * TRANSIENT `ViewIR.diagnostics` field only, which `captureView` populates and the dev client
17
+ * strips before posting (so committed `plates/<name>.json` stays byte-identical to a capture that
18
+ * produced no diagnostics).
22
19
  *
23
- * Centralized on purpose: a follow-up plan (capture-input-validation) reuses
24
- * this same messaging for invalid posted/committed plates, so the code table
25
- * and formatter are kept reusable. Browser and server share `formatDiagnostic`
26
- * so their warning text cannot drift.
20
+ * Centralized on purpose: invalid posted/committed plates (ADR 0023) reuse the same messaging, so
21
+ * the code table and formatter stay reusable. Browser and server share `formatDiagnostic` so their
22
+ * warning text cannot drift.
27
23
  *
28
24
  * @module
29
25
  */
30
26
  /**
31
- * One aggregated capture omission. Serializable on purpose: NO DOM nodes, NO
32
- * CSS bodies, NO captured text — only a stable `code`, a human message, an
33
- * optional aggregate `count`, and an optional short `detail` (e.g. a plate name
34
- * or a node count, never user content). `count` aggregates occurrences: there
35
- * is at most one record per code per capture, never one per skipped selector or
36
- * node.
27
+ * One aggregated capture omission. Serializable on purpose: NO DOM nodes, NO CSS bodies, NO
28
+ * captured text — only a stable `code`, a human message, an optional aggregate `count`, and an
29
+ * optional short `detail` (e.g. a plate name or a node count, never user content). `count`
30
+ * aggregates occurrences: there is at most one record per code per capture, never one per skipped
31
+ * selector or node.
37
32
  */
38
33
  interface CaptureDiagnostic {
39
34
  code: DiagnosticCode;
@@ -42,52 +37,59 @@ interface CaptureDiagnostic {
42
37
  detail?: string;
43
38
  }
44
39
  /**
45
- * The closed set of capture-omission codes. Each maps to one silent site in the
46
- * serializer (or an existing oversized-capture warning folded into the same
47
- * vocabulary). Kept as a const union so the message table is exhaustive.
40
+ * The closed set of capture-omission codes. Each maps to one silent site in the serializer (or an
41
+ * existing oversized-capture warning folded into the same vocabulary). Kept as a const union so the
42
+ * message table is exhaustive.
48
43
  */
49
44
  type DiagnosticCode = 'unreadable-stylesheet' | 'unreadable-import' | 'skipped-dynamic-pseudo' | 'skipped-pseudo-element' | 'unsupported-rule' | 'dropped-tag' | 'dropped-subset' | 'dropped-cropped' | 'too-large' | 'invalid-plate-file';
50
45
  //#endregion
51
46
  //#region src/plate.d.ts
52
- type LeafKind = 'text' | 'text-block' | 'media' | 'box';
53
47
  /**
54
- * The shipped structural capture of a component's rendered DOM the data a
55
- * Skeleton renders from (see CONTEXT.md). Self-contained: pre-serialized chunks
56
- * plus one scoped CSS string; depends on nothing in the consumer's build (ADR
57
- * 0003). Built from the RenderNode tree at plugin load (see chunk.ts), so the
58
- * adapter injects strings instead of walking a tree on the latency-critical
59
- * first paint — and no per-framework serializer ships to the client (ADR 0008).
48
+ * `hidden` is EXPLICIT-ONLY (a `data-xr-bone-kind` hint or a walker `ctx.bone({ kind: 'hidden'
49
+ * })`), never inferred: a bone that holds its measured box in the layout but paints nothing — the
50
+ * space-preserving complement to `data-xr-ignore`, which drops the box entirely.
51
+ */
52
+ type LeafKind = 'text-line' | 'text-block' | 'media' | 'box' | 'hidden';
53
+ /**
54
+ * The shipped structural capture of a component's rendered DOM — the TREE artifact a Skeleton
55
+ * renders from (see the glossary: xray-js.dev/glossary). A Plate carries structure ONLY (ADR 0026):
56
+ * its scoped CSS is an INDEPENDENT sibling artifact — the plugin serves it through Vite's own CSS
57
+ * pipeline (`virtual:xray/plates/<name>.css`, plus the shared `virtual:xray/base.css`, and an
58
+ * `export const css` on the virtual JS module for dev/programmatic reads), and the
59
+ * framework-neutral `core.ts` path hands it back separately (`captureElement` → `{ plate, css }`;
60
+ * `renderPlateHtml(plate, css)` receives it explicitly). Consumers decide how to render either
61
+ * part. Still self-contained in ADR 0003's sense: both artifacts originate from the capture and
62
+ * depend on nothing in the consumer's build. Built from the RenderNode tree at plugin load (see
63
+ * chunk.ts), so the adapter injects strings instead of walking a tree on the latency-critical first
64
+ * paint — and no per-framework serializer ships to the client (ADR 0008).
60
65
  */
61
66
  interface Plate {
62
67
  v: number;
63
68
  /** Capture name; comes from the virtual-module specifier, never typed at a call site. */
64
69
  name: string;
65
70
  /**
66
- * The root's children, pre-serialized (ADR 0008): a stitch-free run is one
67
- * HTML `string`; a stitch is `{ r: name }`; a stitch-bearing element is
68
- * `{ c: className, k: chunk[] }`. Null until first capture.
71
+ * The root's children, pre-serialized (ADR 0008): a stitch-free run is one HTML `string`; a
72
+ * stitch is `{ r: name }`; a stitch-bearing element is `{ c: className, k: chunk[] }`. Null until
73
+ * first capture.
69
74
  */
70
75
  chunks: Chunk[] | null;
71
76
  /** The root node's own content classes (its inherited font context), if any. */
72
77
  rootCls?: string;
73
- /** Layout-relevant CSS (incl. `@media`) scoped under the plate root, paint stripped. */
74
- css: string;
75
78
  /**
76
- * Child plates named by the chunks' `{ r }` stitches, resolved through the
77
- * import graph (ADR 0006). Populated by the loader's generated imports, never
78
- * serialized into the plates JSON; absent in production when there are no
79
- * stitches. In dev the live store takes precedence, so this need not be present.
79
+ * Child plates named by the chunks' `{ r }` stitches, resolved through the import graph (ADR
80
+ * 0006). Populated by the loader's generated imports, never serialized into the plates JSON;
81
+ * absent in production when there are no stitches. In dev the live store takes precedence, so
82
+ * this need not be present.
80
83
  */
81
84
  refs?: Record<string, Plate>;
82
85
  }
83
86
  /**
84
- * One serialized child of a plate root (or of a stitch-bearing node): a
85
- * stitch-free HTML run (`string`), a stitch to a child plate (`{ r: name }`),
86
- * a stitch-bearing element kept real so its inner stitches can mount
87
- * (`{ c: className, k: chunk[] }`), or a templated uniform run (`{ t: html, n }`):
88
- * the cell's HTML once plus a repeat count the adapter expands to N copies (Phase
89
- * 2). The framework-neutral render contract: an adapter injects the strings,
90
- * mounts a child plate at each stitch, and repeats each template (ADR 0008).
87
+ * One serialized child of a plate root (or of a stitch-bearing node): a stitch-free HTML run
88
+ * (`string`), a stitch to a child plate (`{ r: name }`), a stitch-bearing element kept real so its
89
+ * inner stitches can mount (`{ c: className, k: chunk[] }`), or a templated uniform run (`{ t:
90
+ * html, n }`): the cell's HTML once plus a repeat count the adapter expands to N copies (Template,
91
+ * ADR 0022). The framework-neutral render contract: an adapter injects the strings, mounts a child
92
+ * plate at each stitch, and repeats each template (ADR 0008).
91
93
  */
92
94
  type Chunk = string | {
93
95
  r: string;
@@ -99,13 +101,12 @@ type Chunk = string | {
99
101
  n: number;
100
102
  };
101
103
  /**
102
- * A plate after the in-browser Serialize (emit + merge/gate + classify) but
103
- * before chunk serialization: the build-time RenderNode tree plus its scoped
104
- * CSS. Produced by `serializePlateIR` (the structural part of `StoredPlate`) and
105
- * the single-View `capture`, then serialized into the shipped `Plate` (chunks) at
106
- * load (`serializePlate`). Never shipped to the client.
104
+ * A plate after the in-browser Serialize (emit + gate + classify) but before chunk serialization:
105
+ * the build-time RenderNode tree plus its scoped CSS. Produced by `serializePlateIR` (the
106
+ * structural part of `StoredPlate`) and the single-View `capture`, then serialized into the shipped
107
+ * `Plate` (chunks) at load (`serializePlate`). Never shipped to the client.
107
108
  */
108
- interface MergedPlate {
109
+ interface GatedPlate {
109
110
  v: number;
110
111
  name: string;
111
112
  tree: RenderNode | null;
@@ -117,37 +118,38 @@ interface PlateNode {
117
118
  /** Present on paint leaves; absent on layout containers. */
118
119
  leaf?: LeafKind;
119
120
  /**
120
- * A stitch: render the named child plate's skeleton here instead of own
121
- * structure (ADR 0006). Mutually exclusive with `leaf`/`kids`. Resolved
122
- * against `Plate.refs` (prod) or the live store (dev).
121
+ * A stitch: render the named child plate's skeleton here instead of own structure (ADR 0006).
122
+ * Mutually exclusive with `leaf`/`kids`. Resolved against `Plate.refs` (prod) or the live store
123
+ * (dev).
123
124
  */
124
125
  ref?: string;
125
126
  /**
126
- * A templated uniform run (Phase 2): render this node's subtree `count` times.
127
- * The serializer emits the subtree's HTML once plus the count; the adapter
128
- * expands it to N copies at render. Absent on ordinary nodes.
127
+ * A templated uniform run (Template, ADR 0022): render this node's subtree `count` times. The
128
+ * serializer emits the subtree's HTML once plus the count; the adapter expands it to N copies at
129
+ * render. Absent on ordinary nodes.
129
130
  */
130
131
  count?: number;
131
132
  kids?: PlateNode[];
132
133
  }
133
134
  /**
134
- * A node in the shipped tree, after classify (ADR 0007). Carries the
135
- * content-addressed style classes it needs instead of an id — the CSS targets
136
- * those classes, not a per-node `data-xr` value, so node scoping (the old
137
- * `nodeKey`) is gone. `cls` is a space-joined class-token list.
135
+ * A node in the shipped tree, after classify (ADR 0007). Carries the content-addressed style
136
+ * classes it needs instead of an id — the CSS targets those classes, not a per-node `data-xr`
137
+ * value, so no per-node scoping key is needed. `cls` is a space-joined class-token list.
138
138
  */
139
139
  interface RenderNode {
140
140
  leaf?: LeafKind;
141
141
  ref?: string;
142
142
  cls?: string;
143
- /** A templated uniform run (Phase 2): serialize this subtree once + a count the adapter repeats. */
143
+ /**
144
+ * A templated uniform run (Template, ADR 0022): serialize this subtree once + a count the adapter
145
+ * repeats.
146
+ */
144
147
  count?: number;
145
148
  kids?: RenderNode[];
146
149
  }
147
150
  /**
148
- * A captured style rule in id form — selectors are generated at render time,
149
- * so view merging can remap node ids without string surgery. `ids: []`
150
- * targets the plate root.
151
+ * A captured style rule in id form — selectors are generated downstream, so Serialize can remap
152
+ * node ids without string surgery. `ids: []` targets the plate root.
151
153
  */
152
154
  interface PlateRule {
153
155
  ids: number[];
@@ -161,12 +163,10 @@ interface PlateRule {
161
163
  order: number;
162
164
  }
163
165
  /**
164
- * One capture of a component within one viewport view — `[min, max)` in px,
165
- * either side open-ended when undefined. The id-form `{ tree, rules }` shape
166
- * `emit` projects a measured View into (ADR 0004). NO LONGER the disk/wire shape
167
- * after the ADR 0022 cutover `StoredPlate` (below) is. Kept as the
168
- * framework-neutral per-View capture shape, exported for consumers reading the
169
- * id-form projection directly.
166
+ * One capture of a component within one viewport view — `[min, max)` in px, either side open-ended
167
+ * when undefined. The id-form `{ tree, rules }` shape `emit` projects a measured View into (ADR
168
+ * 0004). The disk/wire shape is `StoredPlate` (below); this is the framework-neutral per-View
169
+ * capture shape, exported for consumers reading the id-form projection directly.
170
170
  */
171
171
  interface ViewCapture {
172
172
  /** Viewport width at capture time. */
@@ -176,37 +176,32 @@ interface ViewCapture {
176
176
  tree: PlateNode;
177
177
  rules: PlateRule[];
178
178
  /**
179
- * TRANSIENT, dev-only capture diagnostics (why this view may be lower
180
- * fidelity). Captured in the browser (mirrored onto `ViewIR.diagnostics`); the
181
- * dev client logs it and posts it alongside the `StoredPlate` (so the Vite
182
- * server can print the same text). It MUST NEVER be persisted: it is not part
183
- * of the committed
184
- * `StoredPlate` disk shape, the validator does not carry it, and the POST
185
- * envelope keeps it in a SIBLING field that never reaches the stored artifact
186
- * (ADR 0008). Absent in production (the prod adapter ships no capture). See
187
- * diagnostics.ts and ADR 0008.
179
+ * TRANSIENT, dev-only capture diagnostics (why this view may be lower fidelity). Captured in the
180
+ * browser (mirrored onto `ViewIR.diagnostics`); the dev client logs it and posts it alongside the
181
+ * `StoredPlate` (so the Vite server can print the same text). It MUST NEVER be persisted: it is
182
+ * not part of the committed `StoredPlate` disk shape, the validator does not carry it, and the
183
+ * POST envelope keeps it in a SIBLING field that never reaches the stored artifact (ADR 0008).
184
+ * Absent in production (the prod adapter ships no capture). See diagnostics.ts and ADR 0008.
188
185
  */
189
186
  diagnostics?: CaptureDiagnostic[];
190
187
  }
191
188
  /**
192
- * The committed `plates/<name>.json` shape AFTER the ADR 0022 cutover: the whole
193
- * Plate already projected in the browser (the in-browser Serialize folds emit +
194
- * the multi-View merge/gate + classify in one session), so disk holds ONE gated,
195
- * content-addressed tree + css instead of per-View captures merged at load. It is
196
- * a `MergedPlate` (the structural `{tree, css}` `serializePlate` consumes) PLUS the
197
- * `breakpoints` the HUD coverage reads the analysis-only geometry never crosses
198
- * the wire (ADR 0021). The gen-side runs `serializePlate(stored) → Plate` (chunks)
199
- * at module-load, leaving the Tailwind/Bundle passthrough a clean gen-side seam.
200
- * Replaces `PlateFile` (+ `ViewCapture`) on disk and on the `/__xray` wire.
189
+ * The committed `plates/<name>.json` shape (ADR 0022): the whole Plate already projected in the
190
+ * browser (the in-browser Serialize folds emit + the multi-View gating + classify in one session),
191
+ * so disk holds ONE gated, content-addressed tree + css rather than per-View captures assembled at
192
+ * load. It is a `GatedPlate` (the structural `{tree, css}` `serializePlate` consumes) PLUS the
193
+ * `breakpoints` the HUD coverage reads the analysis-only geometry never crosses the wire (ADR
194
+ * 0021). The gen-side runs `serializePlate(stored) Plate` (chunks) at module-load, leaving the
195
+ * Tailwind/Bundle passthrough a clean gen-side seam.
201
196
  */
202
- interface StoredPlate extends MergedPlate {
197
+ interface StoredPlate extends GatedPlate {
203
198
  /** Width thresholds (view boundaries); the HUD coverage derives its bands from these. */
204
199
  breakpoints: number[];
205
200
  }
206
201
  /**
207
- * A dev-only sticky boolean toggle (get/set/subscribe), persisted per tab.
208
- * Used for both "light box" (render every `<Skeleton>` regardless of readiness)
209
- * and "capture" (whether browsing re-captures plates).
202
+ * A dev-only sticky boolean toggle (get/set/subscribe), persisted per tab. Used for both "light
203
+ * box" (render every `<Skeleton>` regardless of readiness) and "capture" (whether browsing
204
+ * re-captures plates).
210
205
  */
211
206
  interface ToggleStore {
212
207
  get: () => boolean;
@@ -214,19 +209,17 @@ interface ToggleStore {
214
209
  subscribe: (onChange: () => void) => () => void;
215
210
  }
216
211
  /**
217
- * Dev-only store of the freshest plate per name, updated in place when a
218
- * capture lands. `<Skeleton>` reads through it so a new plate re-renders the
219
- * site rather than reloading the module which would remount the capture
220
- * boundary and re-fire the capture, a feedback loop.
212
+ * Dev-only store of the freshest plate per name, updated in place when a capture lands.
213
+ * `<Skeleton>` reads through it so a new plate re-renders the site rather than reloading the module
214
+ * which would remount the capture boundary and re-fire the capture, a feedback loop.
221
215
  */
222
216
  interface PlateStore {
223
217
  get: (name: string) => Plate | undefined;
224
218
  subscribe: (name: string, onChange: () => void) => () => void;
225
219
  /**
226
- * Register a plate imported via the ref graph if the store has none yet — so a
227
- * stitch resolves even before its child is re-captured, and survives a parent
228
- * hot-swap (whose pushed plate carries no resolved `refs`). Never overwrites a
229
- * live capture.
220
+ * Register a plate imported via the ref graph if the store has none yet — so a stitch resolves
221
+ * even before its child is re-captured, and survives a parent hot-swap (whose pushed plate
222
+ * carries no resolved `refs`). Never overwrites a live capture.
230
223
  */
231
224
  seed: (name: string, plate: Plate) => void;
232
225
  }
@@ -236,34 +229,31 @@ interface Span {
236
229
  max?: number;
237
230
  }
238
231
  /**
239
- * Dev-only Fixture store (ADR 0015): stored render-INPUT `data` per Plate,
240
- * recorded to and replayed from a `plates/<name>.fixture.json` sidecar. Entirely
241
- * dev-only — devalue and this store never reach the production bundle.
232
+ * Dev-only Fixture store (ADR 0015): stored render-INPUT `data` per Plate, recorded to and replayed
233
+ * from a `plates/<name>.fixture.json` sidecar. Entirely dev-only — devalue and this store never
234
+ * reach the production bundle.
235
+ *
236
+ * Two cooperating halves, both flowing through the render-prop `data` seam (ADR 0014):
242
237
  *
243
- * Two cooperating halves, both flowing through the render-prop `data` seam (ADR
244
- * 0014):
245
- * - The dev adapter (`react.dev.tsx`) calls `register(name, data)` at
246
- * `loading=false` with the live `data` it is about to render, so the latest
247
- * recordable input for each mounted Plate is always in scope for Record.
248
- * - `record(name)` serializes that registered live `data` with devalue and POSTs
249
- * it to the sidecar (the HUD button and the `first-ready`/`always` modes call
250
- * it). `has(name)` reports whether a Fixture exists (HUD coverage marker), and
251
- * `read(name)` returns the parsed Fixture `data` for Replay to substitute in
252
- * front of the render prop.
238
+ * - The dev adapter (`react.dev.tsx`) calls `register(name, data)` at `loading=false` with the live
239
+ * `data` it is about to render, so the latest recordable input for each mounted Plate is always
240
+ * in scope for Record.
241
+ * - `record(name)` serializes that registered live `data` with devalue and POSTs it to the sidecar
242
+ * (the HUD button and the `first-ready`/`always` modes call it). `has(name)` reports whether a
243
+ * Fixture exists (HUD coverage marker), and `read(name)` returns the parsed Fixture `data` for
244
+ * Replay to substitute in front of the render prop.
253
245
  *
254
- * The store holds DECODED values (the dev client owns the devalue parse, since it
255
- * has the live runtime); only the on-disk sidecar and the wire carry the opaque
256
- * devalue string.
246
+ * The store holds DECODED values (the dev client owns the devalue parse, since it has the live
247
+ * runtime); only the on-disk sidecar and the wire carry the opaque devalue string.
257
248
  */
258
249
  interface FixtureStore {
259
250
  /**
260
- * Record the latest live `data` the dev adapter is about to render for `name`,
261
- * so Record always has the current recordable input in scope. A `register` with
262
- * the same value is cheap; the adapter calls it on every ready render. Returns a
263
- * disposer the adapter runs on unmount so an unmounted Skeleton's `data` never
264
- * lingers in the store: an unmounted instance must never be recorded or shadow a
265
- * still-mounted one (ADR 0015). With several Skeletons of the same plate name
266
- * mounted, the live value is the most-recently-registered still-mounted instance.
251
+ * Record the latest live `data` the dev adapter is about to render for `name`, so Record always
252
+ * has the current recordable input in scope. A `register` with the same value is cheap; the
253
+ * adapter calls it on every ready render. Returns a disposer the adapter runs on unmount so an
254
+ * unmounted Skeleton's `data` never lingers in the store: an unmounted instance must never be
255
+ * recorded or shadow a still-mounted one (ADR 0015). With several Skeletons of the same plate
256
+ * name mounted, the live value is the most-recently-registered still-mounted instance.
267
257
  */
268
258
  register: (name: string, data: unknown) => () => void;
269
259
  /** True once a recorded Fixture exists for `name` (drives the HUD coverage marker). */
@@ -273,25 +263,22 @@ interface FixtureStore {
273
263
  data: unknown;
274
264
  } | undefined;
275
265
  /**
276
- * Serialize the registered live `data` for `name` with devalue and POST it to
277
- * the sidecar. Returns a promise that resolves once the write is
278
- * server-confirmed (so the HUD can surface a failed Record). Throws loudly when
279
- * the live `data` is unrecordable (devalue's contract) or no live `data` was
280
- * registered (a plain-children `<Skeleton>` has none).
266
+ * Serialize the registered live `data` for `name` with devalue and POST it to the sidecar.
267
+ * Returns a promise that resolves once the write is server-confirmed (so the HUD can surface a
268
+ * failed Record). Throws loudly when the live `data` is unrecordable (devalue's contract) or no
269
+ * live `data` was registered (a plain-children `<Skeleton>` has none).
281
270
  */
282
271
  record: (name: string) => Promise<void>;
283
272
  /**
284
- * Record every Skeleton that currently has live `data` registered (the HUD's
285
- * single "Record fixture" button — one click captures the inputs of all mounted
286
- * render-prop Skeletons). Resolves once all writes are server-confirmed; a
287
- * rejected write surfaces to the HUD.
273
+ * Record every Skeleton that currently has live `data` registered (the HUD's single "Record
274
+ * fixture" button — one click captures the inputs of all mounted render-prop Skeletons). Resolves
275
+ * once all writes are server-confirmed; a rejected write surfaces to the HUD.
288
276
  */
289
277
  recordAll: () => Promise<void>;
290
278
  /**
291
- * Seed the store from the on-disk sidecars on boot: a `{ <name>: devalueString }`
292
- * map (the opaque strings the server serves). The client decodes each with
293
- * devalue. Existing entries are not clobbered, so a Fixture recorded this
294
- * session survives a re-seed.
279
+ * Seed the store from the on-disk sidecars on boot: a `{ <name>: devalueString }` map (the opaque
280
+ * strings the server serves). The client decodes each with devalue. Existing entries are not
281
+ * clobbered, so a Fixture recorded this session survives a re-seed.
295
282
  */
296
283
  seed: (encoded: Record<string, string>) => void;
297
284
  /** React subscription so the HUD re-renders when a Fixture is recorded or seeded. */
@@ -303,10 +290,10 @@ interface PlateCoverage {
303
290
  spans: Span[];
304
291
  }
305
292
  /**
306
- * Dev-only per-plate coverage, sourced from the committed plate files (not the
307
- * session): the breakpoints detected so far and which view spans are captured.
308
- * The HUD renders every band — covered ones lit, the rest dim — and the
309
- * capture-all sweep derives its target widths from the breakpoints.
293
+ * Dev-only per-plate coverage, sourced from the committed plate files (not the session): the
294
+ * breakpoints detected so far and which view spans are captured. The HUD renders every band —
295
+ * covered ones lit, the rest dim — and the capture-all sweep derives its target widths from the
296
+ * breakpoints.
310
297
  */
311
298
  interface CoverageStore {
312
299
  get: () => ReadonlyMap<string, PlateCoverage>;
@@ -314,9 +301,9 @@ interface CoverageStore {
314
301
  subscribe: (onChange: () => void) => () => void;
315
302
  }
316
303
  /**
317
- * Dev-only capture hook, installed by the dev client before the app boots.
318
- * Absent in production builds, so `<Skeleton>` carries zero capture weight.
319
- * `captured` returns a dispose callback (stop observing this site).
304
+ * Dev-only capture hook, installed by the dev client before the app boots. Absent in production
305
+ * builds, so `<Skeleton>` carries zero capture weight. `captured` returns a dispose callback (stop
306
+ * observing this site).
320
307
  */
321
308
  interface CaptureHook {
322
309
  captured: (name: string, el: Element, opts?: {
@@ -342,20 +329,18 @@ interface CaptureHook {
342
329
  /** Dev-only per-plate breakpoints + captured spans, from disk; fed by the bootstrap. */
343
330
  coverage?: CoverageStore;
344
331
  /**
345
- * Replay toggle (ADR 0015): when active, the dev adapter substitutes a recorded
346
- * Fixture's `data` for the live `data` at `loading=false`. The boolean is the
347
- * coarse on/off the HUD owns per tab; the per-mode policy (`prefer` vs
348
- * `missing-only`) is carried alongside in `replayMode`.
332
+ * Replay toggle (ADR 0015): when active, the dev adapter substitutes a recorded Fixture's `data`
333
+ * for the live `data` at `loading=false`. The boolean is the coarse on/off the HUD owns per tab;
334
+ * the per-mode policy (`prefer` vs `missing-only`) is carried alongside in `replayMode`.
349
335
  */
350
336
  replay?: ToggleStore;
351
337
  /** Dev-only Replay policy: how a Fixture substitutes for live `data` (ADR 0015). */
352
338
  replayMode?: 'prefer' | 'missing-only';
353
339
  /**
354
- * Dev-only Record policy (ADR 0015): when the dev adapter records the live
355
- * `data` it is about to render. `'manual'` (the default) records nothing
356
- * automatically — only the HUD button does; `'first-ready'` records once when a
357
- * Skeleton goes ready and has no Fixture yet; `'always'` records on every ready
358
- * render.
340
+ * Dev-only Record policy (ADR 0015): when the dev adapter records the live `data` it is about to
341
+ * render. `'manual'` (the default) records nothing automatically — only the HUD button does;
342
+ * `'first-ready'` records once when a Skeleton goes ready and has no Fixture yet; `'always'`
343
+ * records on every ready render.
359
344
  */
360
345
  recordMode?: 'manual' | 'first-ready' | 'always';
361
346
  /** Dev-only Fixture record/replay store (ADR 0015); never present in production. */
@@ -367,40 +352,36 @@ declare global {
367
352
  //#endregion
368
353
  //#region src/chunk.d.ts
369
354
  /**
370
- * Distinct plate names referenced by the tree's stitches, in first-seen order.
371
- * Walks a `RenderNode` tree (the stored, post-classify shape), so it drives the
372
- * gen-side stitch import graph straight off a `StoredPlate.tree`
373
- * (`renderPlateModule`, index.ts). Relocated here from the now-deleted `merge.ts`
374
- * in the ADR 0022 cutover — it is the one node-side primitive that survived the
375
- * `mergeViews`/`addCapture` removal (the multi-View merge/gate now lives in the
376
- * in-browser `serializePlateIR`, project.ts).
355
+ * Distinct plate names referenced by the tree's stitches, in first-seen order. Walks a `RenderNode`
356
+ * tree (the stored, post-classify shape), so it drives the gen-side stitch import graph straight
357
+ * off a `StoredPlate.tree` (`renderPlateModule`, index.ts). The one node-side primitive needed at
358
+ * build time; multi-View gating happens in the in-browser `serializePlateIR` (serialize.ts, ADR
359
+ * 0022).
377
360
  */
378
361
  declare function collectRefs(tree: RenderNode | null): string[];
379
362
  //#endregion
380
363
  //#region src/name.d.ts
381
364
  /**
382
- * Plate-name validation, shared by the Vite plugin (`index.ts`) and the
383
- * Node-only POST validator (`validate.ts`).
365
+ * Plate-name validation, shared by the Vite plugin (`index.ts`) and the Node-only POST validator
366
+ * (`validate.ts`).
384
367
  *
385
- * Lives in its own dependency-free module so `validate.ts` can reuse it without
386
- * importing the plugin entry (which would be a cycle), and so the regex stays
387
- * the single source of truth for what a plate name may be — the same rule the
388
- * loader, the file watcher, the coverage reader, the POST endpoint, and the
389
- * ref-graph emitter all gate on.
368
+ * Lives in its own dependency-free module so `validate.ts` can reuse it without importing the
369
+ * plugin entry (which would be a cycle), and so the regex stays the single source of truth for what
370
+ * a plate name may be — the same rule the loader, the file watcher, the coverage reader, the POST
371
+ * endpoint, and the ref-graph emitter all gate on.
390
372
  *
391
373
  * @module
392
374
  */
393
375
  /**
394
376
  * Names come from import specifiers and POST bodies — keep them inside the plates dir.
395
377
  *
396
- * Each path segment is `\w+(-\w+)*` — word chars with single interior hyphens,
397
- * never doubled and never at a segment edge — and segments are joined by `/`.
398
- * The class-prefix encoder (`classify.ts`) renders `/` as `--` to keep nested
399
- * names readable, and that prefix is the isolation boundary across stitched
400
- * plates (ADR 0007), so the encoding must be injective. Banning a literal `--`
401
- * and any edge hyphen guarantees no `-` ever abuts a `/`, so every `--` in the
402
- * prefix came from exactly one `/` making distinct names always map to
403
- * distinct prefixes (else e.g. `a/-b` and `a-/b` would both encode to `a---b`).
378
+ * Each path segment is `\w+(-\w+)*` — word chars with single interior hyphens, never doubled and
379
+ * never at a segment edge — and segments are joined by `/`. The class-prefix encoder
380
+ * (`classify.ts`) renders `/` as `--` to keep nested names readable, and that prefix is the
381
+ * isolation boundary across stitched plates (ADR 0007), so the encoding must be injective. Banning
382
+ * a literal `--` and any edge hyphen guarantees no `-` ever abuts a `/`, so every `--` in the
383
+ * prefix came from exactly one `/` making distinct names always map to distinct prefixes (else
384
+ * e.g. `a/-b` and `a-/b` would both encode to `a---b`).
404
385
  *
405
386
  * @internal
406
387
  */
@@ -411,32 +392,38 @@ declare function isValidPlateName(name: string): boolean;
411
392
  * Options for `xrayVitePlugin()`.
412
393
  *
413
394
  * Fixed conventions are intentionally not configurable: Plate names come from
414
- * `virtual:xray/plates/<name>`, files live under `platesDir`, and responsive
415
- * breakpoints are derived from captured CSS plus the `matchMedia` queries the
416
- * page evaluates.
395
+ * `virtual:xray/plates/<name>`, files live under `platesDir`, and responsive breakpoints are
396
+ * derived from captured CSS plus the `matchMedia` queries the page evaluates.
417
397
  */
418
398
  interface XrayOptions {
419
- /** Directory where Plate JSON files are written and read, relative to the Vite root. Default: `plates`. */
399
+ /**
400
+ * Directory where Plate JSON files are written and read, relative to the Vite root. Default:
401
+ * `plates`.
402
+ */
420
403
  platesDir?: string;
421
404
  /** Mount the dev HUD for coverage, Capture, Light Box, and capture-all controls. Default: `false`. */
422
405
  hud?: boolean;
423
406
  /**
424
- * Initial state of the browse-time Capture toggle in dev. Keep this off for
425
- * normal browsing and let the HUD, capture-all flow, or `?xray-capture` turn
426
- * Capture on per tab. Default: `false`.
407
+ * Initial state of the browse-time Capture toggle in dev. Keep this off for normal browsing and
408
+ * let the HUD, capture-all flow, or `?xray-capture` turn Capture on per tab. Default: `false`.
427
409
  */
428
410
  capture?: boolean;
429
- /** Settle delay in milliseconds before capturing a ready Skeleton; override one boundary with the `delay` prop. Default: `200`. */
411
+ /**
412
+ * Settle delay in milliseconds before capturing a ready Skeleton; override one boundary with the
413
+ * `delay` prop. Default: `200`.
414
+ */
430
415
  delay?: number;
431
- /** Upper bound in milliseconds on waiting for a busy subtree to go quiet before capturing anyway. Default: `5000`. */
416
+ /**
417
+ * Upper bound in milliseconds on waiting for a busy subtree to go quiet before capturing anyway.
418
+ * Default: `5000`.
419
+ */
432
420
  settleCap?: number;
433
421
  /**
434
- * Collect-stage limits (ADR 0022): refuse to capture a boundary whose measured
435
- * tree exceeds any axis — too big means the Skeleton sits too high in the tree.
436
- * `maxNodes` caps total surviving nodes (default `4096`), `maxBreadth` the widest
437
- * level (default `512`), `maxDepth` the nesting depth (default `32`). Omit
438
- * `maxNodes` and it derives as `round(maxBreadth × maxDepth / 4)`. Override one
439
- * boundary with the flat `maxNodes`/`maxBreadth`/`maxDepth` `<Skeleton>` props.
422
+ * Collect-stage limits (ADR 0022): refuse to capture a boundary whose measured tree exceeds any
423
+ * axis — too big means the Skeleton sits too high in the tree. `maxNodes` caps total surviving
424
+ * nodes (default `4096`), `maxBreadth` the widest level (default `512`), `maxDepth` the nesting
425
+ * depth (default `32`). Omit `maxNodes` and it derives as `round(maxBreadth × maxDepth / 4)`.
426
+ * Override one boundary with the flat `maxNodes`/`maxBreadth`/`maxDepth` `<Skeleton>` props.
440
427
  */
441
428
  collect?: {
442
429
  maxNodes?: number;
@@ -445,31 +432,30 @@ interface XrayOptions {
445
432
  };
446
433
  /**
447
434
  * Dev-only Fixtures (ADR 0015): record a `<Skeleton>`'s render-input `data` to a
448
- * `plates/<name>.fixture.json` sidecar and replay it in place of live data so
449
- * capture is deterministic. Boot state only — the HUD is per-tab authority, just
450
- * like `capture`. Entirely dev-only; nothing here ships in production.
435
+ * `plates/<name>.fixture.json` sidecar and replay it in place of live data so capture is
436
+ * deterministic. Boot state only — the HUD is per-tab authority, just like `capture`. Entirely
437
+ * dev-only; nothing here ships in production.
451
438
  */
452
439
  fixtures?: {
453
440
  /**
454
- * When to record the live `data`: `'manual'` (HUD button only — the default,
455
- * so live app data is never silently committed), `'first-ready'` (record once
456
- * the first time a Skeleton goes ready with no Fixture yet), or `'always'`
457
- * (record on every ready render). Default: `'manual'`.
441
+ * When to record the live `data`: `'manual'` (HUD button only — the default, so live app data
442
+ * is never silently committed), `'first-ready'` (record once the first time a Skeleton goes
443
+ * ready with no Fixture yet), or `'always'` (record on every ready render). Default:
444
+ * `'manual'`.
458
445
  */
459
446
  record?: 'manual' | 'first-ready' | 'always';
460
447
  /**
461
- * Whether to replay a recorded Fixture in place of live `data`: `false` (off,
462
- * the default), `'prefer'` (always use the Fixture when one exists), or
463
- * `'missing-only'` (use the Fixture only when live `data` is nullish —
464
- * `missing nullish`, so an empty array/object is present and stays
465
- * capturable). Default: `false`.
448
+ * Whether to replay a recorded Fixture in place of live `data`: `false` (off, the default),
449
+ * `'prefer'` (always use the Fixture when one exists), or `'missing-only'` (use the Fixture
450
+ * only when live `data` is nullish — `missing ≡ nullish`, so an empty array/object is present
451
+ * and stays capturable). Default: `false`.
466
452
  */
467
453
  replay?: false | 'prefer' | 'missing-only';
468
454
  };
469
455
  }
470
456
  /**
471
- * Create the Vite plugins that resolve committed Plates and, during dev,
472
- * capture new Plates from rendered `<Skeleton>` boundaries.
457
+ * Create the Vite plugins that resolve committed Plates and, during dev, capture new Plates from
458
+ * rendered `<Skeleton>` boundaries.
473
459
  *
474
460
  * Typical setup:
475
461
  *
@@ -482,8 +468,10 @@ interface XrayOptions {
482
468
  * ```
483
469
  *
484
470
  * Returns two plugins:
471
+ *
485
472
  * - `xray:data` resolves `virtual:xray/plates/<name>` from committed Plate files in dev and build.
486
- * - `xray:dev` runs only during `vite serve`; it injects the capture client, receives Captures, writes Plate files, and hot-updates the page.
473
+ * - `xray:dev` runs only during `vite serve`; it injects the capture client, receives Captures,
474
+ * writes Plate files, and hot-updates the page.
487
475
  */
488
476
  declare function xrayVitePlugin(options?: XrayOptions): Plugin[];
489
477
  interface CaptureRequest {
@@ -500,27 +488,37 @@ interface CaptureResponse {
500
488
  interface PlateBroadcaster {
501
489
  send(payload: unknown): void;
502
490
  }
503
- /** The subset of Vite's logger we use; it preserves the formatted string verbatim. */
491
+ /** The subset of Vite's logger used here; it preserves the formatted string verbatim. */
504
492
  interface ServerLogger {
505
493
  warn(message: string): void;
506
494
  }
507
495
  interface CaptureServer {
508
496
  ws: PlateBroadcaster;
509
497
  /**
510
- * Vite's logger (`server.config.logger`). When present its `warn` prints the
511
- * diagnostic text to the server output; otherwise we fall back to
512
- * `console.warn`. Either way the SAME shared formatter (diagnostics.ts) is
513
- * used, so server output matches the browser console exactly.
498
+ * Vite's logger (`server.config.logger`). When present its `warn` prints the diagnostic text to
499
+ * the server output; otherwise `console.warn` is the fallback. Either way the SAME shared
500
+ * formatter (diagnostics.ts) is used, so server output matches the browser console exactly.
514
501
  */
515
502
  config?: {
516
503
  logger?: ServerLogger;
517
504
  };
505
+ /**
506
+ * The subset of Vite's dev server needed to keep BOTH of a plate's virtual modules fresh after a
507
+ * plate file changes (capture POST, hand edit, git checkout). Optional — absent in the unit tests
508
+ * that stub a minimal `server`. `getModuleById`/`invalidateModule`/`reloadModule` mirror
509
+ * `ViteDevServer`'s real shape (a subset, so this module stays free of a hard `vite` import at
510
+ * the type level beyond the `Plugin` type already used elsewhere).
511
+ */
512
+ moduleGraph?: {
513
+ getModuleById(id: string): unknown;
514
+ invalidateModule(mod: unknown): void;
515
+ };
516
+ reloadModule?(mod: unknown): Promise<void>;
518
517
  }
519
518
  /**
520
- * Per-plate breakpoints + derived coverage bands, read from the committed
521
- * StoredPlate files. The `spans` derive from each plate's `breakpoints` (ADR 0022
522
- * cutover per-View spans no longer exist on disk; a StoredPlate sweeps all
523
- * widths in one session, so its coverage is complete).
519
+ * Per-plate breakpoints + derived coverage bands, read from the committed StoredPlate files. The
520
+ * `spans` derive from each plate's `breakpoints` (ADR 0022: a StoredPlate sweeps all widths in one
521
+ * session, so its coverage is complete).
524
522
  *
525
523
  * @internal
526
524
  */
@@ -529,54 +527,74 @@ declare function readCoverage(dir: string): Record<string, {
529
527
  spans: Span[];
530
528
  }>;
531
529
  /**
532
- * All recorded Fixtures as `{ <name>: <devalueString> }`, read from the
533
- * `<name>.fixture.json` sidecars (ADR 0015). The map values are the OPAQUE
534
- * devalue strings exactly as written the node side never parses them; the
535
- * browser dev client owns the devalue decode. The `.fixture.json` suffix keeps
536
- * these distinct from Plate files (`isValidPlateName` rejects the dotted name, so
530
+ * All recorded Fixtures as `{ <name>: <devalueString> }`, read from the `<name>.fixture.json`
531
+ * sidecars (ADR 0015). The map values are the OPAQUE devalue strings exactly as written — the node
532
+ * side never parses them; the browser dev client owns the devalue decode. The `.fixture.json`
533
+ * suffix keeps these distinct from Plate files (`isValidPlateName` rejects the dotted name, so
537
534
  * `readCoverage` never picks them up — Fixtures never leak into Plate coverage).
538
535
  *
539
536
  * @internal
540
537
  */
541
538
  declare function readFixtures(dir: string): Record<string, string>;
542
539
  /**
543
- * Write a recorded Fixture sidecar (ADR 0015). Reuses the capture endpoint's
544
- * front-door checks (method, content type, declared/streamed size cap) and the
545
- * `isValidPlateName` gate, but treats the devalue `data` payload as OPAQUE TEXT:
546
- * it is validated only as a string and written verbatim, never eval'd or parsed
547
- * server-side. The sidecar lives NEXT TO the Plate but is never merged into it,
548
- * so the persisted Plate stays purely structural.
540
+ * Write a recorded Fixture sidecar (ADR 0015). Reuses the capture endpoint's front-door checks
541
+ * (method, content type, declared/streamed size cap) and the `isValidPlateName` gate, but treats
542
+ * the devalue `data` payload as OPAQUE TEXT: it is validated only as a string and written verbatim,
543
+ * never eval'd or parsed server-side. The sidecar lives NEXT TO the Plate but is never merged into
544
+ * it, so the persisted Plate stays purely structural.
549
545
  *
550
546
  * @internal
551
547
  */
552
548
  declare function handleFixturePost(req: CaptureRequest, res: CaptureResponse, fixturePath: (name: string) => string): void;
553
549
  /**
554
- * Validate a posted `StoredPlate` envelope, write it AS-IS, and hot-swap the
555
- * plate in place. After the ADR 0022 cutover the dev client posts a finished
556
- * StoredPlate (the whole Plate already projected in the browser) inside a
557
- * `{ plate, diagnostics? }` envelope there is NO server-side accumulation/merge
558
- * anymore. `diagnostics` is a SIBLING field the server logs and then drops; it
559
- * never reaches the stored artifact (ADR 0008).
550
+ * Validate a posted `StoredPlate` envelope, write it AS-IS, and hot-swap the plate in place. The
551
+ * dev client posts a finished StoredPlate (the whole Plate already projected in the browser, ADR
552
+ * 0022) inside a `{ plate, diagnostics? }` envelope; there is NO server-side accumulation/merge.
553
+ * `diagnostics` is a SIBLING field the server logs and then drops; it never reaches the stored
554
+ * artifact (ADR 0008).
560
555
  *
561
556
  * @internal
562
557
  */
563
558
  declare function handleCapturePost(req: CaptureRequest, res: CaptureResponse, server: CaptureServer, platePath: (name: string) => string): void;
564
559
  /**
565
- * Render a plate as the virtual module's source. Each stitch (`ref` node) becomes
566
- * a real import of that child's virtual module, wired into a `refs` map on the
567
- * default export so the bundler builds the actual reference graph and chunks
568
- * the transitive closure a page uses, with no runtime registry (ADR 0006). A
569
- * stitchless plate is just its JSON. Self-references are dropped (a module can't
570
- * import itself); an unknown child resolves to its own empty plate, never a
571
- * build break.
560
+ * Render a plate as the virtual module's source. Each stitch (`ref` node) becomes a real import of
561
+ * that child's virtual module, wired into a `refs` map on the default export — so the bundler
562
+ * builds the actual reference graph and chunks the transitive closure a page uses, with no runtime
563
+ * registry (ADR 0006). A stitchless plate is just its JSON. Self-references are dropped (a module
564
+ * can't import itself); an unknown child resolves to its own empty plate, never a build break.
565
+ *
566
+ * `name` defaults to the plate's own `name`, but the loader passes the PATH-derived name so a
567
+ * hand-edited StoredPlate whose `name` field drifted from its filename still serializes and
568
+ * self-references under the canonical name (`collectRefs` reads `stored.tree`, a RenderNode).
569
+ *
570
+ * Two-artifact module shape (ADR 0026):
571
+ *
572
+ * ```js
573
+ * import "virtual:xray/base.css"
574
+ * import "virtual:xray/plates/<name>.css"
575
+ * export default { tree-only plate JSON }
576
+ * export const css = "<the plate's scoped css string>"
577
+ * ```
578
+ *
579
+ * The default export is the TREE artifact (`Plate` carries no css field); the page's styling flows
580
+ * through Vite's own CSS pipeline via the two side-effect imports (dev inject + HMR, build
581
+ * extraction into the chunk's `.css` asset). `export const css` is the plate's css as data, for dev
582
+ * tooling / programmatic consumers that want the string itself — production adapters import only
583
+ * the default, so Rollup TREE-SHAKES the unused named export out of built chunks (verified
584
+ * empirically; the side-effect css imports do not pin it).
572
585
  *
573
- * `name` defaults to the plate's own `name`, but the loader passes the
574
- * PATH-derived name so a hand-edited StoredPlate whose `name` field drifted from
575
- * its filename still serializes and self-references under the canonical name
576
- * (`collectRefs` reads `stored.tree`, a RenderNode, unchanged by the cutover).
586
+ * BASE_CSS rides as a JS-level import of the SHARED `virtual:xray/base.css` module (not an
587
+ * `@import` inside each plate's css): Vite dedupes JS module imports by id, so the base rules reach
588
+ * the page exactly once no matter how many plates (nested/stitched included — every plate module
589
+ * emits this import, module identity collapses them) a page mounts, and at build a multi-chunk app
590
+ * gets it hoisted/shared rather than copied per chunk. An `@import` inside each plate's `.css`
591
+ * would instead be INLINED per importer by Vite's postcss-import at build, duplicating BASE_CSS
592
+ * into every plate's asset — the opposite of the dedupe this design locks in. Base rides before the
593
+ * plate's css module so a plate rule still beats a base rule at equal specificity by source order
594
+ * (the `${BASE_CSS}\n${css}` composition order).
577
595
  *
578
596
  * @internal
579
597
  */
580
- declare function renderPlateModule(merged: MergedPlate, name?: string): string;
598
+ declare function renderPlateModule(merged: GatedPlate, name?: string): string;
581
599
  //#endregion
582
600
  export { type LeafKind, type Plate, type PlateNode, type PlateRule, type StoredPlate, type ViewCapture, XrayOptions, collectRefs, handleCapturePost, handleFixturePost, isValidPlateName, readCoverage, readFixtures, renderPlateModule, xrayVitePlugin };