@hueest/xray 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{serialize-Bq6yJnNV.js → breakpoints-CMoFUUOv.js} +426 -104
- package/dist/client.d.ts +38 -3
- package/dist/client.js +716 -27
- package/dist/core.d.ts +8 -3
- package/dist/core.js +12 -6
- package/dist/css-escape-N7bOusGW.js +52 -0
- package/dist/hud.js +46 -0
- package/dist/index.d.ts +231 -5
- package/dist/index.js +753 -71
- package/dist/{plate-DboxMKg-.d.ts → plate-Bv6W-GkA.d.ts} +142 -0
- package/dist/plate-DoE1HEXp.js +152 -0
- package/dist/react.core-BzMG_cDy.d.ts +79 -0
- package/dist/react.core-IyFy2b_8.js +267 -0
- package/dist/react.d.ts +2 -8
- package/dist/react.dev.d.ts +2 -9
- package/dist/react.dev.js +153 -23
- package/dist/react.js +24 -12
- package/dist/serialize-B--oY2bV.d.ts +143 -0
- package/package.json +7 -7
- package/virtual.d.ts +6 -0
- package/dist/breakpoints-CBNlh7lQ.js +0 -47
- package/dist/plate-BuzRMPx4.js +0 -94
- package/dist/react.core-CqnDjfAJ.js +0 -59
- package/dist/react.core-fgWG_svU.d.ts +0 -21
- package/dist/serialize.d.ts +0 -88
- package/dist/serialize.js +0 -2
package/dist/react.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as SkeletonProps } from "./react.core-
|
|
1
|
+
import { t as SkeletonProps } from "./react.core-BzMG_cDy.js";
|
|
2
2
|
|
|
3
3
|
//#region src/react.d.ts
|
|
4
4
|
/**
|
|
@@ -18,12 +18,6 @@ import { t as SkeletonProps } from "./react.core-fgWG_svU.js";
|
|
|
18
18
|
* In production this component ships no capture client, HUD, Light Box, store,
|
|
19
19
|
* or subscriptions. Stitches resolve from the statically imported `refs` graph.
|
|
20
20
|
*/
|
|
21
|
-
declare function Skeleton(
|
|
22
|
-
plate,
|
|
23
|
-
loading,
|
|
24
|
-
suspense,
|
|
25
|
-
fallback,
|
|
26
|
-
children
|
|
27
|
-
}: SkeletonProps): import("react").JSX.Element;
|
|
21
|
+
declare function Skeleton<T = undefined>(props: SkeletonProps<T>): import("react").JSX.Element;
|
|
28
22
|
//#endregion
|
|
29
23
|
export { Skeleton, type SkeletonProps };
|
package/dist/react.dev.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as SkeletonProps } from "./react.core-
|
|
1
|
+
import { t as SkeletonProps } from "./react.core-BzMG_cDy.js";
|
|
2
2
|
|
|
3
3
|
//#region src/react.dev.d.ts
|
|
4
4
|
/**
|
|
@@ -7,13 +7,6 @@ import { t as SkeletonProps } from "./react.core-fgWG_svU.js";
|
|
|
7
7
|
* `@hueest/xray/react` to this module in `serve`; production gets the hook-free
|
|
8
8
|
* `react.tsx`.
|
|
9
9
|
*/
|
|
10
|
-
declare function Skeleton(
|
|
11
|
-
plate,
|
|
12
|
-
loading,
|
|
13
|
-
suspense,
|
|
14
|
-
fallback,
|
|
15
|
-
delay,
|
|
16
|
-
children
|
|
17
|
-
}: SkeletonProps): import("react").JSX.Element;
|
|
10
|
+
declare function Skeleton<T = undefined>(props: SkeletonProps<T>): import("react").JSX.Element;
|
|
18
11
|
//#endregion
|
|
19
12
|
export { Skeleton, type SkeletonProps };
|
package/dist/react.dev.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as BOUNDARY_ATTR } from "./plate-
|
|
2
|
-
import {
|
|
3
|
-
import { Suspense, useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
1
|
+
import { n as BOUNDARY_ATTR } from "./plate-DoE1HEXp.js";
|
|
2
|
+
import { a as renderPlate, i as markStitchesLive, n as isRenderProp, o as resolveContent, r as isStitchLive, s as useSkeletonTiming, t as collectStitchNames } from "./react.core-IyFy2b_8.js";
|
|
3
|
+
import { Suspense, useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
4
4
|
import { Fragment as Fragment$1, jsx } from "react/jsx-runtime";
|
|
5
5
|
//#region src/react.dev.tsx
|
|
6
6
|
const noSubscribe = () => () => {};
|
|
@@ -10,6 +10,67 @@ function useLightBox() {
|
|
|
10
10
|
const lightbox = globalThis.__XRAY__?.lightbox;
|
|
11
11
|
return useSyncExternalStore(lightbox?.subscribe ?? noSubscribe, lightbox?.get ?? alwaysFalse, alwaysFalse);
|
|
12
12
|
}
|
|
13
|
+
/** True while the HUD/console "Replay" toggle is on (ADR 0015). */
|
|
14
|
+
function useReplayOn() {
|
|
15
|
+
const replay = globalThis.__XRAY__?.replay;
|
|
16
|
+
return useSyncExternalStore(replay?.subscribe ?? noSubscribe, replay?.get ?? alwaysFalse, alwaysFalse);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The Fixtures (ADR 0015) seam, applied at the ready (`loading=false`) path right
|
|
20
|
+
* before `resolveContent`. It is the ONE place Record reads `data` and Replay
|
|
21
|
+
* substitutes it — the render-prop `data` argument is in scope here (open Q1,
|
|
22
|
+
* confirmed).
|
|
23
|
+
*
|
|
24
|
+
* Replay: when the toggle is on and a Fixture exists, the recorded `data` is
|
|
25
|
+
* substituted for the live `data` according to `replayMode` — `'prefer'` always
|
|
26
|
+
* uses the Fixture; `'missing-only'` uses it only when live `data` is NULLISH
|
|
27
|
+
* (`missing ≡ nullish`, so an empty array/object is present and stays
|
|
28
|
+
* capturable). The substituted value is returned synchronously so it feeds
|
|
29
|
+
* `resolveContent` for THIS render; the capture-all sweep only re-renders at each
|
|
30
|
+
* width and reads the SAME Fixture from the store (which the sweep never changes
|
|
31
|
+
* per width), so every View is captured from one width-independent Fixture with
|
|
32
|
+
* no new capture trigger (open Q2, confirmed — capture follows whatever renders,
|
|
33
|
+
* ADR 0002).
|
|
34
|
+
*
|
|
35
|
+
* Record: the live `data` is registered (in an effect, so render stays pure) so
|
|
36
|
+
* the HUD button and the auto modes always have the current input in scope. The
|
|
37
|
+
* auto modes (`'first-ready'`, `'always'`) fire from the same effect; `'manual'`
|
|
38
|
+
* (the default) records nothing automatically. We register and auto-record the
|
|
39
|
+
* LIVE `data`, never the replayed value, so Replay and Record never feed each
|
|
40
|
+
* other.
|
|
41
|
+
*
|
|
42
|
+
* Only the render-prop children form is the `data` seam (ADR 0014/0015), so
|
|
43
|
+
* Fixtures apply to it alone: `renderProp` gates participation. A plain-children
|
|
44
|
+
* Skeleton has no `data` input, so it must NOT register, auto-record, or replay —
|
|
45
|
+
* the hook is a pass-through for it, which keeps a never-registered plain name out
|
|
46
|
+
* of `recordAll` and lets `record(name)` throw loudly (no render-prop data) as the
|
|
47
|
+
* store contract documents. A render-prop Skeleton passed `data={undefined}` is
|
|
48
|
+
* still a render prop and participates exactly as any other.
|
|
49
|
+
*/
|
|
50
|
+
function useFixtureData(name, data, renderProp) {
|
|
51
|
+
const replayOn = useReplayOn();
|
|
52
|
+
const fixturesStore = globalThis.__XRAY__?.fixtures;
|
|
53
|
+
const recordedFixture = useSyncExternalStore(useCallback((onChange) => fixturesStore?.subscribe(onChange) ?? noSubscribe(), [fixturesStore]), () => fixturesStore?.read(name), () => void 0);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!renderProp) return void 0;
|
|
56
|
+
const fx = globalThis.__XRAY__?.fixtures;
|
|
57
|
+
if (!fx) return void 0;
|
|
58
|
+
const dispose = fx.register(name, data);
|
|
59
|
+
const mode = globalThis.__XRAY__?.recordMode ?? "manual";
|
|
60
|
+
if (mode === "always" || mode === "first-ready" && !fx.has(name)) fx.record(name).catch((error) => {
|
|
61
|
+
console.error(`[xray] fixture record failed for "${name}":`, error);
|
|
62
|
+
});
|
|
63
|
+
return dispose;
|
|
64
|
+
}, [
|
|
65
|
+
name,
|
|
66
|
+
data,
|
|
67
|
+
renderProp
|
|
68
|
+
]);
|
|
69
|
+
if (!renderProp) return data;
|
|
70
|
+
if (!replayOn || !recordedFixture) return data;
|
|
71
|
+
if ((globalThis.__XRAY__?.replayMode ?? "prefer") === "missing-only" && data != null) return data;
|
|
72
|
+
return recordedFixture.data;
|
|
73
|
+
}
|
|
13
74
|
/**
|
|
14
75
|
* The plate to render: the freshest capture from the client store (hot-swapped
|
|
15
76
|
* in place, no remount) when present, else the imported prop.
|
|
@@ -26,50 +87,119 @@ function useLivePlate(plate) {
|
|
|
26
87
|
* `@hueest/xray/react` to this module in `serve`; production gets the hook-free
|
|
27
88
|
* `react.tsx`.
|
|
28
89
|
*/
|
|
29
|
-
function Skeleton(
|
|
90
|
+
function Skeleton(props) {
|
|
91
|
+
const { plate, loading = false, suspense = false, fallback, delay, children, data, captureWalker } = props;
|
|
92
|
+
const manual = props.root === "manual";
|
|
30
93
|
const lightBoxOn = useLightBox();
|
|
94
|
+
const livePlate = useLivePlate(plate);
|
|
95
|
+
const rootRef = useRef(null);
|
|
96
|
+
const showSkeleton = useSkeletonTiming(loading, rootRef);
|
|
97
|
+
const [instant] = useState(() => isStitchLive(plate.name));
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!showSkeleton) return void 0;
|
|
100
|
+
return markStitchesLive(collectStitchNames(livePlate.chunks));
|
|
101
|
+
}, [showSkeleton, livePlate.chunks]);
|
|
31
102
|
const skeleton = /* @__PURE__ */ jsx(PlateView, {
|
|
32
|
-
plate:
|
|
33
|
-
fallback
|
|
103
|
+
plate: livePlate,
|
|
104
|
+
fallback,
|
|
105
|
+
rootRef,
|
|
106
|
+
instant
|
|
34
107
|
});
|
|
35
108
|
if (lightBoxOn) return skeleton;
|
|
36
|
-
|
|
37
|
-
fallback: skeleton,
|
|
38
|
-
children: /* @__PURE__ */ jsx(Content, {
|
|
39
|
-
plate,
|
|
40
|
-
delay,
|
|
41
|
-
children
|
|
42
|
-
})
|
|
43
|
-
});
|
|
44
|
-
if (loading) return skeleton;
|
|
45
|
-
return /* @__PURE__ */ jsx(Content, {
|
|
109
|
+
const content = /* @__PURE__ */ jsx(Content, {
|
|
46
110
|
plate,
|
|
47
111
|
delay,
|
|
112
|
+
manual,
|
|
113
|
+
data,
|
|
114
|
+
captureWalker,
|
|
48
115
|
children
|
|
49
116
|
});
|
|
117
|
+
if (suspense) return /* @__PURE__ */ jsx(Suspense, {
|
|
118
|
+
fallback: skeleton,
|
|
119
|
+
children: content
|
|
120
|
+
});
|
|
121
|
+
if (showSkeleton) return skeleton;
|
|
122
|
+
return content;
|
|
50
123
|
}
|
|
51
124
|
function Content(props) {
|
|
52
|
-
|
|
125
|
+
const { children, data } = props;
|
|
126
|
+
if (globalThis.__XRAY__ === void 0) return /* @__PURE__ */ jsx(Fragment$1, { children: resolveContent(children, data) });
|
|
127
|
+
if (props.manual) return /* @__PURE__ */ jsx(ManualBoundary, { ...props });
|
|
53
128
|
return /* @__PURE__ */ jsx(CaptureBoundary, { ...props });
|
|
54
129
|
}
|
|
55
|
-
|
|
130
|
+
/**
|
|
131
|
+
* `root="auto"` (default): the existing `display:contents` boundary wrapper marks
|
|
132
|
+
* the capture seam so a parent capture stops here and references this plate by
|
|
133
|
+
* name rather than re-serializing the content (ADR 0006). A render prop is called
|
|
134
|
+
* with the verbatim `data` and a no-op `ref` (the wrapper, not the consumer's
|
|
135
|
+
* element, carries the marker); its result renders inside the wrapper exactly as
|
|
136
|
+
* plain children do.
|
|
137
|
+
*/
|
|
138
|
+
function CaptureBoundary({ plate, delay, children, data, captureWalker }) {
|
|
56
139
|
const ref = useRef(null);
|
|
57
140
|
useEffect(() => {
|
|
58
141
|
const el = ref.current;
|
|
59
142
|
if (!el) return void 0;
|
|
60
|
-
return globalThis.__XRAY__?.captured(plate.name, el, {
|
|
61
|
-
|
|
143
|
+
return globalThis.__XRAY__?.captured(plate.name, el, {
|
|
144
|
+
delay,
|
|
145
|
+
walker: captureWalker,
|
|
146
|
+
mode: "auto"
|
|
147
|
+
});
|
|
148
|
+
}, [
|
|
149
|
+
plate.name,
|
|
150
|
+
delay,
|
|
151
|
+
captureWalker
|
|
152
|
+
]);
|
|
153
|
+
const effective = useFixtureData(plate.name, data, isRenderProp(children));
|
|
62
154
|
return /* @__PURE__ */ jsx("div", {
|
|
63
155
|
[BOUNDARY_ATTR]: plate.name,
|
|
64
156
|
style: { display: "contents" },
|
|
65
157
|
ref,
|
|
66
|
-
children
|
|
158
|
+
children: resolveContent(children, effective)
|
|
67
159
|
});
|
|
68
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* `root="manual"` (ADR 0014): no wrapper. The render prop is handed a CALLBACK
|
|
163
|
+
* ref that, when spread onto the consumer's root element, writes the existing
|
|
164
|
+
* `data-xr-boundary` marker onto it and registers it as the capture root, so a
|
|
165
|
+
* parent capture reads the identical marker (`serialize.ts` does
|
|
166
|
+
* `BOUNDARY_ATTR ?? ROOT_ATTR`) and cannot tell a manual root from an auto one.
|
|
167
|
+
*
|
|
168
|
+
* If the consumer forgets to spread the ref, the marker never lands and the
|
|
169
|
+
* parent capture descends into and silently swallows this subtree's DOM: the
|
|
170
|
+
* exact stitch-isolation breakage ADR 0006 prevents, and the worst failure xray
|
|
171
|
+
* can produce. A dev-time effect asserts the ref fired and loudly `console.error`s
|
|
172
|
+
* when it did not.
|
|
173
|
+
*/
|
|
174
|
+
function ManualBoundary({ plate, delay, children, data, captureWalker }) {
|
|
175
|
+
const disposeRef = useRef(void 0);
|
|
176
|
+
const attachedRef = useRef(false);
|
|
177
|
+
const ref = useCallback((el) => {
|
|
178
|
+
disposeRef.current?.();
|
|
179
|
+
disposeRef.current = void 0;
|
|
180
|
+
if (!el) return;
|
|
181
|
+
el.setAttribute(BOUNDARY_ATTR, plate.name);
|
|
182
|
+
attachedRef.current = true;
|
|
183
|
+
disposeRef.current = globalThis.__XRAY__?.captured(plate.name, el, {
|
|
184
|
+
delay,
|
|
185
|
+
walker: captureWalker,
|
|
186
|
+
mode: "manual"
|
|
187
|
+
});
|
|
188
|
+
}, [
|
|
189
|
+
plate.name,
|
|
190
|
+
delay,
|
|
191
|
+
captureWalker
|
|
192
|
+
]);
|
|
193
|
+
useEffect(() => () => disposeRef.current?.(), []);
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (!attachedRef.current) console.error(`[xray] <Skeleton root="manual" plate="${plate.name}"> never received its \`ref\`: spread it onto your root element, or capture leaks into the parent plate (ADR 0006).`);
|
|
196
|
+
}, [plate.name]);
|
|
197
|
+
return /* @__PURE__ */ jsx(Fragment$1, { children: resolveContent(children, useFixtureData(plate.name, data, isRenderProp(children)), ref) });
|
|
198
|
+
}
|
|
69
199
|
/** Render a plate's skeleton, seeding the live store from its resolved refs so a
|
|
70
200
|
* stitch resolves before its child is re-captured and survives a parent
|
|
71
201
|
* hot-swap (which pushes a refs-less plate). Never overwrites a live capture. */
|
|
72
|
-
function PlateView({ plate, fallback, nested = false }) {
|
|
202
|
+
function PlateView({ plate, fallback, nested = false, rootRef, instant }) {
|
|
73
203
|
useEffect(() => {
|
|
74
204
|
const store = globalThis.__XRAY__?.plates;
|
|
75
205
|
if (!store || !plate.refs) return;
|
|
@@ -79,7 +209,7 @@ function PlateView({ plate, fallback, nested = false }) {
|
|
|
79
209
|
return renderPlate(plate, (name) => /* @__PURE__ */ jsx(RefNode, {
|
|
80
210
|
name,
|
|
81
211
|
staticRefs: plate.refs
|
|
82
|
-
}), nested);
|
|
212
|
+
}), nested, rootRef, instant);
|
|
83
213
|
}
|
|
84
214
|
/**
|
|
85
215
|
* A stitch in dev: resolve the child plate from the live store (so re-capturing
|
package/dist/react.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Suspense } from "react";
|
|
1
|
+
import { a as renderPlate, i as markStitchesLive, o as resolveContent, r as isStitchLive, s as useSkeletonTiming, t as collectStitchNames } from "./react.core-IyFy2b_8.js";
|
|
2
|
+
import { Suspense, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { Fragment as Fragment$1, jsx } from "react/jsx-runtime";
|
|
4
4
|
//#region src/react.tsx
|
|
5
5
|
/**
|
|
@@ -29,30 +29,42 @@ import { Fragment as Fragment$1, jsx } from "react/jsx-runtime";
|
|
|
29
29
|
* In production this component ships no capture client, HUD, Light Box, store,
|
|
30
30
|
* or subscriptions. Stitches resolve from the statically imported `refs` graph.
|
|
31
31
|
*/
|
|
32
|
-
function Skeleton(
|
|
32
|
+
function Skeleton(props) {
|
|
33
|
+
const { plate, loading = false, suspense = false, fallback, children, data } = props;
|
|
34
|
+
const rootRef = useRef(null);
|
|
35
|
+
const showSkeleton = useSkeletonTiming(loading, rootRef);
|
|
36
|
+
const [instant] = useState(() => isStitchLive(plate.name));
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!showSkeleton) return void 0;
|
|
39
|
+
return markStitchesLive(collectStitchNames(plate.chunks));
|
|
40
|
+
}, [showSkeleton, plate.chunks]);
|
|
33
41
|
const skeleton = /* @__PURE__ */ jsx(PlateView, {
|
|
34
42
|
plate,
|
|
35
|
-
fallback
|
|
43
|
+
fallback,
|
|
44
|
+
rootRef,
|
|
45
|
+
instant
|
|
36
46
|
});
|
|
37
47
|
if (suspense) return /* @__PURE__ */ jsx(Suspense, {
|
|
38
48
|
fallback: skeleton,
|
|
39
|
-
children
|
|
49
|
+
children: resolveContent(children, data)
|
|
40
50
|
});
|
|
41
|
-
if (
|
|
42
|
-
return /* @__PURE__ */ jsx(Fragment$1, { children });
|
|
51
|
+
if (showSkeleton) return skeleton;
|
|
52
|
+
return /* @__PURE__ */ jsx(Fragment$1, { children: resolveContent(children, data) });
|
|
43
53
|
}
|
|
44
54
|
/** Render a plate's skeleton, mounting each stitch's child plate from `refs`. */
|
|
45
|
-
function PlateView({ plate, fallback }) {
|
|
55
|
+
function PlateView({ plate, fallback, rootRef, instant }) {
|
|
46
56
|
if (!plate.chunks) return /* @__PURE__ */ jsx(Fragment$1, { children: fallback ?? null });
|
|
47
|
-
return renderStatic(plate, false);
|
|
57
|
+
return renderStatic(plate, false, rootRef, instant);
|
|
48
58
|
}
|
|
49
59
|
/** Resolve stitches statically through the import graph (ADR 0006) — pure
|
|
50
|
-
* recursion, no store, no subscription. An unknown child renders nothing.
|
|
51
|
-
|
|
60
|
+
* 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) {
|
|
52
64
|
return renderPlate(plate, (name) => {
|
|
53
65
|
const child = plate.refs?.[name];
|
|
54
66
|
return child ? renderStatic(child, true) : null;
|
|
55
|
-
}, nested);
|
|
67
|
+
}, nested, rootRef, instant);
|
|
56
68
|
}
|
|
57
69
|
//#endregion
|
|
58
70
|
export { Skeleton };
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { n as MergedPlate, s as ViewCapture, t as LeafKind } from "./plate-Bv6W-GkA.js";
|
|
2
|
+
|
|
3
|
+
//#region src/serialize.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Browser-side capture: walk a live rendered subtree into a Plate — one node
|
|
6
|
+
* tree plus one self-contained, scoped CSS string (ADR 0003). The CSS is
|
|
7
|
+
* lifted from the author rules that matched each node, rewritten to
|
|
8
|
+
* plate-local selectors — it carries `@media` for free and keeps layout live
|
|
9
|
+
* (the M1 bake-off winner; ADR 0005).
|
|
10
|
+
*/
|
|
11
|
+
interface CaptureOptions {
|
|
12
|
+
/** Plate name; comes from the virtual-module specifier. */
|
|
13
|
+
name: string;
|
|
14
|
+
/**
|
|
15
|
+
* Optional programmable capture walker (ADR 0018). The declarative `data-xr-*`
|
|
16
|
+
* annotations cover the 90%; this is the escape hatch for the 10% a markup
|
|
17
|
+
* attribute cannot express. Capture-only: it shapes the resulting `PlateNode`
|
|
18
|
+
* tree but is never persisted into the Plate, and the production adapter never
|
|
19
|
+
* captures so it ignores this entirely.
|
|
20
|
+
*/
|
|
21
|
+
walker?: XrayCaptureWalker;
|
|
22
|
+
/**
|
|
23
|
+
* Whether the top-level capture roots ARE this plate's own boundary, so the
|
|
24
|
+
* stitch guard must be SKIPPED for them (ADR 0006/0014, finding #6). True ONLY
|
|
25
|
+
* for a MANUAL root: there `site.el` is the consumer's own element and its
|
|
26
|
+
* `data-xr-boundary` marks the boundary of THIS plate, so the root must be
|
|
27
|
+
* serialized as a real Entry (its box/classes/padding/border) rather than a
|
|
28
|
+
* stitch-to-itself. Defaults to `false` — the auto path, where the top-level
|
|
29
|
+
* roots are the wrapper's CHILDREN and any boundary marker on a direct-child
|
|
30
|
+
* root is a NESTED `<Skeleton>` that MUST stitch (the capture-leak guard).
|
|
31
|
+
*/
|
|
32
|
+
captureRootIsBoundary?: boolean;
|
|
33
|
+
}
|
|
34
|
+
declare const WALKER_RESULT: unique symbol;
|
|
35
|
+
/**
|
|
36
|
+
* A capture-walk decision. Obtainable ONLY from `ctx.ignore()/bone()/walk()` or
|
|
37
|
+
* `next()` — a consumer cannot hand-build one, so arbitrary `PlateNode`s can
|
|
38
|
+
* never cross the capture boundary (ADR 0018: helpers expose decisions, never
|
|
39
|
+
* raw `PlateNode`s, so xray keeps ownership of the Plate format). Opaque by
|
|
40
|
+
* design: it wraps the internal `PlateNode | null` with zero runtime cost and is
|
|
41
|
+
* unwrapped only inside the engine.
|
|
42
|
+
*/
|
|
43
|
+
interface WalkerResult {
|
|
44
|
+
readonly [WALKER_RESULT]: true;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The context handed to a capture walker's `element` hook (ADR 0018). Every
|
|
48
|
+
* member is a HELPER that returns an opaque `WalkerResult` decision — a walker
|
|
49
|
+
* NEVER hand-builds a `PlateNode`. `el` is the element under consideration; the
|
|
50
|
+
* helpers either drop it, collapse it to one measured Bone, or defer to the
|
|
51
|
+
* built-in walk.
|
|
52
|
+
*/
|
|
53
|
+
interface CaptureWalkerContext {
|
|
54
|
+
/** The element currently being classified. */
|
|
55
|
+
el: Element;
|
|
56
|
+
/** Drop this element and its entire subtree (no bone). */
|
|
57
|
+
ignore: () => WalkerResult;
|
|
58
|
+
/**
|
|
59
|
+
* Collapse this element's subtree into ONE measured Bone leaf. `kind` omitted
|
|
60
|
+
* shares the default classifier's kind-inference heuristic (ADR 0018, Q4).
|
|
61
|
+
*/
|
|
62
|
+
bone: (opts?: {
|
|
63
|
+
kind?: LeafKind;
|
|
64
|
+
}) => WalkerResult;
|
|
65
|
+
/**
|
|
66
|
+
* Run the built-in walk for this element: the annotation walker, then the
|
|
67
|
+
* default classifier (and recurse into the subtree). The decision `next()`
|
|
68
|
+
* would produce if the walker deferred without inspecting it.
|
|
69
|
+
*/
|
|
70
|
+
walk: () => WalkerResult;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The programmable capture walker (ADR 0018), v1. `element` is the ONLY hook
|
|
74
|
+
* implemented in v1; `text`/`declarations`/`media`/`list` are RESERVED in the
|
|
75
|
+
* type for future versions and intentionally unimplemented — declaring them now
|
|
76
|
+
* keeps a later addition non-breaking. `next()` runs the downstream decision
|
|
77
|
+
* (annotation walker, then default classifier) and RETURNS it as an opaque
|
|
78
|
+
* `WalkerResult`, which a walker returns verbatim to defer (ADR 0018, Q3).
|
|
79
|
+
*/
|
|
80
|
+
interface XrayCaptureWalker {
|
|
81
|
+
/**
|
|
82
|
+
* Called for every non-boundary element (the stitch guard runs first and is
|
|
83
|
+
* unreachable here, ADR 0006). Return one of `ctx.ignore()`, `ctx.bone()`,
|
|
84
|
+
* `ctx.walk()`, or the `next()` result — the `WalkerResult` type makes a
|
|
85
|
+
* hand-built `PlateNode` a compile error.
|
|
86
|
+
*/
|
|
87
|
+
element?: (ctx: CaptureWalkerContext, next: () => WalkerResult) => WalkerResult;
|
|
88
|
+
/** RESERVED for a future version; not honored in v1. */
|
|
89
|
+
text?: never;
|
|
90
|
+
/** RESERVED for a future version; not honored in v1. */
|
|
91
|
+
declarations?: never;
|
|
92
|
+
/** RESERVED for a future version; not honored in v1. */
|
|
93
|
+
media?: never;
|
|
94
|
+
/** RESERVED for a future version; not honored in v1. */
|
|
95
|
+
list?: never;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Typed identity wrapper for authoring a capture walker (ADR 0018) — the
|
|
99
|
+
* `defineConfig` pattern. It returns its argument unchanged at runtime; its only
|
|
100
|
+
* job is to infer and check the `XrayCaptureWalker` shape at the call site so an
|
|
101
|
+
* author gets completion and a typo in a hook name is caught.
|
|
102
|
+
*/
|
|
103
|
+
declare function defineXrayCaptureWalker(walker: XrayCaptureWalker): XrayCaptureWalker;
|
|
104
|
+
/**
|
|
105
|
+
* Capture the rendered subtree(s) under a Skeleton into a Plate. `roots` are
|
|
106
|
+
* the component's top-level elements (the children of the layout-neutral
|
|
107
|
+
* wrapper); the synthetic root node (id 0) stands in for the wrapper itself.
|
|
108
|
+
*/
|
|
109
|
+
declare function capture(roots: readonly Element[], options: CaptureOptions): MergedPlate;
|
|
110
|
+
/**
|
|
111
|
+
* Thrown when a subtree has more nodes than the capture limit — almost always
|
|
112
|
+
* a `<Skeleton>` sitting too high in the tree. Raised after the cheap walk and
|
|
113
|
+
* before the O(rules × nodes) CSS extraction, so an oversized capture is
|
|
114
|
+
* refused without freezing the main thread on it.
|
|
115
|
+
*/
|
|
116
|
+
declare class CaptureTooLargeError extends Error {
|
|
117
|
+
readonly nodeCount: number;
|
|
118
|
+
/** Folds the oversized-capture warning into the shared diagnostics vocabulary (diagnostics.ts). */
|
|
119
|
+
readonly code: "too-large";
|
|
120
|
+
constructor(nodeCount: number);
|
|
121
|
+
}
|
|
122
|
+
interface CaptureRegimeOptions {
|
|
123
|
+
/** Refuse (throw `CaptureTooLargeError`) before CSS extraction if the walk exceeds this. */
|
|
124
|
+
maxNodes?: number;
|
|
125
|
+
/** Optional programmable capture walker (ADR 0018); capture-only, never persisted. */
|
|
126
|
+
walker?: XrayCaptureWalker;
|
|
127
|
+
/**
|
|
128
|
+
* Whether the top-level roots ARE this plate's own boundary, so the stitch
|
|
129
|
+
* guard is skipped for them (ADR 0006/0014, finding #6). True ONLY for a
|
|
130
|
+
* MANUAL root; defaults to `false`, the auto path where a boundary marker on a
|
|
131
|
+
* direct-child root is a NESTED `<Skeleton>` that MUST stitch. See `walkAll`.
|
|
132
|
+
*/
|
|
133
|
+
captureRootIsBoundary?: boolean;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Capture one view's worth of a component: the tree plus id-form rules,
|
|
137
|
+
* with the viewport width it was taken at. The caller (the dev client)
|
|
138
|
+
* derives the view interval and ships it off; `capture` is the
|
|
139
|
+
* single-view convenience on top.
|
|
140
|
+
*/
|
|
141
|
+
declare function captureRegime(roots: readonly Element[], options?: CaptureRegimeOptions): ViewCapture;
|
|
142
|
+
//#endregion
|
|
143
|
+
export { WalkerResult as a, captureRegime as c, CaptureWalkerContext as i, defineXrayCaptureWalker as l, CaptureRegimeOptions as n, XrayCaptureWalker as o, CaptureTooLargeError as r, capture as s, CaptureOptions as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hueest/xray",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Auto-captured skeleton loading screens for Vite — capture the structure, ship the plate.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"loading",
|
|
@@ -42,15 +42,11 @@
|
|
|
42
42
|
"types": "./dist/react.dev.d.ts",
|
|
43
43
|
"import": "./dist/react.dev.js"
|
|
44
44
|
},
|
|
45
|
-
"./
|
|
46
|
-
"types": "./dist/serialize.d.ts",
|
|
47
|
-
"import": "./dist/serialize.js"
|
|
48
|
-
},
|
|
49
|
-
"./client": {
|
|
45
|
+
"./internal/client": {
|
|
50
46
|
"types": "./dist/client.d.ts",
|
|
51
47
|
"import": "./dist/client.js"
|
|
52
48
|
},
|
|
53
|
-
"./hud": {
|
|
49
|
+
"./internal/hud": {
|
|
54
50
|
"types": "./dist/hud.d.ts",
|
|
55
51
|
"import": "./dist/hud.js"
|
|
56
52
|
},
|
|
@@ -61,6 +57,10 @@
|
|
|
61
57
|
"publishConfig": {
|
|
62
58
|
"access": "public"
|
|
63
59
|
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"devalue": "5.8.1",
|
|
62
|
+
"valibot": "1.4.1"
|
|
63
|
+
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@types/node": "25.9.3",
|
|
66
66
|
"@types/react": "19.2.17",
|
package/virtual.d.ts
CHANGED
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
* imported Plate. Pass the imported Plate to `<Skeleton>`; do not repeat the
|
|
17
17
|
* name in props.
|
|
18
18
|
*
|
|
19
|
+
* A name is one or more `/`-separated path segments, each `\w+(-\w+)*` — word
|
|
20
|
+
* chars with single interior hyphens. A segment may not contain `--` or a
|
|
21
|
+
* leading/trailing hyphen: the generated class prefix renders `/` as `--` to
|
|
22
|
+
* keep nested names readable, and banning those forms keeps the encoding
|
|
23
|
+
* collision-free (it is the isolation boundary across stitched Plates).
|
|
24
|
+
*
|
|
19
25
|
* If the file does not exist yet, the Vite plugin resolves an empty Plate with
|
|
20
26
|
* the same name so `<Skeleton fallback={...}>` can render until the first
|
|
21
27
|
* Capture writes the file. Nested `<Skeleton>` boundaries become Stitches; the
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
//#region src/breakpoints.ts
|
|
2
|
-
/**
|
|
3
|
-
* Per-plate breakpoint derivation (ADR 0004): the width thresholds that could
|
|
4
|
-
* change this plate come from the `@media` conditions on its own copied rules
|
|
5
|
-
* plus the queries the app evaluated through `matchMedia` during capture.
|
|
6
|
-
* Thresholds partition the width axis into views; identical captures in
|
|
7
|
-
* adjacent views collapse at merge time.
|
|
8
|
-
*/
|
|
9
|
-
const FONT_SIZE_PX = 16;
|
|
10
|
-
/** A breakpoint is the first integer px width of the view ABOVE the boundary. */
|
|
11
|
-
function thresholdsOf(condition) {
|
|
12
|
-
const out = [];
|
|
13
|
-
const toPx = (value, unit) => Number.parseFloat(value) * (unit === "px" ? 1 : FONT_SIZE_PX);
|
|
14
|
-
const push = (raw, exclusiveBelow) => {
|
|
15
|
-
const bp = exclusiveBelow ? Number.isInteger(raw) ? raw + 1 : Math.ceil(raw) : Math.ceil(raw);
|
|
16
|
-
if (Number.isFinite(bp) && bp > 0) out.push(bp);
|
|
17
|
-
};
|
|
18
|
-
for (const m of condition.matchAll(/min-width:\s*([\d.]+)(px|em|rem)/g)) push(toPx(m[1] ?? "", m[2] ?? "px"), false);
|
|
19
|
-
for (const m of condition.matchAll(/max-width:\s*([\d.]+)(px|em|rem)/g)) push(toPx(m[1] ?? "", m[2] ?? "px"), true);
|
|
20
|
-
for (const m of condition.matchAll(/width\s*>=?\s*([\d.]+)(px|em|rem)/g)) push(toPx(m[1] ?? "", m[2] ?? "px"), false);
|
|
21
|
-
for (const m of condition.matchAll(/width\s*<=?\s*([\d.]+)(px|em|rem)/g)) push(toPx(m[1] ?? "", m[2] ?? "px"), true);
|
|
22
|
-
for (const m of condition.matchAll(/([\d.]+)(px|em|rem)\s*<=?\s*width/g)) push(toPx(m[1] ?? "", m[2] ?? "px"), false);
|
|
23
|
-
return out;
|
|
24
|
-
}
|
|
25
|
-
/** Width thresholds for this plate: its rules' media conditions + the app's matchMedia queries. */
|
|
26
|
-
function deriveBreakpoints(rules, matchMediaQueries = []) {
|
|
27
|
-
const set = /* @__PURE__ */ new Set();
|
|
28
|
-
for (const rule of rules) for (const condition of rule.media ?? []) for (const bp of thresholdsOf(condition)) set.add(bp);
|
|
29
|
-
for (const query of matchMediaQueries) for (const bp of thresholdsOf(query)) set.add(bp);
|
|
30
|
-
return [...set].toSorted((a, b) => a - b);
|
|
31
|
-
}
|
|
32
|
-
/** The view interval `[min, max)` a given viewport width falls into. */
|
|
33
|
-
function regimeFor(width, breakpoints) {
|
|
34
|
-
let min;
|
|
35
|
-
let max;
|
|
36
|
-
for (const bp of breakpoints) if (bp <= width) min = bp;
|
|
37
|
-
else {
|
|
38
|
-
max = bp;
|
|
39
|
-
break;
|
|
40
|
-
}
|
|
41
|
-
return {
|
|
42
|
-
...min === void 0 ? {} : { min },
|
|
43
|
-
...max === void 0 ? {} : { max }
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
//#endregion
|
|
47
|
-
export { regimeFor as n, deriveBreakpoints as t };
|
package/dist/plate-BuzRMPx4.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/** Marks the rendered skeleton root; carries the plate name. Also a dev capture marker. */
|
|
2
|
-
const ROOT_ATTR = "data-xr-root";
|
|
3
|
-
/**
|
|
4
|
-
* Marks a `<Content>` capture boundary in dev; carries the plate name. A parent
|
|
5
|
-
* capture treats it as a stitch — it references the child plate by name instead of
|
|
6
|
-
* re-serializing the child's subtree (ADR 0006).
|
|
7
|
-
*/
|
|
8
|
-
const BOUNDARY_ATTR = "data-xr-boundary";
|
|
9
|
-
const SPEC_LEAF = 9e6;
|
|
10
|
-
const SPEC_INLINE = 1e7;
|
|
11
|
-
const SPEC_GATE = 2e7;
|
|
12
|
-
/** Fixed renderer-owned classes every rendered node carries. */
|
|
13
|
-
const ROOT_CLASS = "xr-root";
|
|
14
|
-
const NODE_CLASS = "xr-node";
|
|
15
|
-
const LEAF_CLASS = "xr-leaf";
|
|
16
|
-
/**
|
|
17
|
-
* Renderer-owned base styles, emitted once by the outermost view (ADR 0007),
|
|
18
|
-
* before a plate's node rules so a node's captured class wins by source order.
|
|
19
|
-
* Everything visual is driven by inheritable custom properties, so consumers
|
|
20
|
-
* theme by setting `--xr-*` in `:root` or on any ancestor — and per-plate via
|
|
21
|
-
* `[data-xr-root="name"]` (the root carries its plate name). See ADR 0011 and
|
|
22
|
-
* docs/architecture.md. Class-based and unlayered — `@layer` was rejected
|
|
23
|
-
* (unlayered app CSS would beat layered skeleton rules, and it inverts
|
|
24
|
-
* `!important`).
|
|
25
|
-
*
|
|
26
|
-
* Progressive enhancement (ADR 0011): the plain bone fill + opacity pulse use
|
|
27
|
-
* only universally-supported CSS and are the floor. `light-dark()` is guarded
|
|
28
|
-
* by `@supports` because an unsupported VALUE drops the whole declaration (an
|
|
29
|
-
* invisible bone), and it is not Baseline-widely-available until 2026-11-13 —
|
|
30
|
-
* drop the guard then. Features that merely no-op when absent (`@property`,
|
|
31
|
-
* `prefers-contrast`, relative-color-as-fallback) carry no hard guard.
|
|
32
|
-
*
|
|
33
|
-
* Tokens: `--xr-bone` (fill), `--xr-bone-highlight` (sheen), `--xr-radius`
|
|
34
|
-
* (+ `--xr-radius-text` / `--xr-radius-media`), `--xr-duration`, the pulse range
|
|
35
|
-
* `--xr-pulse-min`/`--xr-pulse-max`, `--xr-anim` (mode: `xr-pulse` | `none` | a
|
|
36
|
-
* custom keyframe), and `--xr-fill` (override the leaf paint, e.g. a sheen
|
|
37
|
-
* gradient built from the two bone tokens).
|
|
38
|
-
*/
|
|
39
|
-
const BASE_CSS = `
|
|
40
|
-
/* One timeline per skeleton, not per leaf: the root animates one inherited
|
|
41
|
-
opacity property and every leaf reads it. Hundreds of independent infinite
|
|
42
|
-
opacity animations would pin the compositor and cook the CPU. */
|
|
43
|
-
@property --xr-o { syntax: "<number>"; inherits: true; initial-value: 1 }
|
|
44
|
-
.${ROOT_CLASS} {
|
|
45
|
-
--xr-bone-highlight: #f4f4f5;
|
|
46
|
-
display: contents;
|
|
47
|
-
animation: var(--xr-anim, xr-pulse) var(--xr-duration, 1.2s) ease-in-out infinite alternate;
|
|
48
|
-
}
|
|
49
|
-
/* No pointer-events:none — it blocks inspecting bones in devtools and buys
|
|
50
|
-
nothing (no real content to click). border-color: a node may capture
|
|
51
|
-
border-width for layout; its paint stays out. list-style: suppress a ::marker
|
|
52
|
-
a captured display:list-item would otherwise paint. */
|
|
53
|
-
.${NODE_CLASS} { border-color: transparent; list-style: none }
|
|
54
|
-
.${LEAF_CLASS} {
|
|
55
|
-
background: var(--xr-fill, var(--xr-bone, #e4e4e7));
|
|
56
|
-
border-radius: var(--xr-radius, 4px);
|
|
57
|
-
opacity: var(--xr-o);
|
|
58
|
-
}
|
|
59
|
-
/* Per-kind defaults: text reads as rounded bars, media as blocks. Re-theme any
|
|
60
|
-
kind via [data-xr-root] .${LEAF_CLASS}-text { ... }. */
|
|
61
|
-
.${LEAF_CLASS}-text { border-radius: var(--xr-radius-text, 999px) }
|
|
62
|
-
.${LEAF_CLASS}-media { border-radius: var(--xr-radius-media, var(--xr-radius, 4px)) }
|
|
63
|
-
@keyframes xr-pulse {
|
|
64
|
-
from { --xr-o: var(--xr-pulse-max, 1) }
|
|
65
|
-
to { --xr-o: var(--xr-pulse-min, 0.5) }
|
|
66
|
-
}
|
|
67
|
-
@media (prefers-reduced-motion: reduce) { .${ROOT_CLASS} { animation: none } }
|
|
68
|
-
/* Dark + high-contrast bone defaults. Guarded: light-dark() is an invalid value
|
|
69
|
-
without support and would drop the declaration — see the note above; remove
|
|
70
|
-
this @supports once widely available (2026-11-13). */
|
|
71
|
-
@supports (color: light-dark(#000, #fff)) {
|
|
72
|
-
.${ROOT_CLASS} { --xr-bone-highlight: light-dark(#f4f4f5, #52525b) }
|
|
73
|
-
.${LEAF_CLASS} { background: var(--xr-fill, var(--xr-bone, light-dark(#e4e4e7, #3f3f46))) }
|
|
74
|
-
@media (prefers-contrast: more) {
|
|
75
|
-
.${LEAF_CLASS} { background: var(--xr-fill, var(--xr-bone, light-dark(#d4d4d8, #52525b))) }
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
/* Derive the sheen highlight from --xr-bone so re-theming one token updates both.
|
|
79
|
-
Relative color isn't widely available yet; the static highlight above is the
|
|
80
|
-
fallback (sheen just looks flatter), so no hard guard is needed. */
|
|
81
|
-
@supports (color: oklch(from red l c h)) {
|
|
82
|
-
.${ROOT_CLASS} { --xr-bone-highlight: oklch(from var(--xr-bone, #e4e4e7) calc(l + 0.08) c h) }
|
|
83
|
-
}`.trim();
|
|
84
|
-
/** The (unserialized) plate an import resolves to before anything has been captured. */
|
|
85
|
-
function emptyPlate(name) {
|
|
86
|
-
return {
|
|
87
|
-
v: 1,
|
|
88
|
-
name,
|
|
89
|
-
tree: null,
|
|
90
|
-
css: ""
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
//#endregion
|
|
94
|
-
export { ROOT_ATTR as a, SPEC_INLINE as c, NODE_CLASS as i, SPEC_LEAF as l, BOUNDARY_ATTR as n, ROOT_CLASS as o, LEAF_CLASS as r, SPEC_GATE as s, BASE_CSS as t, emptyPlate as u };
|