@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/react.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as SkeletonProps } from "./react.core-fgWG_svU.js";
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 };
@@ -1,4 +1,4 @@
1
- import { t as SkeletonProps } from "./react.core-fgWG_svU.js";
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-BuzRMPx4.js";
2
- import { t as renderPlate } from "./react.core-CqnDjfAJ.js";
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({ plate, loading = false, suspense = false, fallback, delay, children }) {
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: useLivePlate(plate),
33
- fallback
103
+ plate: livePlate,
104
+ fallback,
105
+ rootRef,
106
+ instant
34
107
  });
35
108
  if (lightBoxOn) return skeleton;
36
- if (suspense) return /* @__PURE__ */ jsx(Suspense, {
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
- if (globalThis.__XRAY__ === void 0) return /* @__PURE__ */ jsx(Fragment$1, { children: props.children });
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
- function CaptureBoundary({ plate, delay, children }) {
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, { delay });
61
- }, [plate.name, delay]);
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 { t as renderPlate } from "./react.core-CqnDjfAJ.js";
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({ plate, loading = false, suspense = false, fallback, children }) {
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 (loading) return skeleton;
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
- function renderStatic(plate, nested) {
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.1.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
- "./serialize": {
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 };
@@ -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 };