@hueest/xray 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{serialize-Bq6yJnNV.js → breakpoints-CMoFUUOv.js} +426 -104
- package/dist/client.d.ts +38 -3
- package/dist/client.js +716 -27
- package/dist/core.d.ts +8 -3
- package/dist/core.js +12 -6
- package/dist/css-escape-N7bOusGW.js +52 -0
- package/dist/hud.js +46 -0
- package/dist/index.d.ts +231 -5
- package/dist/index.js +753 -71
- package/dist/{plate-DboxMKg-.d.ts → plate-Bv6W-GkA.d.ts} +142 -0
- package/dist/plate-DoE1HEXp.js +152 -0
- package/dist/react.core-BzMG_cDy.d.ts +79 -0
- package/dist/react.core-IyFy2b_8.js +267 -0
- package/dist/react.d.ts +2 -8
- package/dist/react.dev.d.ts +2 -9
- package/dist/react.dev.js +153 -23
- package/dist/react.js +24 -12
- package/dist/serialize-B--oY2bV.d.ts +143 -0
- package/package.json +7 -7
- package/virtual.d.ts +6 -0
- package/dist/breakpoints-CBNlh7lQ.js +0 -47
- package/dist/plate-BuzRMPx4.js +0 -94
- package/dist/react.core-CqnDjfAJ.js +0 -59
- package/dist/react.core-fgWG_svU.d.ts +0 -21
- package/dist/serialize.d.ts +0 -88
- package/dist/serialize.js +0 -2
|
@@ -1,3 +1,51 @@
|
|
|
1
|
+
//#region src/diagnostics.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Dev-only capture diagnostics: a small, serializable record of why a Plate may
|
|
4
|
+
* be incomplete or lower-fidelity, plus the one shared formatter both the
|
|
5
|
+
* browser console (`client.ts`) and the Vite server output (`index.ts`) print.
|
|
6
|
+
*
|
|
7
|
+
* Motivation: the serializer silently swallows a handful of capture omissions
|
|
8
|
+
* (an unreadable cross-origin stylesheet, skipped pseudo-elements, a collapsed
|
|
9
|
+
* list run, an over-the-limit subtree). Early adopters asking "why does this
|
|
10
|
+
* skeleton look wrong?" had to read the serializer source to find out. These
|
|
11
|
+
* diagnostics surface those omissions as data the capture process returns, so a
|
|
12
|
+
* caller can log them — without the core performing any console side effects.
|
|
13
|
+
*
|
|
14
|
+
* Strictly DEV-ONLY (see the plan's decisions and ADR 0008): this type is NOT a
|
|
15
|
+
* public package export and MUST NEVER reach the persisted Plate JSON, the
|
|
16
|
+
* `MergedPlate`, or the production adapter. It rides on the TRANSIENT
|
|
17
|
+
* `ViewCapture.diagnostics` field only, which `captureRegime` populates and the
|
|
18
|
+
* dev client strips before posting (so committed `plates/<name>.json` stays
|
|
19
|
+
* byte-identical to a capture that produced no diagnostics).
|
|
20
|
+
*
|
|
21
|
+
* Centralized on purpose: a follow-up plan (capture-input-validation) reuses
|
|
22
|
+
* this same messaging for invalid posted/committed plates, so the code table
|
|
23
|
+
* and formatter are kept reusable. Browser and server share `formatDiagnostic`
|
|
24
|
+
* so their warning text cannot drift.
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* One aggregated capture omission. Serializable on purpose: NO DOM nodes, NO
|
|
30
|
+
* CSS bodies, NO captured text — only a stable `code`, a human message, an
|
|
31
|
+
* optional aggregate `count`, and an optional short `detail` (e.g. a plate name
|
|
32
|
+
* or a node count, never user content). `count` aggregates occurrences: there
|
|
33
|
+
* is at most one record per code per capture, never one per skipped selector or
|
|
34
|
+
* node.
|
|
35
|
+
*/
|
|
36
|
+
interface CaptureDiagnostic {
|
|
37
|
+
code: DiagnosticCode;
|
|
38
|
+
message: string;
|
|
39
|
+
count?: number;
|
|
40
|
+
detail?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The closed set of capture-omission codes. Each maps to one silent site in the
|
|
44
|
+
* serializer (or an existing oversized-capture warning folded into the same
|
|
45
|
+
* vocabulary). Kept as a const union so the message table is exhaustive.
|
|
46
|
+
*/
|
|
47
|
+
type DiagnosticCode = 'unreadable-stylesheet' | 'unreadable-import' | 'skipped-dynamic-pseudo' | 'skipped-pseudo-element' | 'unsupported-rule' | 'dropped-tag' | 'pruned-run' | 'too-large' | 'invalid-plate-file';
|
|
48
|
+
//#endregion
|
|
1
49
|
//#region src/plate.d.ts
|
|
2
50
|
type LeafKind = 'text' | 'media' | 'box';
|
|
3
51
|
/**
|
|
@@ -108,6 +156,17 @@ interface ViewCapture {
|
|
|
108
156
|
max?: number;
|
|
109
157
|
tree: PlateNode;
|
|
110
158
|
rules: PlateRule[];
|
|
159
|
+
/**
|
|
160
|
+
* TRANSIENT, dev-only capture diagnostics (why this view may be lower
|
|
161
|
+
* fidelity). Populated by `captureRegime` in the browser; the dev client logs
|
|
162
|
+
* it and posts it (so the Vite server can print the same text), then it is
|
|
163
|
+
* STRIPPED at the merge layer (`addCapture`) before persisting. It MUST NEVER
|
|
164
|
+
* be persisted: it is not part of the committed `plates/<name>.json` shape,
|
|
165
|
+
* the `isViewCapture` validator does not require it, and `addCapture` drops it
|
|
166
|
+
* so neither it nor `mergeViews` ever writes it. Absent in production (the
|
|
167
|
+
* prod adapter ships no capture). See diagnostics.ts and ADR 0008.
|
|
168
|
+
*/
|
|
169
|
+
diagnostics?: CaptureDiagnostic[];
|
|
111
170
|
}
|
|
112
171
|
/** The committed `plates/<name>.json`: per-view captures, merged at load. */
|
|
113
172
|
interface PlateFile {
|
|
@@ -151,6 +210,68 @@ interface Span {
|
|
|
151
210
|
min?: number;
|
|
152
211
|
max?: number;
|
|
153
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Dev-only Fixture store (ADR 0015): stored render-INPUT `data` per Plate,
|
|
215
|
+
* recorded to and replayed from a `plates/<name>.fixture.json` sidecar. Entirely
|
|
216
|
+
* dev-only — devalue and this store never reach the production bundle.
|
|
217
|
+
*
|
|
218
|
+
* Two cooperating halves, both flowing through the render-prop `data` seam (ADR
|
|
219
|
+
* 0014):
|
|
220
|
+
* - The dev adapter (`react.dev.tsx`) calls `register(name, data)` at
|
|
221
|
+
* `loading=false` with the live `data` it is about to render, so the latest
|
|
222
|
+
* recordable input for each mounted Plate is always in scope for Record.
|
|
223
|
+
* - `record(name)` serializes that registered live `data` with devalue and POSTs
|
|
224
|
+
* it to the sidecar (the HUD button and the `first-ready`/`always` modes call
|
|
225
|
+
* it). `has(name)` reports whether a Fixture exists (HUD coverage marker), and
|
|
226
|
+
* `read(name)` returns the parsed Fixture `data` for Replay to substitute in
|
|
227
|
+
* front of the render prop.
|
|
228
|
+
*
|
|
229
|
+
* The store holds DECODED values (the dev client owns the devalue parse, since it
|
|
230
|
+
* has the live runtime); only the on-disk sidecar and the wire carry the opaque
|
|
231
|
+
* devalue string.
|
|
232
|
+
*/
|
|
233
|
+
interface FixtureStore {
|
|
234
|
+
/**
|
|
235
|
+
* Record the latest live `data` the dev adapter is about to render for `name`,
|
|
236
|
+
* so Record always has the current recordable input in scope. A `register` with
|
|
237
|
+
* the same value is cheap; the adapter calls it on every ready render. Returns a
|
|
238
|
+
* disposer the adapter runs on unmount so an unmounted Skeleton's `data` never
|
|
239
|
+
* lingers in the store: an unmounted instance must never be recorded or shadow a
|
|
240
|
+
* still-mounted one (ADR 0015). With several Skeletons of the same plate name
|
|
241
|
+
* mounted, the live value is the most-recently-registered still-mounted instance.
|
|
242
|
+
*/
|
|
243
|
+
register: (name: string, data: unknown) => () => void;
|
|
244
|
+
/** True once a recorded Fixture exists for `name` (drives the HUD coverage marker). */
|
|
245
|
+
has: (name: string) => boolean;
|
|
246
|
+
/** The parsed Fixture `data` for `name`, or `undefined` when none is recorded. */
|
|
247
|
+
read: (name: string) => {
|
|
248
|
+
data: unknown;
|
|
249
|
+
} | undefined;
|
|
250
|
+
/**
|
|
251
|
+
* Serialize the registered live `data` for `name` with devalue and POST it to
|
|
252
|
+
* the sidecar. Returns a promise that resolves once the write is
|
|
253
|
+
* server-confirmed (so the HUD can surface a failed Record). Throws loudly when
|
|
254
|
+
* the live `data` is unrecordable (devalue's contract) or no live `data` was
|
|
255
|
+
* registered (a plain-children `<Skeleton>` has none).
|
|
256
|
+
*/
|
|
257
|
+
record: (name: string) => Promise<void>;
|
|
258
|
+
/**
|
|
259
|
+
* Record every Skeleton that currently has live `data` registered (the HUD's
|
|
260
|
+
* single "Record fixture" button — one click captures the inputs of all mounted
|
|
261
|
+
* render-prop Skeletons). Resolves once all writes are server-confirmed; a
|
|
262
|
+
* rejected write surfaces to the HUD.
|
|
263
|
+
*/
|
|
264
|
+
recordAll: () => Promise<void>;
|
|
265
|
+
/**
|
|
266
|
+
* Seed the store from the on-disk sidecars on boot: a `{ <name>: devalueString }`
|
|
267
|
+
* map (the opaque strings the server serves). The client decodes each with
|
|
268
|
+
* devalue. Existing entries are not clobbered, so a Fixture recorded this
|
|
269
|
+
* session survives a re-seed.
|
|
270
|
+
*/
|
|
271
|
+
seed: (encoded: Record<string, string>) => void;
|
|
272
|
+
/** React subscription so the HUD re-renders when a Fixture is recorded or seeded. */
|
|
273
|
+
subscribe: (onChange: () => void) => () => void;
|
|
274
|
+
}
|
|
154
275
|
/** A plate's detected width thresholds and the spans actually captured, from disk. */
|
|
155
276
|
interface PlateCoverage {
|
|
156
277
|
breakpoints: number[];
|
|
@@ -175,6 +296,8 @@ interface CoverageStore {
|
|
|
175
296
|
interface CaptureHook {
|
|
176
297
|
captured: (name: string, el: Element, opts?: {
|
|
177
298
|
delay?: number;
|
|
299
|
+
walker?: unknown;
|
|
300
|
+
mode?: 'auto' | 'manual';
|
|
178
301
|
}) => (() => void) | undefined;
|
|
179
302
|
/** Light Box toggle: force every `<Skeleton>` to its skeleton, read by `<Skeleton>`. */
|
|
180
303
|
lightbox?: ToggleStore;
|
|
@@ -188,6 +311,25 @@ interface CaptureHook {
|
|
|
188
311
|
captureAllViews?: () => Promise<void>;
|
|
189
312
|
/** Dev-only per-plate breakpoints + captured spans, from disk; fed by the bootstrap. */
|
|
190
313
|
coverage?: CoverageStore;
|
|
314
|
+
/**
|
|
315
|
+
* Replay toggle (ADR 0015): when active, the dev adapter substitutes a recorded
|
|
316
|
+
* Fixture's `data` for the live `data` at `loading=false`. The boolean is the
|
|
317
|
+
* coarse on/off the HUD owns per tab; the per-mode policy (`prefer` vs
|
|
318
|
+
* `missing-only`) is carried alongside in `replayMode`.
|
|
319
|
+
*/
|
|
320
|
+
replay?: ToggleStore;
|
|
321
|
+
/** Dev-only Replay policy: how a Fixture substitutes for live `data` (ADR 0015). */
|
|
322
|
+
replayMode?: 'prefer' | 'missing-only';
|
|
323
|
+
/**
|
|
324
|
+
* Dev-only Record policy (ADR 0015): when the dev adapter records the live
|
|
325
|
+
* `data` it is about to render. `'manual'` (the default) records nothing
|
|
326
|
+
* automatically — only the HUD button does; `'first-ready'` records once when a
|
|
327
|
+
* Skeleton goes ready and has no Fixture yet; `'always'` records on every ready
|
|
328
|
+
* render.
|
|
329
|
+
*/
|
|
330
|
+
recordMode?: 'manual' | 'first-ready' | 'always';
|
|
331
|
+
/** Dev-only Fixture record/replay store (ADR 0015); never present in production. */
|
|
332
|
+
fixtures?: FixtureStore;
|
|
191
333
|
}
|
|
192
334
|
declare global {
|
|
193
335
|
var __XRAY__: CaptureHook | undefined;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/** Marks the rendered skeleton root; carries the plate name. Also a dev capture marker. */
|
|
2
|
+
const ROOT_ATTR = "data-xr-root";
|
|
3
|
+
/**
|
|
4
|
+
* Marks 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).
|
|
10
|
+
*/
|
|
11
|
+
const ROOT_INSTANT_ATTR = "data-xr-instant";
|
|
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).
|
|
16
|
+
*/
|
|
17
|
+
const BOUNDARY_ATTR = "data-xr-boundary";
|
|
18
|
+
const SPEC_LEAF = 9e6;
|
|
19
|
+
const SPEC_INLINE = 1e7;
|
|
20
|
+
const SPEC_GATE = 2e7;
|
|
21
|
+
/** Fixed renderer-owned classes every rendered node carries. */
|
|
22
|
+
const ROOT_CLASS = "xr-root";
|
|
23
|
+
const NODE_CLASS = "xr-node";
|
|
24
|
+
const LEAF_CLASS = "xr-leaf";
|
|
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`).
|
|
34
|
+
*
|
|
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.
|
|
41
|
+
*
|
|
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-media-border-radius`), `--xr-bone-animation-duration`, the pulse
|
|
46
|
+
* range `--xr-bone-pulse-opacity-min`/`--xr-bone-pulse-opacity-max`,
|
|
47
|
+
* `--xr-bone-animation` (mode: `xr-pulse` | `none` | a custom keyframe), and
|
|
48
|
+
* `--xr-bone-fill` (override the leaf paint, e.g. a sheen gradient built from the
|
|
49
|
+
* two bone color tokens). The `--xr-skeleton-*` family governs whole-skeleton
|
|
50
|
+
* display timing (ADR 0016): `--xr-skeleton-delay` (grace before the skeleton
|
|
51
|
+
* becomes visible), `--xr-skeleton-min-duration` (minimum visible time once
|
|
52
|
+
* shown; read by the `useSkeletonTiming` hook, not used in CSS), and
|
|
53
|
+
* `--xr-skeleton-transition-duration` (the reveal/swap fade). The private
|
|
54
|
+
* channels `--xr-o` (pulse) and `--xr-reveal` (reveal) stay undocumented
|
|
55
|
+
* `@property` plumbing, not a theming surface.
|
|
56
|
+
*/
|
|
57
|
+
const BASE_CSS = `
|
|
58
|
+
/* One timeline per skeleton, not per leaf: the root animates one inherited
|
|
59
|
+
opacity property and every leaf reads it. Hundreds of independent infinite
|
|
60
|
+
opacity animations would pin the compositor and cook the CPU. */
|
|
61
|
+
@property --xr-o { syntax: "<number>"; inherits: true; initial-value: 1 }
|
|
62
|
+
/* The reveal channel (ADR 0016): a second inherited opacity factor the leaf
|
|
63
|
+
multiplies into --xr-o. Transitioning THIS (not opacity directly) sidesteps
|
|
64
|
+
the animation-vs-transition collision ADR 0012 flagged. initial-value 1 (NOT
|
|
65
|
+
0) is the degradation floor: without @starting-style the reveal can never be
|
|
66
|
+
driven 0->1, so it must REST at 1 (fully visible). The delay-hide below is
|
|
67
|
+
pure progressive enhancement layered on top. */
|
|
68
|
+
@property --xr-reveal { syntax: "<number>"; inherits: true; initial-value: 1 }
|
|
69
|
+
.${ROOT_CLASS} {
|
|
70
|
+
--xr-bone-highlight-color: #f4f4f5;
|
|
71
|
+
display: contents;
|
|
72
|
+
animation: var(--xr-bone-animation, xr-pulse) var(--xr-bone-animation-duration, 1.2s) ease-in-out infinite alternate;
|
|
73
|
+
}
|
|
74
|
+
/* No pointer-events:none — it blocks inspecting bones in devtools and buys
|
|
75
|
+
nothing (no real content to click). border-color: a node may capture
|
|
76
|
+
border-width for layout; its paint stays out. list-style: suppress a ::marker
|
|
77
|
+
a captured display:list-item would otherwise paint. */
|
|
78
|
+
.${NODE_CLASS} { border-color: transparent; list-style: none }
|
|
79
|
+
.${LEAF_CLASS} {
|
|
80
|
+
background: var(--xr-bone-fill, var(--xr-bone-color, #e4e4e7));
|
|
81
|
+
border-radius: var(--xr-bone-border-radius, 4px);
|
|
82
|
+
/* Composed opacity (ADR 0016): the pulse channel times the reveal channel.
|
|
83
|
+
Never transition opacity directly (the pulse animation owns it). */
|
|
84
|
+
opacity: calc(var(--xr-o) * var(--xr-reveal));
|
|
85
|
+
}
|
|
86
|
+
/* Per-kind defaults: text reads as rounded bars, media as blocks. Re-theme any
|
|
87
|
+
kind via [data-xr-root] .${LEAF_CLASS}-text { ... }. */
|
|
88
|
+
.${LEAF_CLASS}-text { border-radius: var(--xr-bone-text-border-radius, 999px) }
|
|
89
|
+
.${LEAF_CLASS}-media { border-radius: var(--xr-bone-media-border-radius, var(--xr-bone-border-radius, 4px)) }
|
|
90
|
+
@keyframes xr-pulse {
|
|
91
|
+
from { --xr-o: var(--xr-bone-pulse-opacity-max, 1) }
|
|
92
|
+
to { --xr-o: var(--xr-bone-pulse-opacity-min, 0.5) }
|
|
93
|
+
}
|
|
94
|
+
/* Reveal delay-hide (ADR 0016), a progressive enhancement guarded so it can NEVER
|
|
95
|
+
strand a skeleton invisible (open Q3). @starting-style is the trigger: only a
|
|
96
|
+
browser that supports it enters this block at all, and such a browser also
|
|
97
|
+
honors @property + transitions, so the 0->1 reveal completes. The @supports
|
|
98
|
+
selector(...) probe is the broadest cross-engine @starting-style feature test
|
|
99
|
+
available. Without support the block is skipped entirely and --xr-reveal stays
|
|
100
|
+
at its initial-value 1 = fully visible instant skeleton. INSIDE the block we
|
|
101
|
+
start at 0 and transition to 1 after the delay: invisible for
|
|
102
|
+
--xr-skeleton-delay, then a --xr-skeleton-transition-duration fade-in; a fast
|
|
103
|
+
load unmounts before the delay elapses and the skeleton is never seen. */
|
|
104
|
+
@supports (selector(:has(*))) {
|
|
105
|
+
/* The delay-hide applies to every root EXCEPT a stitch continuation (ADR 0020).
|
|
106
|
+
Scoping with :not([data-xr-instant]) keeps an ordinary top-level skeleton
|
|
107
|
+
byte-identical to before: it still mounts at --xr-reveal 0 and fades in after
|
|
108
|
+
the delay. The transition/initial-1 rest state matches the original. */
|
|
109
|
+
.${ROOT_CLASS}:not([${ROOT_INSTANT_ATTR}]) {
|
|
110
|
+
--xr-reveal: 1;
|
|
111
|
+
transition: --xr-reveal var(--xr-skeleton-transition-duration, 150ms) linear var(--xr-skeleton-delay, 250ms);
|
|
112
|
+
}
|
|
113
|
+
@starting-style { .${ROOT_CLASS}:not([${ROOT_INSTANT_ATTR}]) { --xr-reveal: 0 } }
|
|
114
|
+
/* A stitch continuation (ADR 0020) reveals INSTANTLY: it was already visible as a
|
|
115
|
+
stitch under a still-showing parent, so re-paying the delay would flash it away
|
|
116
|
+
and back. No @starting-style and no transition — it rests at fully visible from
|
|
117
|
+
its first frame, matching how the stitch inherited the parent's reveal. */
|
|
118
|
+
.${ROOT_CLASS}[${ROOT_INSTANT_ATTR}] { --xr-reveal: 1 }
|
|
119
|
+
}
|
|
120
|
+
/* Reduced motion (open Q2): keep the anti-flash delay + min-duration (they are
|
|
121
|
+
not motion), drop only the FADE — collapse the reveal transition to instant and
|
|
122
|
+
kill the pulse animation (as before). */
|
|
123
|
+
@media (prefers-reduced-motion: reduce) {
|
|
124
|
+
.${ROOT_CLASS} { animation: none; --xr-skeleton-transition-duration: 0s }
|
|
125
|
+
}
|
|
126
|
+
/* Dark + high-contrast bone defaults. Guarded: light-dark() is an invalid value
|
|
127
|
+
without support and would drop the declaration — see the note above; remove
|
|
128
|
+
this @supports once widely available (2026-11-13). */
|
|
129
|
+
@supports (color: light-dark(#000, #fff)) {
|
|
130
|
+
.${ROOT_CLASS} { --xr-bone-highlight-color: light-dark(#f4f4f5, #52525b) }
|
|
131
|
+
.${LEAF_CLASS} { background: var(--xr-bone-fill, var(--xr-bone-color, light-dark(#e4e4e7, #3f3f46))) }
|
|
132
|
+
@media (prefers-contrast: more) {
|
|
133
|
+
.${LEAF_CLASS} { background: var(--xr-bone-fill, var(--xr-bone-color, light-dark(#d4d4d8, #52525b))) }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/* Derive the sheen highlight from --xr-bone-color so re-theming one token updates
|
|
137
|
+
both. Relative color isn't widely available yet; the static highlight above is
|
|
138
|
+
the fallback (sheen just looks flatter), so no hard guard is needed. */
|
|
139
|
+
@supports (color: oklch(from red l c h)) {
|
|
140
|
+
.${ROOT_CLASS} { --xr-bone-highlight-color: oklch(from var(--xr-bone-color, #e4e4e7) calc(l + 0.08) c h) }
|
|
141
|
+
}`.trim();
|
|
142
|
+
/** The (unserialized) plate an import resolves to before anything has been captured. */
|
|
143
|
+
function emptyPlate(name) {
|
|
144
|
+
return {
|
|
145
|
+
v: 1,
|
|
146
|
+
name,
|
|
147
|
+
tree: null,
|
|
148
|
+
css: ""
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
export { ROOT_ATTR as a, SPEC_GATE as c, emptyPlate as d, 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,79 @@
|
|
|
1
|
+
import { r as Plate } from "./plate-Bv6W-GkA.js";
|
|
2
|
+
import { o as XrayCaptureWalker } from "./serialize-B--oY2bV.js";
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
//#region src/react.core.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* The additive render-prop children form (ADR 0014). Called only at
|
|
8
|
+
* `loading=false` (null included — `loading` stays the sole readiness axis, ADR
|
|
9
|
+
* 0002), it hands back the typed `data` verbatim plus a `ref` for the manual
|
|
10
|
+
* capture root.
|
|
11
|
+
*
|
|
12
|
+
* `ref` is a CALLBACK ref by contract: in `root="manual"` (dev) it imperatively
|
|
13
|
+
* writes the `data-xr-boundary` marker onto the consumer's own element and
|
|
14
|
+
* registers it as the capture root, so it must run on the real DOM node. In
|
|
15
|
+
* `root="auto"` and in production it is a no-op (omitted), and the existing
|
|
16
|
+
* `display:contents` boundary wrapper does the marking instead.
|
|
17
|
+
*
|
|
18
|
+
* It is typed as a callback ref over the base `Element` rather than a `Ref<T>`.
|
|
19
|
+
* A callback that accepts `Element | null` is, by parameter contravariance,
|
|
20
|
+
* assignable to a more specific `ref` slot (`<section ref={ref}>` wants
|
|
21
|
+
* `(el: HTMLElement | null) => void`), so the consumer spreads it onto any host
|
|
22
|
+
* element without a cast. A `RefObject` would NOT compose this way and is not
|
|
23
|
+
* offered: writing the boundary attribute needs the imperative callback anyway.
|
|
24
|
+
*/
|
|
25
|
+
type SkeletonRef = (el: Element | null) => void;
|
|
26
|
+
type SkeletonRenderProp<T> = (ctx: {
|
|
27
|
+
data: T;
|
|
28
|
+
ref?: SkeletonRef;
|
|
29
|
+
}) => ReactNode;
|
|
30
|
+
/** Fields shared by both `root` variants of `<Skeleton>`. */
|
|
31
|
+
interface SkeletonBase {
|
|
32
|
+
/** The plate to render while loading, imported from `virtual:xray/plates/<name>`. */
|
|
33
|
+
plate: Plate;
|
|
34
|
+
/** Explicit readiness mode: render the Skeleton while true, then render children and capture them in dev when false. */
|
|
35
|
+
loading?: boolean;
|
|
36
|
+
/** Suspense mode: make `<Skeleton>` the Suspense boundary and use the Plate as the fallback until children resolve. */
|
|
37
|
+
suspense?: boolean;
|
|
38
|
+
/** Shown while loading when the plate has no capture yet. */
|
|
39
|
+
fallback?: ReactNode;
|
|
40
|
+
/** Override the plugin's settle delay before this site is captured (dev only). */
|
|
41
|
+
delay?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Programmable capture walker for this Skeleton's subtree (ADR 0018, dev only).
|
|
44
|
+
* The escape hatch beyond the declarative `data-xr-*` annotations: a closure
|
|
45
|
+
* that decides, per element, whether to ignore it, collapse it to one Bone, or
|
|
46
|
+
* defer to the built-in walk. Threaded through the dev adapter into
|
|
47
|
+
* `captureRegime`; PRODUCTION ignores it entirely (it never captures). On both
|
|
48
|
+
* `root` variants of the union, so a manual root may carry one too.
|
|
49
|
+
*/
|
|
50
|
+
captureWalker?: XrayCaptureWalker;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Props accepted by the React `<Skeleton>` component, generic over the optional
|
|
54
|
+
* render-prop `data` payload `T` (ADR 0014). The generic defaults to `undefined`
|
|
55
|
+
* so existing call sites — `<Skeleton plate loading />`, plain children — keep
|
|
56
|
+
* working with zero type arguments; passing `data` infers `T` into the render
|
|
57
|
+
* prop. `data` is a pure pass-through (the consumer owns nullability), never
|
|
58
|
+
* `NonNullable<T>`.
|
|
59
|
+
*
|
|
60
|
+
* The two members discriminate on `root`:
|
|
61
|
+
* - `root="auto"` (default): the existing `display:contents` boundary wrapper
|
|
62
|
+
* marks the capture seam (ADR 0006); children may be plain `ReactNode` OR a
|
|
63
|
+
* render prop, and the surfaced `ref` is a no-op.
|
|
64
|
+
* - `root="manual"`: no wrapper; the consumer owns the capture root element. This
|
|
65
|
+
* REQUIRES render-prop children (the only way to hand back the `ref` that
|
|
66
|
+
* writes the boundary marker), so plain children + `root="manual"` is a type
|
|
67
|
+
* error.
|
|
68
|
+
*/
|
|
69
|
+
type SkeletonProps<T = undefined> = SkeletonBase & ({
|
|
70
|
+
/** Auto capture root (default): xray inserts a `display:contents` boundary wrapper. */root?: 'auto'; /** Render-prop payload, passed back to the children function verbatim as `T`. */
|
|
71
|
+
data?: T; /** Real content. In dev, these children are also the capture source once ready. Either plain `ReactNode` or a render prop. */
|
|
72
|
+
children?: ReactNode | SkeletonRenderProp<T>;
|
|
73
|
+
} | {
|
|
74
|
+
/** Manual capture root: the consumer's element owns the boundary marker; no wrapper (ADR 0014). */root: 'manual'; /** Render-prop payload, passed back to the children function verbatim as `T`. */
|
|
75
|
+
data?: T; /** Render prop required: spread the supplied `ref` onto your root element so capture does not leak into the parent plate. */
|
|
76
|
+
children: SkeletonRenderProp<T>;
|
|
77
|
+
});
|
|
78
|
+
//#endregion
|
|
79
|
+
export { SkeletonProps as t };
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { a as ROOT_ATTR, o as ROOT_CLASS, s as ROOT_INSTANT_ATTR, t as BASE_CSS } from "./plate-DoE1HEXp.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 ("r" in chunk) names.push(chunk.r);
|
|
78
|
+
else walk(chunk.k);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
walk(chunks);
|
|
82
|
+
return names;
|
|
83
|
+
}
|
|
84
|
+
/** Narrow unknown children to the render-prop form. */
|
|
85
|
+
function isRenderProp(children) {
|
|
86
|
+
return typeof children === "function";
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve `<Skeleton>` content at the ready (`loading=false`) path: invoke the
|
|
90
|
+
* render prop with the verbatim `data` and the adapter's `ref` (a no-op in prod
|
|
91
|
+
* and in `root="auto"`, the boundary-writing callback in dev `root="manual"`), or
|
|
92
|
+
* pass plain children through untouched. Centralizes the single `data as T` step:
|
|
93
|
+
* `data` is declared `data?: T`, so it reads back as `T | undefined`, but it is
|
|
94
|
+
* the exact value the consumer passed for the render prop's `T` (ADR 0014 keeps it
|
|
95
|
+
* a pass-through, not `NonNullable<T>`).
|
|
96
|
+
*/
|
|
97
|
+
function resolveContent(children, data, ref) {
|
|
98
|
+
if (!isRenderProp(children)) return children;
|
|
99
|
+
return children({
|
|
100
|
+
data,
|
|
101
|
+
ref
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The token defaults that mirror the BASE_CSS `var(--xr-skeleton-*, …)` fallbacks
|
|
106
|
+
* (ADR 0016). The hook reads the COMPUTED token off the boundary element, but a
|
|
107
|
+
* consumer who never sets it leaves the computed value empty (the tokens are not
|
|
108
|
+
* declared on `.xr-root`, only referenced with these inline fallbacks). So when a
|
|
109
|
+
* read comes back empty/unparseable the hook falls back to these same numbers,
|
|
110
|
+
* keeping the JS hold and the CSS reveal in lockstep on identical defaults.
|
|
111
|
+
*/
|
|
112
|
+
const DEFAULT_SKELETON_DELAY_MS = 250;
|
|
113
|
+
const DEFAULT_SKELETON_MIN_DURATION_MS = 600;
|
|
114
|
+
/**
|
|
115
|
+
* Parse a CSS time token (`getPropertyValue` returns a trimmed string like
|
|
116
|
+
* `"250ms"`, `"0.6s"`, or `""`) to milliseconds, falling back to `fallback` when
|
|
117
|
+
* empty or unparseable. Supports `ms` and `s`; a bare number is treated as ms.
|
|
118
|
+
*/
|
|
119
|
+
function parseMs(value, fallback) {
|
|
120
|
+
const text = value.trim();
|
|
121
|
+
if (text === "") return fallback;
|
|
122
|
+
const match = /^(-?[\d.]+)(ms|s)?$/.exec(text);
|
|
123
|
+
if (!match?.[1]) return fallback;
|
|
124
|
+
const n = Number.parseFloat(match[1]);
|
|
125
|
+
if (!Number.isFinite(n)) return fallback;
|
|
126
|
+
return match[2] === "s" ? n * 1e3 : n;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The min-duration hold (ADR 0016), shared by the production (`react.tsx`) and dev
|
|
130
|
+
* (`react.dev.tsx`) adapters so both keep identical timing. Returns whether the
|
|
131
|
+
* skeleton should render RIGHT NOW.
|
|
132
|
+
*
|
|
133
|
+
* The reveal DELAY is pure CSS (BASE_CSS `--xr-reveal`, zero JS); this hook is
|
|
134
|
+
* ONLY the part CSS cannot express: keeping the skeleton mounted past
|
|
135
|
+
* `loading=false` for a minimum visible window. The hook returns `loading ||
|
|
136
|
+
* holding`: the `loading` term is SYNCHRONOUS, so the skeleton shows on the very
|
|
137
|
+
* render `loading` flips true (no content frame at a false→true restart, and no
|
|
138
|
+
* stale-state lag), and `holding` is state used SOLELY for the post-ready hold.
|
|
139
|
+
*
|
|
140
|
+
* While loading, an effect anchors `t0` (loading start) and pre-arms `holding`
|
|
141
|
+
* true, so that pre-armed value carries into the first `loading=false` render with
|
|
142
|
+
* no content flash at the true→false transition. When `loading` flips false the
|
|
143
|
+
* effect reads the COMPUTED `--xr-skeleton-delay` + `--xr-skeleton-min-duration`
|
|
144
|
+
* off `ref` and holds until `t0 + delay + min-duration`. Two cases skip the hold
|
|
145
|
+
* entirely (swap synchronously, no timer): min-duration resolves to 0, or `ref` is
|
|
146
|
+
* null / no `t0` was recorded — meaning no skeleton root was ever shown (e.g. a
|
|
147
|
+
* plate with no capture, just a fallback), so there is nothing to hold and a hold
|
|
148
|
+
* would only keep blank/fallback UI on screen after data is ready.
|
|
149
|
+
*
|
|
150
|
+
* Reading computed custom props off the skeleton root (open Q4): the root carries
|
|
151
|
+
* `display:contents`, but custom-property inheritance and resolution are
|
|
152
|
+
* unaffected by `display` — `getComputedStyle(el).getPropertyValue('--xr-…')`
|
|
153
|
+
* returns the inherited token value on a `display:contents` element just as on any
|
|
154
|
+
* other. The tokens are not declared on `.xr-root` (only referenced with inline
|
|
155
|
+
* fallbacks), so a consumer who never themes them yields an empty read; `parseMs`
|
|
156
|
+
* then applies the same defaults the CSS uses, so the hold matches the reveal.
|
|
157
|
+
*/
|
|
158
|
+
function useSkeletonTiming(loading, ref) {
|
|
159
|
+
const [holding, setHolding] = useState(loading);
|
|
160
|
+
const startRef = useRef(null);
|
|
161
|
+
const timerRef = useRef(void 0);
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
const clear = () => {
|
|
164
|
+
if (timerRef.current !== void 0) {
|
|
165
|
+
clearTimeout(timerRef.current);
|
|
166
|
+
timerRef.current = void 0;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
if (loading) {
|
|
170
|
+
clear();
|
|
171
|
+
if (startRef.current === null) startRef.current = Date.now();
|
|
172
|
+
setHolding(true);
|
|
173
|
+
return clear;
|
|
174
|
+
}
|
|
175
|
+
const start = startRef.current;
|
|
176
|
+
startRef.current = null;
|
|
177
|
+
const el = ref.current;
|
|
178
|
+
if (el === null || start === null) {
|
|
179
|
+
clear();
|
|
180
|
+
setHolding(false);
|
|
181
|
+
return clear;
|
|
182
|
+
}
|
|
183
|
+
const style = getComputedStyle(el);
|
|
184
|
+
const delay = parseMs(style.getPropertyValue("--xr-skeleton-delay"), DEFAULT_SKELETON_DELAY_MS);
|
|
185
|
+
const minDuration = parseMs(style.getPropertyValue("--xr-skeleton-min-duration"), DEFAULT_SKELETON_MIN_DURATION_MS);
|
|
186
|
+
if (minDuration <= 0) {
|
|
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
|
+
/** Layout-neutral wrapper for an HTML island sitting beside React stitch nodes —
|
|
207
|
+
* its children participate in the grandparent's flow, so grid/flex item counts
|
|
208
|
+
* are unchanged. */
|
|
209
|
+
const DISPLAY_CONTENTS = { display: "contents" };
|
|
210
|
+
/**
|
|
211
|
+
* Render a shipped plate's skeleton. `resolveStitch` turns a child plate name
|
|
212
|
+
* into the node that mounts it (static `refs` in prod, a store-subscribed
|
|
213
|
+
* component in dev). `nested` is true for a stitch-mounted child, which skips
|
|
214
|
+
* BASE_CSS — the outermost view already emitted it (ADR 0007). Returns null
|
|
215
|
+
* when the plate has no capture yet; the caller renders the fallback.
|
|
216
|
+
*
|
|
217
|
+
* `instant` (ADR 0020) marks this root a stitch continuation: a standalone
|
|
218
|
+
* `<Skeleton>` replacing a stitch a still-showing parent was just rendering. It
|
|
219
|
+
* lands as `data-xr-instant` on the OUTERMOST root only (`instant && !nested`) so
|
|
220
|
+
* BASE_CSS reveals it without the `--xr-skeleton-delay` fade.
|
|
221
|
+
*/
|
|
222
|
+
function renderPlate(plate, resolveStitch, nested, rootRef, instant) {
|
|
223
|
+
if (!plate.chunks) return null;
|
|
224
|
+
const className = plate.rootCls ? `${ROOT_CLASS} ${plate.rootCls}` : ROOT_CLASS;
|
|
225
|
+
const rootAttrs = {
|
|
226
|
+
[ROOT_ATTR]: plate.name,
|
|
227
|
+
className,
|
|
228
|
+
"aria-hidden": true,
|
|
229
|
+
"aria-busy": true,
|
|
230
|
+
...rootRef ? { ref: rootRef } : {},
|
|
231
|
+
...instant && !nested ? { [ROOT_INSTANT_ATTR]: "" } : {}
|
|
232
|
+
};
|
|
233
|
+
const css = nested ? plate.css : `${BASE_CSS}\n${plate.css}`;
|
|
234
|
+
const chunks = plate.chunks;
|
|
235
|
+
if (chunks.length === 1 && typeof chunks[0] === "string") {
|
|
236
|
+
const html = `<style>${escapeStyleText(css)}</style>${chunks[0]}`;
|
|
237
|
+
return /* @__PURE__ */ jsx("div", {
|
|
238
|
+
...rootAttrs,
|
|
239
|
+
dangerouslySetInnerHTML: { __html: html }
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
243
|
+
...rootAttrs,
|
|
244
|
+
children: [/* @__PURE__ */ jsx("style", { children: css }), renderChunks(chunks, resolveStitch)]
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Each `string` chunk is injected as a display:contents innerHTML island; each
|
|
249
|
+
* `{ r }` stitch resolves through `resolveStitch`; a `{ c, k }` node stays a
|
|
250
|
+
* real element and recurses (it holds a stitch deeper). No per-node tree walk —
|
|
251
|
+
* the strings are already serialized.
|
|
252
|
+
*/
|
|
253
|
+
function renderChunks(chunks, resolveStitch) {
|
|
254
|
+
return chunks.map((chunk, i) => {
|
|
255
|
+
if (typeof chunk === "string") return /* @__PURE__ */ jsx("div", {
|
|
256
|
+
style: DISPLAY_CONTENTS,
|
|
257
|
+
dangerouslySetInnerHTML: { __html: chunk }
|
|
258
|
+
}, i);
|
|
259
|
+
if ("r" in chunk) return /* @__PURE__ */ jsx(Fragment, { children: resolveStitch(chunk.r) }, i);
|
|
260
|
+
return /* @__PURE__ */ jsx("div", {
|
|
261
|
+
className: chunk.c,
|
|
262
|
+
children: renderChunks(chunk.k, resolveStitch)
|
|
263
|
+
}, i);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
//#endregion
|
|
267
|
+
export { renderPlate as a, markStitchesLive as i, isRenderProp as n, resolveContent as o, isStitchLive as r, useSkeletonTiming as s, collectStitchNames as t };
|