@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 +57 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +3 -2
- package/dist/core.d.ts +35 -17
- package/dist/core.js +34 -22
- package/dist/index.d.ts +59 -8
- package/dist/index.js +191 -16
- package/dist/{plate-BRR6d8Se.js → plate-BwJUgHDc.js} +27 -4
- package/dist/{project-BZosujs9.js → project-BAXKpnfF.js} +12 -8
- package/dist/{react.core-BC02ukac.js → react.core-DeEli0Lp.js} +19 -6
- package/dist/{react.core-NhWc9qan.d.ts → react.core-sJfIcaXX.d.ts} +14 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.dev.d.ts +1 -1
- package/dist/react.dev.js +6 -5
- package/dist/react.js +10 -8
- package/dist/{serialize-BgdGt34A.d.ts → serialize-BqbQVxYl.d.ts} +72 -9
- package/package.json +1 -1
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
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-
|
|
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
|
-
|
|
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
|
|
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
|
|
25
|
-
*
|
|
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.
|
|
28
|
-
* are the top-level elements to capture (a component's rendered
|
|
29
|
-
*
|
|
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
|
|
44
|
+
* capture-only and never persisted into the returned artifacts.
|
|
35
45
|
*/
|
|
36
|
-
declare function captureElement(roots: readonly Element[], options: CaptureOptions):
|
|
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
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* `dangerouslySetInnerHTML`.
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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-
|
|
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-
|
|
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
|
|
7
|
-
*
|
|
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.
|
|
10
|
-
* are the top-level elements to capture (a component's rendered
|
|
11
|
-
*
|
|
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
|
|
19
|
+
* capture-only and never persisted into the returned artifacts.
|
|
17
20
|
*/
|
|
18
21
|
function captureElement(roots, options) {
|
|
19
|
-
const
|
|
22
|
+
const plateIR = captureView(roots, {
|
|
20
23
|
collect: options.collect,
|
|
21
24
|
walker: options.walker,
|
|
22
25
|
captureRootIsBoundary: options.captureRootIsBoundary
|
|
23
26
|
});
|
|
24
|
-
|
|
25
|
-
runPlatePasses(
|
|
26
|
-
|
|
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
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* `dangerouslySetInnerHTML`.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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>${
|
|
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
|
|
55
|
-
* Skeleton renders from (see CONTEXT.md).
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
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
|
|
853
|
+
const merged = stored ? {
|
|
714
854
|
v: stored.v,
|
|
715
855
|
name,
|
|
716
856
|
tree: stored.tree,
|
|
717
857
|
css: stored.css
|
|
718
|
-
}
|
|
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
|
-
|
|
1044
|
-
|
|
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
|
-
}
|
|
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,
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
package/dist/react.dev.d.ts
CHANGED
package/dist/react.dev.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as BOUNDARY_ATTR } from "./plate-
|
|
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-
|
|
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-
|
|
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)
|
|
62
|
-
*
|
|
63
|
-
|
|
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
|
|
53
|
-
* Skeleton renders from (see CONTEXT.md).
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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 {
|
|
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 };
|