@hueest/xray 0.5.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.
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/client.d.ts +42 -51
- package/dist/client.js +136 -142
- package/dist/core.d.ts +42 -34
- package/dist/core.js +39 -33
- package/dist/css-escape-DJ-BsrNm.js +47 -0
- package/dist/hud.d.ts +7 -8
- package/dist/hud.js +5 -5
- package/dist/index.d.ts +268 -250
- package/dist/index.js +369 -240
- package/dist/measure-CUQxB-Ic.d.ts +524 -0
- package/dist/{plate-BRR6d8Se.js → plate-CXIh5_OB.js} +61 -45
- package/dist/react.core-BS2yvqeh.js +301 -0
- package/dist/react.core-BkJoCgWJ.d.ts +115 -0
- package/dist/react.d.ts +11 -13
- package/dist/react.dev.d.ts +4 -6
- package/dist/react.dev.js +58 -64
- package/dist/react.js +27 -25
- package/dist/{project-BZosujs9.js → serialize-Cj-EHvMv.js} +877 -1027
- package/package.json +8 -6
- package/virtual.d.ts +25 -32
- package/dist/css-escape-N7bOusGW.js +0 -52
- package/dist/react.core-BC02ukac.js +0 -294
- package/dist/react.core-NhWc9qan.d.ts +0 -89
- package/dist/serialize-BgdGt34A.d.ts +0 -501
|
@@ -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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
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,40 +23,39 @@ 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
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
-
*
|
|
44
|
-
* `--xr-bone-border-radius`
|
|
45
|
-
* `--xr-bone-
|
|
46
|
-
* `--xr-bone-
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* `--xr-
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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.
|
|
51
|
+
*
|
|
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.
|
|
57
57
|
*/
|
|
58
|
-
const BASE_CSS =
|
|
59
|
-
/* One timeline per skeleton, not per leaf: the root animates one inherited
|
|
58
|
+
const BASE_CSS = `/* One timeline per skeleton, not per leaf: the root animates one inherited
|
|
60
59
|
opacity property and every leaf reads it. Hundreds of independent infinite
|
|
61
60
|
opacity animations would pin the compositor and cook the CPU. */
|
|
62
61
|
@property --xr-o { syntax: "<number>"; inherits: true; initial-value: 1 }
|
|
@@ -87,10 +86,14 @@ const BASE_CSS = `
|
|
|
87
86
|
/* Per-kind defaults: a single text LINE reads as a rounded bar; a multi-line
|
|
88
87
|
text BLOCK as a soft rect (a tall pill misreads as a button); media carries
|
|
89
88
|
the author radius, defaulting to the same soft rect. Re-theme any kind via
|
|
90
|
-
[data-xr-root] .${LEAF_CLASS}-text { ... }. */
|
|
91
|
-
.${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) }
|
|
92
91
|
.${LEAF_CLASS}-text-block { border-radius: var(--xr-bone-text-block-border-radius, var(--xr-bone-border-radius, 4px)) }
|
|
93
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 }
|
|
94
97
|
@keyframes xr-pulse {
|
|
95
98
|
from { --xr-o: var(--xr-bone-pulse-opacity-max, 1) }
|
|
96
99
|
to { --xr-o: var(--xr-bone-pulse-opacity-min, 0.5) }
|
|
@@ -142,15 +145,28 @@ const BASE_CSS = `
|
|
|
142
145
|
the fallback (sheen just looks flatter), so no hard guard is needed. */
|
|
143
146
|
@supports (color: oklch(from red l c h)) {
|
|
144
147
|
.${ROOT_CLASS} { --xr-bone-highlight-color: oklch(from var(--xr-bone-color, #e4e4e7) calc(l + 0.08) c h) }
|
|
145
|
-
}
|
|
148
|
+
}`;
|
|
149
|
+
/**
|
|
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.
|
|
158
|
+
*/
|
|
159
|
+
function composeCss(css) {
|
|
160
|
+
return `${BASE_CSS}\n${css}`;
|
|
161
|
+
}
|
|
146
162
|
/** The (unserialized) plate an import resolves to before anything has been captured. */
|
|
147
163
|
function emptyPlate(name) {
|
|
148
164
|
return {
|
|
149
|
-
v:
|
|
165
|
+
v: 1,
|
|
150
166
|
name,
|
|
151
167
|
tree: null,
|
|
152
168
|
css: ""
|
|
153
169
|
};
|
|
154
170
|
}
|
|
155
171
|
//#endregion
|
|
156
|
-
export { ROOT_ATTR as a, SPEC_GATE as c,
|
|
172
|
+
export { ROOT_ATTR as a, SPEC_GATE as c, composeCss as d, emptyPlate as f, NODE_CLASS as i, SPEC_INLINE as l, BOUNDARY_ATTR as n, ROOT_CLASS as o, LEAF_CLASS as r, ROOT_INSTANT_ATTR as s, BASE_CSS as t, SPEC_LEAF as u };
|
|
@@ -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-
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
*
|
|
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
|
package/dist/react.dev.d.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { t as SkeletonProps } from "./react.core-
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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
|