@hueest/xray 0.6.0 → 0.7.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.
@@ -1,18 +1,18 @@
1
1
  /** Marks the rendered skeleton root; carries the plate name. Also a dev capture marker. */
2
2
  const ROOT_ATTR = "data-xr-root";
3
3
  /**
4
- * Marks an OUTERMOST skeleton root that is a stitch continuation (ADR 0020): a
5
- * standalone `<Skeleton>` replacing a stitch a still-showing parent skeleton was
6
- * just rendering. BASE_CSS reveals such a root instantly (no `--xr-skeleton-delay`,
7
- * no fade) so the bones do not flash away and back. Never present on a nested
8
- * stitch (which inherits the parent's reveal and has no `@starting-style` of its
9
- * own) nor on an ordinary top-level skeleton (which keeps the normal delay).
4
+ * Marks an OUTERMOST skeleton root that is a stitch continuation (ADR 0020): a standalone
5
+ * `<Skeleton>` replacing a stitch a still-showing parent skeleton was just rendering. BASE_CSS
6
+ * reveals such a root instantly (no `--xr-skeleton-delay`, no fade) so the bones do not flash away
7
+ * and back. Never present on a nested stitch (which inherits the parent's reveal and has no
8
+ * `@starting-style` of its own) nor on an ordinary top-level skeleton (which keeps the normal
9
+ * delay).
10
10
  */
11
11
  const ROOT_INSTANT_ATTR = "data-xr-instant";
12
12
  /**
13
- * Marks a `<Content>` capture boundary in dev; carries the plate name. A parent
14
- * capture treats it as a stitch — it references the child plate by name instead of
15
- * re-serializing the child's subtree (ADR 0006).
13
+ * Marks a `<Content>` capture boundary in dev; carries the plate name. A parent capture treats it
14
+ * as a stitch — it references the child plate by name instead of re-serializing the child's subtree
15
+ * (ADR 0006).
16
16
  */
17
17
  const BOUNDARY_ATTR = "data-xr-boundary";
18
18
  const SPEC_LEAF = 9e6;
@@ -23,46 +23,37 @@ const ROOT_CLASS = "xr-root";
23
23
  const NODE_CLASS = "xr-node";
24
24
  const LEAF_CLASS = "xr-leaf";
