@hueest/xray 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # @hueest/xray
2
+
3
+ Auto-captured skeleton loading screens for [Vite](https://vite.dev). Capture the structure, ship the plate.
4
+
5
+ xray captures the **real rendered DOM** of your components as you browse your own dev server, then renders pixel-faithful skeleton loading states from those captures — no hand-drawn placeholders, no separate headless browser, no fixed-height/absolute hacks.
6
+
7
+ > Beta — APIs may still move before 1.0. Docs, guides, and the API reference live at
8
+ > **[xray-js.dev](https://xray-js.dev)**; source and issues at
9
+ > [github.com/hueest/xray](https://github.com/hueest/xray).
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ pnpm add @hueest/xray
15
+ ```
16
+
17
+ ## Use
18
+
19
+ ```ts
20
+ // vite.config.ts
21
+ import { xrayVitePlugin } from '@hueest/xray'
22
+
23
+ export default defineConfig({
24
+ plugins: [xrayVitePlugin({ hud: true })],
25
+ })
26
+ ```
27
+
28
+ ```tsx
29
+ import { Skeleton } from '@hueest/xray/react'
30
+ import plate from 'virtual:xray/plates/my-banner'
31
+
32
+ // skeleton while loading; content + (dev) capture when ready
33
+ ;<Skeleton plate={plate} loading={loading} fallback={<Spinner />}>
34
+ <RealBanner data={data} />
35
+ </Skeleton>
36
+ ```
37
+
38
+ To eyeball skeletons without waiting on loading states, tick **Light Box**
39
+ in the HUD (or open the page with `?xray-lightbox`) — every
40
+ mounted `<Skeleton>` flips to its skeleton until you untick it.
41
+
42
+ Capture starts off: tick **Capture** in the HUD (or pass `capture: true` to
43
+ `xrayVitePlugin`) to start writing Plates as you browse. With Capture on, browse
44
+ your dev server and captures land in `plates/` (commit them). Resize to
45
+ capture other views — JS-driven layout differences merge into the one plate
46
+ as `display:none` projections. Add `"@hueest/xray/virtual"` to your tsconfig `types`
47
+ for the `virtual:xray/plates/*` imports.
48
+
49
+ ## Learn more
50
+
51
+ - **[xray-js.dev](https://xray-js.dev)** — the docs site: how it works, the
52
+ radiographic mental model (Plate / Capture / Stitch / View / Projection / Light
53
+ Box), and guides for responsive Views, the HUD & Light Box, and capture-all-views.
54
+ - **[xray-js.dev/reference/api/xray](https://xray-js.dev/reference/api/xray)** —
55
+ full API reference, generated from source.
56
+ - **[GitHub](https://github.com/hueest/xray)** — source, issues, and the
57
+ architecture/decision-record trail.
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { h as StoredPlate, o as CollectLimits, v as CaptureDiagnostic } from "./serialize-BgdGt34A.js";
1
+ import { b as CaptureDiagnostic, g as StoredPlate, o as CollectLimits } from "./serialize-BqbQVxYl.js";
2
2
 
3
3
  //#region src/client.d.ts
4
4
  /**
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as serializePlate, d as formatDiagnostic, f as makeDiagnostic, l as runPlatePasses, n as deriveBreakpoints, o as CaptureTooLargeError, p as emit, r as regimeFor, s as captureView, t as serializePlateIR, u as diagnosticsHeader } from "./project-BZosujs9.js";
1
+ import { a as serializePlate, d as formatDiagnostic, f as makeDiagnostic, l as runPlatePasses, n as deriveBreakpoints, o as CaptureTooLargeError, p as emit, r as regimeFor, s as captureView, t as serializePlateIR, u as diagnosticsHeader } from "./project-BAXKpnfF.js";
2
2
  //#region ../../node_modules/.pnpm/devalue@5.8.1/node_modules/devalue/src/base64.js
3
3
  /** @type {(array_buffer: ArrayBuffer) => string} */
4
4
  function encode_native(array_buffer) {
@@ -752,7 +752,8 @@ function installXrayClient(options) {
752
752
  };
753
753
  const previewRecording = (buf) => {
754
754
  const stored = projectRecording(buf);
755
- return stored ? updatePlate(buf.name, serializePlate(stored)) : false;
755
+ if (!stored) return false;
756
+ return updatePlate(buf.name, serializePlate(stored));
756
757
  };
757
758
  const finalizeRecording = async () => {
758
759
  const entries = [...recording];
package/dist/core.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as emptyPlate, a as CollectAxis, c as XrayCaptureWalker, d as MergedPlate, f as Plate, g as ViewCapture, i as CaptureWalkerContext, l as defineXrayCaptureWalker, m as RenderNode, n as CaptureSize, o as CollectLimits, p as PlateNode, r as CaptureTooLargeError, s as WalkerDecision, t as CaptureOptions, u as LeafKind } from "./serialize-BgdGt34A.js";
1
+ import { _ as ViewCapture, a as CollectAxis, c as XrayCaptureWalker, d as LeafKind, f as MergedPlate, h as RenderNode, i as CaptureWalkerContext, l as defineXrayCaptureWalker, m as PlateNode, n as CaptureSize, o as CollectLimits, p as Plate, r as CaptureTooLargeError, s as WalkerDecision, t as CaptureOptions, u as BASE_CSS, v as composeCss, y as emptyPlate } from "./serialize-BqbQVxYl.js";
2
2
 
3
3
  //#region src/chunk.d.ts
4
4
  /**
@@ -6,6 +6,13 @@ import { _ as emptyPlate, a as CollectAxis, c as XrayCaptureWalker, d as MergedP
6
6
  * root's own content classes travel as `rootCls` (the adapter puts them on the
7
7
  * root element); its children become `chunks`. A capture-less plate has null
8
8
  * chunks and renders the fallback.
9
+ *
10
+ * Two-artifact contract (plan 005 round 2): the returned `Plate` is the TREE
11
+ * artifact only — the input's `css` is deliberately NOT copied onto it. The
12
+ * css string stays the producer's (`MergedPlate`/`StoredPlate`) field; each
13
+ * lowering site decides independently how its css travels (the plugin's
14
+ * virtual `.css` modules + `export const css`, `captureElement`'s
15
+ * `{ plate, css }` pair, or the `xray:plate` dev event, which ships no css).
9
16
  */
10
17
  declare function serializePlate(merged: MergedPlate): Plate;
11
18
  /**
@@ -21,28 +28,39 @@ declare function collectRefs(tree: RenderNode | null): string[];
21
28
  //#endregion
22
29
  //#region src/core.d.ts
23
30
  /**
24
- * Capture a live DOM subtree straight to a renderable Plate. Runs the same
25
- * uniform pipeline the dev client does for a single View measure (`captureView`),
31
+ * Capture a live DOM subtree straight to the two renderable artifacts: the
32
+ * structural `plate` (tree, lowered to chunks) and its scoped `css` string
33
+ * independent parts, per the two-artifact contract. Runs the same uniform
34
+ * pipeline the dev client does for a single View — measure (`captureView`),
26
35
  * reduce (`runPlatePasses`), project (`serializePlateIR` → the structured
27
- * `StoredPlate`), then lower to chunks (`serializePlate`) — in one call. `roots`
28
- * are the top-level elements to capture (a component's rendered roots). Pass the
29
- * result to a `<Skeleton plate>`.
36
+ * `StoredPlate`), then lower to chunks (`serializePlate`) — in one call.
37
+ * `roots` are the top-level elements to capture (a component's rendered
38
+ * roots). Render however suits: `renderPlateHtml(plate, composeCss(css))`,
39
+ * or `<Skeleton plate={plate} css={composeCss(css)}>` with the React adapter.
30
40
  *
31
41
  * `options.walker` is the public programmable capture walker (ADR 0018, Q2 —
32
42
  * yes, public): the same `XrayCaptureWalker` the `<Skeleton captureWalker>` prop
33
43
  * threads through in dev, available here for driving capture directly. It is
34
- * capture-only and never persisted into the returned Plate.
44
+ * capture-only and never persisted into the returned artifacts.
35
45
  */
36
- declare function captureElement(roots: readonly Element[], options: CaptureOptions): Plate;
46
+ declare function captureElement(roots: readonly Element[], options: CaptureOptions): {
47
+ plate: Plate;
48
+ css: string;
49
+ };
37
50
  /**
38
- * Render a stitch-free Plate to a self-contained HTML string — the
39
- * framework-neutral equivalent of `<Skeleton plate loading />`, for when you
40
- * don't want to pull a framework adapter (demos, plain DOM, server strings).
41
- * Drop it in with `el.innerHTML = renderPlateHtml(plate)` or React's
42
- * `dangerouslySetInnerHTML`. The string is self-contained: it carries BASE_CSS
43
- * + the plate's scoped rules in its own `<style>`. Stitched plates (a nested
44
- * `<Skeleton>`) need an adapter to mount their child plates, so they throw here.
51
+ * Render a stitch-free Plate to an HTML string — the framework-neutral
52
+ * equivalent of `<Skeleton plate loading />`, for when you don't want to pull
53
+ * a framework adapter (demos, plain DOM, server strings). Drop it in with
54
+ * `el.innerHTML = renderPlateHtml(plate, composeCss(css))` or React's
55
+ * `dangerouslySetInnerHTML`.
56
+ *
57
+ * `css` is the caller's explicit input (two-artifact contract): pass
58
+ * `composeCss(css)` for the standard BASE_CSS + plate-rules composition
59
+ * (BASE_CSS is what makes bones paint and pulse), the raw capture css if the
60
+ * base rules are already on the page, or your own composition. Stitched
61
+ * plates (a nested `<Skeleton>`) need an adapter to mount their child plates,
62
+ * so they throw here.
45
63
  */
46
- declare function renderPlateHtml(plate: Plate): string;
64
+ declare function renderPlateHtml(plate: Plate, css: string): string;
47
65
  //#endregion
48
- export { type CaptureOptions, type CaptureSize, CaptureTooLargeError, type CaptureWalkerContext, type CollectAxis, type CollectLimits, type LeafKind, type MergedPlate, type Plate, type PlateNode, type ViewCapture, type WalkerDecision, type XrayCaptureWalker, captureElement, collectRefs, defineXrayCaptureWalker, emptyPlate, renderPlateHtml, serializePlate };
66
+ export { BASE_CSS, type CaptureOptions, type CaptureSize, CaptureTooLargeError, type CaptureWalkerContext, type CollectAxis, type CollectLimits, type LeafKind, type MergedPlate, type Plate, type PlateNode, type ViewCapture, type WalkerDecision, type XrayCaptureWalker, captureElement, collectRefs, composeCss, defineXrayCaptureWalker, emptyPlate, renderPlateHtml, serializePlate };
package/dist/core.js CHANGED
@@ -1,44 +1,56 @@
1
- import { a as ROOT_ATTR, d as emptyPlate, o as ROOT_CLASS, t as BASE_CSS } from "./plate-BRR6d8Se.js";
2
- import { a as serializePlate, c as defineXrayCaptureWalker, i as collectRefs, l as runPlatePasses, o as CaptureTooLargeError, s as captureView, t as serializePlateIR } from "./project-BZosujs9.js";
1
+ import { a as ROOT_ATTR, d as composeCss, f as emptyPlate, o as ROOT_CLASS, t as BASE_CSS } from "./plate-BwJUgHDc.js";
2
+ import { a as serializePlate, c as defineXrayCaptureWalker, i as collectRefs, l as runPlatePasses, o as CaptureTooLargeError, s as captureView, t as serializePlateIR } from "./project-BAXKpnfF.js";
3
3
  import { t as escapeStyleText } from "./css-escape-N7bOusGW.js";
4
4
  //#region src/core.ts
5
5
  /**
6
- * Capture a live DOM subtree straight to a renderable Plate. Runs the same
7
- * uniform pipeline the dev client does for a single View measure (`captureView`),
6
+ * Capture a live DOM subtree straight to the two renderable artifacts: the
7
+ * structural `plate` (tree, lowered to chunks) and its scoped `css` string
8
+ * independent parts, per the two-artifact contract. Runs the same uniform
9
+ * pipeline the dev client does for a single View — measure (`captureView`),
8
10
  * reduce (`runPlatePasses`), project (`serializePlateIR` → the structured
9
- * `StoredPlate`), then lower to chunks (`serializePlate`) — in one call. `roots`
10
- * are the top-level elements to capture (a component's rendered roots). Pass the
11
- * result to a `<Skeleton plate>`.
11
+ * `StoredPlate`), then lower to chunks (`serializePlate`) — in one call.
12
+ * `roots` are the top-level elements to capture (a component's rendered
13
+ * roots). Render however suits: `renderPlateHtml(plate, composeCss(css))`,
14
+ * or `<Skeleton plate={plate} css={composeCss(css)}>` with the React adapter.
12
15
  *
13
16
  * `options.walker` is the public programmable capture walker (ADR 0018, Q2 —
14
17
  * yes, public): the same `XrayCaptureWalker` the `<Skeleton captureWalker>` prop
15
18
  * threads through in dev, available here for driving capture directly. It is
16
- * capture-only and never persisted into the returned Plate.
19
+ * capture-only and never persisted into the returned artifacts.
17
20
  */
18
21
  function captureElement(roots, options) {
19
- const plate = captureView(roots, {
22
+ const plateIR = captureView(roots, {
20
23
  collect: options.collect,
21
24
  walker: options.walker,
22
25
  captureRootIsBoundary: options.captureRootIsBoundary
23
26
  });
24
- plate.name = options.name;
25
- runPlatePasses(plate);
26
- return serializePlate(serializePlateIR(plate));
27
+ plateIR.name = options.name;
28
+ runPlatePasses(plateIR);
29
+ const stored = serializePlateIR(plateIR);
30
+ return {
31
+ plate: serializePlate(stored),
32
+ css: stored.css
33
+ };
27
34
  }
28
35
  /**
29
- * Render a stitch-free Plate to a self-contained HTML string — the
30
- * framework-neutral equivalent of `<Skeleton plate loading />`, for when you
31
- * don't want to pull a framework adapter (demos, plain DOM, server strings).
32
- * Drop it in with `el.innerHTML = renderPlateHtml(plate)` or React's
33
- * `dangerouslySetInnerHTML`. The string is self-contained: it carries BASE_CSS
34
- * + the plate's scoped rules in its own `<style>`. Stitched plates (a nested
35
- * `<Skeleton>`) need an adapter to mount their child plates, so they throw here.
36
+ * Render a stitch-free Plate to an HTML string — the framework-neutral
37
+ * equivalent of `<Skeleton plate loading />`, for when you don't want to pull
38
+ * a framework adapter (demos, plain DOM, server strings). Drop it in with
39
+ * `el.innerHTML = renderPlateHtml(plate, composeCss(css))` or React's
40
+ * `dangerouslySetInnerHTML`.
41
+ *
42
+ * `css` is the caller's explicit input (two-artifact contract): pass
43
+ * `composeCss(css)` for the standard BASE_CSS + plate-rules composition
44
+ * (BASE_CSS is what makes bones paint and pulse), the raw capture css if the
45
+ * base rules are already on the page, or your own composition. Stitched
46
+ * plates (a nested `<Skeleton>`) need an adapter to mount their child plates,
47
+ * so they throw here.
36
48
  */
37
- function renderPlateHtml(plate) {
49
+ function renderPlateHtml(plate, css) {
38
50
  const chunks = plate.chunks;
39
51
  if (!chunks) return "";
40
52
  const className = plate.rootCls ? `${ROOT_CLASS} ${plate.rootCls}` : ROOT_CLASS;
41
- return `<div ${ROOT_ATTR}="${plate.name}" class="${className}" aria-hidden="true" aria-busy="true"><style>${BASE_CSS}\n${escapeStyleText(plate.css)}</style>${chunksToHtml(chunks)}</div>`;
53
+ return `<div ${ROOT_ATTR}="${plate.name}" class="${className}" aria-hidden="true" aria-busy="true"><style>${escapeStyleText(css)}</style>${chunksToHtml(chunks)}</div>`;
42
54
  }
43
55
  /**
44
56
  * Flatten chunks to one HTML string. A `string` concatenates; a template chunk
@@ -57,4 +69,4 @@ function chunksToHtml(chunks) {
57
69
  return html;
58
70
  }
59
71
  //#endregion
60
- export { CaptureTooLargeError, captureElement, collectRefs, defineXrayCaptureWalker, emptyPlate, renderPlateHtml, serializePlate };
72
+ export { BASE_CSS, CaptureTooLargeError, captureElement, collectRefs, composeCss, defineXrayCaptureWalker, emptyPlate, renderPlateHtml, serializePlate };
package/dist/index.d.ts CHANGED
@@ -51,12 +51,21 @@ type DiagnosticCode = 'unreadable-stylesheet' | 'unreadable-import' | 'skipped-d
51
51
  //#region src/plate.d.ts
52
52
  type LeafKind = 'text' | 'text-block' | 'media' | 'box';
53
53
  /**
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).
54
+ * The shipped structural capture of a component's rendered DOM — the TREE
55
+ * artifact a Skeleton renders from (see CONTEXT.md). Since the two-artifact
56
+ * cutover (plan 005 round 2), a Plate carries structure ONLY: its scoped CSS
57
+ * is an INDEPENDENT sibling artifact the plugin serves it through Vite's
58
+ * own CSS pipeline (`virtual:xray/plates/<name>.css`, plus the shared
59
+ * `virtual:xray/base.css`, and an `export const css` on the virtual JS module
60
+ * for dev/programmatic reads), and the framework-neutral `core.ts` path hands
61
+ * it back separately (`captureElement` → `{ plate, css }`;
62
+ * `renderPlateHtml(plate, css)` receives it explicitly). Consumers decide how
63
+ * to render either part. Still self-contained in ADR 0003's sense: both
64
+ * artifacts originate from the capture and depend on nothing in the
65
+ * consumer's build. Built from the RenderNode tree at plugin load (see
66
+ * chunk.ts), so the adapter injects strings instead of walking a tree on the
67
+ * latency-critical first paint — and no per-framework serializer ships to the
68
+ * client (ADR 0008).
60
69
  */
61
70
  interface Plate {
62
71
  v: number;
@@ -70,8 +79,6 @@ interface Plate {
70
79
  chunks: Chunk[] | null;
71
80
  /** The root node's own content classes (its inherited font context), if any. */
72
81
  rootCls?: string;
73
- /** Layout-relevant CSS (incl. `@media`) scoped under the plate root, paint stripped. */
74
- css: string;
75
82
  /**
76
83
  * Child plates named by the chunks' `{ r }` stitches, resolved through the
77
84
  * import graph (ADR 0006). Populated by the loader's generated imports, never
@@ -515,6 +522,20 @@ interface CaptureServer {
515
522
  config?: {
516
523
  logger?: ServerLogger;
517
524
  };
525
+ /**
526
+ * The subset of Vite's dev server needed to keep BOTH of a plate's virtual
527
+ * modules fresh after a plate file changes (capture POST, hand edit, git
528
+ * checkout). Optional — absent in the unit tests that stub a minimal
529
+ * `server`. `getModuleById`/`invalidateModule`/`reloadModule` mirror
530
+ * `ViteDevServer`'s real shape (a subset, so this module stays free of a
531
+ * hard `vite` import at the type level beyond the `Plugin` type already
532
+ * used elsewhere).
533
+ */
534
+ moduleGraph?: {
535
+ getModuleById(id: string): unknown;
536
+ invalidateModule(mod: unknown): void;
537
+ };
538
+ reloadModule?(mod: unknown): Promise<void>;
518
539
  }
519
540
  /**
520
541
  * Per-plate breakpoints + derived coverage bands, read from the committed
@@ -575,6 +596,36 @@ declare function handleCapturePost(req: CaptureRequest, res: CaptureResponse, se
575
596
  * its filename still serializes and self-references under the canonical name
576
597
  * (`collectRefs` reads `stored.tree`, a RenderNode, unchanged by the cutover).
577
598
  *
599
+ * Two-artifact module shape (plan 005 round 2):
600
+ *
601
+ * ```js
602
+ * import "virtual:xray/base.css"
603
+ * import "virtual:xray/plates/<name>.css"
604
+ * export default { tree-only plate JSON }
605
+ * export const css = "<the plate's scoped css string>"
606
+ * ```
607
+ *
608
+ * The default export is the TREE artifact (`Plate` carries no css field
609
+ * anymore); the page's styling flows through Vite's own CSS pipeline via the
610
+ * two side-effect imports (dev inject + HMR, build extraction into the
611
+ * chunk's `.css` asset). `export const css` is the plate's css as data, for
612
+ * dev tooling / programmatic consumers that want the string itself —
613
+ * production adapters import only the default, so Rollup TREE-SHAKES the
614
+ * unused named export out of built chunks (verified empirically in the plan's
615
+ * scratch app; the side-effect css imports do not pin it).
616
+ *
617
+ * BASE_CSS rides as a JS-level import of the SHARED `virtual:xray/base.css`
618
+ * module (not an `@import` inside each plate's css): Vite dedupes JS module
619
+ * imports by id, so the base rules reach the page exactly once no matter how
620
+ * many plates (nested/stitched included — every plate module emits this
621
+ * import, module identity collapses them) a page mounts, and at build a
622
+ * multi-chunk app gets it hoisted/shared rather than copied per chunk. An
623
+ * `@import` inside each plate's `.css` would instead be INLINED per importer
624
+ * by Vite's postcss-import at build, duplicating BASE_CSS into every plate's
625
+ * asset — the opposite of the dedupe this design locks in. Base rides before
626
+ * the plate's css module so a plate rule still beats a base rule at equal
627
+ * specificity by source order (the old inline `${BASE_CSS}\n${css}` order).
628
+ *
578
629
  * @internal
579
630
  */
580
631
  declare function renderPlateModule(merged: MergedPlate, name?: string): string;
package/dist/index.js CHANGED
@@ -16,8 +16,49 @@ const ROOT_INSTANT_ATTR = "data-xr-instant";
16
16
  const ROOT_CLASS = "xr-root";
17
17
  const NODE_CLASS = "xr-node";
18
18
  const LEAF_CLASS = "xr-leaf";
19
- `
20
- /* One timeline per skeleton, not per leaf: the root animates one inherited
19
+ /**
20
+ * Renderer-owned base styles, emitted once by the outermost view (ADR 0007),
21
+ * before a plate's node rules so a node's captured class wins by source order.
22
+ * Everything visual is driven by inheritable custom properties, so consumers
23
+ * theme by setting `--xr-*` in `:root` or on any ancestor — and per-plate via
24
+ * `[data-xr-root="name"]` (the root carries its plate name). See ADR 0011 and
25
+ * docs/architecture.md. Class-based and unlayered — `@layer` was rejected
26
+ * (unlayered app CSS would beat layered skeleton rules, and it inverts
27
+ * `!important`).
28
+ *
29
+ * Progressive enhancement (ADR 0011): the plain bone fill + opacity pulse use
30
+ * only universally-supported CSS and are the floor. `light-dark()` is guarded
31
+ * by `@supports` because an unsupported VALUE drops the whole declaration (an
32
+ * invisible bone), and it is not Baseline-widely-available until 2026-11-13 —
33
+ * drop the guard then. Features that merely no-op when absent (`@property`,
34
+ * `prefers-contrast`, relative-color-as-fallback) carry no hard guard.
35
+ *
36
+ * Tokens (ADR 0017 taxonomy). The `--xr-bone-*` family styles an individual
37
+ * Bone: `--xr-bone-color` (fill), `--xr-bone-highlight-color` (sheen),
38
+ * `--xr-bone-border-radius` (+ `--xr-bone-text-border-radius` /
39
+ * `--xr-bone-text-block-border-radius` / `--xr-bone-media-border-radius`),
40
+ * `--xr-bone-animation-duration`, the pulse
41
+ * range `--xr-bone-pulse-opacity-min`/`--xr-bone-pulse-opacity-max`,
42
+ * `--xr-bone-animation` (mode: `xr-pulse` | `none` | a custom keyframe), and
43
+ * `--xr-bone-fill` (override the leaf paint, e.g. a sheen gradient built from the
44
+ * two bone color tokens). The `--xr-skeleton-*` family governs whole-skeleton
45
+ * display timing (ADR 0016): `--xr-skeleton-delay` (grace before the skeleton
46
+ * becomes visible), `--xr-skeleton-min-duration` (minimum visible time once
47
+ * shown; read by the `useSkeletonTiming` hook, not used in CSS), and
48
+ * `--xr-skeleton-transition-duration` (the reveal/swap fade). The private
49
+ * channels `--xr-o` (pulse) and `--xr-reveal` (reveal) stay undocumented
50
+ * `@property` plumbing, not a theming surface.
51
+ *
52
+ * Declared as a PLAIN template literal — no `.trim()` (or any other call) on
53
+ * the initializer. A method call is not provably pure to a conservative
54
+ * tree-shaker (rolldown/Rollup), and it single-handedly pinned this ~5 KB
55
+ * string into every consumer chunk importing the React adapter even after the
56
+ * adapter stopped referencing it (plan 005 round 2 finding). The template
57
+ * therefore starts immediately after the backtick (the old leading newline
58
+ * `.trim()` used to strip simply isn't there), keeping the string value
59
+ * byte-identical to the trimmed original.
60
+ */
61
+ const BASE_CSS = `/* One timeline per skeleton, not per leaf: the root animates one inherited
21
62
  opacity property and every leaf reads it. Hundreds of independent infinite
22
63
  opacity animations would pin the compositor and cook the CPU. */
23
64
  @property --xr-o { syntax: "<number>"; inherits: true; initial-value: 1 }
@@ -103,7 +144,7 @@ const LEAF_CLASS = "xr-leaf";
103
144
  the fallback (sheen just looks flatter), so no hard guard is needed. */
104
145
  @supports (color: oklch(from red l c h)) {
105
146
  .${ROOT_CLASS} { --xr-bone-highlight-color: oklch(from var(--xr-bone-color, #e4e4e7) calc(l + 0.08) c h) }
106
- }`.trim();
147
+ }`;
107
148
  /** The (unserialized) plate an import resolves to before anything has been captured. */
108
149
  function emptyPlate(name) {
109
150
  return {
@@ -183,27 +224,31 @@ function toChunks(nodes) {
183
224
  * root's own content classes travel as `rootCls` (the adapter puts them on the
184
225
  * root element); its children become `chunks`. A capture-less plate has null
185
226
  * chunks and renders the fallback.
227
+ *
228
+ * Two-artifact contract (plan 005 round 2): the returned `Plate` is the TREE
229
+ * artifact only — the input's `css` is deliberately NOT copied onto it. The
230
+ * css string stays the producer's (`MergedPlate`/`StoredPlate`) field; each
231
+ * lowering site decides independently how its css travels (the plugin's
232
+ * virtual `.css` modules + `export const css`, `captureElement`'s
233
+ * `{ plate, css }` pair, or the `xray:plate` dev event, which ships no css).
186
234
  */
187
235
  function serializePlate(merged) {
188
- const { v, name, tree, css } = merged;
236
+ const { v, name, tree } = merged;
189
237
  if (!tree) return {
190
238
  v,
191
239
  name,
192
- chunks: null,
193
- css
240
+ chunks: null
194
241
  };
195
242
  const chunks = toChunks(tree.kids ?? []);
196
243
  return tree.cls ? {
197
244
  v,
198
245
  name,
199
246
  chunks,
200
- rootCls: tree.cls,
201
- css
247
+ rootCls: tree.cls
202
248
  } : {
203
249
  v,
204
250
  name,
205
- chunks,
206
- css
251
+ chunks
207
252
  };
208
253
  }
209
254
  /**
@@ -483,6 +528,17 @@ const BOOT_ID = "virtual:xray/client";
483
528
  const RESOLVED_BOOT_ID = `\0${BOOT_ID}`;
484
529
  const ENDPOINT = "/__xray";
485
530
  /**
531
+ * The shared base-css virtual module every plate's `.css` module imports, so
532
+ * Vite dedupes BASE_CSS across plates instead of repeating it per plate.
533
+ * Deliberately NOT `\0`-prefixed: an unprefixed virtual id is tried first (the
534
+ * vanilla-extract pattern) because Vite's CSS plugins match on the module id,
535
+ * and this id must be routed through that pipeline (it ends in `.css`).
536
+ */
537
+ const BASE_CSS_ID = "virtual:xray/base.css";
538
+ /** A plate's per-name css virtual module id. Also un-`\0`-prefixed for the
539
+ * same reason as `BASE_CSS_ID` — see above. */
540
+ const plateCssId = (name) => `${VIRTUAL_PREFIX}${name}.css`;
541
+ /**
486
542
  * Create the Vite plugins that resolve committed Plates and, during dev,
487
543
  * capture new Plates from rendered `<Skeleton>` boundaries.
488
544
  *
@@ -518,9 +574,17 @@ function xrayVitePlugin(options = {}) {
518
574
  root = config.root;
519
575
  },
520
576
  resolveId(id) {
577
+ if (id === BASE_CSS_ID) return id;
578
+ if (id.startsWith("virtual:xray/plates/") && id.endsWith(".css")) return id;
521
579
  if (id.startsWith("virtual:xray/plates/")) return `\0${id}`;
522
580
  },
523
581
  load(id) {
582
+ if (id === BASE_CSS_ID) return BASE_CSS;
583
+ if (id.startsWith("virtual:xray/plates/") && id.endsWith(".css")) {
584
+ const name = id.slice(20, -4);
585
+ if (!isValidPlateName(name)) return "";
586
+ return readStoredPlate(platePath(name))?.css ?? "";
587
+ }
524
588
  if (!id.startsWith(RESOLVED_PREFIX)) return void 0;
525
589
  const name = id.slice(RESOLVED_PREFIX.length);
526
590
  if (!isValidPlateName(name)) return renderPlateModule(emptyPlate(name));
@@ -564,6 +628,7 @@ function xrayVitePlugin(options = {}) {
564
628
  const name = relative(dir(), file).slice(0, -5);
565
629
  if (!isValidPlateName(name)) return;
566
630
  announcePlate(server.ws, name, readStoredPlate(file));
631
+ invalidatePlateModules(server, name);
567
632
  };
568
633
  server.watcher.on("add", onPlateFile);
569
634
  server.watcher.on("change", onPlateFile);
@@ -578,6 +643,12 @@ function xrayVitePlugin(options = {}) {
578
643
  res.end(JSON.stringify(readFixtures(dir())));
579
644
  return;
580
645
  }
646
+ if (req.method === "POST" && !isSameOriginWrite(req)) {
647
+ res.statusCode = 403;
648
+ res.end("forbidden");
649
+ console.warn("[xray] rejected a cross-origin POST to /__xray (origin/host mismatch).");
650
+ return;
651
+ }
581
652
  if (req.url === "/fixture") {
582
653
  handleFixturePost(req, res, fixturePath);
583
654
  return;
@@ -649,6 +720,47 @@ function bootstrapCode(delay, settleCap, collect, hud, capture, recordMode, repl
649
720
  ].join("\n");
650
721
  }
651
722
  /**
723
+ * Drop both of a plate's virtual modules from Vite's dev transform cache after
724
+ * its file changes, each through the channel appropriate to it:
725
+ *
726
+ * - The `.css` module (UN-prefixed id — see `plateCssId`) is invalidated AND
727
+ * reloaded: standard Vite CSS HMR (`reloadModule` re-transforms and pushes
728
+ * the update over the ws), so fresh styles land in the open page with no
729
+ * reload. The css module has no capture boundary, so a live reload of it
730
+ * carries no remount-loop risk.
731
+ * - The JS (tree) module (`\0`-prefixed id) is invalidated ONLY — deliberately
732
+ * NO `reloadModule`/HMR push. A live re-import would remount the capture
733
+ * boundary and re-fire the capture (the exact infinite loop the "no
734
+ * addWatchFile" comment in `load` guards against; live tree updates keep
735
+ * flowing over the `xray:plate` announce into the dev store instead). Bare
736
+ * `invalidateModule` is server-side cache marking only — nothing reaches
737
+ * the client until the NEXT full page load, which then re-transforms from
738
+ * disk and serves a tree consistent with the css.
739
+ *
740
+ * WITHOUT the JS half (the round-3 QA bug, found dogfooding the masterworks
741
+ * frontend): after a capture, a page reload paired Vite's CACHED old tree
742
+ * transform (stale content-addressed class names) with the freshly reloaded
743
+ * css — class names no longer matched rules and skeletons rendered garbled
744
+ * until a dev-server restart cleared the transform cache. The tree/css split
745
+ * de-synchronized the two modules' invalidation policies; this helper is the
746
+ * single place that keeps them in lockstep.
747
+ *
748
+ * Scope note (stitched plates): a parent's module embeds ONLY its own stored
749
+ * tree; a child's data arrives at runtime through the child module import
750
+ * (`refs: { "<child>": __xr0 }`), never inlined into the parent's transform.
751
+ * So invalidating the re-captured plate's OWN two modules is sufficient —
752
+ * parent modules hold nothing child-derived that could go stale.
753
+ *
754
+ * No-op per module when it was never resolved/imported yet (a fresh plate no
755
+ * page has loaded, or a server stub without the module-graph subset).
756
+ */
757
+ async function invalidatePlateModules(server, name) {
758
+ const jsMod = server.moduleGraph?.getModuleById(RESOLVED_PREFIX + name);
759
+ if (jsMod) server.moduleGraph?.invalidateModule(jsMod);
760
+ const cssMod = server.moduleGraph?.getModuleById(plateCssId(name));
761
+ if (cssMod) await server.reloadModule?.(cssMod);
762
+ }
763
+ /**
652
764
  * Print a plate's diagnostics to the Vite server output using the SAME grouped
653
765
  * header + shared formatter the browser console uses (diagnostics.ts), so the
654
766
  * two can never drift. Prefers Vite's logger (it preserves the formatting);
@@ -680,6 +792,24 @@ function isJsonContentType(value) {
680
792
  if (typeof header !== "string") return false;
681
793
  return /^application\/json\b/i.test(header.trim());
682
794
  }
795
+ /**
796
+ * Reject cross-origin browser writes. Browsers always attach `Origin` to
797
+ * POSTs; a request from the app's own page carries the dev server's origin,
798
+ * so its host must equal the `Host` the request arrived on. A request with
799
+ * NO Origin header is not from a browser page (curl, a node script) and is
800
+ * allowed — the gate targets drive-by browser writes to Plate and Fixture
801
+ * files, not local tooling.
802
+ */
803
+ function isSameOriginWrite(req) {
804
+ const origin = Array.isArray(req.headers?.origin) ? req.headers.origin[0] : req.headers?.origin;
805
+ if (origin === void 0 || origin === "null") return origin === void 0;
806
+ const host = Array.isArray(req.headers?.host) ? req.headers.host[0] : req.headers?.host;
807
+ try {
808
+ return new URL(origin).host === host;
809
+ } catch {
810
+ return false;
811
+ }
812
+ }
683
813
  function isRecord(value) {
684
814
  return typeof value === "object" && value !== null;
685
815
  }
@@ -707,21 +837,31 @@ function spansFromBreakpoints(breakpoints) {
707
837
  /** Send the stored plate (lowered to chunks) and its derived coverage bands over
708
838
  * the HMR event so the client hot-swaps it in place and the HUD updates its band
709
839
  * display. The gen-side runs `serializePlate(stored)` (the Bundle seam) here; the
710
- * `spans` derive from the stored breakpoints (the per-View list is gone). */
840
+ * `spans` derive from the stored breakpoints (the per-View list is gone).
841
+ *
842
+ * Two-artifact contract (plan 005 round 2): the event carries the TREE only —
843
+ * `serializePlate` now returns a css-less `Plate`, so the old ws css
844
+ * side-channel is gone by construction. The css half of a capture travels
845
+ * exclusively over Vite's CSS-HMR (`invalidatePlateModules` re-pushes the
846
+ * `.css` virtual module after the write). This is also what killed the
847
+ * round-1 duplication bug class for good: the live-store plate that outranks
848
+ * the statically-imported module in `useLivePlate` CANNOT carry inline css
849
+ * anymore, so no push can revert a mounted `<Skeleton>` to inline-`<style>`
850
+ * rendering. */
711
851
  function announcePlate(ws, name, stored) {
712
852
  const breakpoints = stored?.breakpoints ?? [];
713
- const plate = stored ? serializePlate({
853
+ const merged = stored ? {
714
854
  v: stored.v,
715
855
  name,
716
856
  tree: stored.tree,
717
857
  css: stored.css
718
- }) : serializePlate(emptyPlate(name));
858
+ } : emptyPlate(name);
719
859
  ws.send({
720
860
  type: "custom",
721
861
  event: "xray:plate",
722
862
  data: {
723
863
  name,
724
- plate,
864
+ plate: serializePlate(merged),
725
865
  breakpoints,
726
866
  spans: spansFromBreakpoints(breakpoints)
727
867
  }
@@ -951,6 +1091,7 @@ function handleCapturePost(req, res, server, platePath) {
951
1091
  mkdirSync(dirname(file), { recursive: true });
952
1092
  writeFileSync(file, serialized);
953
1093
  announcePlate(server.ws, stored.name, stored);
1094
+ invalidatePlateModules(server, stored.name);
954
1095
  res.statusCode = 204;
955
1096
  res.end();
956
1097
  } catch {
@@ -1032,6 +1173,36 @@ function readStoredPlate(file) {
1032
1173
  * its filename still serializes and self-references under the canonical name
1033
1174
  * (`collectRefs` reads `stored.tree`, a RenderNode, unchanged by the cutover).
1034
1175
  *
1176
+ * Two-artifact module shape (plan 005 round 2):
1177
+ *
1178
+ * ```js
1179
+ * import "virtual:xray/base.css"
1180
+ * import "virtual:xray/plates/<name>.css"
1181
+ * export default { tree-only plate JSON }
1182
+ * export const css = "<the plate's scoped css string>"
1183
+ * ```
1184
+ *
1185
+ * The default export is the TREE artifact (`Plate` carries no css field
1186
+ * anymore); the page's styling flows through Vite's own CSS pipeline via the
1187
+ * two side-effect imports (dev inject + HMR, build extraction into the
1188
+ * chunk's `.css` asset). `export const css` is the plate's css as data, for
1189
+ * dev tooling / programmatic consumers that want the string itself —
1190
+ * production adapters import only the default, so Rollup TREE-SHAKES the
1191
+ * unused named export out of built chunks (verified empirically in the plan's
1192
+ * scratch app; the side-effect css imports do not pin it).
1193
+ *
1194
+ * BASE_CSS rides as a JS-level import of the SHARED `virtual:xray/base.css`
1195
+ * module (not an `@import` inside each plate's css): Vite dedupes JS module
1196
+ * imports by id, so the base rules reach the page exactly once no matter how
1197
+ * many plates (nested/stitched included — every plate module emits this
1198
+ * import, module identity collapses them) a page mounts, and at build a
1199
+ * multi-chunk app gets it hoisted/shared rather than copied per chunk. An
1200
+ * `@import` inside each plate's `.css` would instead be INLINED per importer
1201
+ * by Vite's postcss-import at build, duplicating BASE_CSS into every plate's
1202
+ * asset — the opposite of the dedupe this design locks in. Base rides before
1203
+ * the plate's css module so a plate rule still beats a base rule at equal
1204
+ * specificity by source order (the old inline `${BASE_CSS}\n${css}` order).
1205
+ *
1035
1206
  * @internal
1036
1207
  */
1037
1208
  function renderPlateModule(merged, name = merged.name) {
@@ -1040,8 +1211,12 @@ function renderPlateModule(merged, name = merged.name) {
1040
1211
  ...merged,
1041
1212
  name
1042
1213
  }));
1043
- if (refs.length === 0) return `export default ${json}`;
1044
- return `${refs.map((ref, i) => `import __xr${i} from ${JSON.stringify(VIRTUAL_PREFIX + ref)}`).join("\n")}\nexport default Object.assign(${json}, { refs: { ${refs.map((ref, i) => `${JSON.stringify(ref)}: __xr${i}`).join(", ")} } })`;
1214
+ const cssImports = `import ${JSON.stringify(BASE_CSS_ID)}\nimport ${JSON.stringify(plateCssId(name))}`;
1215
+ const cssExport = `export const css = ${JSON.stringify(merged.css)}`;
1216
+ if (refs.length === 0) return `${cssImports}\nexport default ${json}\n${cssExport}`;
1217
+ const imports = refs.map((ref, i) => `import __xr${i} from ${JSON.stringify(VIRTUAL_PREFIX + ref)}`).join("\n");
1218
+ const map = refs.map((ref, i) => `${JSON.stringify(ref)}: __xr${i}`).join(", ");
1219
+ return `${[cssImports, imports].join("\n")}\nexport default Object.assign(${json}, { refs: { ${map} } })\n${cssExport}`;
1045
1220
  }
1046
1221
  //#endregion
1047
1222
  export { collectRefs, handleCapturePost, handleFixturePost, isValidPlateName, readCoverage, readFixtures, renderPlateModule, xrayVitePlugin };
@@ -54,9 +54,17 @@ const LEAF_CLASS = "xr-leaf";
54
54
  * `--xr-skeleton-transition-duration` (the reveal/swap fade). The private
55
55
  * channels `--xr-o` (pulse) and `--xr-reveal` (reveal) stay undocumented
56
56
  * `@property` plumbing, not a theming surface.
57
+ *
58
+ * Declared as a PLAIN template literal — no `.trim()` (or any other call) on
59
+ * the initializer. A method call is not provably pure to a conservative
60
+ * tree-shaker (rolldown/Rollup), and it single-handedly pinned this ~5 KB
61
+ * string into every consumer chunk importing the React adapter even after the
62
+ * adapter stopped referencing it (plan 005 round 2 finding). The template
63
+ * therefore starts immediately after the backtick (the old leading newline
64
+ * `.trim()` used to strip simply isn't there), keeping the string value
65
+ * byte-identical to the trimmed original.
57
66
  */
58
- const BASE_CSS = `
59
- /* One timeline per skeleton, not per leaf: the root animates one inherited
67
+ const BASE_CSS = `/* One timeline per skeleton, not per leaf: the root animates one inherited
60
68
  opacity property and every leaf reads it. Hundreds of independent infinite
61
69
  opacity animations would pin the compositor and cook the CPU. */
62
70
  @property --xr-o { syntax: "<number>"; inherits: true; initial-value: 1 }
@@ -142,7 +150,22 @@ const BASE_CSS = `
142
150
  the fallback (sheen just looks flatter), so no hard guard is needed. */
143
151
  @supports (color: oklch(from red l c h)) {
144
152
  .${ROOT_CLASS} { --xr-bone-highlight-color: oklch(from var(--xr-bone-color, #e4e4e7) calc(l + 0.08) c h) }
145
- }`.trim();
153
+ }`;
154
+ /**
155
+ * Compose a plate's scoped css with the renderer-owned BASE_CSS, base first so
156
+ * a plate rule beats a base rule at equal specificity by source order — the
157
+ * standard composition for rendering a skeleton outside the Vite plugin
158
+ * (where `virtual:xray/base.css` + `virtual:xray/plates/<name>.css` do this
159
+ * through the CSS pipeline instead). Since the two-artifact cutover (plan 005
160
+ * round 2) css is the CALLER's input: `renderPlateHtml(plate,
161
+ * composeCss(css))`, or `<Skeleton plate={plate} css={composeCss(css)}>` for
162
+ * a runtime-captured plate. Rendering a NESTED stitch under an
163
+ * already-composed parent needs the raw css only (base is already on the
164
+ * page) — pass the string uncomposed there.
165
+ */
166
+ function composeCss(css) {
167
+ return `${BASE_CSS}\n${css}`;
168
+ }
146
169
  /** The (unserialized) plate an import resolves to before anything has been captured. */
147
170
  function emptyPlate(name) {
148
171
  return {
@@ -153,4 +176,4 @@ function emptyPlate(name) {
153
176
  };
154
177
  }
155
178
  //#endregion
156
- export { ROOT_ATTR as a, SPEC_GATE as c, emptyPlate as d, NODE_CLASS as i, SPEC_INLINE as l, BOUNDARY_ATTR as n, ROOT_CLASS as o, LEAF_CLASS as r, ROOT_INSTANT_ATTR as s, BASE_CSS as t, SPEC_LEAF as u };
179
+ export { ROOT_ATTR as a, SPEC_GATE as c, composeCss as d, emptyPlate as f, NODE_CLASS as i, SPEC_INLINE as l, BOUNDARY_ATTR as n, ROOT_CLASS as o, LEAF_CLASS as r, ROOT_INSTANT_ATTR as s, BASE_CSS as t, SPEC_LEAF as u };
@@ -1,4 +1,4 @@
1
- import { a as ROOT_ATTR, c as SPEC_GATE, i as NODE_CLASS, l as SPEC_INLINE, n as BOUNDARY_ATTR, r as LEAF_CLASS, u as SPEC_LEAF } from "./plate-BRR6d8Se.js";
1
+ import { a as ROOT_ATTR, c as SPEC_GATE, i as NODE_CLASS, l as SPEC_INLINE, n as BOUNDARY_ATTR, r as LEAF_CLASS, u as SPEC_LEAF } from "./plate-BwJUgHDc.js";
2
2
  //#region src/ir.ts
3
3
  /** A frozen `LiteStyleDeclaration` of empty strings, for entry-less nodes (root/stitch/synthetic). */
4
4
  const EMPTY_STYLE = Object.freeze({
@@ -9793,27 +9793,31 @@ function toChunks(nodes) {
9793
9793
  * root's own content classes travel as `rootCls` (the adapter puts them on the
9794
9794
  * root element); its children become `chunks`. A capture-less plate has null
9795
9795
  * chunks and renders the fallback.
9796
+ *
9797
+ * Two-artifact contract (plan 005 round 2): the returned `Plate` is the TREE
9798
+ * artifact only — the input's `css` is deliberately NOT copied onto it. The
9799
+ * css string stays the producer's (`MergedPlate`/`StoredPlate`) field; each
9800
+ * lowering site decides independently how its css travels (the plugin's
9801
+ * virtual `.css` modules + `export const css`, `captureElement`'s
9802
+ * `{ plate, css }` pair, or the `xray:plate` dev event, which ships no css).
9796
9803
  */
9797
9804
  function serializePlate(merged) {
9798
- const { v, name, tree, css } = merged;
9805
+ const { v, name, tree } = merged;
9799
9806
  if (!tree) return {
9800
9807
  v,
9801
9808
  name,
9802
- chunks: null,
9803
- css
9809
+ chunks: null
9804
9810
  };
9805
9811
  const chunks = toChunks(tree.kids ?? []);
9806
9812
  return tree.cls ? {
9807
9813
  v,
9808
9814
  name,
9809
9815
  chunks,
9810
- rootCls: tree.cls,
9811
- css
9816
+ rootCls: tree.cls
9812
9817
  } : {
9813
9818
  v,
9814
9819
  name,
9815
- chunks,
9816
- css
9820
+ chunks
9817
9821
  };
9818
9822
  }
9819
9823
  /**
@@ -1,4 +1,4 @@
1
- import { a as ROOT_ATTR, o as ROOT_CLASS, s as ROOT_INSTANT_ATTR, t as BASE_CSS } from "./plate-BRR6d8Se.js";
1
+ import { a as ROOT_ATTR, o as ROOT_CLASS, s as ROOT_INSTANT_ATTR } from "./plate-BwJUgHDc.js";
2
2
  import { t as escapeStyleText } from "./css-escape-N7bOusGW.js";
3
3
  import { Fragment, useEffect, useRef, useState } from "react";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -211,16 +211,26 @@ const DISPLAY_CONTENTS = { display: "contents" };
211
211
  /**
212
212
  * Render a shipped plate's skeleton. `resolveStitch` turns a child plate name
213
213
  * into the node that mounts it (static `refs` in prod, a store-subscribed
214
- * component in dev). `nested` is true for a stitch-mounted child, which skips
215
- * BASE_CSS — the outermost view already emitted it (ADR 0007). Returns null
214
+ * component in dev). `nested` is true for a stitch-mounted child. Returns null
216
215
  * when the plate has no capture yet; the caller renders the fallback.
217
216
  *
217
+ * Two-artifact contract (plan 005 round 2): a `Plate` is tree-only, and this
218
+ * renderer neither reads css off it nor references `BASE_CSS` (keeping the
219
+ * constant out of consumer bundles — Vite-path styling is entirely the CSS
220
+ * pipeline's job: `virtual:xray/base.css` + `virtual:xray/plates/<name>.css`).
221
+ * `css` is the one optional styling input: the consumer-composed inline css
222
+ * for a runtime-captured (`@hueest/xray/core`) plate, threaded from the
223
+ * `<Skeleton css>` prop and injected as a `<style>` on this root. It rides the
224
+ * OUTERMOST root only — the adapters never pass it for a nested stitch (the
225
+ * page-level/base styles are already in scope there, ADR 0007's once-per-page
226
+ * rule by construction).
227
+ *
218
228
  * `instant` (ADR 0020) marks this root a stitch continuation: a standalone
219
229
  * `<Skeleton>` replacing a stitch a still-showing parent was just rendering. It
220
230
  * lands as `data-xr-instant` on the OUTERMOST root only (`instant && !nested`) so
221
231
  * BASE_CSS reveals it without the `--xr-skeleton-delay` fade.
222
232
  */
223
- function renderPlate(plate, resolveStitch, nested, rootRef, instant) {
233
+ function renderPlate(plate, resolveStitch, nested, rootRef, instant, css) {
224
234
  if (!plate.chunks) return null;
225
235
  const className = plate.rootCls ? `${ROOT_CLASS} ${plate.rootCls}` : ROOT_CLASS;
226
236
  const rootAttrs = {
@@ -231,9 +241,12 @@ function renderPlate(plate, resolveStitch, nested, rootRef, instant) {
231
241
  ...rootRef ? { ref: rootRef } : {},
232
242
  ...instant && !nested ? { [ROOT_INSTANT_ATTR]: "" } : {}
233
243
  };
234
- const css = nested ? plate.css : `${BASE_CSS}\n${plate.css}`;
235
244
  const chunks = plate.chunks;
236
245
  if (chunks.length === 1 && typeof chunks[0] === "string") {
246
+ if (css === void 0) return /* @__PURE__ */ jsx("div", {
247
+ ...rootAttrs,
248
+ dangerouslySetInnerHTML: { __html: chunks[0] }
249
+ });
237
250
  const html = `<style>${escapeStyleText(css)}</style>${chunks[0]}`;
238
251
  return /* @__PURE__ */ jsx("div", {
239
252
  ...rootAttrs,
@@ -242,7 +255,7 @@ function renderPlate(plate, resolveStitch, nested, rootRef, instant) {
242
255
  }
243
256
  return /* @__PURE__ */ jsxs("div", {
244
257
  ...rootAttrs,
245
- children: [/* @__PURE__ */ jsx("style", { children: css }), renderChunks(chunks, resolveStitch)]
258
+ children: [css === void 0 ? null : /* @__PURE__ */ jsx("style", { children: css }), renderChunks(chunks, resolveStitch)]
246
259
  });
247
260
  }
248
261
  /**
@@ -1,4 +1,4 @@
1
- import { c as XrayCaptureWalker, f as Plate } from "./serialize-BgdGt34A.js";
1
+ import { c as XrayCaptureWalker, p as Plate } from "./serialize-BqbQVxYl.js";
2
2
  import { ReactNode } from "react";
3
3
 
4
4
  //#region src/react.core.d.ts
@@ -30,6 +30,19 @@ type SkeletonRenderProp<T> = (ctx: {
30
30
  interface SkeletonBase {
31
31
  /** The plate to render while loading, imported from `virtual:xray/plates/<name>`. */
32
32
  plate: Plate;
33
+ /**
34
+ * Inline skeleton css for a plate whose styling does NOT flow through the
35
+ * Vite plugin's CSS pipeline — i.e. a runtime-captured plate from
36
+ * `@hueest/xray/core`'s `captureElement` (ADR 0003: the no-Vite path). Pass
37
+ * the composed string (`composeCss(css)` for the standard BASE_CSS + plate
38
+ * rules); it is injected as a `<style>` on the skeleton root, exactly like
39
+ * the pre-two-artifact inline path. OMIT for plugin-imported plates — their
40
+ * css (base included) is already on the page via `virtual:xray/base.css` +
41
+ * `virtual:xray/plates/<name>.css`, and an inline copy would double-apply
42
+ * and re-fight the cascade. Applies to THIS plate's root only; stitched
43
+ * child plates resolve through refs/the dev store and carry no inline css.
44
+ */
45
+ css?: string;
33
46
  /** Explicit readiness mode: render the Skeleton while true, then render children and capture them in dev when false. */
34
47
  loading?: boolean;
35
48
  /** Suspense mode: make `<Skeleton>` the Suspense boundary and use the Plate as the fallback until children resolve. */
package/dist/react.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as SkeletonProps } from "./react.core-NhWc9qan.js";
1
+ import { t as SkeletonProps } from "./react.core-sJfIcaXX.js";
2
2
 
3
3
  //#region src/react.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { t as SkeletonProps } from "./react.core-NhWc9qan.js";
1
+ import { t as SkeletonProps } from "./react.core-sJfIcaXX.js";
2
2
 
3
3
  //#region src/react.dev.d.ts
4
4
  /**
package/dist/react.dev.js CHANGED
@@ -1,5 +1,5 @@
1
- import { n as BOUNDARY_ATTR } from "./plate-BRR6d8Se.js";
2
- import { a as markStitchesLive, c as useSkeletonTiming, i as isStitchLive, n as collectStitchNames, o as renderPlate, r as isRenderProp, s as resolveContent, t as SuspendWhileLoading } from "./react.core-BC02ukac.js";
1
+ import { n as BOUNDARY_ATTR } from "./plate-BwJUgHDc.js";
2
+ import { a as markStitchesLive, c as useSkeletonTiming, i as isStitchLive, n as collectStitchNames, o as renderPlate, r as isRenderProp, s as resolveContent, t as SuspendWhileLoading } from "./react.core-DeEli0Lp.js";
3
3
  import { Suspense, useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
4
4
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/react.dev.tsx
@@ -88,7 +88,7 @@ function useLivePlate(plate) {
88
88
  * `react.tsx`.
89
89
  */
90
90
  function Skeleton(props) {
91
- const { plate, loading = false, suspense = false, fallback, delay, children, data, captureWalker, maxNodes, maxBreadth, maxDepth } = props;
91
+ const { plate, css, loading = false, suspense = false, fallback, delay, children, data, captureWalker, maxNodes, maxBreadth, maxDepth } = props;
92
92
  const manual = props.root === "manual";
93
93
  const lightBoxOn = useLightBox();
94
94
  const livePlate = useLivePlate(plate);
@@ -101,6 +101,7 @@ function Skeleton(props) {
101
101
  }, [showSkeleton, livePlate.chunks]);
102
102
  const skeleton = /* @__PURE__ */ jsx(PlateView, {
103
103
  plate: livePlate,
104
+ css,
104
105
  fallback,
105
106
  rootRef,
106
107
  instant
@@ -224,7 +225,7 @@ function ManualBoundary({ plate, delay, children, data, captureWalker, maxNodes,
224
225
  /** Render a plate's skeleton, seeding the live store from its resolved refs so a
225
226
  * stitch resolves before its child is re-captured and survives a parent
226
227
  * hot-swap (which pushes a refs-less plate). Never overwrites a live capture. */
227
- function PlateView({ plate, fallback, nested = false, rootRef, instant }) {
228
+ function PlateView({ plate, css, fallback, nested = false, rootRef, instant }) {
228
229
  useEffect(() => {
229
230
  const store = globalThis.__XRAY__?.plates;
230
231
  if (!store || !plate.refs) return;
@@ -234,7 +235,7 @@ function PlateView({ plate, fallback, nested = false, rootRef, instant }) {
234
235
  return renderPlate(plate, (name) => /* @__PURE__ */ jsx(RefNode, {
235
236
  name,
236
237
  staticRefs: plate.refs
237
- }), nested, rootRef, instant);
238
+ }), nested, rootRef, instant, css);
238
239
  }
239
240
  /**
240
241
  * A stitch in dev: resolve the child plate from the live store (so re-capturing
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as markStitchesLive, c as useSkeletonTiming, i as isStitchLive, n as collectStitchNames, o as renderPlate, s as resolveContent, t as SuspendWhileLoading } from "./react.core-BC02ukac.js";
1
+ import { a as markStitchesLive, c as useSkeletonTiming, i as isStitchLive, n as collectStitchNames, o as renderPlate, s as resolveContent, t as SuspendWhileLoading } from "./react.core-DeEli0Lp.js";
2
2
  import { Suspense, useEffect, useRef, useState } from "react";
3
3
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
4
4
  //#region src/react.tsx
@@ -30,7 +30,7 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
30
30
  * or subscriptions. Stitches resolve from the statically imported `refs` graph.
31
31
  */
32
32
  function Skeleton(props) {
33
- const { plate, loading = false, suspense = false, fallback, children, data } = props;
33
+ const { plate, css, loading = false, suspense = false, fallback, children, data } = props;
34
34
  const rootRef = useRef(null);
35
35
  const showSkeleton = useSkeletonTiming(loading, rootRef);
36
36
  const [instant] = useState(() => isStitchLive(plate.name));
@@ -40,6 +40,7 @@ function Skeleton(props) {
40
40
  }, [showSkeleton, plate.chunks]);
41
41
  const skeleton = /* @__PURE__ */ jsx(PlateView, {
42
42
  plate,
43
+ css,
43
44
  fallback,
44
45
  rootRef,
45
46
  instant
@@ -52,19 +53,20 @@ function Skeleton(props) {
52
53
  return /* @__PURE__ */ jsx(Fragment$1, { children: resolveContent(children, data) });
53
54
  }
54
55
  /** Render a plate's skeleton, mounting each stitch's child plate from `refs`. */
55
- function PlateView({ plate, fallback, rootRef, instant }) {
56
+ function PlateView({ plate, css, fallback, rootRef, instant }) {
56
57
  if (!plate.chunks) return /* @__PURE__ */ jsx(Fragment$1, { children: fallback ?? null });
57
- return renderStatic(plate, false, rootRef, instant);
58
+ return renderStatic(plate, false, rootRef, instant, css);
58
59
  }
59
60
  /** Resolve stitches statically through the import graph (ADR 0006) — pure
60
61
  * recursion, no store, no subscription. An unknown child renders nothing. The
61
- * `instant` flag (ADR 0020) rides the outermost root only; nested stitches pass
62
- * none, so `renderPlate` never marks a nested root. */
63
- function renderStatic(plate, nested, rootRef, instant) {
62
+ * `instant` flag (ADR 0020) and the inline `css` (two-artifact contract) ride
63
+ * the outermost root only; nested stitches pass neither, so `renderPlate`
64
+ * never marks or styles a nested root. */
65
+ function renderStatic(plate, nested, rootRef, instant, css) {
64
66
  return renderPlate(plate, (name) => {
65
67
  const child = plate.refs?.[name];
66
68
  return child ? renderStatic(child, true) : null;
67
- }, nested, rootRef, instant);
69
+ }, nested, rootRef, instant, css);
68
70
  }
69
71
  //#endregion
70
72
  export { Skeleton };
@@ -49,12 +49,21 @@ type DiagnosticCode = 'unreadable-stylesheet' | 'unreadable-import' | 'skipped-d
49
49
  //#region src/plate.d.ts
50
50
  type LeafKind = 'text' | 'text-block' | 'media' | 'box';
51
51
  /**
52
- * The shipped structural capture of a component's rendered DOM — the data a
53
- * Skeleton renders from (see CONTEXT.md). Self-contained: pre-serialized chunks
54
- * plus one scoped CSS string; depends on nothing in the consumer's build (ADR
55
- * 0003). Built from the RenderNode tree at plugin load (see chunk.ts), so the
56
- * adapter injects strings instead of walking a tree on the latency-critical
57
- * first paint — and no per-framework serializer ships to the client (ADR 0008).
52
+ * The shipped structural capture of a component's rendered DOM — the TREE
53
+ * artifact a Skeleton renders from (see CONTEXT.md). Since the two-artifact
54
+ * cutover (plan 005 round 2), a Plate carries structure ONLY: its scoped CSS
55
+ * is an INDEPENDENT sibling artifact the plugin serves it through Vite's
56
+ * own CSS pipeline (`virtual:xray/plates/<name>.css`, plus the shared
57
+ * `virtual:xray/base.css`, and an `export const css` on the virtual JS module
58
+ * for dev/programmatic reads), and the framework-neutral `core.ts` path hands
59
+ * it back separately (`captureElement` → `{ plate, css }`;
60
+ * `renderPlateHtml(plate, css)` receives it explicitly). Consumers decide how
61
+ * to render either part. Still self-contained in ADR 0003's sense: both
62
+ * artifacts originate from the capture and depend on nothing in the
63
+ * consumer's build. Built from the RenderNode tree at plugin load (see
64
+ * chunk.ts), so the adapter injects strings instead of walking a tree on the
65
+ * latency-critical first paint — and no per-framework serializer ships to the
66
+ * client (ADR 0008).
58
67
  */
59
68
  interface Plate {
60
69
  v: number;
@@ -68,8 +77,6 @@ interface Plate {
68
77
  chunks: Chunk[] | null;
69
78
  /** The root node's own content classes (its inherited font context), if any. */
70
79
  rootCls?: string;
71
- /** Layout-relevant CSS (incl. `@media`) scoped under the plate root, paint stripped. */
72
- css: string;
73
80
  /**
74
81
  * Child plates named by the chunks' `{ r }` stitches, resolved through the
75
82
  * import graph (ADR 0006). Populated by the loader's generated imports, never
@@ -201,6 +208,62 @@ interface StoredPlate extends MergedPlate {
201
208
  /** Width thresholds (view boundaries); the HUD coverage derives its bands from these. */
202
209
  breakpoints: number[];
203
210
  }
211
+ /**
212
+ * Renderer-owned base styles, emitted once by the outermost view (ADR 0007),
213
+ * before a plate's node rules so a node's captured class wins by source order.
214
+ * Everything visual is driven by inheritable custom properties, so consumers
215
+ * theme by setting `--xr-*` in `:root` or on any ancestor — and per-plate via
216
+ * `[data-xr-root="name"]` (the root carries its plate name). See ADR 0011 and
217
+ * docs/architecture.md. Class-based and unlayered — `@layer` was rejected
218
+ * (unlayered app CSS would beat layered skeleton rules, and it inverts
219
+ * `!important`).
220
+ *
221
+ * Progressive enhancement (ADR 0011): the plain bone fill + opacity pulse use
222
+ * only universally-supported CSS and are the floor. `light-dark()` is guarded
223
+ * by `@supports` because an unsupported VALUE drops the whole declaration (an
224
+ * invisible bone), and it is not Baseline-widely-available until 2026-11-13 —
225
+ * drop the guard then. Features that merely no-op when absent (`@property`,
226
+ * `prefers-contrast`, relative-color-as-fallback) carry no hard guard.
227
+ *
228
+ * Tokens (ADR 0017 taxonomy). The `--xr-bone-*` family styles an individual
229
+ * Bone: `--xr-bone-color` (fill), `--xr-bone-highlight-color` (sheen),
230
+ * `--xr-bone-border-radius` (+ `--xr-bone-text-border-radius` /
231
+ * `--xr-bone-text-block-border-radius` / `--xr-bone-media-border-radius`),
232
+ * `--xr-bone-animation-duration`, the pulse
233
+ * range `--xr-bone-pulse-opacity-min`/`--xr-bone-pulse-opacity-max`,
234
+ * `--xr-bone-animation` (mode: `xr-pulse` | `none` | a custom keyframe), and
235
+ * `--xr-bone-fill` (override the leaf paint, e.g. a sheen gradient built from the
236
+ * two bone color tokens). The `--xr-skeleton-*` family governs whole-skeleton
237
+ * display timing (ADR 0016): `--xr-skeleton-delay` (grace before the skeleton
238
+ * becomes visible), `--xr-skeleton-min-duration` (minimum visible time once
239
+ * shown; read by the `useSkeletonTiming` hook, not used in CSS), and
240
+ * `--xr-skeleton-transition-duration` (the reveal/swap fade). The private
241
+ * channels `--xr-o` (pulse) and `--xr-reveal` (reveal) stay undocumented
242
+ * `@property` plumbing, not a theming surface.
243
+ *
244
+ * Declared as a PLAIN template literal — no `.trim()` (or any other call) on
245
+ * the initializer. A method call is not provably pure to a conservative
246
+ * tree-shaker (rolldown/Rollup), and it single-handedly pinned this ~5 KB
247
+ * string into every consumer chunk importing the React adapter even after the
248
+ * adapter stopped referencing it (plan 005 round 2 finding). The template
249
+ * therefore starts immediately after the backtick (the old leading newline
250
+ * `.trim()` used to strip simply isn't there), keeping the string value
251
+ * byte-identical to the trimmed original.
252
+ */
253
+ 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 \u2014 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 { ... }. */\n.xr-leaf-text { border-radius: var(--xr-bone-text-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@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 \u2014 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 \u2014 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 \u2014 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}";
254
+ /**
255
+ * Compose a plate's scoped css with the renderer-owned BASE_CSS, base first so
256
+ * a plate rule beats a base rule at equal specificity by source order — the
257
+ * standard composition for rendering a skeleton outside the Vite plugin
258
+ * (where `virtual:xray/base.css` + `virtual:xray/plates/<name>.css` do this
259
+ * through the CSS pipeline instead). Since the two-artifact cutover (plan 005
260
+ * round 2) css is the CALLER's input: `renderPlateHtml(plate,
261
+ * composeCss(css))`, or `<Skeleton plate={plate} css={composeCss(css)}>` for
262
+ * a runtime-captured plate. Rendering a NESTED stitch under an
263
+ * already-composed parent needs the raw css only (base is already on the
264
+ * page) — pass the string uncomposed there.
265
+ */
266
+ declare function composeCss(css: string): string;
204
267
  /** The (unserialized) plate an import resolves to before anything has been captured. */
205
268
  declare function emptyPlate(name: string): MergedPlate;
206
269
  /**
@@ -498,4 +561,4 @@ type CollectLimits = {
498
561
  maxDepth?: number;
499
562
  };
500
563
  //#endregion
501
- export { emptyPlate as _, CollectAxis as a, XrayCaptureWalker as c, MergedPlate as d, Plate as f, ViewCapture as g, StoredPlate as h, CaptureWalkerContext as i, defineXrayCaptureWalker as l, RenderNode as m, CaptureSize as n, CollectLimits as o, PlateNode as p, CaptureTooLargeError as r, WalkerDecision as s, CaptureOptions as t, LeafKind as u, CaptureDiagnostic as v };
564
+ export { ViewCapture as _, CollectAxis as a, CaptureDiagnostic as b, XrayCaptureWalker as c, LeafKind as d, MergedPlate 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hueest/xray",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Auto-captured skeleton loading screens for Vite — capture the structure, ship the plate.",
5
5
  "keywords": [
6
6
  "loading",