mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-08-31 02:41:44 +00:00
docs(examples): make hover-audio accessible, lazy-loaded, and guard-complete
HoverVideo now: (1) exposes a focusable button that toggles/plays sound by
keyboard, touch and pointer — hover is an enhancement, not the only path;
(2) gates the source behind an IntersectionObserver so offscreen cards no longer
download/decode, releasing the buffer on exit; (3) still blocks autoplay under
reduced motion while allowing voluntary playback via the button. Routed all 30
Thirty-Days cards through it (hasAudio={false} for the 12 silent ones) so no raw
<video autoPlay> bypasses the motion guard. Flagged by Magi (P1 #4/#5/#6).
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
export const HoverVideo = ({ src, poster, className }) => {
|
||||
export const HoverVideo = ({ src, poster, className, hasAudio = true }) => {
|
||||
const videoRef = useRef(null);
|
||||
const wrapRef = useRef(null);
|
||||
const [muted, setMuted] = useState(true);
|
||||
const [inView, setInView] = useState(false);
|
||||
|
||||
// Lazy initializer, not a post-mount effect: with useState(false) the first
|
||||
// committed render would emit `<video src autoPlay loop>` and only then pull the
|
||||
// attributes, so a reduce-motion visitor has already started fetching the file.
|
||||
// Lazy initializer, not a post-mount effect: the preference is known on the
|
||||
// first committed render, so a reduce-motion visitor never autoplays first.
|
||||
const [reduced, setReduced] = useState(
|
||||
() =>
|
||||
typeof window !== "undefined" &&
|
||||
@@ -18,24 +19,44 @@ export const HoverVideo = ({ src, poster, className }) => {
|
||||
return () => query.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
// React can drop src/autoPlay/loop from the DOM, but that neither pauses a
|
||||
// playing element nor aborts its resource: pause(), removeAttribute("src") and
|
||||
// load() are all required when the preference flips to reduce mid-session.
|
||||
// Only assign/decode a source near the viewport. Without this every card on
|
||||
// the page downloads and decodes its MP4 immediately, on- or off-screen.
|
||||
useEffect(() => {
|
||||
if (!reduced) return;
|
||||
const el = wrapRef.current;
|
||||
if (!el || typeof IntersectionObserver !== "function") {
|
||||
setInView(true);
|
||||
return;
|
||||
}
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => setInView(entries[0]?.isIntersecting ?? false),
|
||||
{ rootMargin: "200px" },
|
||||
);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// Release the decoded resource once the card leaves the viewport. React props
|
||||
// alone neither pause an element nor abort its download, so pause() +
|
||||
// removeAttribute("src") + load() are all required. This same teardown is what
|
||||
// stops autoplay when the reduce-motion preference flips on mid-session.
|
||||
useEffect(() => {
|
||||
if (inView && !reduced) return;
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
video.pause();
|
||||
video.removeAttribute("src");
|
||||
video.load();
|
||||
}, [reduced]);
|
||||
if (!inView) {
|
||||
video.removeAttribute("src");
|
||||
video.load();
|
||||
setMuted(true);
|
||||
}
|
||||
}, [inView, reduced]);
|
||||
|
||||
const hear = () => {
|
||||
if (reduced) return;
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
if (!video || !hasAudio) return;
|
||||
setMuted(false);
|
||||
video.muted = false;
|
||||
// Voluntary playback — allowed even under reduced motion (autoplay is not).
|
||||
video.play().catch(() => {});
|
||||
};
|
||||
const silence = () => {
|
||||
@@ -43,20 +64,43 @@ export const HoverVideo = ({ src, poster, className }) => {
|
||||
const video = videoRef.current;
|
||||
if (video) video.muted = true;
|
||||
};
|
||||
const toggle = () => (muted ? hear() : silence());
|
||||
|
||||
const autoplaying = inView && !reduced;
|
||||
|
||||
return (
|
||||
<video
|
||||
ref={videoRef}
|
||||
className={className}
|
||||
src={reduced ? undefined : src}
|
||||
poster={poster}
|
||||
autoPlay={!reduced}
|
||||
muted={muted}
|
||||
loop={!reduced}
|
||||
playsInline
|
||||
preload="none"
|
||||
onMouseEnter={hear}
|
||||
onMouseLeave={silence}
|
||||
/>
|
||||
<div ref={wrapRef} className={`relative overflow-hidden ${className ?? ""}`}>
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
src={inView ? src : undefined}
|
||||
poster={poster}
|
||||
autoPlay={autoplaying}
|
||||
muted={muted}
|
||||
loop={autoplaying}
|
||||
playsInline
|
||||
preload="none"
|
||||
onMouseEnter={() => {
|
||||
if (hasAudio && !reduced) hear();
|
||||
}}
|
||||
onMouseLeave={silence}
|
||||
/>
|
||||
{hasAudio && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
aria-pressed={!muted}
|
||||
aria-label={muted ? "Play this preview with sound" : "Mute this preview"}
|
||||
className="absolute left-2 top-2 z-10 inline-flex items-center gap-1 rounded-full bg-black/60 px-2.5 py-1 text-xs font-medium text-white backdrop-blur transition hover:bg-black/75 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
|
||||
>
|
||||
{muted ? (
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M11 5 6 9H2v6h4l5 4V5z" /><line x1="23" y1="9" x2="17" y2="15" /><line x1="17" y1="9" x2="23" y2="15" /></svg>
|
||||
) : (
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M11 5 6 9H2v6h4l5 4V5z" /><path d="M15.5 8.5a5 5 0 0 1 0 7" /><path d="M18.5 5.5a9 9 0 0 1 0 13" /></svg>
|
||||
)}
|
||||
{muted ? "Sound" : "On"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+12
-12
@@ -19,13 +19,13 @@ Days 1 to 9 cover the inputs: a repo, a track, a topic, a speaker, a design file
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Day 1 — Install HyperFrames" href="https://x.com/HeyGen/status/2074176916819685648">
|
||||
<video className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-01.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-01.jpg" autoPlay muted loop playsInline preload="none" />
|
||||
<HoverVideo className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-01.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-01.jpg" hasAudio={false} />
|
||||
|
||||
Give your coding agent the HyperFrames skills. One router reads the request and picks the workflow.
|
||||
|
||||
</Card>
|
||||
<Card title="Day 2 — Give your brand a motion language" href="https://x.com/HeyGen/status/2074574265714905167">
|
||||
<video className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-02.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-02.jpg" autoPlay muted loop playsInline preload="none" />
|
||||
<HoverVideo className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-02.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-02.jpg" hasAudio={false} />
|
||||
|
||||
Turn a visual identity into reusable motion direction, written down once as `FRAME.md` and read on every later build.
|
||||
|
||||
@@ -55,7 +55,7 @@ Days 1 to 9 cover the inputs: a repo, a track, a topic, a speaker, a design file
|
||||
|
||||
</Card>
|
||||
<Card title="Day 7 — Motion graphics" href="https://x.com/HeyGen/status/2076381187640324427">
|
||||
<video className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-07.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-07.jpg" autoPlay muted loop playsInline preload="none" />
|
||||
<HoverVideo className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-07.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-07.jpg" hasAudio={false} />
|
||||
|
||||
Make a short, design-led animation where the motion carries the message.
|
||||
|
||||
@@ -67,7 +67,7 @@ Days 1 to 9 cover the inputs: a repo, a track, a topic, a speaker, a design file
|
||||
|
||||
</Card>
|
||||
<Card title="Day 9 — Figma to HyperFrames" href="https://x.com/HeyGen/status/2077072141309382770">
|
||||
<video className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-09.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-09.jpg" autoPlay muted loop playsInline preload="none" />
|
||||
<HoverVideo className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-09.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-09.jpg" hasAudio={false} />
|
||||
|
||||
Turn an existing design into a video composition — as live HTML, not a flattened export.
|
||||
|
||||
@@ -80,7 +80,7 @@ Days 10 to 19 are about steering: how you ask, what you inspect, and what you fi
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Day 10 — Anatomy of a prompt" href="https://x.com/HeyGen/status/2077438104982667282">
|
||||
<video className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-10.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-10.jpg" autoPlay muted loop playsInline preload="none" />
|
||||
<HoverVideo className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-10.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-10.jpg" hasAudio={false} />
|
||||
|
||||
Structure a request so the agent gets the important intent right the first time.
|
||||
|
||||
@@ -98,19 +98,19 @@ Days 10 to 19 are about steering: how you ask, what you inspect, and what you fi
|
||||
|
||||
</Card>
|
||||
<Card title="Day 13 — Studio preview, part 1" href="https://x.com/HeyGen/status/2078589328562016559">
|
||||
<video className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-13.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-13.jpg" autoPlay muted loop playsInline preload="none" />
|
||||
<HoverVideo className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-13.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-13.jpg" hasAudio={false} />
|
||||
|
||||
Inspect a composition before rendering — in Studio, through your agent, or from the terminal.
|
||||
|
||||
</Card>
|
||||
<Card title="Day 14 — Studio preview, part 2: keyframes" href="https://x.com/HeyGen/status/2078945075787452660">
|
||||
<video className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-14.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-14.jpg" autoPlay muted loop playsInline preload="none" />
|
||||
<HoverVideo className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-14.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-14.jpg" hasAudio={false} />
|
||||
|
||||
Adjust motion with visible keyframe controls, one lane per animated property.
|
||||
|
||||
</Card>
|
||||
<Card title="Day 15 — Sample the web" href="https://x.com/HeyGen/status/2079251321484722675">
|
||||
<video className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-15.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-15.jpg" autoPlay muted loop playsInline preload="none" />
|
||||
<HoverVideo className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-15.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-15.jpg" hasAudio={false} />
|
||||
|
||||
Bring useful web motion into a composition by capturing the real page.
|
||||
|
||||
@@ -147,13 +147,13 @@ Days 20 to 30 move from one composition to a repeatable pipeline: reusable block
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Day 20 — Claude Design" href="https://x.com/HyperFrames_/status/2081184676430160379">
|
||||
<video className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-20.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-20.jpg" autoPlay muted loop playsInline preload="none" />
|
||||
<HoverVideo className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-20.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-20.jpg" hasAudio={false} />
|
||||
|
||||
Take a design direction into a HyperFrames project without redrawing it by hand.
|
||||
|
||||
</Card>
|
||||
<Card title="Day 21 — Cloud rendering" href="https://x.com/HyperFrames_/status/2081491370485952790">
|
||||
<video className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-21.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-21.jpg" autoPlay muted loop playsInline preload="none" />
|
||||
<HoverVideo className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-21.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-21.jpg" hasAudio={false} />
|
||||
|
||||
Render without making one local computer the bottleneck. Two commands hand the job to the cloud.
|
||||
|
||||
@@ -183,7 +183,7 @@ Days 20 to 30 move from one composition to a repeatable pipeline: reusable block
|
||||
|
||||
</Card>
|
||||
<Card title="Day 26 — Media effects and overlays" href="https://x.com/HyperFrames_/status/2083324288632045910">
|
||||
<video className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-26.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-26.jpg" autoPlay muted loop playsInline preload="none" />
|
||||
<HoverVideo className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-26.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-26.jpg" hasAudio={false} />
|
||||
|
||||
Add designed treatments to real media. Two to six authored colour ramps change the whole visual language.
|
||||
|
||||
@@ -207,7 +207,7 @@ Days 20 to 30 move from one composition to a repeatable pipeline: reusable block
|
||||
|
||||
</Card>
|
||||
<Card title="Day 30 — Thirty for thirty">
|
||||
<video className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-30.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-30.jpg" autoPlay muted loop playsInline preload="none" />
|
||||
<HoverVideo className="w-full aspect-video rounded-lg object-cover bg-zinc-100 dark:bg-zinc-900" src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-30.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-30.jpg" hasAudio={false} />
|
||||
|
||||
Thirty days in one cut — a second from every lesson, back to back. From install on Day 1 to the whole series here.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user