@babylonjs-toolkit/agent 1.1.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.
Files changed (34) hide show
  1. package/README.md +344 -0
  2. package/bin/bt-agent.js +266 -0
  3. package/lib/doctor.js +59 -0
  4. package/lib/install.js +145 -0
  5. package/lib/manifest.js +46 -0
  6. package/lib/paths.js +66 -0
  7. package/lib/payload.js +56 -0
  8. package/lib/persona.js +177 -0
  9. package/lib/targets.js +105 -0
  10. package/package.json +43 -0
  11. package/persona.md +5 -0
  12. package/scripts/postinstall.js +49 -0
  13. package/skills/bt-atlas/SKILL.md +192 -0
  14. package/skills/bt-atlas/scripts/composite_skin.py +58 -0
  15. package/skills/bt-atlas/scripts/preview.py +70 -0
  16. package/skills/bt-atlas/scripts/requirements.txt +2 -0
  17. package/skills/bt-atlas/scripts/uv_island_mask.py +87 -0
  18. package/skills/bt-convert/SKILL.md +32 -0
  19. package/skills/bt-copycat/SKILL.md +184 -0
  20. package/skills/bt-design/SKILL.md +187 -0
  21. package/skills/bt-design/references/3d-hero-docs.md +976 -0
  22. package/skills/bt-design/references/3d-hero-scroll.md +269 -0
  23. package/skills/bt-design/templates/3d-hero-scroll/HeroScroll.tsx +167 -0
  24. package/skills/bt-design/templates/3d-hero-scroll/hero-scroll.css +268 -0
  25. package/skills/bt-design/templates/3d-hero-scroll/hero-scroll.d.ts +67 -0
  26. package/skills/bt-design/templates/3d-hero-scroll/hero-scroll.html +78 -0
  27. package/skills/bt-design/templates/3d-hero-scroll/hero-scroll.js +559 -0
  28. package/skills/bt-execute/SKILL.md +130 -0
  29. package/skills/bt-gauntlet/SKILL.md +335 -0
  30. package/skills/bt-hero/SKILL.md +158 -0
  31. package/skills/bt-landing/SKILL.md +126 -0
  32. package/skills/bt-plan/SKILL.md +172 -0
  33. package/skills/bt-prototype/SKILL.md +161 -0
  34. package/skills/bt-spec/SKILL.md +328 -0
