@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hueest/xray",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Auto-captured skeleton loading screens for Vite — capture the structure, ship the plate.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"loading",
|
|
@@ -60,20 +60,22 @@
|
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@chenglou/pretext": "0.0.8",
|
|
62
62
|
"devalue": "5.8.1",
|
|
63
|
-
"valibot": "1.4.
|
|
63
|
+
"valibot": "1.4.2"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
+
"@arethetypeswrong/core": "0.18.5",
|
|
66
67
|
"@types/node": "25.9.3",
|
|
67
68
|
"@types/react": "19.2.17",
|
|
68
69
|
"@types/react-dom": "19.2.3",
|
|
69
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
70
|
+
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
70
71
|
"happy-dom": "20.10.6",
|
|
72
|
+
"publint": "0.3.21",
|
|
71
73
|
"react": "19.2.7",
|
|
72
74
|
"react-dom": "19.2.7",
|
|
73
|
-
"tsdown": "0.22.
|
|
75
|
+
"tsdown": "0.22.5",
|
|
74
76
|
"typescript": "6.0.3",
|
|
75
|
-
"vite": "8.
|
|
76
|
-
"vitest": "4.1.
|
|
77
|
+
"vite": "8.1.4",
|
|
78
|
+
"vitest": "4.1.10"
|
|
77
79
|
},
|
|
78
80
|
"peerDependencies": {
|
|
79
81
|
"react": "^18.2.0 || ^19.0.0",
|
package/virtual.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Type declarations for xray virtual modules.
|
|
3
3
|
*
|
|
4
|
-
* Reference from the consuming app, e.g. in tsconfig:
|
|
5
|
-
*
|
|
6
|
-
* `/// <reference types="@hueest/xray/virtual" />`.
|
|
4
|
+
* Reference from the consuming app, e.g. in tsconfig: `"types": ["@hueest/xray/virtual"]`, or per
|
|
5
|
+
* file: `/// <reference types="@hueest/xray/virtual" />`.
|
|
7
6
|
*
|
|
8
7
|
* @module @hueest/xray/virtual
|
|
9
8
|
*/
|
|
@@ -11,25 +10,22 @@
|
|
|
11
10
|
/**
|
|
12
11
|
* Import a committed or not-yet-captured Plate by name.
|
|
13
12
|
*
|
|
14
|
-
* The import specifier is the Plate identity: `virtual:xray/plates/my-banner`
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* name in props.
|
|
13
|
+
* The import specifier is the Plate identity: `virtual:xray/plates/my-banner` reads and writes
|
|
14
|
+
* `plates/my-banner.json`, and that name travels with the imported Plate. Pass the imported Plate
|
|
15
|
+
* to `<Skeleton>`; do not repeat the name in props.
|
|
18
16
|
*
|
|
19
|
-
* A name is one or more `/`-separated path segments, each `\w+(-\w+)*` — word
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* collision-free (it is the isolation boundary across stitched Plates).
|
|
17
|
+
* A name is one or more `/`-separated path segments, each `\w+(-\w+)*` — word chars with single
|
|
18
|
+
* interior hyphens. A segment may not contain `--` or a leading/trailing hyphen: the generated
|
|
19
|
+
* class prefix renders `/` as `--` to keep nested names readable, and banning those forms keeps the
|
|
20
|
+
* encoding collision-free (it is the isolation boundary across stitched Plates).
|
|
24
21
|
*
|
|
25
|
-
* If the file does not exist yet, the Vite plugin resolves an empty Plate with
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* rendering follows the static import graph with no global registry.
|
|
22
|
+
* If the file does not exist yet, the Vite plugin resolves an empty Plate with the same name so
|
|
23
|
+
* `<Skeleton fallback={...}>` can render until the first Capture writes the file. Nested
|
|
24
|
+
* `<Skeleton>` boundaries become Stitches; the generated virtual module wires child Plates into
|
|
25
|
+
* `Plate.refs`, so production rendering follows the static import graph with no global registry.
|
|
30
26
|
*
|
|
31
|
-
* Captured Plate files are source artifacts. Commit files under `plates/` with
|
|
32
|
-
*
|
|
27
|
+
* Captured Plate files are source artifacts. Commit files under `plates/` with the component change
|
|
28
|
+
* they describe.
|
|
33
29
|
*/
|
|
34
30
|
declare module 'virtual:xray/plates/*' {
|
|
35
31
|
import type { Plate } from '@hueest/xray'
|
|
@@ -37,22 +33,19 @@ declare module 'virtual:xray/plates/*' {
|
|
|
37
33
|
/**
|
|
38
34
|
* The captured Plate named by the `virtual:xray/plates/<name>` import specifier.
|
|
39
35
|
*
|
|
40
|
-
* Enable this import in a consuming app with
|
|
41
|
-
*
|
|
42
|
-
* `/// <reference types="@hueest/xray/virtual" />` directive.
|
|
36
|
+
* Enable this import in a consuming app with `"types": ["@hueest/xray/virtual"]` in tsconfig, or
|
|
37
|
+
* with a per-file `/// <reference types="@hueest/xray/virtual" />` directive.
|
|
43
38
|
*
|
|
44
|
-
* The specifier maps to a file under `plates/`; for example,
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* Pass the imported Plate to `<Skeleton>`; do not repeat the name in props.
|
|
39
|
+
* The specifier maps to a file under `plates/`; for example, `virtual:xray/plates/my-banner` maps
|
|
40
|
+
* to `plates/my-banner.json`. If that file does not exist yet, the plugin returns an empty Plate
|
|
41
|
+
* with the same name so a `<Skeleton fallback={...}>` can render until the first Capture. Pass
|
|
42
|
+
* the imported Plate to `<Skeleton>`; do not repeat the name in props.
|
|
49
43
|
*
|
|
50
|
-
* Child Plates referenced by Stitches are wired into `Plate.refs` by the
|
|
51
|
-
*
|
|
52
|
-
*
|
|
44
|
+
* Child Plates referenced by Stitches are wired into `Plate.refs` by the generated virtual
|
|
45
|
+
* module. Production rendering follows that static import graph with no global Plate registry or
|
|
46
|
+
* runtime name lookup.
|
|
53
47
|
*
|
|
54
|
-
* Captured Plate files are source artifacts. Commit them with the component
|
|
55
|
-
* change they describe.
|
|
48
|
+
* Captured Plate files are source artifacts. Commit them with the component change they describe.
|
|
56
49
|
*/
|
|
57
50
|
const plate: Plate
|
|
58
51
|
export default plate
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
//#region src/css-escape.ts
|
|
2
|
-
/**
|
|
3
|
-
* Dependency-free raw-text safety for the one place a Plate's CSS string is
|
|
4
|
-
* interpolated into `<style>…</style>` markup.
|
|
5
|
-
*
|
|
6
|
-
* A plate's `css` is normally produced by the capture path (`filterLayoutDecls`
|
|
7
|
-
* + `classify`), which only ever emits well-formed `selector { prop: value }`
|
|
8
|
-
* rules — never a raw-text terminator. But a committed `plates/<name>.json` is
|
|
9
|
-
* UNTRUSTED at render time (it can be hand-edited or arrive over git, see the
|
|
10
|
-
* capture-input-validation plan's decisions), and a corrupt/malicious `css`
|
|
11
|
-
* could carry a `</style>` or an HTML comment delimiter. `<style>` is a
|
|
12
|
-
* raw-text element: the parser ends the element at the first `</style` it sees
|
|
13
|
-
* and resumes parsing the rest as ordinary HTML, so an unescaped terminator in
|
|
14
|
-
* the CSS body turns the tail of the plate into live markup.
|
|
15
|
-
*
|
|
16
|
-
* The React text-child path (`<style>{css}</style>`) is already safe — React
|
|
17
|
-
* escapes the child — but the two STRING paths interpolate `css` directly into
|
|
18
|
-
* a markup string (`renderPlateHtml` builds an HTML string; `react.core.tsx`
|
|
19
|
-
* feeds one to `dangerouslySetInnerHTML`). Those must run the body through this
|
|
20
|
-
* helper first.
|
|
21
|
-
*
|
|
22
|
-
* This lives in its own dependency-free module ON PURPOSE: it is imported by
|
|
23
|
-
* render/browser paths, so it must NOT pull in the Node-only validation code
|
|
24
|
-
* (valibot). It neutralizes only the sequences that can escape a raw-text
|
|
25
|
-
* `<style>` context — a `<` that begins `</style`, and the HTML comment / CDATA
|
|
26
|
-
* delimiters the tokenizer treats specially inside raw text — by inserting a
|
|
27
|
-
* CSS-comment break (`/**/`) that the CSS parser ignores. It does not, and is
|
|
28
|
-
* not meant to, sanitize CSS semantically; POST-time validation
|
|
29
|
-
* (`validate.ts`) is what rejects junk declarations.
|
|
30
|
-
*
|
|
31
|
-
* @module
|
|
32
|
-
*/
|
|
33
|
-
/**
|
|
34
|
-
* Make a CSS body safe to interpolate into a `<style>…</style>` STRING. Breaks
|
|
35
|
-
* up the only sequences the HTML tokenizer acts on inside a raw-text `<style>`
|
|
36
|
-
* element, so none can terminate the element early or open a comment/CDATA run:
|
|
37
|
-
*
|
|
38
|
-
* - `</style` (the end-tag open, case-insensitive) — inserts the break between
|
|
39
|
-
* `<` and `/style` so the parser never matches the end tag.
|
|
40
|
-
* - `<!--`, `-->`, `<![CDATA[` — the comment/CDATA delimiters; split so neither
|
|
41
|
-
* a comment nor a CDATA section can open.
|
|
42
|
-
*
|
|
43
|
-
* The inserted `/**/` is an empty CSS comment: inert to the CSS parser (it
|
|
44
|
-
* cannot appear mid-token in a value we emit, and a stray one only ends a
|
|
45
|
-
* token early), so a well-formed plate is rendered byte-for-byte equivalently
|
|
46
|
-
* while a corrupt one can no longer break out of the style element.
|
|
47
|
-
*/
|
|
48
|
-
function escapeStyleText(css) {
|
|
49
|
-
return css.replace(/<(\/style)/gi, "</**/$1").replace(/<!--/g, "<!/**/--").replace(/-->/g, "--/**/>").replace(/<!\[CDATA\[/g, "<![/**/CDATA[");
|
|
50
|
-
}
|
|
51
|
-
//#endregion
|
|
52
|
-
export { escapeStyleText as t };
|
|
@@ -1,294 +0,0 @@
|
|
|
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
|
-
/**
|
|
272
|
-
* A never-settling promise for the loading suspender below. It never needs to
|
|
273
|
-
* resolve: React retries a suspended boundary when its parent re-renders with new
|
|
274
|
-
* props (here, `loading` flipping to false), so the prop change — not the promise —
|
|
275
|
-
* drives recovery.
|
|
276
|
-
*/
|
|
277
|
-
const SUSPEND_UNTIL_READY = new Promise(() => {});
|
|
278
|
-
/**
|
|
279
|
-
* Suspends while `loading` is true. It exists so a `suspense`-mode `<Skeleton>` can
|
|
280
|
-
* show its plate through the ONE Suspense fallback across BOTH readiness phases — the
|
|
281
|
-
* data-loading phase (this suspender) AND the lazy code-chunk phase (the content
|
|
282
|
-
* itself suspending). Because the plate is the SAME fallback DOM node the whole time,
|
|
283
|
-
* there is no unmount/remount between the two phases and thus no flicker (a top-level
|
|
284
|
-
* `return skeleton` for the data phase followed by a Suspense-fallback skeleton for
|
|
285
|
-
* the chunk phase are different tree positions, so they DID remount). While this
|
|
286
|
-
* throws, the content sibling is never committed, preserving "children never render
|
|
287
|
-
* without data" (ADR 0002).
|
|
288
|
-
*/
|
|
289
|
-
function SuspendWhileLoading({ loading }) {
|
|
290
|
-
if (loading) throw SUSPEND_UNTIL_READY;
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
//#endregion
|
|
294
|
-
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 };
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { c as XrayCaptureWalker, f as Plate } from "./serialize-BgdGt34A.js";
|
|
2
|
-
import { ReactNode } from "react";
|
|
3
|
-
|
|
4
|
-
//#region src/react.core.d.ts
|
|
5
|
-
/**
|
|
6
|
-
* The additive render-prop children form (ADR 0014). Called only at
|
|
7
|
-
* `loading=false` (null included — `loading` stays the sole readiness axis, ADR
|
|
8
|
-
* 0002), it hands back the typed `data` verbatim plus a `ref` for the manual
|
|
9
|
-
* capture root.
|
|
10
|
-
*
|
|
11
|
-
* `ref` is a CALLBACK ref by contract: in `root="manual"` (dev) it imperatively
|
|
12
|
-
* writes the `data-xr-boundary` marker onto the consumer's own element and
|
|
13
|
-
* registers it as the capture root, so it must run on the real DOM node. In
|
|
14
|
-
* `root="auto"` and in production it is a no-op (omitted), and the existing
|
|
15
|
-
* `display:contents` boundary wrapper does the marking instead.
|
|
16
|
-
*
|
|
17
|
-
* It is typed as a callback ref over the base `Element` rather than a `Ref<T>`.
|
|
18
|
-
* A callback that accepts `Element | null` is, by parameter contravariance,
|
|
19
|
-
* assignable to a more specific `ref` slot (`<section ref={ref}>` wants
|
|
20
|
-
* `(el: HTMLElement | null) => void`), so the consumer spreads it onto any host
|
|
21
|
-
* element without a cast. A `RefObject` would NOT compose this way and is not
|
|
22
|
-
* offered: writing the boundary attribute needs the imperative callback anyway.
|
|
23
|
-
*/
|
|
24
|
-
type SkeletonRef = (el: Element | null) => void;
|
|
25
|
-
type SkeletonRenderProp<T> = (ctx: {
|
|
26
|
-
data: T;
|
|
27
|
-
ref?: SkeletonRef;
|
|
28
|
-
}) => ReactNode;
|
|
29
|
-
/** Fields shared by both `root` variants of `<Skeleton>`. */
|
|
30
|
-
interface SkeletonBase {
|
|
31
|
-
/** The plate to render while loading, imported from `virtual:xray/plates/<name>`. */
|
|
32
|
-
plate: Plate;
|
|
33
|
-
/** Explicit readiness mode: render the Skeleton while true, then render children and capture them in dev when false. */
|
|
34
|
-
loading?: boolean;
|
|
35
|
-
/** Suspense mode: make `<Skeleton>` the Suspense boundary and use the Plate as the fallback until children resolve. */
|
|
36
|
-
suspense?: boolean;
|
|
37
|
-
/** Shown while loading when the plate has no capture yet. */
|
|
38
|
-
fallback?: ReactNode;
|
|
39
|
-
/** Override the plugin's settle delay before this site is captured (dev only). */
|
|
40
|
-
delay?: number;
|
|
41
|
-
/**
|
|
42
|
-
* Programmable capture walker for this Skeleton's subtree (ADR 0018, dev only).
|
|
43
|
-
* The escape hatch beyond the declarative `data-xr-*` annotations: a closure
|
|
44
|
-
* that decides, per element, whether to ignore it, collapse it to one Bone, or
|
|
45
|
-
* defer to the built-in walk. Threaded through the dev adapter into
|
|
46
|
-
* `captureView`; PRODUCTION ignores it entirely (it never captures). On both
|
|
47
|
-
* `root` variants of the union, so a manual root may carry one too.
|
|
48
|
-
*/
|
|
49
|
-
captureWalker?: XrayCaptureWalker;
|
|
50
|
-
/**
|
|
51
|
-
* Per-Skeleton Collect limits (ADR 0022, dev only): override the plugin's
|
|
52
|
-
* `collect` defaults for THIS boundary's capture. Flat primitives — not a nested
|
|
53
|
-
* object — so they stay referentially stable across renders and never churn the
|
|
54
|
-
* dev capture effect's deps. `maxNodes` caps total surviving nodes, `maxBreadth`
|
|
55
|
-
* the widest level, `maxDepth` the nesting depth; an omitted `maxNodes` derives
|
|
56
|
-
* from the resolved breadth × depth. PRODUCTION ignores them (it never captures).
|
|
57
|
-
*/
|
|
58
|
-
maxNodes?: number;
|
|
59
|
-
maxBreadth?: number;
|
|
60
|
-
maxDepth?: number;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Props accepted by the React `<Skeleton>` component, generic over the optional
|
|
64
|
-
* render-prop `data` payload `T` (ADR 0014). The generic defaults to `undefined`
|
|
65
|
-
* so existing call sites — `<Skeleton plate loading />`, plain children — keep
|
|
66
|
-
* working with zero type arguments; passing `data` infers `T` into the render
|
|
67
|
-
* prop. `data` is a pure pass-through (the consumer owns nullability), never
|
|
68
|
-
* `NonNullable<T>`.
|
|
69
|
-
*
|
|
70
|
-
* The two members discriminate on `root`:
|
|
71
|
-
* - `root="auto"` (default): the existing `display:contents` boundary wrapper
|
|
72
|
-
* marks the capture seam (ADR 0006); children may be plain `ReactNode` OR a
|
|
73
|
-
* render prop, and the surfaced `ref` is a no-op.
|
|
74
|
-
* - `root="manual"`: no wrapper; the consumer owns the capture root element. This
|
|
75
|
-
* REQUIRES render-prop children (the only way to hand back the `ref` that
|
|
76
|
-
* writes the boundary marker), so plain children + `root="manual"` is a type
|
|
77
|
-
* error.
|
|
78
|
-
*/
|
|
79
|
-
type SkeletonProps<T = undefined> = SkeletonBase & ({
|
|
80
|
-
/** 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`. */
|
|
81
|
-
data?: T; /** Real content. In dev, these children are also the capture source once ready. Either plain `ReactNode` or a render prop. */
|
|
82
|
-
children?: ReactNode | SkeletonRenderProp<T>;
|
|
83
|
-
} | {
|
|
84
|
-
/** 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`. */
|
|
85
|
-
data?: T; /** Render prop required: spread the supplied `ref` onto your root element so capture does not leak into the parent plate. */
|
|
86
|
-
children: SkeletonRenderProp<T>;
|
|
87
|
-
});
|
|
88
|
-
//#endregion
|
|
89
|
-
export { SkeletonProps as t };
|