@hueest/xray 0.1.0 → 0.3.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/client.d.ts +64 -16
- package/dist/client.js +903 -72
- package/dist/core.d.ts +21 -27
- package/dist/core.js +39 -210
- package/dist/css-escape-N7bOusGW.js +52 -0
- package/dist/hud.js +46 -0
- package/dist/index.d.ts +297 -48
- package/dist/index.js +674 -353
- package/dist/plate-BIr62u7l.d.ts +363 -0
- package/dist/plate-BRR6d8Se.js +156 -0
- package/dist/project-DORoPAo7.js +10224 -0
- package/dist/react.core-BUj8ziwh.d.ts +79 -0
- package/dist/react.core-DMIhXHZF.js +272 -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-Cs7hUxmK.d.ts +107 -0
- package/package.json +8 -7
- package/virtual.d.ts +6 -0
- package/dist/breakpoints-CBNlh7lQ.js +0 -47
- package/dist/plate-BuzRMPx4.js +0 -94
- package/dist/plate-DboxMKg-.d.ts +0 -196
- package/dist/react.core-CqnDjfAJ.js +0 -59
- package/dist/react.core-fgWG_svU.d.ts +0 -21
- package/dist/serialize-Bq6yJnNV.js +0 -1071
- package/dist/serialize.d.ts +0 -88
- package/dist/serialize.js +0 -2
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { r as Plate } from "./plate-BIr62u7l.js";
|
|
2
|
+
import { a as XrayCaptureWalker } from "./serialize-Cs7hUxmK.js";
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
//#region src/react.core.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* The additive render-prop children form (ADR 0014). Called only at
|
|
8
|
+
* `loading=false` (null included — `loading` stays the sole readiness axis, ADR
|
|
9
|
+
* 0002), it hands back the typed `data` verbatim plus a `ref` for the manual
|
|
10
|
+
* capture root.
|
|
11
|
+
*
|
|
12
|
+
* `ref` is a CALLBACK ref by contract: in `root="manual"` (dev) it imperatively
|
|
13
|
+
* writes the `data-xr-boundary` marker onto the consumer's own element and
|
|
14
|
+
* registers it as the capture root, so it must run on the real DOM node. In
|
|
15
|
+
* `root="auto"` and in production it is a no-op (omitted), and the existing
|
|
16
|
+
* `display:contents` boundary wrapper does the marking instead.
|
|
17
|
+
*
|
|
18
|
+
* It is typed as a callback ref over the base `Element` rather than a `Ref<T>`.
|
|
19
|
+
* A callback that accepts `Element | null` is, by parameter contravariance,
|
|
20
|
+
* assignable to a more specific `ref` slot (`<section ref={ref}>` wants
|
|
21
|
+
* `(el: HTMLElement | null) => void`), so the consumer spreads it onto any host
|
|
22
|
+
* element without a cast. A `RefObject` would NOT compose this way and is not
|
|
23
|
+
* offered: writing the boundary attribute needs the imperative callback anyway.
|
|
24
|
+
*/
|
|
25
|
+
type SkeletonRef = (el: Element | null) => void;
|
|
26
|
+
type SkeletonRenderProp<T> = (ctx: {
|
|
27
|
+
data: T;
|
|
28
|
+
ref?: SkeletonRef;
|
|
29
|
+
}) => ReactNode;
|
|
30
|
+
/** Fields shared by both `root` variants of `<Skeleton>`. */
|
|
31
|
+
interface SkeletonBase {
|
|
32
|
+
/** The plate to render while loading, imported from `virtual:xray/plates/<name>`. */
|
|
33
|
+
plate: Plate;
|
|
34
|
+
/** Explicit readiness mode: render the Skeleton while true, then render children and capture them in dev when false. */
|
|
35
|
+
loading?: boolean;
|
|
36
|
+
/** Suspense mode: make `<Skeleton>` the Suspense boundary and use the Plate as the fallback until children resolve. */
|
|
37
|
+
suspense?: boolean;
|
|
38
|
+
/** Shown while loading when the plate has no capture yet. */
|
|
39
|
+
fallback?: ReactNode;
|
|
40
|
+
/** Override the plugin's settle delay before this site is captured (dev only). */
|
|
41
|
+
delay?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Programmable capture walker for this Skeleton's subtree (ADR 0018, dev only).
|
|
44
|
+
* The escape hatch beyond the declarative `data-xr-*` annotations: a closure
|
|
45
|
+
* that decides, per element, whether to ignore it, collapse it to one Bone, or
|
|
46
|
+
* defer to the built-in walk. Threaded through the dev adapter into
|
|
47
|
+
* `captureView`; PRODUCTION ignores it entirely (it never captures). On both
|
|
48
|
+
* `root` variants of the union, so a manual root may carry one too.
|
|
49
|
+
*/
|
|
50
|
+
captureWalker?: XrayCaptureWalker;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Props accepted by the React `<Skeleton>` component, generic over the optional
|
|
54
|
+
* render-prop `data` payload `T` (ADR 0014). The generic defaults to `undefined`
|
|
55
|
+
* so existing call sites — `<Skeleton plate loading />`, plain children — keep
|
|
56
|
+
* working with zero type arguments; passing `data` infers `T` into the render
|
|
57
|
+
* prop. `data` is a pure pass-through (the consumer owns nullability), never
|
|
58
|
+
* `NonNullable<T>`.
|
|
59
|
+
*
|
|
60
|
+
* The two members discriminate on `root`:
|
|
61
|
+
* - `root="auto"` (default): the existing `display:contents` boundary wrapper
|
|
62
|
+
* marks the capture seam (ADR 0006); children may be plain `ReactNode` OR a
|
|
63
|
+
* render prop, and the surfaced `ref` is a no-op.
|
|
64
|
+
* - `root="manual"`: no wrapper; the consumer owns the capture root element. This
|
|
65
|
+
* REQUIRES render-prop children (the only way to hand back the `ref` that
|
|
66
|
+
* writes the boundary marker), so plain children + `root="manual"` is a type
|
|
67
|
+
* error.
|
|
68
|
+
*/
|
|
69
|
+
type SkeletonProps<T = undefined> = SkeletonBase & ({
|
|
70
|
+
/** Auto capture root (default): xray inserts a `display:contents` boundary wrapper. */root?: 'auto'; /** Render-prop payload, passed back to the children function verbatim as `T`. */
|
|
71
|
+
data?: T; /** Real content. In dev, these children are also the capture source once ready. Either plain `ReactNode` or a render prop. */
|
|
72
|
+
children?: ReactNode | SkeletonRenderProp<T>;
|
|
73
|
+
} | {
|
|
74
|
+
/** Manual capture root: the consumer's element owns the boundary marker; no wrapper (ADR 0014). */root: 'manual'; /** Render-prop payload, passed back to the children function verbatim as `T`. */
|
|
75
|
+
data?: T; /** Render prop required: spread the supplied `ref` onto your root element so capture does not leak into the parent plate. */
|
|
76
|
+
children: SkeletonRenderProp<T>;
|
|
77
|
+
});
|
|
78
|
+
//#endregion
|
|
79
|
+
export { SkeletonProps as t };
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { a as ROOT_ATTR, o as ROOT_CLASS, s as ROOT_INSTANT_ATTR, t as BASE_CSS } from "./plate-BRR6d8Se.js";
|
|
2
|
+
import { t as escapeStyleText } from "./css-escape-N7bOusGW.js";
|
|
3
|
+
import { Fragment, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/react.core.tsx
|
|
6
|
+
/**
|
|
7
|
+
* The live-stitch registry (ADR 0020): a module-level ref-counted set of stitch
|
|
8
|
+
* names that are CURRENTLY rendered as a stitch inside a still-SHOWING parent
|
|
9
|
+
* skeleton. It exists to kill the "stitch flash": when a parent skeleton resolves
|
|
10
|
+
* and its real content mounts a descendant's OWN standalone `<Skeleton>` for the
|
|
11
|
+
* same plate, that standalone is a NEW outermost skeleton — it would re-pay the
|
|
12
|
+
* full `--xr-skeleton-delay` (ADR 0016) from zero and visually disappear for
|
|
13
|
+
* ~250ms even though the child was continuously visible as a stitch. A standalone
|
|
14
|
+
* skeleton replacing such a live stitch must reveal INSTANTLY (no delay, no fade),
|
|
15
|
+
* matching how the stitch inherited its parent's already-revealed channel.
|
|
16
|
+
*
|
|
17
|
+
* Why a loading-keyed registry and not wall-clock time: the parent marks its
|
|
18
|
+
* direct stitch names live in an effect WHILE it shows its skeleton (an earlier
|
|
19
|
+
* commit). When the parent resolves, React renders the content tree — the child's
|
|
20
|
+
* standalone Skeleton first-renders and reads `isStitchLive(child)` BEFORE the
|
|
21
|
+
* parent's unmark cleanup runs (render happens before that commit's effects), so
|
|
22
|
+
* the read deterministically sees `child` still live. The unmark then clears it.
|
|
23
|
+
* "Recently" is defined by React's render-before-effect ordering, not a timer, so
|
|
24
|
+
* it is race-free AND does not over-apply to independent nested skeletons (a
|
|
25
|
+
* tab-hidden skeleton was never marked live by a currently-showing parent → not
|
|
26
|
+
* live → normal delay). It is portal-correct for free: only genuine stitches
|
|
27
|
+
* (names in a parent plate's stitch set) are ever registered, so a portaled child
|
|
28
|
+
* is never a stitch, never live, and reveals on the normal delay (ADR 0020).
|
|
29
|
+
*
|
|
30
|
+
* This ships to PROD: the flash is a production issue too, and the cost is a Map
|
|
31
|
+
* plus a register effect and a read-once `useState` per `<Skeleton>` — a few
|
|
32
|
+
* bytes, consistent with the prod-hooks relaxation ADR 0016 opened.
|
|
33
|
+
*/
|
|
34
|
+
const liveStitches = /* @__PURE__ */ new Map();
|
|
35
|
+
/**
|
|
36
|
+
* Mark a parent skeleton's direct stitch names live (ref-counted, so concurrent
|
|
37
|
+
* showing parents that stitch the same child stack), returning a disposer that
|
|
38
|
+
* decrements each. The adapter calls this in an effect while `showSkeleton` is
|
|
39
|
+
* true and runs the disposer when it stops showing (resolve/unmount). Several
|
|
40
|
+
* marks of the same name increment the count; the count drops to 0 (entry
|
|
41
|
+
* removed) only when every showing parent has unmarked it (ADR 0020).
|
|
42
|
+
*/
|
|
43
|
+
function markStitchesLive(names) {
|
|
44
|
+
for (const name of names) liveStitches.set(name, (liveStitches.get(name) ?? 0) + 1);
|
|
45
|
+
return () => {
|
|
46
|
+
queueMicrotask(() => {
|
|
47
|
+
for (const name of names) {
|
|
48
|
+
const next = (liveStitches.get(name) ?? 0) - 1;
|
|
49
|
+
if (next <= 0) liveStitches.delete(name);
|
|
50
|
+
else liveStitches.set(name, next);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** True while `name` is rendered as a stitch inside a still-showing parent
|
|
56
|
+
* skeleton — i.e. a standalone `<Skeleton>` for it is a continuation, not a fresh
|
|
57
|
+
* skeleton, and must reveal instantly (ADR 0020). */
|
|
58
|
+
function isStitchLive(name) {
|
|
59
|
+
return (liveStitches.get(name) ?? 0) > 0;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The direct stitch names a plate's chunks reference (ADR 0020): a `{ r: name }`
|
|
63
|
+
* chunk IS a stitch; a `{ c, k }` chunk is a stitch-bearing element kept real, so
|
|
64
|
+
* recurse its `k`. Walks the SHIPPED `chunks` (the render contract), not
|
|
65
|
+
* `plate.refs` (empty in dev, where the live store resolves stitches) nor
|
|
66
|
+
* `collectRefs` (which walks the build-time RenderNode tree, not the shipped
|
|
67
|
+
* chunks). One level of direct stitch names per Skeleton is enough: each level
|
|
68
|
+
* self-registers as it becomes a real `<Skeleton>` (the cascade), so arbitrary
|
|
69
|
+
* nesting is covered without recursing into child plates here.
|
|
70
|
+
*/
|
|
71
|
+
function collectStitchNames(chunks) {
|
|
72
|
+
if (!chunks) return [];
|
|
73
|
+
const names = [];
|
|
74
|
+
const walk = (cs) => {
|
|
75
|
+
for (const chunk of cs) {
|
|
76
|
+
if (typeof chunk === "string") continue;
|
|
77
|
+
if ("t" in chunk) continue;
|
|
78
|
+
if ("r" in chunk) names.push(chunk.r);
|
|
79
|
+
else walk(chunk.k);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
walk(chunks);
|
|
83
|
+
return names;
|
|
84
|
+
}
|
|
85
|
+
/** Narrow unknown children to the render-prop form. */
|
|
86
|
+
function isRenderProp(children) {
|
|
87
|
+
return typeof children === "function";
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Resolve `<Skeleton>` content at the ready (`loading=false`) path: invoke the
|
|
91
|
+
* render prop with the verbatim `data` and the adapter's `ref` (a no-op in prod
|
|
92
|
+
* and in `root="auto"`, the boundary-writing callback in dev `root="manual"`), or
|
|
93
|
+
* pass plain children through untouched. Centralizes the single `data as T` step:
|
|
94
|
+
* `data` is declared `data?: T`, so it reads back as `T | undefined`, but it is
|
|
95
|
+
* the exact value the consumer passed for the render prop's `T` (ADR 0014 keeps it
|
|
96
|
+
* a pass-through, not `NonNullable<T>`).
|
|
97
|
+
*/
|
|
98
|
+
function resolveContent(children, data, ref) {
|
|
99
|
+
if (!isRenderProp(children)) return children;
|
|
100
|
+
return children({
|
|
101
|
+
data,
|
|
102
|
+
ref
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The token defaults that mirror the BASE_CSS `var(--xr-skeleton-*, …)` fallbacks
|
|
107
|
+
* (ADR 0016). The hook reads the COMPUTED token off the boundary element, but a
|
|
108
|
+
* consumer who never sets it leaves the computed value empty (the tokens are not
|
|
109
|
+
* declared on `.xr-root`, only referenced with these inline fallbacks). So when a
|
|
110
|
+
* read comes back empty/unparseable the hook falls back to these same numbers,
|
|
111
|
+
* keeping the JS hold and the CSS reveal in lockstep on identical defaults.
|
|
112
|
+
*/
|
|
113
|
+
const DEFAULT_SKELETON_DELAY_MS = 250;
|
|
114
|
+
const DEFAULT_SKELETON_MIN_DURATION_MS = 600;
|
|
115
|
+
/**
|
|
116
|
+
* Parse a CSS time token (`getPropertyValue` returns a trimmed string like
|
|
117
|
+
* `"250ms"`, `"0.6s"`, or `""`) to milliseconds, falling back to `fallback` when
|
|
118
|
+
* empty or unparseable. Supports `ms` and `s`; a bare number is treated as ms.
|
|
119
|
+
*/
|
|
120
|
+
function parseMs(value, fallback) {
|
|
121
|
+
const text = value.trim();
|
|
122
|
+
if (text === "") return fallback;
|
|
123
|
+
const match = /^(-?[\d.]+)(ms|s)?$/.exec(text);
|
|
124
|
+
if (!match?.[1]) return fallback;
|
|
125
|
+
const n = Number.parseFloat(match[1]);
|
|
126
|
+
if (!Number.isFinite(n)) return fallback;
|
|
127
|
+
return match[2] === "s" ? n * 1e3 : n;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The min-duration hold (ADR 0016), shared by the production (`react.tsx`) and dev
|
|
131
|
+
* (`react.dev.tsx`) adapters so both keep identical timing. Returns whether the
|
|
132
|
+
* skeleton should render RIGHT NOW.
|
|
133
|
+
*
|
|
134
|
+
* The reveal DELAY is pure CSS (BASE_CSS `--xr-reveal`, zero JS); this hook is
|
|
135
|
+
* ONLY the part CSS cannot express: keeping the skeleton mounted past
|
|
136
|
+
* `loading=false` for a minimum visible window. The hook returns `loading ||
|
|
137
|
+
* holding`: the `loading` term is SYNCHRONOUS, so the skeleton shows on the very
|
|
138
|
+
* render `loading` flips true (no content frame at a false→true restart, and no
|
|
139
|
+
* stale-state lag), and `holding` is state used SOLELY for the post-ready hold.
|
|
140
|
+
*
|
|
141
|
+
* While loading, an effect anchors `t0` (loading start) and pre-arms `holding`
|
|
142
|
+
* true, so that pre-armed value carries into the first `loading=false` render with
|
|
143
|
+
* no content flash at the true→false transition. When `loading` flips false the
|
|
144
|
+
* effect reads the COMPUTED `--xr-skeleton-delay` + `--xr-skeleton-min-duration`
|
|
145
|
+
* off `ref` and holds until `t0 + delay + min-duration`. Two cases skip the hold
|
|
146
|
+
* entirely (swap synchronously, no timer): min-duration resolves to 0, or `ref` is
|
|
147
|
+
* null / no `t0` was recorded — meaning no skeleton root was ever shown (e.g. a
|
|
148
|
+
* plate with no capture, just a fallback), so there is nothing to hold and a hold
|
|
149
|
+
* would only keep blank/fallback UI on screen after data is ready.
|
|
150
|
+
*
|
|
151
|
+
* Reading computed custom props off the skeleton root (open Q4): the root carries
|
|
152
|
+
* `display:contents`, but custom-property inheritance and resolution are
|
|
153
|
+
* unaffected by `display` — `getComputedStyle(el).getPropertyValue('--xr-…')`
|
|
154
|
+
* returns the inherited token value on a `display:contents` element just as on any
|
|
155
|
+
* other. The tokens are not declared on `.xr-root` (only referenced with inline
|
|
156
|
+
* fallbacks), so a consumer who never themes them yields an empty read; `parseMs`
|
|
157
|
+
* then applies the same defaults the CSS uses, so the hold matches the reveal.
|
|
158
|
+
*/
|
|
159
|
+
function useSkeletonTiming(loading, ref) {
|
|
160
|
+
const [holding, setHolding] = useState(loading);
|
|
161
|
+
const startRef = useRef(null);
|
|
162
|
+
const timerRef = useRef(void 0);
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
const clear = () => {
|
|
165
|
+
if (timerRef.current !== void 0) {
|
|
166
|
+
clearTimeout(timerRef.current);
|
|
167
|
+
timerRef.current = void 0;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
if (loading) {
|
|
171
|
+
clear();
|
|
172
|
+
if (startRef.current === null) startRef.current = Date.now();
|
|
173
|
+
setHolding(true);
|
|
174
|
+
return clear;
|
|
175
|
+
}
|
|
176
|
+
const start = startRef.current;
|
|
177
|
+
startRef.current = null;
|
|
178
|
+
const el = ref.current;
|
|
179
|
+
if (el === null || start === null) {
|
|
180
|
+
clear();
|
|
181
|
+
setHolding(false);
|
|
182
|
+
return clear;
|
|
183
|
+
}
|
|
184
|
+
const style = getComputedStyle(el);
|
|
185
|
+
const delay = parseMs(style.getPropertyValue("--xr-skeleton-delay"), DEFAULT_SKELETON_DELAY_MS);
|
|
186
|
+
const minDuration = parseMs(style.getPropertyValue("--xr-skeleton-min-duration"), DEFAULT_SKELETON_MIN_DURATION_MS);
|
|
187
|
+
if (minDuration <= 0) {
|
|
188
|
+
clear();
|
|
189
|
+
setHolding(false);
|
|
190
|
+
return clear;
|
|
191
|
+
}
|
|
192
|
+
const remaining = start + delay + minDuration - Date.now();
|
|
193
|
+
if (remaining <= 0) {
|
|
194
|
+
clear();
|
|
195
|
+
setHolding(false);
|
|
196
|
+
return clear;
|
|
197
|
+
}
|
|
198
|
+
clear();
|
|
199
|
+
timerRef.current = setTimeout(() => {
|
|
200
|
+
timerRef.current = void 0;
|
|
201
|
+
setHolding(false);
|
|
202
|
+
}, remaining);
|
|
203
|
+
return clear;
|
|
204
|
+
}, [loading, ref]);
|
|
205
|
+
return loading || holding;
|
|
206
|
+
}
|
|
207
|
+
/** Layout-neutral wrapper for an HTML island sitting beside React stitch nodes —
|
|
208
|
+
* its children participate in the grandparent's flow, so grid/flex item counts
|
|
209
|
+
* are unchanged. */
|
|
210
|
+
const DISPLAY_CONTENTS = { display: "contents" };
|
|
211
|
+
/**
|
|
212
|
+
* Render a shipped plate's skeleton. `resolveStitch` turns a child plate name
|
|
213
|
+
* into the node that mounts it (static `refs` in prod, a store-subscribed
|
|
214
|
+
* component in dev). `nested` is true for a stitch-mounted child, which skips
|
|
215
|
+
* BASE_CSS — the outermost view already emitted it (ADR 0007). Returns null
|
|
216
|
+
* when the plate has no capture yet; the caller renders the fallback.
|
|
217
|
+
*
|
|
218
|
+
* `instant` (ADR 0020) marks this root a stitch continuation: a standalone
|
|
219
|
+
* `<Skeleton>` replacing a stitch a still-showing parent was just rendering. It
|
|
220
|
+
* lands as `data-xr-instant` on the OUTERMOST root only (`instant && !nested`) so
|
|
221
|
+
* BASE_CSS reveals it without the `--xr-skeleton-delay` fade.
|
|
222
|
+
*/
|
|
223
|
+
function renderPlate(plate, resolveStitch, nested, rootRef, instant) {
|
|
224
|
+
if (!plate.chunks) return null;
|
|
225
|
+
const className = plate.rootCls ? `${ROOT_CLASS} ${plate.rootCls}` : ROOT_CLASS;
|
|
226
|
+
const rootAttrs = {
|
|
227
|
+
[ROOT_ATTR]: plate.name,
|
|
228
|
+
className,
|
|
229
|
+
"aria-hidden": true,
|
|
230
|
+
"aria-busy": true,
|
|
231
|
+
...rootRef ? { ref: rootRef } : {},
|
|
232
|
+
...instant && !nested ? { [ROOT_INSTANT_ATTR]: "" } : {}
|
|
233
|
+
};
|
|
234
|
+
const css = nested ? plate.css : `${BASE_CSS}\n${plate.css}`;
|
|
235
|
+
const chunks = plate.chunks;
|
|
236
|
+
if (chunks.length === 1 && typeof chunks[0] === "string") {
|
|
237
|
+
const html = `<style>${escapeStyleText(css)}</style>${chunks[0]}`;
|
|
238
|
+
return /* @__PURE__ */ jsx("div", {
|
|
239
|
+
...rootAttrs,
|
|
240
|
+
dangerouslySetInnerHTML: { __html: html }
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
244
|
+
...rootAttrs,
|
|
245
|
+
children: [/* @__PURE__ */ jsx("style", { children: css }), renderChunks(chunks, resolveStitch)]
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Each `string` chunk is injected as a display:contents innerHTML island; each
|
|
250
|
+
* `{ r }` stitch resolves through `resolveStitch`; a `{ c, k }` node stays a
|
|
251
|
+
* real element and recurses (it holds a stitch deeper). No per-node tree walk —
|
|
252
|
+
* the strings are already serialized.
|
|
253
|
+
*/
|
|
254
|
+
function renderChunks(chunks, resolveStitch) {
|
|
255
|
+
return chunks.map((chunk, i) => {
|
|
256
|
+
if (typeof chunk === "string") return /* @__PURE__ */ jsx("div", {
|
|
257
|
+
style: DISPLAY_CONTENTS,
|
|
258
|
+
dangerouslySetInnerHTML: { __html: chunk }
|
|
259
|
+
}, i);
|
|
260
|
+
if ("t" in chunk) return /* @__PURE__ */ jsx("div", {
|
|
261
|
+
style: DISPLAY_CONTENTS,
|
|
262
|
+
dangerouslySetInnerHTML: { __html: chunk.t.repeat(chunk.n) }
|
|
263
|
+
}, i);
|
|
264
|
+
if ("r" in chunk) return /* @__PURE__ */ jsx(Fragment, { children: resolveStitch(chunk.r) }, i);
|
|
265
|
+
return /* @__PURE__ */ jsx("div", {
|
|
266
|
+
className: chunk.c,
|
|
267
|
+
children: renderChunks(chunk.k, resolveStitch)
|
|
268
|
+
}, i);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
//#endregion
|
|
272
|
+
export { renderPlate as a, markStitchesLive as i, isRenderProp as n, resolveContent as o, isStitchLive as r, useSkeletonTiming as s, collectStitchNames as t };
|
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-BUj8ziwh.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-BUj8ziwh.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-BRR6d8Se.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-DMIhXHZF.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-DMIhXHZF.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 };
|