25
25
  /**
26
- * Renderer-owned base styles, emitted once by the outermost view (ADR 0007),
27
- * before a plate's node rules so a node's captured class wins by source order.
28
- * Everything visual is driven by inheritable custom properties, so consumers
29
- * theme by setting `--xr-*` in `:root` or on any ancestor and per-plate via
30
- * `[data-xr-root="name"]` (the root carries its plate name). See ADR 0011 and
31
- * docs/architecture.md. Class-based and unlayered `@layer` was rejected
32
- * (unlayered app CSS would beat layered skeleton rules, and it inverts
33
- * `!important`).
26
+ * Renderer-owned base styles, emitted once by the outermost view (ADR 0007), before a plate's node
27
+ * rules so a node's captured class wins by source order. Everything visual is driven by inheritable
28
+ * custom properties, so consumers theme by setting `--xr-*` in `:root` or on any ancestor — and
29
+ * per-plate via `[data-xr-root="name"]` (the root carries its plate name). See ADR 0011 and
30
+ * docs/architecture.md. Class-based and unlayered `@layer` was rejected (unlayered app CSS would
31
+ * beat layered skeleton rules, and it inverts `!important`).
34
32
  *
35
- * Progressive enhancement (ADR 0011): the plain bone fill + opacity pulse use
36
- * only universally-supported CSS and are the floor. `light-dark()` is guarded
37
- * by `@supports` because an unsupported VALUE drops the whole declaration (an
38
- * invisible bone), and it is not Baseline-widely-available until 2026-11-13 —
39
- * drop the guard then. Features that merely no-op when absent (`@property`,
40
- * `prefers-contrast`, relative-color-as-fallback) carry no hard guard.
33
+ * Progressive enhancement (ADR 0011): the plain bone fill + opacity pulse use only
34
+ * universally-supported CSS and are the floor. `light-dark()` is guarded by `@supports` because an
35
+ * unsupported VALUE drops the whole declaration (an invisible bone), and it is not
36
+ * Baseline-widely-available until 2026-11-13 — drop the guard then. Features that merely no-op when
37
+ * absent (`@property`, `prefers-contrast`, relative-color-as-fallback) carry no hard guard.
41
38
  *
42
- * Tokens (ADR 0017 taxonomy). The `--xr-bone-*` family styles an individual
43
- * Bone: `--xr-bone-color` (fill), `--xr-bone-highlight-color` (sheen),
44
- * `--xr-bone-border-radius` (+ `--xr-bone-text-border-radius` /
45
- * `--xr-bone-text-block-border-radius` / `--xr-bone-media-border-radius`),
46
- * `--xr-bone-animation-duration`, the pulse
47
- * range `--xr-bone-pulse-opacity-min`/`--xr-bone-pulse-opacity-max`,
48
- * `--xr-bone-animation` (mode: `xr-pulse` | `none` | a custom keyframe), and
49
- * `--xr-bone-fill` (override the leaf paint, e.g. a sheen gradient built from the
50
- * two bone color tokens). The `--xr-skeleton-*` family governs whole-skeleton
51
- * display timing (ADR 0016): `--xr-skeleton-delay` (grace before the skeleton
52
- * becomes visible), `--xr-skeleton-min-duration` (minimum visible time once
53
- * shown; read by the `useSkeletonTiming` hook, not used in CSS), and
54
- * `--xr-skeleton-transition-duration` (the reveal/swap fade). The private
55
- * channels `--xr-o` (pulse) and `--xr-reveal` (reveal) stay undocumented
56
- * `@property` plumbing, not a theming surface.
39
+ * Tokens (ADR 0017 taxonomy). The `--xr-bone-*` family styles an individual Bone: `--xr-bone-color`
40
+ * (fill), `--xr-bone-highlight-color` (sheen), `--xr-bone-border-radius` (+
41
+ * `--xr-bone-text-line-border-radius` / `--xr-bone-text-block-border-radius` /
42
+ * `--xr-bone-media-border-radius`), `--xr-bone-animation-duration`, the pulse range
43
+ * `--xr-bone-pulse-opacity-min`/`--xr-bone-pulse-opacity-max`, `--xr-bone-animation` (mode:
44
+ * `xr-pulse` | `none` | a custom keyframe), and `--xr-bone-fill` (override the leaf paint, e.g. a
45
+ * sheen gradient built from the two bone color tokens). The `--xr-skeleton-*` family governs
46
+ * whole-skeleton display timing (ADR 0016): `--xr-skeleton-delay` (grace before the skeleton
47
+ * becomes visible), `--xr-skeleton-min-duration` (minimum visible time once shown; read by the
48
+ * `useSkeletonTiming` hook, not used in CSS), and `--xr-skeleton-transition-duration` (the
49
+ * reveal/swap fade). The private channels `--xr-o` (pulse) and `--xr-reveal` (reveal) stay
50
+ * undocumented `@property` plumbing, not a theming surface.
57
51
  *
58
- * Declared as a PLAIN template literal — no `.trim()` (or any other call) on
59
- * the initializer. A method call is not provably pure to a conservative
60
- * tree-shaker (rolldown/Rollup), and it single-handedly pinned this ~5 KB
61
- * string into every consumer chunk importing the React adapter even after the
62
- * adapter stopped referencing it (plan 005 round 2 finding). The template
63
- * therefore starts immediately after the backtick (the old leading newline
64
- * `.trim()` used to strip simply isn't there), keeping the string value
65
- * byte-identical to the trimmed original.
52
+ * Declared as a PLAIN template literal — no `.trim()` (or any other call) on the initializer. A
53
+ * method call is not provably pure to a conservative tree-shaker (rolldown/Rollup) and would pin
54
+ * this ~5 KB string into every consumer chunk that imports the React adapter, referenced or not
55
+ * (ADR 0026). The template starts immediately after the backtick, so there is no leading newline to
56
+ * strip.
66
57
  */
67
58
  const BASE_CSS = `/* One timeline per skeleton, not per leaf: the root animates one inherited
68
59
  opacity property and every leaf reads it. Hundreds of independent infinite
@@ -95,10 +86,14 @@ const BASE_CSS = `/* One timeline per skeleton, not per leaf: the root animates
95
86
  /* Per-kind defaults: a single text LINE reads as a rounded bar; a multi-line
96
87
  text BLOCK as a soft rect (a tall pill misreads as a button); media carries
97
88
  the author radius, defaulting to the same soft rect. Re-theme any kind via
98
- [data-xr-root] .${LEAF_CLASS}-text { ... }. */
99
- .${LEAF_CLASS}-text { border-radius: var(--xr-bone-text-border-radius, 999px) }
89
+ [data-xr-root] .${LEAF_CLASS}-text-line { ... }. */
90
+ .${LEAF_CLASS}-text-line { border-radius: var(--xr-bone-text-line-border-radius, 999px) }
100
91
  .${LEAF_CLASS}-text-block { border-radius: var(--xr-bone-text-block-border-radius, var(--xr-bone-border-radius, 4px)) }
101
92
  .${LEAF_CLASS}-media { border-radius: var(--xr-bone-media-border-radius, var(--xr-bone-border-radius, 4px)) }
