@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.
@@ -0,0 +1,524 @@
1
+ //#region src/diagnostics.d.ts
2
+ /**
3
+ * Dev-only capture diagnostics: a small, serializable record of why a Plate may be incomplete or
4
+ * lower-fidelity, plus the one shared formatter both the browser console (`client.ts`) and the Vite
5
+ * server output (`index.ts`) print.
6
+ *
7
+ * Motivation: the serializer silently swallows a handful of capture omissions (an unreadable
8
+ * cross-origin stylesheet, skipped pseudo-elements, off-screen bones dropped at the fold, an
9
+ * over-the-limit subtree). Without these records, "why does this skeleton look wrong?" has no
10
+ * answer short of reading the serializer source. Diagnostics surface those omissions as data the
11
+ * capture returns, so a caller can log them; the core performs no console side effects.
12
+ *
13
+ * Strictly DEV-ONLY (ADR 0024, ADR 0008): this type is NOT a public package export and MUST NEVER
14
+ * reach the persisted Plate JSON, the `GatedPlate`, or the production adapter. It rides on the
15
+ * TRANSIENT `ViewIR.diagnostics` field only, which `captureView` populates and the dev client
16
+ * strips before posting (so committed `plates/<name>.json` stays byte-identical to a capture that
17
+ * produced no diagnostics).
18
+ *
19
+ * Centralized on purpose: invalid posted/committed plates (ADR 0023) reuse the same messaging, so
20
+ * the code table and formatter stay reusable. Browser and server share `formatDiagnostic` so their
21
+ * warning text cannot drift.
22
+ *
23
+ * @module
24
+ */
25
+ /**
26
+ * One aggregated capture omission. Serializable on purpose: NO DOM nodes, NO CSS bodies, NO
27
+ * captured text — only a stable `code`, a human message, an optional aggregate `count`, and an
28
+ * optional short `detail` (e.g. a plate name or a node count, never user content). `count`
29
+ * aggregates occurrences: there is at most one record per code per capture, never one per skipped
30
+ * selector or node.
31
+ */
32
+ interface CaptureDiagnostic {
33
+ code: DiagnosticCode;
34
+ message: string;
35
+ count?: number;
36
+ detail?: string;
37
+ }
38
+ /**
39
+ * The closed set of capture-omission codes. Each maps to one silent site in the serializer (or an
40
+ * existing oversized-capture warning folded into the same vocabulary). Kept as a const union so the
41
+ * message table is exhaustive.
42
+ */
43
+ 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';
44
+ //#endregion
45
+ //#region src/plate.d.ts
46
+ /**
47
+ * `hidden` is EXPLICIT-ONLY (a `data-xr-bone-kind` hint or a walker `ctx.bone({ kind: 'hidden'
48
+ * })`), never inferred: a bone that holds its measured box in the layout but paints nothing — the
49
+ * space-preserving complement to `data-xr-ignore`, which drops the box entirely.
50
+ */
51
+ type LeafKind = 'text-line' | 'text-block' | 'media' | 'box' | 'hidden';
52
+ /**
53
+ * The shipped structural capture of a component's rendered DOM — the TREE artifact a Skeleton
54
+ * renders from (see the glossary: xray-js.dev/glossary). A Plate carries structure ONLY (ADR 0026):
55
+ * its scoped CSS is an INDEPENDENT sibling artifact — the plugin serves it through Vite's own CSS
56
+ * pipeline (`virtual:xray/plates/<name>.css`, plus the shared `virtual:xray/base.css`, and an
57
+ * `export const css` on the virtual JS module for dev/programmatic reads), and the
58
+ * framework-neutral `core.ts` path hands it back separately (`captureElement` → `{ plate, css }`;
59
+ * `renderPlateHtml(plate, css)` receives it explicitly). Consumers decide how to render either
60
+ * part. Still self-contained in ADR 0003's sense: both artifacts originate from the capture and
61
+ * depend on nothing in the consumer's build. Built from the RenderNode tree at plugin load (see
62
+ * chunk.ts), so the adapter injects strings instead of walking a tree on the latency-critical first
63
+ * paint — and no per-framework serializer ships to the client (ADR 0008).
64
+ */
65
+ interface Plate {
66
+ v: number;
67
+ /** Capture name; comes from the virtual-module specifier, never typed at a call site. */
68
+ name: string;
69
+ /**
70
+ * The root's children, pre-serialized (ADR 0008): a stitch-free run is one HTML `string`; a
71
+ * stitch is `{ r: name }`; a stitch-bearing element is `{ c: className, k: chunk[] }`. Null until
72
+ * first capture.
73
+ */
74
+ chunks: Chunk[] | null;
75
+ /** The root node's own content classes (its inherited font context), if any. */
76
+ rootCls?: string;
77
+ /**
78
+ * Child plates named by the chunks' `{ r }` stitches, resolved through the import graph (ADR
79
+ * 0006). Populated by the loader's generated imports, never serialized into the plates JSON;
80
+ * absent in production when there are no stitches. In dev the live store takes precedence, so
81
+ * this need not be present.
82
+ */
83
+ refs?: Record<string, Plate>;
84
+ }
85
+ /**
86
+ * One serialized child of a plate root (or of a stitch-bearing node): a stitch-free HTML run
87
+ * (`string`), a stitch to a child plate (`{ r: name }`), a stitch-bearing element kept real so its
88
+ * inner stitches can mount (`{ c: className, k: chunk[] }`), or a templated uniform run (`{ t:
89
+ * html, n }`): the cell's HTML once plus a repeat count the adapter expands to N copies (Template,
90
+ * ADR 0022). The framework-neutral render contract: an adapter injects the strings, mounts a child
91
+ * plate at each stitch, and repeats each template (ADR 0008).
92
+ */
93
+ type Chunk = string | {
94
+ r: string;
95
+ } | {
96
+ c: string;
97
+ k: Chunk[];
98
+ } | {
99
+ t: string;
100
+ n: number;
101
+ };
102
+ /**
103
+ * A plate after the in-browser Serialize (emit + gate + classify) but before chunk serialization:
104
+ * the build-time RenderNode tree plus its scoped CSS. Produced by `serializePlateIR` (the
105
+ * structural part of `StoredPlate`) and the single-View `capture`, then serialized into the shipped
106
+ * `Plate` (chunks) at load (`serializePlate`). Never shipped to the client.
107
+ */
108
+ interface GatedPlate {
109
+ v: number;
110
+ name: string;
111
+ tree: RenderNode | null;
112
+ css: string;
113
+ }
114
+ interface PlateNode {
115
+ /** Plate-local id; rules reference it pre-classify. Stripped from the shipped tree. */
116
+ id: number;
117
+ /** Present on paint leaves; absent on layout containers. */
118
+ leaf?: LeafKind;
119
+ /**
120
+ * A stitch: render the named child plate's skeleton here instead of own structure (ADR 0006).
121
+ * Mutually exclusive with `leaf`/`kids`. Resolved against `Plate.refs` (prod) or the live store
122
+ * (dev).
123
+ */
124
+ ref?: string;
125
+ /**
126
+ * A templated uniform run (Template, ADR 0022): render this node's subtree `count` times. The
127
+ * serializer emits the subtree's HTML once plus the count; the adapter expands it to N copies at
128
+ * render. Absent on ordinary nodes.
129
+ */
130
+ count?: number;
131
+ kids?: PlateNode[];
132
+ }
133
+ /**
134
+ * A node in the shipped tree, after classify (ADR 0007). Carries the content-addressed style
135
+ * classes it needs instead of an id — the CSS targets those classes, not a per-node `data-xr`
136
+ * value, so no per-node scoping key is needed. `cls` is a space-joined class-token list.
137
+ */
138
+ interface RenderNode {
139
+ leaf?: LeafKind;
140
+ ref?: string;
141
+ cls?: string;
142
+ /**
143
+ * A templated uniform run (Template, ADR 0022): serialize this subtree once + a count the adapter
144
+ * repeats.
145
+ */
146
+ count?: number;
147
+ kids?: RenderNode[];
148
+ }
149
+ /**
150
+ * A captured style rule in id form — selectors are generated downstream, so Serialize can remap
151
+ * node ids without string surgery. `ids: []` targets the plate root.
152
+ */
153
+ interface PlateRule {
154
+ ids: number[];
155
+ decls: string[];
156
+ /** Media-condition stack, outermost first; conditions AND together. */
157
+ media?: string[];
158
+ /** `@container`-condition stack, outermost first; conditions AND together. Absent when none. */
159
+ container?: string[];
160
+ layered?: boolean;
161
+ spec: number;
162
+ order: number;
163
+ }
164
+ /**
165
+ * One capture of a component within one viewport view — `[min, max)` in px, either side open-ended
166
+ * when undefined. The id-form `{ tree, rules }` shape `emit` projects a measured View into (ADR
167
+ * 0004). The disk/wire shape is `StoredPlate` (below); this is the framework-neutral per-View
168
+ * capture shape, exported for consumers reading the id-form projection directly.
169
+ */
170
+ interface ViewCapture {
171
+ /** Viewport width at capture time. */
172
+ width: number;
173
+ min?: number;
174
+ max?: number;
175
+ tree: PlateNode;
176
+ rules: PlateRule[];
177
+ /**
178
+ * TRANSIENT, dev-only capture diagnostics (why this view may be lower fidelity). Captured in the
179
+ * browser (mirrored onto `ViewIR.diagnostics`); the dev client logs it and posts it alongside the
180
+ * `StoredPlate` (so the Vite server can print the same text). It MUST NEVER be persisted: it is
181
+ * not part of the committed `StoredPlate` disk shape, the validator does not carry it, and the
182
+ * POST envelope keeps it in a SIBLING field that never reaches the stored artifact (ADR 0008).
183
+ * Absent in production (the prod adapter ships no capture). See diagnostics.ts and ADR 0008.
184
+ */
185
+ diagnostics?: CaptureDiagnostic[];
186
+ }
187
+ /**
188
+ * The committed `plates/<name>.json` shape (ADR 0022): the whole Plate already projected in the
189
+ * browser (the in-browser Serialize folds emit + the multi-View gating + classify in one session),
190
+ * so disk holds ONE gated, content-addressed tree + css rather than per-View captures assembled at
191
+ * load. It is a `GatedPlate` (the structural `{tree, css}` `serializePlate` consumes) PLUS the
192
+ * `breakpoints` the HUD coverage reads — the analysis-only geometry never crosses the wire (ADR
193
+ * 0021). The gen-side runs `serializePlate(stored) → Plate` (chunks) at module-load, leaving the
194
+ * Tailwind/Bundle passthrough a clean gen-side seam.
195
+ */
196
+ interface StoredPlate extends GatedPlate {
197
+ /** Width thresholds (view boundaries); the HUD coverage derives its bands from these. */
198
+ breakpoints: number[];
199
+ }
200
+ /**
201
+ * Renderer-owned base styles, emitted once by the outermost view (ADR 0007), before a plate's node
202
+ * rules so a node's captured class wins by source order. Everything visual is driven by inheritable
203
+ * custom properties, so consumers theme by setting `--xr-*` in `:root` or on any ancestor — and
204
+ * per-plate via `[data-xr-root="name"]` (the root carries its plate name). See ADR 0011 and
205
+ * docs/architecture.md. Class-based and unlayered — `@layer` was rejected (unlayered app CSS would
206
+ * beat layered skeleton rules, and it inverts `!important`).
207
+ *
208
+ * Progressive enhancement (ADR 0011): the plain bone fill + opacity pulse use only
209
+ * universally-supported CSS and are the floor. `light-dark()` is guarded by `@supports` because an
210
+ * unsupported VALUE drops the whole declaration (an invisible bone), and it is not
211
+ * Baseline-widely-available until 2026-11-13 — drop the guard then. Features that merely no-op when
212
+ * absent (`@property`, `prefers-contrast`, relative-color-as-fallback) carry no hard guard.
213
+ *
214
+ * Tokens (ADR 0017 taxonomy). The `--xr-bone-*` family styles an individual Bone: `--xr-bone-color`
215
+ * (fill), `--xr-bone-highlight-color` (sheen), `--xr-bone-border-radius` (+
216
+ * `--xr-bone-text-line-border-radius` / `--xr-bone-text-block-border-radius` /
217
+ * `--xr-bone-media-border-radius`), `--xr-bone-animation-duration`, the pulse range
218
+ * `--xr-bone-pulse-opacity-min`/`--xr-bone-pulse-opacity-max`, `--xr-bone-animation` (mode:
219
+ * `xr-pulse` | `none` | a custom keyframe), and `--xr-bone-fill` (override the leaf paint, e.g. a
220
+ * sheen gradient built from the two bone color tokens). The `--xr-skeleton-*` family governs
221
+ * whole-skeleton display timing (ADR 0016): `--xr-skeleton-delay` (grace before the skeleton
222
+ * becomes visible), `--xr-skeleton-min-duration` (minimum visible time once shown; read by the
223
+ * `useSkeletonTiming` hook, not used in CSS), and `--xr-skeleton-transition-duration` (the
224
+ * reveal/swap fade). The private channels `--xr-o` (pulse) and `--xr-reveal` (reveal) stay
225
+ * undocumented `@property` plumbing, not a theming surface.
226
+ *
227
+ * Declared as a PLAIN template literal — no `.trim()` (or any other call) on the initializer. A
228
+ * method call is not provably pure to a conservative tree-shaker (rolldown/Rollup) and would pin
229
+ * this ~5 KB string into every consumer chunk that imports the React adapter, referenced or not
230
+ * (ADR 0026). The template starts immediately after the backtick, so there is no leading newline to
231
+ * strip.
232
+ */
233
+ declare const BASE_CSS = "/* One timeline per skeleton, not per leaf: the root animates one inherited\n opacity property and every leaf reads it. Hundreds of independent infinite\n opacity animations would pin the compositor and cook the CPU. */\n@property --xr-o { syntax: \"<number>\"; inherits: true; initial-value: 1 }\n/* The reveal channel (ADR 0016): a second inherited opacity factor the leaf\n multiplies into --xr-o. Transitioning THIS (not opacity directly) sidesteps\n the animation-vs-transition collision ADR 0012 flagged. initial-value 1 (NOT\n 0) is the degradation floor: without @starting-style the reveal can never be\n driven 0->1, so it must REST at 1 (fully visible). The delay-hide below is\n pure progressive enhancement layered on top. */\n@property --xr-reveal { syntax: \"<number>\"; inherits: true; initial-value: 1 }\n.xr-root {\n --xr-bone-highlight-color: #f4f4f5;\n display: contents;\n animation: var(--xr-bone-animation, xr-pulse) var(--xr-bone-animation-duration, 1.2s) ease-in-out infinite alternate;\n}\n/* No pointer-events:none — it blocks inspecting bones in devtools and buys\n nothing (no real content to click). border-color: a node may capture\n border-width for layout; its paint stays out. list-style: suppress a ::marker\n a captured display:list-item would otherwise paint. */\n.xr-node { border-color: transparent; list-style: none }\n.xr-leaf {\n background: var(--xr-bone-fill, var(--xr-bone-color, #e4e4e7));\n border-radius: var(--xr-bone-border-radius, 4px);\n /* Composed opacity (ADR 0016): the pulse channel times the reveal channel.\n Never transition opacity directly (the pulse animation owns it). */\n opacity: calc(var(--xr-o) * var(--xr-reveal));\n}\n/* Per-kind defaults: a single text LINE reads as a rounded bar; a multi-line\n text BLOCK as a soft rect (a tall pill misreads as a button); media carries\n the author radius, defaulting to the same soft rect. Re-theme any kind via\n [data-xr-root] .xr-leaf-text-line { ... }. */\n.xr-leaf-text-line { border-radius: var(--xr-bone-text-line-border-radius, 999px) }\n.xr-leaf-text-block { border-radius: var(--xr-bone-text-block-border-radius, var(--xr-bone-border-radius, 4px)) }\n.xr-leaf-media { border-radius: var(--xr-bone-media-border-radius, var(--xr-bone-border-radius, 4px)) }\n/* A hidden bone holds its measured box in the flow but paints nothing: the\n space-preserving complement to data-xr-ignore. visibility (not display) so\n the captured size rules still lay the box out. */\n.xr-leaf-hidden { visibility: hidden }\n@keyframes xr-pulse {\n from { --xr-o: var(--xr-bone-pulse-opacity-max, 1) }\n to { --xr-o: var(--xr-bone-pulse-opacity-min, 0.5) }\n}\n/* Reveal delay-hide (ADR 0016), a progressive enhancement guarded so it can NEVER\n strand a skeleton invisible (open Q3). @starting-style is the trigger: only a\n browser that supports it enters this block at all, and such a browser also\n honors @property + transitions, so the 0->1 reveal completes. The @supports\n selector(...) probe is the broadest cross-engine @starting-style feature test\n available. Without support the block is skipped entirely and --xr-reveal stays\n at its initial-value 1 = fully visible instant skeleton. INSIDE the block we\n start at 0 and transition to 1 after the delay: invisible for\n --xr-skeleton-delay, then a --xr-skeleton-transition-duration fade-in; a fast\n load unmounts before the delay elapses and the skeleton is never seen. */\n@supports (selector(:has(*))) {\n /* The delay-hide applies to every root EXCEPT a stitch continuation (ADR 0020).\n Scoping with :not([data-xr-instant]) keeps an ordinary top-level skeleton\n byte-identical to before: it still mounts at --xr-reveal 0 and fades in after\n the delay. The transition/initial-1 rest state matches the original. */\n .xr-root:not([data-xr-instant]) {\n --xr-reveal: 1;\n transition: --xr-reveal var(--xr-skeleton-transition-duration, 150ms) linear var(--xr-skeleton-delay, 250ms);\n }\n @starting-style { .xr-root:not([data-xr-instant]) { --xr-reveal: 0 } }\n /* A stitch continuation (ADR 0020) reveals INSTANTLY: it was already visible as a\n stitch under a still-showing parent, so re-paying the delay would flash it away\n and back. No @starting-style and no transition — it rests at fully visible from\n its first frame, matching how the stitch inherited the parent's reveal. */\n .xr-root[data-xr-instant] { --xr-reveal: 1 }\n}\n/* Reduced motion (open Q2): keep the anti-flash delay + min-duration (they are\n not motion), drop only the FADE — collapse the reveal transition to instant and\n kill the pulse animation (as before). */\n@media (prefers-reduced-motion: reduce) {\n .xr-root { animation: none; --xr-skeleton-transition-duration: 0s }\n}\n/* Dark + high-contrast bone defaults. Guarded: light-dark() is an invalid value\n without support and would drop the declaration — see the note above; remove\n this @supports once widely available (2026-11-13). */\n@supports (color: light-dark(#000, #fff)) {\n .xr-root { --xr-bone-highlight-color: light-dark(#f4f4f5, #52525b) }\n .xr-leaf { background: var(--xr-bone-fill, var(--xr-bone-color, light-dark(#e4e4e7, #3f3f46))) }\n @media (prefers-contrast: more) {\n .xr-leaf { background: var(--xr-bone-fill, var(--xr-bone-color, light-dark(#d4d4d8, #52525b))) }\n }\n}\n/* Derive the sheen highlight from --xr-bone-color so re-theming one token updates\n both. Relative color isn't widely available yet; the static highlight above is\n the fallback (sheen just looks flatter), so no hard guard is needed. */\n@supports (color: oklch(from red l c h)) {\n .xr-root { --xr-bone-highlight-color: oklch(from var(--xr-bone-color, #e4e4e7) calc(l + 0.08) c h) }\n}";
234
+ /**
235
+ * Compose a plate's scoped css with the renderer-owned BASE_CSS, base first so a plate rule beats a
236
+ * base rule at equal specificity by source order — the standard composition for rendering a
237
+ * skeleton outside the Vite plugin (where `virtual:xray/base.css` +
238
+ * `virtual:xray/plates/<name>.css` do this through the CSS pipeline instead). css is the CALLER's
239
+ * input (ADR 0026): `renderPlateHtml(plate, composeCss(css))`, or `<Skeleton plate={plate}
240
+ * css={composeCss(css)}>` for a runtime-captured plate. Rendering a NESTED stitch under an
241
+ * already-composed parent needs the raw css only (base is already on the page) — pass the string
242
+ * uncomposed there.
243
+ */
244
+ declare function composeCss(css: string): string;
245
+ /** The (unserialized) plate an import resolves to before anything has been captured. */
246
+ declare function emptyPlate(name: string): GatedPlate;
247
+ /**
248
+ * A dev-only sticky boolean toggle (get/set/subscribe), persisted per tab. Used for both "light
249
+ * box" (render every `<Skeleton>` regardless of readiness) and "capture" (whether browsing
250
+ * re-captures plates).
251
+ */
252
+ interface ToggleStore {
253
+ get: () => boolean;
254
+ set: (value: boolean) => void;
255
+ subscribe: (onChange: () => void) => () => void;
256
+ }
257
+ /**
258
+ * Dev-only store of the freshest plate per name, updated in place when a capture lands.
259
+ * `<Skeleton>` reads through it so a new plate re-renders the site rather than reloading the module
260
+ * — which would remount the capture boundary and re-fire the capture, a feedback loop.
261
+ */
262
+ interface PlateStore {
263
+ get: (name: string) => Plate | undefined;
264
+ subscribe: (name: string, onChange: () => void) => () => void;
265
+ /**
266
+ * Register a plate imported via the ref graph if the store has none yet — so a stitch resolves
267
+ * even before its child is re-captured, and survives a parent hot-swap (whose pushed plate
268
+ * carries no resolved `refs`). Never overwrites a live capture.
269
+ */
270
+ seed: (name: string, plate: Plate) => void;
271
+ }
272
+ /** A view interval `[min, max)`; an open end is undefined (ADR 0004). */
273
+ interface Span {
274
+ min?: number;
275
+ max?: number;
276
+ }
277
+ /**
278
+ * Dev-only Fixture store (ADR 0015): stored render-INPUT `data` per Plate, recorded to and replayed
279
+ * from a `plates/<name>.fixture.json` sidecar. Entirely dev-only — devalue and this store never
280
+ * reach the production bundle.
281
+ *
282
+ * Two cooperating halves, both flowing through the render-prop `data` seam (ADR 0014):
283
+ *
284
+ * - The dev adapter (`react.dev.tsx`) calls `register(name, data)` at `loading=false` with the live
285
+ * `data` it is about to render, so the latest recordable input for each mounted Plate is always
286
+ * in scope for Record.
287
+ * - `record(name)` serializes that registered live `data` with devalue and POSTs it to the sidecar
288
+ * (the HUD button and the `first-ready`/`always` modes call it). `has(name)` reports whether a
289
+ * Fixture exists (HUD coverage marker), and `read(name)` returns the parsed Fixture `data` for
290
+ * Replay to substitute in front of the render prop.
291
+ *
292
+ * The store holds DECODED values (the dev client owns the devalue parse, since it has the live
293
+ * runtime); only the on-disk sidecar and the wire carry the opaque devalue string.
294
+ */
295
+ interface FixtureStore {
296
+ /**
297
+ * Record the latest live `data` the dev adapter is about to render for `name`, so Record always
298
+ * has the current recordable input in scope. A `register` with the same value is cheap; the
299
+ * adapter calls it on every ready render. Returns a disposer the adapter runs on unmount so an
300
+ * unmounted Skeleton's `data` never lingers in the store: an unmounted instance must never be
301
+ * recorded or shadow a still-mounted one (ADR 0015). With several Skeletons of the same plate
302
+ * name mounted, the live value is the most-recently-registered still-mounted instance.
303
+ */
304
+ register: (name: string, data: unknown) => () => void;
305
+ /** True once a recorded Fixture exists for `name` (drives the HUD coverage marker). */
306
+ has: (name: string) => boolean;
307
+ /** The parsed Fixture `data` for `name`, or `undefined` when none is recorded. */
308
+ read: (name: string) => {
309
+ data: unknown;
310
+ } | undefined;
311
+ /**
312
+ * Serialize the registered live `data` for `name` with devalue and POST it to the sidecar.
313
+ * Returns a promise that resolves once the write is server-confirmed (so the HUD can surface a
314
+ * failed Record). Throws loudly when the live `data` is unrecordable (devalue's contract) or no
315
+ * live `data` was registered (a plain-children `<Skeleton>` has none).
316
+ */
317
+ record: (name: string) => Promise<void>;
318
+ /**
319
+ * Record every Skeleton that currently has live `data` registered (the HUD's single "Record
320
+ * fixture" button — one click captures the inputs of all mounted render-prop Skeletons). Resolves
321
+ * once all writes are server-confirmed; a rejected write surfaces to the HUD.
322
+ */
323
+ recordAll: () => Promise<void>;
324
+ /**
325
+ * Seed the store from the on-disk sidecars on boot: a `{ <name>: devalueString }` map (the opaque
326
+ * strings the server serves). The client decodes each with devalue. Existing entries are not
327
+ * clobbered, so a Fixture recorded this session survives a re-seed.
328
+ */
329
+ seed: (encoded: Record<string, string>) => void;
330
+ /** React subscription so the HUD re-renders when a Fixture is recorded or seeded. */
331
+ subscribe: (onChange: () => void) => () => void;
332
+ }
333
+ /** A plate's detected width thresholds and the spans actually captured, from disk. */
334
+ interface PlateCoverage {
335
+ breakpoints: number[];
336
+ spans: Span[];
337
+ }
338
+ /**
339
+ * Dev-only per-plate coverage, sourced from the committed plate files (not the session): the
340
+ * breakpoints detected so far and which view spans are captured. The HUD renders every band —
341
+ * covered ones lit, the rest dim — and the capture-all sweep derives its target widths from the
342
+ * breakpoints.
343
+ */
344
+ interface CoverageStore {
345
+ get: () => ReadonlyMap<string, PlateCoverage>;
346
+ set: (name: string, coverage: PlateCoverage) => void;
347
+ subscribe: (onChange: () => void) => () => void;
348
+ }
349
+ /**
350
+ * Dev-only capture hook, installed by the dev client before the app boots. Absent in production
351
+ * builds, so `<Skeleton>` carries zero capture weight. `captured` returns a dispose callback (stop
352
+ * observing this site).
353
+ */
354
+ interface CaptureHook {
355
+ captured: (name: string, el: Element, opts?: {
356
+ delay?: number;
357
+ walker?: unknown;
358
+ mode?: 'auto' | 'manual';
359
+ collect?: {
360
+ maxNodes?: number;
361
+ maxBreadth?: number;
362
+ maxDepth?: number;
363
+ };
364
+ }) => (() => void) | undefined;
365
+ /** Light Box toggle: force every `<Skeleton>` to its skeleton, read by `<Skeleton>`. */
366
+ lightbox?: ToggleStore;
367
+ /** Capture toggle: whether browsing re-captures plates, flipped from the HUD. */
368
+ capture?: ToggleStore;
369
+ /** Read side for `<Skeleton>`. */
370
+ plates?: PlateStore;
371
+ /** Write side for the dev client's HMR-event handler. Returns whether the plate changed. */
372
+ updatePlate?: (name: string, plate: Plate) => boolean;
373
+ /** Dev-only: run the HUD-triggered capture-all-views sweep via the extension (ADR 0010). */
374
+ captureAllViews?: () => Promise<void>;
375
+ /** Dev-only per-plate breakpoints + captured spans, from disk; fed by the bootstrap. */
376
+ coverage?: CoverageStore;
377
+ /**
378
+ * Replay toggle (ADR 0015): when active, the dev adapter substitutes a recorded Fixture's `data`
379
+ * for the live `data` at `loading=false`. The boolean is the coarse on/off the HUD owns per tab;
380
+ * the per-mode policy (`prefer` vs `missing-only`) is carried alongside in `replayMode`.
381
+ */
382
+ replay?: ToggleStore;
383
+ /** Dev-only Replay policy: how a Fixture substitutes for live `data` (ADR 0015). */
384
+ replayMode?: 'prefer' | 'missing-only';
385
+ /**
386
+ * Dev-only Record policy (ADR 0015): when the dev adapter records the live `data` it is about to
387
+ * render. `'manual'` (the default) records nothing automatically — only the HUD button does;
388
+ * `'first-ready'` records once when a Skeleton goes ready and has no Fixture yet; `'always'`
389
+ * records on every ready render.
390
+ */
391
+ recordMode?: 'manual' | 'first-ready' | 'always';
392
+ /** Dev-only Fixture record/replay store (ADR 0015); never present in production. */
393
+ fixtures?: FixtureStore;
394
+ }
395
+ declare global {
396
+ var __XRAY__: CaptureHook | undefined;
397
+ }
398
+ //#endregion
399
+ //#region src/measure.d.ts
400
+ /**
401
+ * Browser-side capture: walk a live rendered subtree into a Plate — one node tree plus one
402
+ * self-contained, scoped CSS string (ADR 0003). The CSS is lifted from the author rules that
403
+ * matched each node, rewritten to plate-local selectors — it carries `@media` for free and keeps
404
+ * layout live (the bake-off winner; ADR 0005).
405
+ */
406
+ interface CaptureOptions {
407
+ /** Plate name; comes from the virtual-module specifier. */
408
+ name: string;
409
+ /** Collect limits (ADR 0022): refuse a capture whose measured tree exceeds any axis. */
410
+ collect?: CollectLimits;
411
+ /**
412
+ * Optional programmable capture walker (ADR 0018). The declarative `data-xr-*` hints cover the
413
+ * 90%; this is the escape hatch for the 10% a markup attribute cannot express. Capture-only: it
414
+ * shapes the resulting `PlateNode` tree but is never persisted into the Plate, and the production
415
+ * adapter never captures so it ignores this entirely.
416
+ */
417
+ walker?: XrayCaptureWalker;
418
+ /**
419
+ * Whether the top-level capture roots ARE this plate's own boundary, so the stitch guard must be
420
+ * SKIPPED for them (ADR 0006/0014). True ONLY for a MANUAL root: there `site.el` is the
421
+ * consumer's own element and its `data-xr-boundary` marks the boundary of THIS plate, so the root
422
+ * must be serialized as a real Entry (its box/classes/padding/border) rather than a
423
+ * stitch-to-itself. Defaults to `false` — the auto path, where the top-level roots are the
424
+ * wrapper's CHILDREN and any boundary marker on a direct-child root is a NESTED `<Skeleton>` that
425
+ * MUST stitch (the capture-leak guard).
426
+ */
427
+ captureRootIsBoundary?: boolean;
428
+ }
429
+ /**
430
+ * What a capture walker decides for one element (ADR 0018; ADR 0022 §5). The `applyWalker` PASS
431
+ * reads the decision and applies it to the measured IR node — a walker NEVER hand-builds a
432
+ * `PlateNode`, so xray keeps ownership of the Plate format. `ignore` drops the element + its
433
+ * subtree; `bone` collapses it to one Bone leaf (inferring the kind when omitted); `keep` defers to
434
+ * the default classifier (`markInk`).
435
+ */
436
+ type WalkerDecision = {
437
+ type: 'ignore';
438
+ } | {
439
+ type: 'bone';
440
+ kind?: LeafKind;
441
+ } | {
442
+ type: 'keep';
443
+ };
444
+ /**
445
+ * The context handed to a capture walker's `element` hook (ADR 0018). The helpers are PURE — each
446
+ * returns a `WalkerDecision` the `applyWalker` pass applies; they do NOT mutate. `el` is the
447
+ * element under consideration.
448
+ */
449
+ interface CaptureWalkerContext {
450
+ /** The element currently being classified. */
451
+ el: Element;
452
+ /** Drop this element AND its whole subtree (no bone). */
453
+ ignore(): WalkerDecision;
454
+ /**
455
+ * Collapse this element's subtree into ONE Bone leaf. `kind` omitted shares the default
456
+ * classifier's kind-inference heuristic (`inferLeafKind`; ADR 0018, Q4 — ONE code path).
457
+ */
458
+ bone(opts?: {
459
+ kind?: LeafKind;
460
+ }): WalkerDecision;
461
+ /** Defer to the default classifier (`markInk`) — recurse into the subtree. */
462
+ keep(): WalkerDecision;
463
+ }
464
+ /**
465
+ * The programmable capture walker (ADR 0018; ADR 0022 §5). `element` is offered each non-boundary
466
+ * element TOP-DOWN over the measured IR (the `applyWalker` pass, NOT the DOM walk) and returns a
467
+ * `WalkerDecision` the pass applies. The declarative `data-xr-*` hints cover the 90%; this is the
468
+ * 10% escape hatch for what a markup attribute cannot express.
469
+ */
470
+ interface XrayCaptureWalker {
471
+ /**
472
+ * Called for every non-boundary element node (a stitch carries no element and is skipped — ADR
473
+ * 0006 — so a boundary never reaches here). Return one of `ctx.ignore()`, `ctx.bone()`, or
474
+ * `ctx.keep()`. An `ignore`/`bone` decision stops the descent (the subtree lowers); `keep`
475
+ * recurses.
476
+ */
477
+ element?(ctx: CaptureWalkerContext): WalkerDecision;
478
+ }
479
+ /**
480
+ * Typed identity wrapper for authoring a capture walker (ADR 0018) — the `defineConfig` pattern. It
481
+ * returns its argument unchanged at runtime; its only job is to infer and check the
482
+ * `XrayCaptureWalker` shape at the call site so an author gets completion and a typo in a hook name
483
+ * is caught.
484
+ */
485
+ declare function defineXrayCaptureWalker(walker: XrayCaptureWalker): XrayCaptureWalker;
486
+ /** The measured size of a capture's surviving tree, one number per Collect axis (ADR 0022). */
487
+ type CaptureSize = {
488
+ nodes: number;
489
+ breadth: number;
490
+ depth: number;
491
+ };
492
+ /** Which Collect axis a too-large capture tripped. */
493
+ type CollectAxis = 'nodes' | 'breadth' | 'depth';
494
+ /**
495
+ * Thrown when a capture's measured tree exceeds a Collect limit — almost always a `<Skeleton>`
496
+ * sitting too high in the tree. Raised after the cheap walk and before the O(rules × nodes) CSS
497
+ * extraction, so an oversized capture is refused without freezing the main thread on it.
498
+ */
499
+ declare class CaptureTooLargeError extends Error {
500
+ /** The full measured size of the surviving tree that tripped the gate. */
501
+ readonly size: CaptureSize;
502
+ /** Which axis exceeded its cap, and the cap it exceeded. */
503
+ readonly axis: CollectAxis;
504
+ readonly max: number;
505
+ /** Back-compat alias for `size.nodes` (the sole axis before breadth/depth). */
506
+ readonly nodeCount: number;
507
+ /** Folds the oversized-capture warning into the shared diagnostics vocabulary (diagnostics.ts). */
508
+ readonly code: "too-large";
509
+ constructor(size: CaptureSize, axis: CollectAxis, max: number);
510
+ }
511
+ /**
512
+ * Collect-stage size limits (ADR 0022): refuse to capture a boundary whose measured tree exceeds
513
+ * any axis. `maxNodes` caps the total surviving-node count; `maxBreadth` the widest level (most
514
+ * nodes at any one depth); `maxDepth` the nesting depth. Each axis is enforced only when its cap is
515
+ * defined. The defaults and the `maxNodes = round(maxBreadth × maxDepth / 4)` derivation live in
516
+ * the client (where the other defaults live); this core only enforces what it is handed.
517
+ */
518
+ type CollectLimits = {
519
+ maxNodes?: number;
520
+ maxBreadth?: number;
521
+ maxDepth?: number;
522
+ };
523
+ //#endregion
524
+ export { ViewCapture as _, CollectAxis as a, CaptureDiagnostic as b, XrayCaptureWalker as c, GatedPlate as d, LeafKind as f, StoredPlate as g, RenderNode as h, CaptureWalkerContext as i, defineXrayCaptureWalker as l, PlateNode as m, CaptureSize as n, CollectLimits as o, Plate as p, CaptureTooLargeError as r, WalkerDecision as s, CaptureOptions as t, BASE_CSS as u, composeCss as v, emptyPlate as y };