@@ -0,0 +1,269 @@
1
+ # 3D Scrolling Hero Section With Optional Cinematic Controls (3D-Hero-Scroll)
2
+
3
+ A scroll-scrubbed cinematic hero: the product moves through real terrain as the
4
+ user scrolls — the scroll position IS the film's playhead. Proven pattern
5
+ (Scout-Motors-style site-of-the-year mechanics) with optional chrome: telemetry
6
+ HUD, film-speed autoplay, veiled jump cuts.
7
+
8
+ **Triggers:** "3D scrolling hero", "scroll-scrub", "cinematic scroll",
9
+ "redesign the hero with 3D scrolling", "the product drives/flies/moves as you
10
+ scroll", "Scout Motors style".
11
+
12
+ **Human quickstart:** `3d-hero-docs.md` (this directory)
13
+ maps every prompt field to the exact code it generates — hand it to developers
14
+ writing invocation prompts.
15
+
16
+ **Templates (drop-in, in this skill) — `templates/3d-hero-scroll/`:**
17
+ `hero-scroll.js` is one framework-agnostic ESM engine shared by both hosts;
18
+ copy the files that match the target:
19
+ - **Non-React (plain HTML/static/Astro/Vue/plain JS):** `hero-scroll.html` +
20
+ `hero-scroll.css` + `hero-scroll.js`. The engine **auto-boots** against the
21
+ document when `window.HS_CONFIG` is set, loaded via
22
+ `<script type="module" src="hero-scroll.js">`.
23
+ - **React / TypeScript:** `HeroScroll.tsx` + `hero-scroll.js` +
24
+ `hero-scroll.d.ts` + `hero-scroll.css`. `HeroScroll.tsx` renders the markup
25
+ as JSX and mounts the SAME engine via `initHeroScroll(rootEl, config)` in a
26
+ `useEffect` (destroy on unmount). Props gate the optional chrome; import the
27
+ scrub mp4 + poster as assets.
28
+
29
+ The engine is config-driven (`HS_CONFIG` / the config arg) and presence-gated:
30
+ delete an optional element (or omit a `HeroScroll` prop) and its feature
31
+ vanishes cleanly. It resolves the real scroll container (window / `body` /
32
+ overflow ancestor) so body-scroll apps work. Do NOT rewrite the engine from
33
+ memory in any framework; copy it and configure it.
34
+
35
+ ---
36
+
37
+ ## 1 · Intake — resolve these BEFORE coding
38
+
39
+ 1. **Footage source** (pick one):
40
+ - **(a) Provided video** — skip to §3 (still needs the scrub re-encode).
41
+ - **(b) Generate the footage** with whatever image/video generation is
42
+ configured — KIE MCP servers (`kie-image-mcp` / `kie-video-mcp`),
43
+ Higgsfield MCP, the model's own built-in image/video generation, or any
44
+ other configured image/video tool — §2.
45
+ - **(c) No footage possible** — this pattern is wrong; use a static hero.
46
+ 2. **Brand tokens** — map `--hs-bg / --hs-ink / --hs-dim / --hs-accent /
47
+ --hs-display / --hs-mono` to the host site's design system (DESIGN.md or
48
+ existing CSS custom properties). Never ship the template defaults into a
49
+ branded site.
50
+ 3. **Optional controls** — ask or infer which to include:
51
+ HUD (telemetry value + segments) · PLAY (film-speed autoplay) ·
52
+ TOP/END (veiled jumps) · overlays (copy choreographed over the film) ·
53
+ loader gate. Each is one HTML block; the engine self-configures.
54
+ 4. **Telemetry metric** — mph is only the obvious one. Altitude, depth, RPM,
55
+ temperature, distance, watts — pick what fits the product. Configure
56
+ `max`, `unit` (in markup), and the curve in `HS_CONFIG.telemetry`.
57
+ 5. **Scroll length** — 15–20vh of journey height per second of footage
58
+ (32s film → 560–640vh). Shorter feels rushed; longer feels sticky.
59
+ 6. **Sweep** — the engine is page-aware by default, hero-scoped by option
60
+ (`HS_CONFIG.sweep`). It only ever owns the journey block + fixed chrome;
61
+ it never touches the host page's DOM below the hero. `sweep: "page"`
62
+ (default): PLAY glides on through the rest of the page to the bottom and
63
+ END jumps there — the full cinematic ride, for when the whole landing
64
+ page shares the film's design system and should be swept through.
65
+ `sweep: "hero"`: PLAY and END stop at the journey's end — you land on the
66
+ regular HTML section right after the hero, exactly as a normal scroll
67
+ would, the polite mode that never drives the user through the rest of
68
+ the page uninvited; opt into this explicitly in the prompt (or
69
+ `HS_CONFIG`) when only the hero should be owned.
70
+ Scrub smoothness is identical in both modes; sweep only changes where
71
+ autoplay/jumps consider "the end". (Named `sweep`, never `reach`, so it
72
+ can't be confused with a spec/plan's route/DOM-scope terminology.)
73
+
74
+ ## 2 · Footage pipeline (generation path)
75
+
76
+ The generation backend is pluggable — the pipeline below is the same whatever
77
+ produces the frames. Route the image/video calls to whatever is configured:
78
+ KIE MCP servers (`kie-image-mcp` / `kie-video-mcp`), Higgsfield MCP, the model's
79
+ own built-in image/video generation, or any other configured image/video tool.
80
+ The parameter names below (`reference_paths`, `image_paths`) are KIE/Kling's;
81
+ other backends expose the equivalent reference-image and first/last-frame inputs
82
+ under their own names — map to whichever you're using.
83
+
84
+ > **If using the KIE MCP servers** (Babylon Toolkit projects): fetch the
85
+ > Babylon Toolkit Agent Reference and its `web-kie-servers.md` sub-document
86
+ > first (per CLAUDE.md). Those servers read the key from `.env` — a file named
87
+ > `env` is ignored.
88
+
89
+ 1. **One hero anchor image first** (image model, e.g. nano-banana-pro, 16:9,
90
+ 2K). Every other asset references it (reference/anchor image input) so it is
91
+ the same product everywhere.
92
+ 2. **Chain clips start→end frame** so the journey is continuous (pin each
93
+ clip's first frame, and where supported its last frame — KIE/Kling exposes
94
+ this as `image_paths`: `[0]` = first frame, `[1]` = last frame):
95
+ - Clip 1: first = a generated "concealed" variant (dust/fog/dark), last =
96
+ the hero anchor → the reveal.
97
+ - Clip N: first = **ffmpeg-extracted last frame of clip N-1**:
98
+ `ffmpeg -y -sseof -0.1 -i clipN-1.mp4 -frames:v 1 -update 1 -q:v 2 last.jpg`
99
+ - Final clip: pin the last frame to a generated destination still.
100
+ - Big lighting changes (day→night) must happen INSIDE a clip's prompt
101
+ ("light dies to dusk as…"), never across a cut — chaining can't bridge a
102
+ lighting jump.
103
+ 3. **Know your video model's real output** (example — KIE/Kling 3: `std` mode
104
+ returns 1284×716, not 1080p — use `pro` if that matters; clips run ~8.04s
105
+ for a requested 8s). Whatever the backend, ffprobe everything; never assume
106
+ resolutions or durations.
107
+ 4. **Concat + scrub encode** (the scrub encode is NON-NEGOTIABLE):
108
+ ```
109
+ ffmpeg -y -f concat -safe 0 -i list.txt -c:v libx264 -preset slow -crf 18 -pix_fmt yuv420p -an journey.mp4
110
+ ffmpeg -y -i journey.mp4 -vf "scale=1280:720,fps=24" -c:v libx264 -preset slow -crf 21 \
111
+ -g 1 -keyint_min 1 -pix_fmt yuv420p -movflags +faststart -an journey-scrub.mp4
112
+ ```
113
+ `-g 1` makes every frame a keyframe → `currentTime` seeks are instant.
114
+ Without it, scrubbing stutters and the whole illusion dies. Expect the
115
+ scrub file to be LARGER than the source (~20MB for 32s @720p — fine, it's
116
+ blob-preloaded once).
117
+ 5. **Poster** = the FIRST frame of journey.mp4 (`-frames:v 1`), not a glamour
118
+ still — otherwise the video swap after preload is a visible jump-cut.
119
+
120
+ ## 3 · Wire-up
121
+
122
+ 0. **Pick the host path (§ Templates above).** Non-React: copy
123
+ `hero-scroll.{html,css,js}`. React/TS: copy `HeroScroll.tsx` +
124
+ `hero-scroll.js` + `hero-scroll.d.ts` + `hero-scroll.css`, render
125
+ `<HeroScroll config={…} … />` where the hero goes, and import the scrub
126
+ mp4 + poster as assets. Either way the engine (`hero-scroll.js`) is
127
+ identical — do not fork it per framework. In React, `HeroScroll.tsx`
128
+ already handles mount (`initHeroScroll`) and unmount (`.destroy()`); under
129
+ StrictMode the init/destroy cycle runs twice in dev — that's fine, teardown
130
+ is complete.
131
+ 1. Copy the files; rename/prefix if the host has conflicts (everything is
132
+ `hs-` namespaced already).
133
+ 2. Replace the placeholder 100vh hero with the `#hs-journey` block (or the
134
+ `<HeroScroll>` element); with `sweep: "page"` the rest of the page below it
135
+ automatically becomes the autoplay "tail". **Sticky constraint:** the
136
+ journey block must sit in plain document flow — ideally a direct child of
137
+ `<body>` (in React, avoid wrapping it in transformed/overflow-clipped
138
+ layout containers). Any ancestor with `transform`, `filter`, `perspective`,
139
+ or `overflow: hidden/auto` breaks `position: sticky` and kills the entire
140
+ effect. Check for this FIRST when retrofitting. (The engine auto-resolves
141
+ whether the window or `document.body`/an overflow ancestor is the scroller,
142
+ so body-scroll apps scrub correctly without config.)
143
+ 3. **Handoff rule:** the first section below the journey must tonally
144
+ continue the film's final frame (e.g. a film that ends dark must hand off
145
+ into a dark section, not a bright one).
146
+ If the host page goes bright the pixel after the sticky stage releases,
147
+ the cut is jarring and no engine work hides it. Either restyle that first
148
+ section to match the film's exit tone, insert a short bridge section that
149
+ grades from the film's last frame to the host palette, or use
150
+ `sweep: "hero"` and let the user cross the seam themselves.
151
+ 4. Author overlays as `.hs-ovl` sections with `data-from`/`data-to` progress
152
+ windows. Leave gaps between windows — moments of pure film are what make
153
+ it cinematic. Overlay copy over bright footage gets `.hs-scrim`.
154
+ 5. Set the config: `window.HS_CONFIG` in the page (non-React) or the
155
+ `config` / control props on `<HeroScroll>` (React) — video path, telemetry
156
+ curve, segments, sweep.
157
+ 6. The engine blob-preloads the footage with a loading bar, locks scroll
158
+ until ready (`body[data-hs-state]`), degrades to the poster if footage is
159
+ missing, and honors `prefers-reduced-motion`.
160
+
161
+ ## 4 · Calibration (what makes it feel expensive)
162
+
163
+ - **Telemetry launch point:** extract frames around the moment the subject
164
+ first MOVES (`ffmpeg -ss <t> -i journey-scrub.mp4 -frames:v 1 f.jpg`, eye
165
+ them) and set `telemetry.startP = t_launch / film_duration`. Generated
166
+ clips hold the subject still for a beat after a cut — the clip boundary is
167
+ NOT the launch. A needle pinned at 0 until the exact frame motion begins,
168
+ then ripping (exponent ≈ 0.45), reads as real telemetry; starting early
169
+ reads as fake and users notice immediately.
170
+ - **Segments** align to the source clips (equal clips → quarters).
171
+ - **Autoplay speed is film speed:** the engine converts journey pixels ÷ film
172
+ seconds so PLAY scrubs at exactly 1× real time. Do not "improve" this with
173
+ a fixed px/s rate.
174
+ - **Veiled cuts, never visible rewinds:** replay/jumps fade to black, snap
175
+ scroll + smoothed progress + video time UNDER the veil, then lift. The
176
+ smoothed-progress snap is mandatory — skipping it causes a reverse-scrub
177
+ flash when the veil lifts.
178
+
179
+ ## 5 · Verification protocol (do not skip; run in a real browser)
180
+
181
+ Drive the page with chrome-devtools MCP and require ALL of:
182
+
183
+ 1. **Scrub sync sweep** — for p in {0, .1, .25, .34, .5, .62, .75, .87, 1}:
184
+ scroll to `p × (journeyHeight − innerHeight)`, wait ~1.5s for the lerp,
185
+ assert `|video.currentTime − p × duration| ≤ 0.05s`.
186
+ 2. **Telemetry** — value is 0 for all p ≤ startP (sample densely around
187
+ startP: e.g. startP − .01, startP + .01), monotonic after, max at endP,
188
+ segment labels flip at their thresholds.
189
+ 3. **Autoplay** — after PLAY, video.currentTime advances ≈1.0s per real
190
+ second; reaches document bottom; button resets.
191
+ 4. **Cancel** — dispatch a wheel event mid-autoplay: scroll halts within a
192
+ frame, button resets. (Dispatch on `document.body`, not `window` — and the
193
+ engine's guard must survive non-Node targets.)
194
+ 5. **Veiled cut timeline** — sample during replay-from-end: scrollY must NOT
195
+ change until veil computed opacity = 1; video time snaps while opacity =
196
+ 1; veil fully lifts; film rolls.
197
+ 6. **Console** — zero errors across the whole run.
198
+
199
+ ## 6 · What the prompt supplies vs. what this skill decides
200
+
201
+ The mechanics are deterministic — the invoking prompt should NEVER need to
202
+ specify scrub encoding, preloading, autoplay speed, veils, cancel semantics,
203
+ or verification. It supplies only the creative decisions; everything missing
204
+ falls back to the defaults in parentheses:
205
+
206
+ - **Product + footage beats** — what moves through what terrain, per clip
207
+ (or an existing video file path).
208
+ - **Sweep** — `page` or `hero` (default `page`: PLAY/END sweep through to the
209
+ document bottom; specify `sweep: hero` explicitly to have PLAY/END stop at
210
+ the journey's end so the next HTML section follows normally instead).
211
+ - **Controls** — which of HUD / PLAY / TOP+END to include (default: all
212
+ three on `sweep: page`, HUD + PLAY only on `sweep: hero`).
213
+ - **Telemetry metric** — value, unit, max, segment names (default: none —
214
+ omit the HUD rather than invent a metric that doesn't fit the product).
215
+ - **Overlays** — the marketing copy and which film moment each block rides
216
+ (default: a single title overlay at the start).
217
+ - **Brand** — tokens/typography, mapped onto `--hs-*` (default: inherit the
218
+ host site's design system; never ship the template placeholders).
219
+
220
+ The subject is whatever the prompt names — the mechanics are identical for a
221
+ watch, a submarine, a building, a person, a dashboard, a landscape. Do NOT
222
+ default to any one look (a vehicle, a desert, cyan-on-black); derive every
223
+ visual from the prompt's own subject. The skeletons below are product-agnostic.
224
+
225
+ > **Easy front door:** the sibling `bt-hero` skill automates this intake — it
226
+ > takes an idea / image / partial answers in any combination, asks only the
227
+ > unanswered questions, applies the defaults, writes a repeatable
228
+ > `_hero-brief.md`, and builds in one shot (or contributes intake when named
229
+ > inside a bt-spec brief). The skeletons below are the manual alternative.
230
+ > Note the skeletons carry ONLY creative slots — pipeline facts (aspect, clip
231
+ > duration, muting, encode, verification) are skill/backend-owned and never
232
+ > belong in an invoking prompt.
233
+
234
+ **Example — greenfield, full-page (skeleton):**
235
+ > Redesign this starter template as a cinematic 3D-scroll site for `<PRODUCT>` —
236
+ > `<one-line description>` (bt-design → 3D-Hero-Scroll, sweep: page).
237
+ > FOOTAGE (generate) — hero anchor first: `<the product, one
238
+ > clear look>`; chain: ① `<reveal beat>` ② `<motion beat>` ③ `<terrain/scene
239
+ > change>` ④ `<finale beat>`. CONTROLS — HUD: `<METRIC> 0→<MAX> <UNIT>`,
240
+ > segments `<A/B/C/D>` · PLAY (`"<label>"`) · TOP/END. OVERLAYS — `<wordmark>`
241
+ > → count-up stats (`<A · B · C>`) → `<feature moment>` → `<closing line>`.
242
+ > BELOW THE JOURNEY — `<sections>` → `<CTA>`. BRAND — `<palette>`, `<accent>`,
243
+ > `<type>` (derived from the subject, not a fixed house style).
244
+
245
+ **Example — retrofit, hero only (skeleton):**
246
+ > Redesign the hero section with 3D scrolling (sweep: hero). Footage:
247
+ > `<existing file | generate: anchor + beats 1..N>`. HUD: `<METRIC> 0→<MAX>
248
+ > <UNIT>` (or none). PLAY only, no jump nav. One headline overlay at the start,
249
+ > `<N>` stats mid-run. Match our existing design tokens; don't touch anything
250
+ > below the hero.
251
+
252
+ Anything below the journey (configurators, CTAs, footers) is ordinary
253
+ bt-design work — this reference governs only the journey block, its
254
+ overlays, and the fixed chrome. Apply the handoff rule (§3) at the seam.
255
+
256
+ ## 7 · Known pitfalls (all hit in production; all handled by the template)
257
+
258
+ - Overlay windows starting at 0 must skip their fade-in (`rise = 1` when
259
+ `from ≤ 0`) or the hero overlay renders invisible at page load.
260
+ - Cancel-input handlers must tolerate non-Node event targets, and must
261
+ exempt keydown on the play button itself (else Enter = stop-then-restart).
262
+ - All veil/autoplay callbacks are token-guarded: any stop/cancel increments
263
+ the token, so a cancel during a fade can't leave a ghost timeline running.
264
+ - Blob-preload (not streaming) is what makes seeking instant even without
265
+ range-request support on the dev server.
266
+ - Count-up stats re-arm when their overlay fully exits, so they replay on
267
+ re-entry in either scroll direction.
268
+ - The HUD hides itself when the journey hands off to normal-flow sections;
269
+ jump-nav buttons dim at the end they're already at.
@@ -0,0 +1,167 @@
1
+ /* ═══════════════════════════════════════════════════════════════════
2
+ HeroScroll.tsx — React wrapper for the framework-agnostic hero-scroll
3
+ engine. Renders the `hs-` markup as JSX and mounts `initHeroScroll`
4
+ against its own root, tearing down cleanly on unmount.
5
+
6
+ The engine (hero-scroll.js) is the SAME code the plain-HTML template
7
+ uses — do NOT re-implement the scrubbing/autoplay/veil logic in React.
8
+ Copy this file + hero-scroll.js + hero-scroll.d.ts + hero-scroll.css
9
+ into the app, import your scrub mp4 + poster as assets, and configure.
10
+
11
+ Example (Vite/React):
12
+
13
+ import HeroScroll from "./HeroScroll";
14
+ import scrub from "./assets/journey-scrub.mp4";
15
+ import poster from "./assets/poster.jpg";
16
+
17
+ // Values below are placeholders — derive every label, metric, segment,
18
+ // and overlay from the prompt's own subject; do not copy these verbatim.
19
+ <HeroScroll
20
+ config={{ video: scrub, sweep: "page",
21
+ telemetry: { max: MAX, startP: START_P, endP: 0.95,
22
+ segments: [[0,"SEG A"],[0.25,"SEG B"],
23
+ [0.5,"SEG C"],[0.75,"SEG D"]] } }}
24
+ posterSrc={poster}
25
+ journeyVh={640}
26
+ hud={{ unit: "UNIT" }}
27
+ play={{ idle: "PLAY" }}
28
+ jumpNav
29
+ overlays={
30
+ <>
31
+ <section className="hs-ovl" data-from="0" data-to="0.115">
32
+ <h1>WORDMARK</h1>
33
+ </section>
34
+ <section className="hs-ovl hs-scrim" data-from="0.26" data-to="0.46">
35
+ <span data-hs-count data-target={STAT} data-group="1">0</span> STAT
36
+ </section>
37
+ </>
38
+ }
39
+ />
40
+
41
+ Brand: map --hs-* tokens to the host design system in hero-scroll.css
42
+ (or override on a wrapper). Sticky constraint: no ancestor of this
43
+ component may have transform / filter / perspective / overflow:hidden —
44
+ it breaks position:sticky and kills the effect.
45
+ ═══════════════════════════════════════════════════════════════════ */
46
+
47
+ import { useEffect, useRef, type ReactNode } from "react";
48
+ import { initHeroScroll, type HeroScrollConfig } from "./hero-scroll";
49
+ import "./hero-scroll.css";
50
+
51
+ export interface HeroScrollProps {
52
+ /** Engine config — video path (imported asset URL), telemetry, sweep, etc. */
53
+ config: HeroScrollConfig;
54
+ /** Poster = the FIRST frame of the scrub file (not a glamour still). */
55
+ posterSrc?: string;
56
+ /** Scroll length in vh: ~15–20vh per second of footage (default 640). */
57
+ journeyVh?: number;
58
+ /** Preload gate. Pass a node for the loader mark, `true` for a bare bar,
59
+ * or omit/false to skip the gate. */
60
+ loader?: ReactNode | boolean;
61
+ /** Telemetry HUD (requires config.telemetry). `unit` is the label text. */
62
+ hud?: { unit: string } | false;
63
+ /** Film-speed autoplay control with its idle/busy labels. */
64
+ play?: { idle: string; busy?: string } | false;
65
+ /** Veiled TOP/END jump nav. */
66
+ jumpNav?: boolean;
67
+ /** `.hs-ovl[data-from][data-to]` overlay sections (author as children). */
68
+ overlays?: ReactNode;
69
+ /** Extra class on the journey wrapper. */
70
+ className?: string;
71
+ }
72
+
73
+ function HeroScroll({
74
+ config,
75
+ posterSrc,
76
+ journeyVh = 640,
77
+ loader,
78
+ hud,
79
+ play,
80
+ jumpNav,
81
+ overlays,
82
+ className,
83
+ }: HeroScrollProps) {
84
+ const rootRef = useRef<HTMLDivElement>(null);
85
+
86
+ useEffect(() => {
87
+ if (!rootRef.current) return;
88
+ const hs = initHeroScroll(rootRef.current, config);
89
+ return () => hs.destroy();
90
+ // Re-init only if the video source changes; other config is read once.
91
+ // eslint-disable-next-line react-hooks/exhaustive-deps
92
+ }, [config.video]);
93
+
94
+ return (
95
+ <div ref={rootRef} className={className}>
96
+ {loader ? (
97
+ <div id="hs-loader" role="status" aria-label="Loading">
98
+ {typeof loader !== "boolean" ? (
99
+ <div className="hs-loader-mark">{loader}</div>
100
+ ) : null}
101
+ <div className="hs-loader-bar">
102
+ <span id="hs-loader-fill" />
103
+ </div>
104
+ <span id="hs-loader-pct">000</span>
105
+ </div>
106
+ ) : null}
107
+
108
+ {play ? (
109
+ <button id="hs-play" aria-pressed="false" title="Play — cinematic auto-scroll">
110
+ <span className="hs-play-icon" aria-hidden="true" />
111
+ <span
112
+ className="hs-play-label"
113
+ data-idle={play.idle}
114
+ data-busy={play.busy ?? "STOP"}
115
+ >
116
+ {play.idle}
117
+ </span>
118
+ </button>
119
+ ) : null}
120
+
121
+ {jumpNav ? (
122
+ <nav id="hs-jump-nav" aria-label="Quick navigation">
123
+ <button id="hs-top" title="Go to top">
124
+ <span className="hs-arrow" aria-hidden="true">↑</span>
125
+ <span>TOP</span>
126
+ </button>
127
+ <button id="hs-end" title="Skip to the end">
128
+ <span className="hs-arrow" aria-hidden="true">↓</span>
129
+ <span>END</span>
130
+ </button>
131
+ </nav>
132
+ ) : null}
133
+
134
+ {hud ? (
135
+ <aside id="hs-hud" aria-label="Telemetry">
136
+ <div>
137
+ <span id="hs-hud-value">0</span>{" "}
138
+ <span className="hs-hud-unit">{hud.unit}</span>
139
+ </div>
140
+ <div className="hs-hud-bar">
141
+ <span id="hs-hud-progress" />
142
+ </div>
143
+ <div className="hs-hud-meta">
144
+ <span id="hs-hud-seg" />
145
+ <span id="hs-hud-pct">000%</span>
146
+ </div>
147
+ </aside>
148
+ ) : null}
149
+
150
+ <div id="hs-journey" style={{ height: `${journeyVh}vh` }}>
151
+ <div className="hs-stage">
152
+ <video
153
+ id="hs-video"
154
+ muted
155
+ playsInline
156
+ preload="none"
157
+ poster={posterSrc}
158
+ />
159
+ <div className="hs-stage-shade" aria-hidden="true" />
160
+ {overlays}
161
+ </div>
162
+ </div>
163
+ </div>
164
+ );
165
+ }
166
+
167
+ export default HeroScroll;