93
+ /* A hidden bone holds its measured box in the flow but paints nothing: the
94
+ space-preserving complement to data-xr-ignore. visibility (not display) so
95
+ the captured size rules still lay the box out. */
96
+ .${LEAF_CLASS}-hidden { visibility: hidden }
102
97
  @keyframes xr-pulse {
103
98
  from { --xr-o: var(--xr-bone-pulse-opacity-max, 1) }
104
99
  to { --xr-o: var(--xr-bone-pulse-opacity-min, 0.5) }
@@ -152,16 +147,14 @@ const BASE_CSS = `/* One timeline per skeleton, not per leaf: the root animates
152
147
  .${ROOT_CLASS} { --xr-bone-highlight-color: oklch(from var(--xr-bone-color, #e4e4e7) calc(l + 0.08) c h) }
153
148
  }`;
154
149
  /**
155
- * Compose a plate's scoped css with the renderer-owned BASE_CSS, base first so
156
- * a plate rule beats a base rule at equal specificity by source order — the
157
- * standard composition for rendering a skeleton outside the Vite plugin
158
- * (where `virtual:xray/base.css` + `virtual:xray/plates/<name>.css` do this
159
- * through the CSS pipeline instead). Since the two-artifact cutover (plan 005
160
- * round 2) css is the CALLER's input: `renderPlateHtml(plate,
161
- * composeCss(css))`, or `<Skeleton plate={plate} css={composeCss(css)}>` for
162
- * a runtime-captured plate. Rendering a NESTED stitch under an
163
- * already-composed parent needs the raw css only (base is already on the
164
- * page) — pass the string uncomposed there.
150
+ * Compose a plate's scoped css with the renderer-owned BASE_CSS, base first so a plate rule beats a
151
+ * base rule at equal specificity by source order — the standard composition for rendering a
152
+ * skeleton outside the Vite plugin (where `virtual:xray/base.css` +
153
+ * `virtual:xray/plates/<name>.css` do this through the CSS pipeline instead). css is the CALLER's
154
+ * input (ADR 0026): `renderPlateHtml(plate, composeCss(css))`, or `<Skeleton plate={plate}
155
+ * css={composeCss(css)}>` for a runtime-captured plate. Rendering a NESTED stitch under an
156
+ * already-composed parent needs the raw css only (base is already on the page) — pass the string
157
+ * uncomposed there.
165
158
  */
166
159
  function composeCss(css) {
167
160
  return `${BASE_CSS}\n${css}`;
@@ -169,7 +162,7 @@ function composeCss(css) {
169
162
  /** The (unserialized) plate an import resolves to before anything has been captured. */
170
163
  function emptyPlate(name) {
171
164
  return {
172
- v: 2,
165
+ v: 1,
173
166
  name,
174
167
  tree: null,
175
168
  css: ""
@@ -0,0 +1,301 @@
1
+ import { a as ROOT_ATTR, o as ROOT_CLASS, s as ROOT_INSTANT_ATTR } from "./plate-CXIh5_OB.js";
2
+ import { t as escapeStyleText } from "./css-escape-DJ-BsrNm.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 names that are
8
+ * CURRENTLY rendered as a stitch inside a still-SHOWING parent skeleton. It exists to kill the
9
+ * "stitch flash": when a parent skeleton resolves and its real content mounts a descendant's OWN
10
+ * standalone `<Skeleton>` for the same plate, that standalone is a NEW outermost skeleton — it
11
+ * would re-pay the full `--xr-skeleton-delay` (ADR 0016) from zero and visually disappear for
12
+ * ~250ms even though the child was continuously visible as a stitch. A standalone skeleton
13
+ * replacing such a live stitch must reveal INSTANTLY (no delay, no fade), matching how the stitch
14
+ * inherited its parent's already-revealed channel.
15
+ *
16
+ * Why a loading-keyed registry and not wall-clock time: the parent marks its direct stitch names
17
+ * live in an effect WHILE it shows its skeleton (an earlier commit). When the parent resolves,
18
+ * React renders the content tree — the child's standalone Skeleton first-renders and reads
19
+ * `isStitchLive(child)` BEFORE the parent's unmark cleanup runs (render happens before that
20
+ * commit's effects), so the read deterministically sees `child` still live. The unmark then clears
21
+ * it. "Recently" is defined by React's render-before-effect ordering, not a timer, so it is
22
+ * race-free AND does not over-apply to independent nested skeletons (a tab-hidden skeleton was
23
+ * never marked live by a currently-showing parent → not live → normal delay). It is portal-correct
24
+ * for free: only genuine stitches (names in a parent plate's stitch set) are ever registered, so a
25
+ * portaled child is never a stitch, never live, and reveals on the normal delay (ADR 0020).
26
+ *
27
+ * This ships to PROD: the flash is a production issue too, and the cost is a Map plus a register
28
+ * effect and a read-once `useState` per `<Skeleton>` — a few bytes, consistent with the prod-hooks
29
+ * relaxation ADR 0016 opened.
30
+ */
31
+ const liveStitches = /* @__PURE__ */ new Map();
32
+ /**
33
+ * Mark a parent skeleton's direct stitch names live (ref-counted, so concurrent showing parents
34
+ * that stitch the same child stack), returning a disposer that decrements each. The adapter calls
35
+ * this in an effect while `showSkeleton` is true and runs the disposer when it stops showing
36
+ * (resolve/unmount). Several marks of the same name increment the count; the count drops to 0
37
+ * (entry removed) only when every showing parent has unmarked it (ADR 0020).
38
+ */
39
+ function markStitchesLive(names) {
40
+ for (const name of names) liveStitches.set(name, (liveStitches.get(name) ?? 0) + 1);
41
+ return () => {
42
+ queueMicrotask(() => {
43
+ for (const name of names) {
44
+ const next = (liveStitches.get(name) ?? 0) - 1;
45
+ if (next <= 0) liveStitches.delete(name);
46
+ else liveStitches.set(name, next);
47
+ }
48
+ });
49
+ };
50
+ }
51
+ /**
52
+ * True while `name` is rendered as a stitch inside a still-showing parent skeleton — i.e. a
53
+ * standalone `<Skeleton>` for it is a continuation, not a fresh skeleton, and must reveal instantly
54
+ * (ADR 0020).
55
+ */
56
+ function isStitchLive(name) {
57
+ return (liveStitches.get(name) ?? 0) > 0;
58
+ }
59
+ /**
60
+ * The direct stitch names a plate's chunks reference (ADR 0020): a `{ r: name }` chunk IS a stitch;
61
+ * a `{ c, k }` chunk is a stitch-bearing element kept real, so recurse its `k`. Walks the SHIPPED
62
+ * `chunks` (the render contract), not `plate.refs` (empty in dev, where the live store resolves
63
+ * stitches) nor `collectRefs` (which walks the build-time RenderNode tree, not the shipped chunks).
64
+ * One level of direct stitch names per Skeleton is enough: each level self-registers as it becomes
65
+ * a real `<Skeleton>` (the cascade), so arbitrary nesting is covered without recursing into child
66
+ * plates here.
67
+ */
68
+ function collectStitchNames(chunks) {
69
+ if (!chunks) return [];
70
+ const names = [];
71
+ const walk = (cs) => {
72
+ for (const chunk of cs) {
73
+ if (typeof chunk === "string") continue;
74
+ if ("t" in chunk) continue;
75
+ if ("r" in chunk) names.push(chunk.r);
76
+ else walk(chunk.k);
77
+ }
78
+ };
79
+ walk(chunks);
80
+ return names;
81
+ }
82
+ /** Narrow unknown children to the render-prop form. */
83
+ function isRenderProp(children) {
84
+ return typeof children === "function";
85
+ }
86
+ /**
87
+ * Resolve `<Skeleton>` content at the ready (`loading=false`) path: invoke the render prop with the
88
+ * verbatim `data` and the adapter's `ref` (a no-op in prod and in `root="auto"`, the
89
+ * boundary-writing callback in dev `root="manual"`), or pass plain children through untouched.
90
+ * Centralizes the single `data as T` step: `data` is declared `data?: T`, so it reads back as `T |
91
+ * undefined`, but it is the exact value the consumer passed for the render prop's `T` (ADR 0014
92
+ * keeps it a pass-through, not `NonNullable<T>`).
93
+ */
94
+ function resolveContent(children, data, ref) {
95
+ if (!isRenderProp(children)) return children;
96
+ return children({
97
+ data,
98
+ ref
99
+ });
100
+ }
101
+ /**
102
+ * The token defaults that mirror the BASE_CSS `var(--xr-skeleton-*, …)` fallbacks (ADR 0016). The
103
+ * hook reads the COMPUTED token off the boundary element, but a consumer who never sets it leaves
104
+ * the computed value empty (the tokens are not declared on `.xr-root`, only referenced with these
105
+ * inline fallbacks). So when a read comes back empty/unparseable the hook falls back to these same
106
+ * numbers, keeping the JS hold and the CSS reveal in lockstep on identical defaults.
107
+ */
108
+ const DEFAULT_SKELETON_DELAY_MS = 250;
109
+ const DEFAULT_SKELETON_MIN_DURATION_MS = 600;
110
+ /**
111
+ * Parse a CSS time token (`getPropertyValue` returns a trimmed string like `"250ms"`, `"0.6s"`, or
112
+ * `""`) to milliseconds, falling back to `fallback` when empty or unparseable. Supports `ms` and
113
+ * `s`; a bare number is treated as ms.
114
+ */
115
+ function parseMs(value, fallback) {
116
+ const text = value.trim();
117
+ if (text === "") return fallback;
118
+ const match = /^(-?[\d.]+)(ms|s)?$/.exec(text);
119
+ if (!match?.[1]) return fallback;
120
+ const n = Number.parseFloat(match[1]);
121
+ if (!Number.isFinite(n)) return fallback;
122
+ return match[2] === "s" ? n * 1e3 : n;
123
+ }
124
+ /**
125
+ * The min-duration hold (ADR 0016), shared by the production (`react.tsx`) and dev
126
+ * (`react.dev.tsx`) adapters so both keep identical timing. Returns whether the skeleton should
127
+ * render RIGHT NOW.
128
+ *
129
+ * The reveal DELAY is pure CSS (BASE_CSS `--xr-reveal`, zero JS); this hook is ONLY the part CSS
130
+ * cannot express: keeping the skeleton mounted past `loading=false` for a minimum visible window.
131
+ * The hook returns `loading || holding`: the `loading` term is SYNCHRONOUS, so the skeleton shows
132
+ * on the very render `loading` flips true (no content frame at a false→true restart, and no
133
+ * stale-state lag), and `holding` is state used SOLELY for the post-ready hold.
134
+ *
135
+ * While loading, an effect anchors `t0` (loading start) and pre-arms `holding` true, so that
136
+ * pre-armed value carries into the first `loading=false` render with no content flash at the
137
+ * true→false transition. When `loading` flips false the effect reads the COMPUTED
138
+ * `--xr-skeleton-delay` + `--xr-skeleton-min-duration` off `ref` and holds until `t0 + delay +
139
+ * min-duration`. Three cases skip the hold entirely (swap synchronously, no timer): min-duration
140
+ * resolves to 0; `ref` is null / no `t0` was recorded — meaning no skeleton root was ever shown
141
+ * (e.g. a plate with no capture, just a fallback), so there is nothing to hold and a hold would
142
+ * only keep blank/fallback UI on screen after data is ready; or `loading` resolved inside the grace
143
+ * delay — the skeleton is still in its invisible window (BASE_CSS reveals at `t0 + delay`), so no
144
+ * visible-time floor is owed and the fast load never sees a skeleton (ADR 0016).
145
+ *
146
+ * Reading computed custom props off the skeleton root: the root carries `display:contents`, but
147
+ * custom-property inheritance and resolution are unaffected by `display` —
148
+ * `getComputedStyle(el).getPropertyValue('--xr-…')` returns the inherited token value on a
149
+ * `display:contents` element just as on any other. The tokens are not declared on `.xr-root` (only
150
+ * referenced with inline fallbacks), so a consumer who never themes them yields an empty read;
151
+ * `parseMs` then applies the same defaults the CSS uses, so the hold matches the reveal.
152
+ */
153
+ function useSkeletonTiming(loading, ref) {
154
+ const [holding, setHolding] = useState(loading);
155
+ const startRef = useRef(null);
156
+ const timerRef = useRef(void 0);
157
+ useEffect(() => {
158
+ const clear = () => {
159
+ if (timerRef.current !== void 0) {
160
+ clearTimeout(timerRef.current);
161
+ timerRef.current = void 0;
162
+ }
163
+ };
164
+ if (loading) {
165
+ clear();
166
+ if (startRef.current === null) startRef.current = Date.now();
167
+ setHolding(true);
168
+ return clear;
169
+ }
170
+ const start = startRef.current;
171
+ startRef.current = null;
172
+ const el = ref.current;
173
+ if (el === null || start === null) {
174
+ clear();
175
+ setHolding(false);
176
+ return clear;
177
+ }
178
+ const style = getComputedStyle(el);
179
+ const delay = parseMs(style.getPropertyValue("--xr-skeleton-delay"), DEFAULT_SKELETON_DELAY_MS);
180
+ const minDuration = parseMs(style.getPropertyValue("--xr-skeleton-min-duration"), DEFAULT_SKELETON_MIN_DURATION_MS);
181
+ if (minDuration <= 0) {
182
+ clear();
183
+ setHolding(false);
184
+ return clear;
185
+ }
186
+ if (Date.now() < start + delay) {
187
+ clear();
188
+ setHolding(false);
189
+ return clear;
190
+ }
191
+ const remaining = start + delay + minDuration - Date.now();
192
+ if (remaining <= 0) {
193
+ clear();
194
+ setHolding(false);
195
+ return clear;
196
+ }
197
+ clear();
198
+ timerRef.current = setTimeout(() => {
199
+ timerRef.current = void 0;
200
+ setHolding(false);
201
+ }, remaining);
202
+ return clear;
203
+ }, [loading, ref]);
204
+ return loading || holding;
205
+ }
206
+ /**
207
+ * Layout-neutral wrapper for an HTML island sitting beside React stitch nodes — its children
208
+ * participate in the grandparent's flow, so grid/flex item counts are unchanged.
209
+ */
210
+ const DISPLAY_CONTENTS = { display: "contents" };
211
+ /**
212
+ * Render a shipped plate's skeleton. `resolveStitch` turns a child plate name into the node that
213
+ * mounts it (static `refs` in prod, a store-subscribed component in dev). `nested` is true for a
214
+ * stitch-mounted child. Returns null when the plate has no capture yet; the caller renders the
215
+ * fallback.
216
+ *
217
+ * Two-artifact contract (ADR 0026): a `Plate` is tree-only, and this renderer neither reads css off
218
+ * it nor references `BASE_CSS` (keeping the constant out of consumer bundles — Vite-path styling is
219
+ * entirely the CSS pipeline's job: `virtual:xray/base.css` + `virtual:xray/plates/<name>.css`).
220
+ * `css` is the one optional styling input: the consumer-composed inline css for a runtime-captured
221
+ * (`@hueest/xray/core`) plate, threaded from the `<Skeleton css>` prop and injected as a `<style>`
222
+ * on this root. It rides the OUTERMOST root only — the adapters never pass it for a nested stitch
223
+ * (the page-level/base styles are already in scope there, ADR 0007's once-per-page rule by
224
+ * construction).
225
+ *
226
+ * `instant` (ADR 0020) marks this root a stitch continuation: a standalone `<Skeleton>` replacing a
227
+ * stitch a still-showing parent was just rendering. It lands as `data-xr-instant` on the OUTERMOST
228
+ * root only (`instant && !nested`) so BASE_CSS reveals it without the `--xr-skeleton-delay` fade.
229
+ */
230
+ function renderPlate(plate, resolveStitch, nested, rootRef, instant, css) {
231
+ if (!plate.chunks) return null;
232
+ const className = plate.rootCls ? `${ROOT_CLASS} ${plate.rootCls}` : ROOT_CLASS;
233
+ const rootAttrs = {
234
+ [ROOT_ATTR]: plate.name,
235
+ className,
236
+ "aria-hidden": true,
237
+ "aria-busy": true,
238
+ ...rootRef ? { ref: rootRef } : {},
239
+ ...instant && !nested ? { [ROOT_INSTANT_ATTR]: "" } : {}
240
+ };
241
+ const chunks = plate.chunks;
242
+ if (chunks.length === 1 && typeof chunks[0] === "string") {
243
+ if (css === void 0) return /* @__PURE__ */ jsx("div", {
244
+ ...rootAttrs,
245
+ dangerouslySetInnerHTML: { __html: chunks[0] }
246
+ });
247
+ const html = `<style>${escapeStyleText(css)}</style>${chunks[0]}`;
248
+ return /* @__PURE__ */ jsx("div", {
249
+ ...rootAttrs,
250
+ dangerouslySetInnerHTML: { __html: html }
251
+ });
252
+ }
253
+ return /* @__PURE__ */ jsxs("div", {
254
+ ...rootAttrs,
255
+ children: [css === void 0 ? null : /* @__PURE__ */ jsx("style", { children: css }), renderChunks(chunks, resolveStitch)]
256
+ });
257
+ }
258
+ /**
259
+ * Each `string` chunk is injected as a display:contents innerHTML island; each `{ r }` stitch
260
+ * resolves through `resolveStitch`; a `{ c, k }` node stays a real element and recurses (it holds a
261
+ * stitch deeper). No per-node tree walk — the strings are already serialized.
262
+ */
263
+ function renderChunks(chunks, resolveStitch) {
264
+ return chunks.map((chunk, i) => {
265
+ if (typeof chunk === "string") return /* @__PURE__ */ jsx("div", {
266
+ style: DISPLAY_CONTENTS,
267
+ dangerouslySetInnerHTML: { __html: chunk }
268
+ }, i);
269
+ if ("t" in chunk) return /* @__PURE__ */ jsx("div", {
270
+ style: DISPLAY_CONTENTS,
271
+ dangerouslySetInnerHTML: { __html: chunk.t.repeat(chunk.n) }
272
+ }, i);
273
+ if ("r" in chunk) return /* @__PURE__ */ jsx(Fragment, { children: resolveStitch(chunk.r) }, i);
274
+ return /* @__PURE__ */ jsx("div", {
275
+ className: chunk.c,
276
+ children: renderChunks(chunk.k, resolveStitch)
277
+ }, i);
278
+ });
279
+ }
280
+ /**
281
+ * A never-settling promise for the loading suspender below. It never needs to resolve: React
282
+ * retries a suspended boundary when its parent re-renders with new props (here, `loading` flipping
283
+ * to false), so the prop change — not the promise — drives recovery.
284
+ */
285
+ const SUSPEND_UNTIL_READY = new Promise(() => {});
286
+ /**
287
+ * Suspends while `loading` is true. It exists so a `suspense`-mode `<Skeleton>` can show its plate
288
+ * through the ONE Suspense fallback across BOTH readiness phases — the data-loading phase (this
289
+ * suspender) AND the lazy code-chunk phase (the content itself suspending). Because the plate is
290
+ * the SAME fallback DOM node the whole time, there is no unmount/remount between the two phases and
291
+ * thus no flicker (a top-level `return skeleton` for the data phase followed by a Suspense-fallback
292
+ * skeleton for the chunk phase would sit at different tree positions and remount). While this
293
+ * throws, the content sibling is never committed, preserving "children never render without data"
294
+ * (ADR 0002).
295
+ */
296
+ function SuspendWhileLoading({ loading }) {
297
+ if (loading) throw SUSPEND_UNTIL_READY;
298
+ return null;
299
+ }
300
+ //#endregion
301
+ export { markStitchesLive as a, useSkeletonTiming as c, isStitchLive as i, collectStitchNames as n, renderPlate as o, isRenderProp as r, resolveContent as s, SuspendWhileLoading as t };
@@ -0,0 +1,115 @@
1
+ import { c as XrayCaptureWalker, p as Plate } from "./measure-CUQxB-Ic.js";
2
+ import { ReactNode } from "react";
3
+ //#region src/react.core.d.ts
4
+ /**
5
+ * The additive render-prop children form (ADR 0014). Called only at `loading=false` (null included
6
+ * — `loading` stays the sole readiness axis, ADR 0002), it hands back the typed `data` verbatim
7
+ * plus a `ref` for the manual capture root.
8
+ *
9
+ * `ref` is a CALLBACK ref by contract: in `root="manual"` (dev) it imperatively writes the
10
+ * `data-xr-boundary` marker onto the consumer's own element and registers it as the capture root,
11
+ * so it must run on the real DOM node. In `root="auto"` and in production it is a no-op (omitted),
12
+ * and the existing `display:contents` boundary wrapper does the marking instead.
13
+ *
14
+ * It is typed as a callback ref over the base `Element` rather than a `Ref<T>`. A callback that
15
+ * accepts `Element | null` is, by parameter contravariance, assignable to a more specific `ref`
16
+ * slot (`<section ref={ref}>` wants `(el: HTMLElement | null) => void`), so the consumer spreads it
17
+ * onto any host element without a cast. A `RefObject` would NOT compose this way and is not
18
+ * offered: writing the boundary attribute needs the imperative callback anyway.
19
+ */
20
+ type SkeletonRef = (el: Element | null) => void;
21
+ type SkeletonRenderProp<T> = (ctx: {
22
+ data: T;
23
+ ref?: SkeletonRef;
24
+ }) => ReactNode;
25
+ /** Fields shared by both `root` variants of `<Skeleton>`. */
26
+ interface SkeletonBase {
27
+ /** The plate to render while loading, imported from `virtual:xray/plates/<name>`. */
28
+ plate: Plate;
29
+ /**
30
+ * Inline skeleton css for a plate whose styling does NOT flow through the Vite plugin's CSS
31
+ * pipeline — i.e. a runtime-captured plate from `@hueest/xray/core`'s `captureElement` (ADR 0003:
32
+ * the no-Vite path). Pass the composed string (`composeCss(css)` for the standard BASE_CSS +
33
+ * plate rules); it is injected as a `<style>` on the skeleton root. OMIT for plugin-imported
34
+ * plates — their css (base included) is already on the page via `virtual:xray/base.css` +
35
+ * `virtual:xray/plates/<name>.css`, and an inline copy would double-apply and re-fight the
36
+ * cascade. Applies to THIS plate's root only; stitched child plates resolve through refs/the dev
37
+ * store and carry no inline css.
38
+ */
39
+ css?: string;
40
+ /**
41
+ * Explicit readiness mode: render the Skeleton while true, then render children and capture them
42
+ * in dev when false.
43
+ */
44
+ loading?: boolean;
45
+ /**
46
+ * Suspense mode: make `<Skeleton>` the Suspense boundary and use the Plate as the fallback until
47
+ * children resolve.
48
+ */
49
+ suspense?: boolean;
50
+ /** Shown while loading when the plate has no capture yet. */
51
+ fallback?: ReactNode;
52
+ /** Override the plugin's settle delay before this site is captured (dev only). */
53
+ delay?: number;
54
+ /**
55
+ * Programmable capture walker for this Skeleton's subtree (ADR 0018, dev only). The escape hatch
56
+ * beyond the declarative `data-xr-*` hints: a closure that decides, per element, whether to
57
+ * ignore it, collapse it to one Bone, or defer to the built-in walk. Threaded through the dev
58
+ * adapter into `captureView`; PRODUCTION ignores it entirely (it never captures). On both `root`
59
+ * variants of the union, so a manual root may carry one too.
60
+ */
61
+ captureWalker?: XrayCaptureWalker;
62
+ /**
63
+ * Per-Skeleton Collect limits (ADR 0022, dev only): override the plugin's `collect` defaults for
64
+ * THIS boundary's capture. Flat primitives — not a nested object — so they stay referentially
65
+ * stable across renders and never churn the dev capture effect's deps. `maxNodes` caps total
66
+ * surviving nodes, `maxBreadth` the widest level, `maxDepth` the nesting depth; an omitted
67
+ * `maxNodes` derives from the resolved breadth × depth. PRODUCTION ignores them (it never
68
+ * captures).
69
+ */
70
+ maxNodes?: number;
71
+ maxBreadth?: number;
72
+ maxDepth?: number;
73
+ }
74
+ /**
75
+ * Props accepted by the React `<Skeleton>` component, generic over the optional render-prop `data`
76
+ * payload `T` (ADR 0014). The generic defaults to `undefined` so existing call sites — `<Skeleton
77
+ * plate loading />`, plain children — keep working with zero type arguments; passing `data` infers
78
+ * `T` into the render prop. `data` is a pure pass-through (the consumer owns nullability), never
79
+ * `NonNullable<T>`.
80
+ *
81
+ * The two members discriminate on `root`:
82
+ *
83
+ * - `root="auto"` (default): the existing `display:contents` boundary wrapper marks the capture seam
84
+ * (ADR 0006); children may be plain `ReactNode` OR a render prop, and the surfaced `ref` is a
85
+ * no-op.
86
+ * - `root="manual"`: no wrapper; the consumer owns the capture root element. This REQUIRES
87
+ * render-prop children (the only way to hand back the `ref` that writes the boundary marker), so
88
+ * plain children + `root="manual"` is a type error.
89
+ */
90
+ type SkeletonProps<T = undefined> = SkeletonBase & ({
91
+ /** Auto capture root (default): xray inserts a `display:contents` boundary wrapper. */
92
+ root?: 'auto';
93
+ /** Render-prop payload, passed back to the children function verbatim as `T`. */
94
+ data?: T;
95
+ /**
96
+ * Real content. In dev, these children are also the capture source once ready. Either plain
97
+ * `ReactNode` or a render prop.
98
+ */
99
+ children?: ReactNode | SkeletonRenderProp<T>;
100
+ } | {
101
+ /**
102
+ * Manual capture root: the consumer's element owns the boundary marker; no wrapper (ADR
103
+ * 0014).
104
+ */
105
+ root: 'manual';
106
+ /** Render-prop payload, passed back to the children function verbatim as `T`. */
107
+ data?: T;
108
+ /**
109
+ * Render prop required: spread the supplied `ref` onto your root element so capture does
110
+ * not leak into the parent plate.
111
+ */
112
+ children: SkeletonRenderProp<T>;
113
+ });
114
+ //#endregion
115
+ export { SkeletonProps as t };
package/dist/react.d.ts CHANGED
@@ -1,22 +1,20 @@
1
- import { t as SkeletonProps } from "./react.core-sJfIcaXX.js";
2
-
1
+ import { t as SkeletonProps } from "./react.core-BkJoCgWJ.js";
3
2
  //#region src/react.d.ts
4
3
  /**
5
4
  * Render a committed Plate while content is loading.
6
5
  *
7
- * Use `loading` when your data source exposes an explicit pending state. Use
8
- * `suspense` when the child suspends; in that mode `<Skeleton>` is the Suspense
9
- * boundary and the Plate is the fallback. You can also render a bare Skeleton
10
- * with no children when another framework or router owns the loading boundary.
6
+ * Use `loading` when your data source exposes an explicit pending state. Use `suspense` when the
7
+ * child suspends; in that mode `<Skeleton>` is the Suspense boundary and the Plate is the fallback.
8
+ * You can also render a bare Skeleton with no children when another framework or router owns the
9
+ * loading boundary.
11
10
  *
12
- * In dev, the plugin aliases this component to the dev adapter. When children
13
- * are ready, that adapter marks the capture boundary, waits for the subtree to
14
- * settle, and sends the rendered DOM to the xray client. Nested `<Skeleton>`
15
- * boundaries become Stitches so a parent Plate records a child Plate reference
16
- * instead of serializing the child subtree again.
11
+ * In dev, the plugin aliases this component to the dev adapter. When children are ready, that
12
+ * adapter marks the capture boundary, waits for the subtree to settle, and sends the rendered DOM
13
+ * to the xray client. Nested `<Skeleton>` boundaries become Stitches so a parent Plate records a
14
+ * child Plate reference instead of serializing the child subtree again.
17
15
  *
18
- * In production this component ships no capture client, HUD, Light Box, store,
19
- * or subscriptions. Stitches resolve from the statically imported `refs` graph.
16
+ * In production this component ships no capture client, HUD, Light Box, store, or subscriptions.
17
+ * Stitches resolve from the statically imported `refs` graph.
20
18
  */
21
19
  declare function Skeleton<T = undefined>(props: SkeletonProps<T>): import("react").JSX.Element;
22
20
  //#endregion
@@ -1,11 +1,9 @@
1
- import { t as SkeletonProps } from "./react.core-sJfIcaXX.js";
2
-
1
+ import { t as SkeletonProps } from "./react.core-BkJoCgWJ.js";
3
2
  //#region src/react.dev.d.ts
4
3
  /**
5
- * Dev `<Skeleton>`: production behavior plus the Light Box override, the
6
- * live-store hot-swap, and browse-time capture. The xray plugin aliases
7
- * `@hueest/xray/react` to this module in `serve`; production gets the hook-free
8
- * `react.tsx`.
4
+ * Dev `<Skeleton>`: production behavior plus the Light Box override, the live-store hot-swap, and
5
+ * browse-time capture. The xray plugin aliases `@hueest/xray/react` to this module in `serve`;
6
+ * production gets the lean `react.tsx`.
9
7
  */
10
8
  declare function Skeleton<T = undefined>(props: SkeletonProps<T>): import("react").JSX.Element;
11
9
  //#endregion