From 89c3beb099c6d05cc401a63f770afa4178826ae1 Mon Sep 17 00:00:00 2001 From: ukimsanov Date: Sun, 25 Oct 2026 21:59:14 -0400 Subject: [PATCH 1/5] docs: an FAQ, a free-agents page, and 30 Days as a numbered list The FAQ answers the questions people ask the docs assistant most, each linking to its page. Make videos with a free agent shows OpenCode's free models in the Studio app, local models through Ollama, and how open models scored in Code2Video. 30 Days of HyperFrames becomes one numbered row per lesson with its film on hover, and drops a link to a section that no longer exists. --- docs/custom.css | 19 ++++ docs/docs.json | 4 +- docs/faq.mdx | 157 ++++++++++++++++++++++++++ docs/guides/free-agents.mdx | 54 +++++++++ docs/snippets/day-list.jsx | 62 +++++++++++ docs/thirty-days.mdx | 214 +++--------------------------------- 6 files changed, 313 insertions(+), 197 deletions(-) create mode 100644 docs/faq.mdx create mode 100644 docs/guides/free-agents.mdx create mode 100644 docs/snippets/day-list.jsx diff --git a/docs/custom.css b/docs/custom.css index a5480fe1948..d8a4ba88750 100644 --- a/docs/custom.css +++ b/docs/custom.css @@ -1670,3 +1670,22 @@ pre.shiki { .code-block-fade-overlay { background-image: linear-gradient(to right, transparent, var(--hf-code-bg) 24px) !important; } + +/* 30 Days: a numbered syllabus, one row per lesson, the film previews on hover */ +.hf-days { list-style: none; margin: 1rem 0 2rem; padding: 0; border-top: 1px solid rgb(0 0 0 / 0.08); } +.hf-day { display: grid; grid-template-columns: 3.25rem minmax(0, 1fr) 10.5rem; gap: 1.25rem; align-items: center; + padding: 0.9rem 0; border-bottom: 1px solid rgb(0 0 0 / 0.08); margin: 0; } +.hf-day-n { font-size: 1.75rem; font-weight: 600; line-height: 1; letter-spacing: -0.02em; color: rgb(0 0 0 / 0.28); + font-variant-numeric: tabular-nums; } +.hf-day-title { font-weight: 600; font-size: 1rem; color: inherit; text-decoration: none; } +.hf-day-title:hover { text-decoration: underline; text-underline-offset: 3px; } +.hf-day-text p { margin: 0.2rem 0 0; font-size: 0.875rem; line-height: 1.45; opacity: 0.72; } +.hf-day-film { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; border-radius: 0.5rem; background: rgb(0 0 0 / 0.06); } +.dark .hf-days, .dark .hf-day { border-color: rgb(255 255 255 / 0.1); } +.dark .hf-day-n { color: rgb(255 255 255 / 0.3); } +.dark .hf-day-film { background: rgb(255 255 255 / 0.06); } +@media (max-width: 640px) { + .hf-day { grid-template-columns: 2.25rem minmax(0, 1fr) 6.5rem; gap: 0.75rem; } + .hf-day-n { font-size: 1.25rem; } +} + diff --git a/docs/docs.json b/docs/docs.json index f39d97e4b41..316b075492d 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -56,7 +56,8 @@ "pages": [ "introduction", "quickstart", - "examples" + "examples", + "faq" ] }, { @@ -82,6 +83,7 @@ "guides/with-codex", "guides/with-cursor", "guides/mcp", + "guides/free-agents", "guides/skills", "guides/plugins", "guides/design-tools" diff --git a/docs/faq.mdx b/docs/faq.mdx new file mode 100644 index 00000000000..11ff84c65f8 --- /dev/null +++ b/docs/faq.mdx @@ -0,0 +1,157 @@ +--- +title: "HyperFrames FAQ" +sidebarTitle: "FAQ" +description: "Short answers to the questions people ask most about HyperFrames: cost, which agent to use, Windows, Studio, Remotion, rendering, audio, and fonts." +--- + +Short answers to the questions people ask the docs assistant most often. Each +answer links to the page with the details. + +## Is HyperFrames free? + +Yes. The framework, CLI, and skills are open source under Apache 2.0, and +rendering on your own computer is free and needs no account. Your AI agent's +plan and optional HeyGen-hosted features, such as HeyGen voices, avatars, and +cloud rendering, bill through their own accounts. See +[what's free and what HeyGen adds](/guides/voice-and-audio#free-or-heygen-quality), and +[Make videos with a free agent](/guides/free-agents) for an agent that costs nothing. + +## Can I use HyperFrames commercially? + +Yes. The [Apache 2.0 license](https://github.com/heygen-com/hyperframes/blob/main/LICENSE) +lets you use, change, and distribute HyperFrames, including in commercial work, +under its terms. Media you add, such as fonts, music, or footage, keeps its own +license. + +## Which AI agent should I use? + +Any coding agent that supports skills works, including Claude Code, Codex, +Cursor, Gemini CLI, GitHub Copilot, Google Antigravity, IBM Bob, and OpenCode. +If you have none yet, start with Claude Code or Codex. Setup guides: +[Claude Code](/guides/with-claude-code), [Codex](/guides/with-codex), +[Cursor](/guides/with-cursor), and [other agents](/guides/skills). + +## Can I use it in the Claude or ChatGPT chat app? + +Yes, through the hosted HyperFrames connector. It creates and renders videos in +your HeyGen account, with nothing installed on your computer. You give up the +local project folder and Studio. Your chat product's plan decides whether you +can add a custom connector. See [Make videos in Claude.ai or ChatGPT](/guides/mcp). + +## How do I install it? + +For Claude Code, install the plugin: + +```bash +claude plugin marketplace add heygen-com/hyperframes +claude plugin install hyperframes@hyperframes +``` + +For most other agents, run `npx skills add heygen-com/hyperframes` and choose +**Core Skills**. Then start a new agent session. See +[Make your first video](/quickstart). + +## Do I need a HeyGen account or API key? + +Not to make and render videos on your computer. You need one for HeyGen voices +and music, avatars, cloud rendering, the hosted connector, and the Studio app. +Sign in with `npx hyperframes auth login`. See +[Authentication and API keys](/guides/authentication). + +## Does HyperFrames work on Windows? + +The CLI and the agent skills run on Windows, including rendering from +PowerShell. The HyperFrames Studio app is available for Mac and Linux only. On +Windows, edit in the browser editor with `npx hyperframes preview`. For WSL +rendering tips, see [The render is slow under WSL2](/help#the-render-is-slow-under-wsl2). + +## What is the difference between the Studio app and the browser editor? + +The **Studio app** is a desktop app for Mac and Linux: a chat with your agent, a +preview, and an editor in one window, with no terminal needed. The **browser +editor** is the editor that opens from any project when you run +`npx hyperframes preview`. Both edit the same project files. See +[Get the Studio app](/studio-app) and +[Edit in the browser editor](/studio). + +## How is HyperFrames different from Remotion? + +Remotion videos are React components. HyperFrames videos are HTML, CSS, and +JavaScript, timed with data attributes and a paused animation timeline. +HyperFrames does not use or run Remotion. See +[HyperFrames vs Remotion](/guides/hyperframes-vs-remotion) and +[Move from Remotion to HyperFrames](/prompting/remotion-migration). + +## How do I preview and render a video? + +Inside the project folder, open the live preview: + +```bash +npx hyperframes preview +``` + +Then render the MP4: + +```bash +npx hyperframes render --output final.mp4 +``` + +Rendering needs FFmpeg. Run `npx hyperframes doctor` to check your machine. See +[Render from the command line](/guides/rendering). + +## How long does a render take? + +It depends on length, resolution, and effects. In one measured example, a +300-frame 1920×1080 composition rendered in 9.8 seconds, and in 25 seconds with +one expensive CSS declaration left in. Use `--quality draft` for a faster review +render. See [Fix a slow preview or render](/guides/performance). + +## Can it add voiceover, music, and sound effects? + +Yes. Ask the agent for them in your request. With a HeyGen account it uses HeyGen +voices and a 10,000-track music catalog. Without one, voice uses Kokoro on your +computer, music is tracks you add (or local generation if you install it), and +sound effects come from a bundled set. +See [Add voiceover, music, sound effects and captions](/guides/voice-and-audio). + +## Can it add captions to my video? + +Yes. Give the agent your talking-head footage and ask for captions. It +transcribes the speech, times each word, and adds captions without changing the +footage. See [Add captions to a video with AI](/guides/captions-and-recuts). + +## Can I use my own fonts? + +Yes. Put the font file in the project and declare it with `@font-face`. A font +the project does not declare can fall back to a default face in the render, and +`npx hyperframes lint` reports it. See the +[font lint rules](/reference/lint-rules#fonts). + +## Can I render in the cloud or on my own servers? + +Yes. `hyperframes cloud render` renders on HeyGen's cloud and bills credits to +your HeyGen account. You can also render on your own AWS Lambda or Google Cloud +Run, or in Docker. See [Choose a rendering path](/deploy/overview). + +## Does HyperFrames send my video to HeyGen? + +Not when you preview and render locally: the project stays on your computer. +Cloud rendering uploads the project to HeyGen, and the hosted connector builds it +in your HeyGen account. The CLI sends usage data, such as command names and +render performance, which stays anonymous until you sign in. It never sends +project names or video content. Turn it off with `npx hyperframes telemetry disable`. Your +AI agent sends your prompts to its own provider. See +[the telemetry reference](/packages/cli#telemetry). + +## Something failed. What do I check first? + +Run these in the project folder and give the agent the full output: + +```bash +npx hyperframes doctor +npx hyperframes lint +npx hyperframes check +``` + +See [Troubleshooting](/help) for specific errors, and +[Share feedback](/help#share-feedback) to report a bug. diff --git a/docs/guides/free-agents.mdx b/docs/guides/free-agents.mdx new file mode 100644 index 00000000000..213301688de --- /dev/null +++ b/docs/guides/free-agents.mdx @@ -0,0 +1,54 @@ +--- +title: "Make videos with a free agent" +sidebarTitle: "Free agents" +description: "HyperFrames is free; the agent is the only cost. Use OpenCode's free models in the Studio app, or a model on your own computer, and see how open models score." +--- + +import { DocsVideo } from "/snippets/docs-video.jsx"; + +HyperFrames costs nothing. What you pay for is the AI agent that writes the video, and that can be free too. + +## Same prompt, paid model and open model + +In HeyGen's [Code2Video benchmark](https://www.heygen.com/research/introducing-code2video-benchmark), models wrote +videos with the HyperFrames skills from 168 briefs and were ranked against each other. The open-weight models +(Qwen 3.8 Max, DeepSeek V4.1 Flash, Kimi K3, GLM 5.3) all landed within 50 Elo points of first place. The same prompt, +given to a paid model and to an open one: + +
+
+

Claude Fable 5

+ +
+
+

DeepSeek V4 Flash

+ +
+
+ +The top models are still better at the hard parts: timing, composition, finish. For a first draft, a social post or +learning the tool, an open model is often enough. + +## In the Studio app: OpenCode + +[Connect an agent](/studio-app/connect-your-agent) and choose **OpenCode**. The app installs it, and it runs free +models with no sign-in. It needs **Full access**, and the app asks to turn it on the first time. + +## On your own computer + +OpenCode can also use a model running locally, so nothing leaves your machine and nothing is billed. With +[Ollama](https://ollama.com) running, OpenCode finds its models by itself; pick one that supports tools. See [OpenCode's model docs](https://opencode.ai/v2/docs/models). + +Local models are slower and weaker than the hosted ones, so start with short videos. + +## What else is free + +Rendering, previews, Kokoro voices, background removal and the bundled sound effects run on your computer at no +cost, and so does transcription when your own agent does it. The Studio app is free with a free HeyGen account. HeyGen's voices, avatars and generated clips use HeyGen credits; +everything else in a video is code. See [free or HeyGen quality](/guides/voice-and-audio#free-or-heygen-quality). + +## Related topics + +- [Connect your agent](/studio-app/connect-your-agent) +- [Use HyperFrames with Claude Code](/guides/with-claude-code) +- [The Code2Video benchmark on Kaggle](https://www.kaggle.com/benchmarks/heygen/code2video) diff --git a/docs/snippets/day-list.jsx b/docs/snippets/day-list.jsx new file mode 100644 index 00000000000..4f71eba95ae --- /dev/null +++ b/docs/snippets/day-list.jsx @@ -0,0 +1,62 @@ +export const DayList = ({ part }) => { + // [part, day, title, post id, account, film file, one line] + const days = [ + [0, 1, "Install HyperFrames", "2074176916819685648", "HeyGen", "01", "Give your coding agent the HyperFrames skills. One router reads the request and picks the workflow."], + [0, 2, "Give your brand a motion language", "2074574265714905167", "HeyGen", "02", "Turn a visual identity into reusable motion direction, written down once as `FRAME.md` and read on every later build."], + [0, 3, "PR-to-Video", "2074954182583554051", "HeyGen", "03-s", "Explain an open pull request as a short video."], + [0, 4, "Music-to-Video", "2075262117964615956", "HeyGen", "04-s", "Build a video around a track and its beat."], + [0, 5, "Faceless explainer", "2075682104302882908", "HeyGen", "05-s", "Turn a topic into a designed explainer without footage. The visuals are invented per scene."], + [0, 6, "Talking-head recut", "2075998619325604238", "HeyGen", "06-s", "Add synchronized graphics to an existing speaker video. The source clip plays untouched underneath."], + [0, 7, "Motion graphics", "2076381187640324427", "HeyGen", "07", "Make a short, design-led animation where the motion carries the message."], + [0, 8, "Music and SFX with the HeyGen CLI", "2076719036742922504", "HeyGen", "08-s", "Find and use audio from the command line, then let the mix duck itself under the narration."], + [0, 9, "Figma to HyperFrames", "2077072141309382770", "HeyGen", "09", "Turn an existing design into a video composition — as live HTML, not a flattened export."], + [1, 10, "Anatomy of a prompt", "2077438104982667282", "HeyGen", "10", "Structure a request so the agent gets the important intent right the first time."], + [1, 11, "Repurpose an existing video", "2077798995762823593", "HeyGen", "11-s", "Extract useful patterns from a reference instead of describing them from memory."], + [1, 12, "Dynamic captions", "2078190361932103716", "HeyGen", "12-s", "Make captions part of the visual design rather than a track laid on top."], + [1, 13, "Studio preview, part 1", "2078589328562016559", "HeyGen", "13", "Inspect a composition before rendering — in the browser editor, through your agent, or from the terminal."], + [1, 14, "Studio preview, part 2: keyframes", "2078945075787452660", "HeyGen", "14", "Adjust motion with visible keyframe controls, one lane per animated property."], + [1, 15, "Sample the web", "2079251321484722675", "HeyGen", "15", "Bring useful web motion into a composition by capturing the real page."], + [1, 16, "Media use", "2079609229737111978", "HeyGen", "16-s", "Work with the available asset libraries, and control how each one crops and fits."], + [1, 17, "Storyboard", "2080012076454719554", "HeyGen", "17-s", "Agree on the sequence before building every scene, so revisions land on the plan rather than the render."], + [1, 18, "HyperFrames in Video Agent", "2080407266755510452", "HeyGen", "18-s", "Create with HyperFrames inside HeyGen Video Agent."], + [1, 19, "Background removal", "2080762012330807438", "HeyGen", "19-s", "Use alpha channels to layer subjects into a scene, with the matte holding while the subject moves."], + [2, 20, "Claude Design", "2081184676430160379", "HyperFrames_", "20", "Take a design direction into a HyperFrames project without redrawing it by hand."], + [2, 21, "Cloud rendering", "2081491370485952790", "HyperFrames_", "21", "Render without making one local computer the bottleneck. Two commands hand the job to the cloud."], + [2, 22, "Components Catalog", "2081798071353278651", "HyperFrames_", "22-s", "Add reusable scenes and effects instead of rebuilding them each time."], + [2, 23, "Templates and variables", "2082197435246600341", "HyperFrames_", "23-s", "Turn one composition into repeatable versions."], + [2, 24, "Color grading", "2082536413829235004", "HyperFrames_", "24-s", "Correct or stylize images and footage in code, and bring your own LUT packs."], + [2, 25, "Prompt Guide", "2082892893027749917", "HyperFrames_", "25-s", "Find practical language and examples for directing an agent. The written guide lives in these docs, at [Prompting overview](/prompting/overview)."], + [2, 26, "Media effects and overlays", "2083324288632045910", "HyperFrames_", "26", "Add designed treatments to real media. Two to six authored colour ramps change the whole visual language."], + [2, 27, "Deploy", "2083633492894900456", "HyperFrames_", "27-s", "Run rendering on your own cloud infrastructure. See [Deploy](/guides/deploy) for the four render routes."], + [2, 28, "Contribute to the Catalog", "2084070152975634743", "HyperFrames_", "28-s", "Publish reusable building blocks for other creators. The [Catalog](/catalog) lists what has landed so far."], + [2, 29, "Components update", "2084338269194732003", "HyperFrames_", "29-s", "Combine the expanded component set in a single project."] + ]; + const CDN = "https://static.heygen.ai/hyperframes-oss/docs/images/showcase/thirty-days-day-"; + // Inline `code` and [links](/path) in the one-liners. + const rich = (text) => + text.split(/(`[^`]+`|\[[^\]]+\]\([^)]+\))/).map((bit, i) => { + if (bit.startsWith("`")) return {bit.slice(1, -1)}; + const link = bit.match(/^\[([^\]]+)\]\(([^)]+)\)$/); + return link ? {link[1]} : bit; + }); + const play = (event, on) => { + const video = event.currentTarget.querySelector("video"); + if (!video) return; + if (on) video.play().catch(() => {}); + else { video.pause(); video.currentTime = 0; } + }; + return ( +
    + {days.filter((d) => d[0] === part).map(([, n, title, post, account, file, line]) => ( +
  1. play(e, true)} onMouseLeave={(e) => play(e, false)}> + {String(n).padStart(2, "0")} +
    + {title} +

    {rich(line)}

    +
    +
  2. + ))} +
+ ); +}; diff --git a/docs/thirty-days.mdx b/docs/thirty-days.mdx index 52833be0c5f..2d5808c4548 100644 --- a/docs/thirty-days.mdx +++ b/docs/thirty-days.mdx @@ -1,221 +1,43 @@ --- title: 30 Days of HyperFrames sidebarTitle: 30 Days of HyperFrames -description: "The official 30 Days of HyperFrames lessons, collected in one place as the series is published." +description: "All 30 lessons of 30 Days of HyperFrames in order: what each day teaches, a preview of its film, and the post with its prompt." --- -import { HoverVideo } from "/snippets/hover-video.jsx"; +import { DocsVideo } from "/snippets/docs-video.jsx"; +import { DayList } from "/snippets/day-list.jsx"; +Thirty short lessons, one a day, each built and rendered before it was posted. Day 30 is all of them in one cut: -One lesson a day, each one built and rendered before it was posted. Every day below links to its original post for the prompt and the discussion; where the film or a frame from it is published, you can see the result here first. - - -The series began on [@HeyGen](https://x.com/HeyGen) and continues on [@HyperFrames_](https://x.com/HyperFrames_). + +Each title opens the original post with its prompt. On a computer, hover a row to preview its film. The series began on +[@HeyGen](https://x.com/HeyGen) and continues on [@HyperFrames_](https://x.com/HyperFrames_). ## Start and create -Days 1 to 9 cover the inputs: a repo, a track, a topic, a speaker, a design file. Every card opens the original post. - - - - - - Give your coding agent the HyperFrames skills. One router reads the request and picks the workflow. - - - - - - Turn a visual identity into reusable motion direction, written down once as `FRAME.md` and read on every later build. - - - - - - Explain an open pull request as a short video. Plays in full [above](#three-lessons-in-full). - - - - - - Build a video around a track and its beat. Plays in full [above](#three-lessons-in-full). - - - - - - Turn a topic into a designed explainer without footage. The visuals are invented per scene. - - - - - - Add synchronized graphics to an existing speaker video. The source clip plays untouched underneath. - - - - - - Make a short, design-led animation where the motion carries the message. +Days 1 to 9 cover the inputs: a repo, a track, a topic, a speaker, a design file. - - - - - Find and use audio from the command line, then let the mix duck itself under the narration. - - - - - - Turn an existing design into a video composition — as live HTML, not a flattened export. - - - + ## Direct and refine Days 10 to 19 are about steering: how you ask, what you inspect, and what you fix before rendering. - - - - - Structure a request so the agent gets the important intent right the first time. - - - - - - Extract useful patterns from a reference instead of describing them from memory. - - - - - - Make captions part of the visual design rather than a track laid on top. - - - - - - Inspect a composition before rendering — in Studio, through your agent, or from the terminal. - - - - - - Adjust motion with visible keyframe controls, one lane per animated property. - - - - - - Bring useful web motion into a composition by capturing the real page. - - - - - - Work with the available asset libraries, and control how each one crops and fits. - - - - - - Agree on the sequence before building every scene, so revisions land on the plan rather than the render. - - - - - - Create with HyperFrames inside HeyGen Video Agent. - - - - - - Use alpha channels to layer subjects into a scene, with the matte holding while the subject moves. - - - + ## Extend and ship Days 20 to 30 move from one composition to a repeatable pipeline: reusable blocks, variables, grading, and rendering somewhere other than your laptop. - - - - - Take a design direction into a HyperFrames project without redrawing it by hand. - - - - - - Render without making one local computer the bottleneck. Two commands hand the job to the cloud. - - - - - - Add reusable scenes and effects instead of rebuilding them each time. - - - - - - Turn one composition into repeatable versions. Plays in full [above](#three-lessons-in-full). - - - - - - Correct or stylize images and footage in code, and bring your own LUT packs. - - - - - - Find practical language and examples for directing an agent. The written guide lives in these docs, at [Prompting overview](/prompting/overview). - - - - - - Add designed treatments to real media. Two to six authored colour ramps change the whole visual language. - - - - - - Run rendering on your own cloud infrastructure. See [Deploy](/guides/deploy) for the four render routes. - - - - - - Publish reusable building blocks for other creators. The [Catalog](/catalog) lists what has landed so far. - - - - - - Combine the expanded component set in a single project. - - - - - - Thirty days in one cut — a second from every lesson, back to back. From install on Day 1 to the whole series here. - - - + ## Related topics -- [Choose a creation workflow](/workflows) -- [Direct your agent with the Prompt Guide](/prompting/overview) -- [Browse reusable Catalog elements](/catalog) +- [What do you want to make?](/workflows) +- [Ask for a great video](/prompting/overview) +- [Browse the Catalog](/catalog) From c6b3d953450668d78df00520f8a132ba0c8851d9 Mon Sep 17 00:00:00 2001 From: ukimsanov Date: Mon, 26 Oct 2026 00:53:06 -0400 Subject: [PATCH 2/5] docs: 30 Days previews honour reduced motion and have a play button Hovering a lesson started its film even with reduced motion on, and keyboard and touch visitors had no way to play it. Hover previews now follow the setting (read on the first render, and stopped if it turns on), and each film is a button that plays and pauses on purpose. --- docs/custom.css | 2 ++ docs/snippets/day-list.jsx | 55 +++++++++++++++++++++++++++++++++----- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/docs/custom.css b/docs/custom.css index d8a4ba88750..c40d19642bb 100644 --- a/docs/custom.css +++ b/docs/custom.css @@ -1689,3 +1689,5 @@ pre.shiki { .hf-day-n { font-size: 1.25rem; } } +.hf-day-play { display: block; width: 100%; padding: 0; border: 0; background: none; cursor: pointer; border-radius: 0.5rem; } +.hf-day-play:focus-visible { outline: 2px solid var(--hf-brand); outline-offset: 2px; } diff --git a/docs/snippets/day-list.jsx b/docs/snippets/day-list.jsx index 4f71eba95ae..d3a75011d84 100644 --- a/docs/snippets/day-list.jsx +++ b/docs/snippets/day-list.jsx @@ -39,22 +39,65 @@ export const DayList = ({ part }) => { const link = bit.match(/^\[([^\]]+)\]\(([^)]+)\)$/); return link ? {link[1]} : bit; }); - const play = (event, on) => { + // Hover previews follow the visitor's reduced-motion setting, known on the first render. The film itself is a + // button, so a keyboard or touch visitor can play and pause it on purpose; that is allowed under reduced motion. + const listRef = useRef(null); + const [reduced, setReduced] = useState( + () => typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches, + ); + const [chosen, setChosen] = useState(null); + useEffect(() => { + const query = window.matchMedia("(prefers-reduced-motion: reduce)"); + const follow = (event) => { + setReduced(event.matches); + // Turned on mid-visit: stop any hover preview that is running. + if (event.matches) for (const v of listRef.current?.querySelectorAll("video") ?? []) if (v.dataset.hover) v.pause(); + }; + query.addEventListener("change", follow); + return () => query.removeEventListener("change", follow); + }, []); + const hover = (event, on, n) => { + const video = event.currentTarget.querySelector("video"); + if (!video || chosen === n) return; + if (on && !reduced) { + video.dataset.hover = "1"; + video.play().catch(() => {}); + } else if (!on) { + delete video.dataset.hover; + video.pause(); + video.currentTime = 0; + } + }; + const toggle = (event, n) => { const video = event.currentTarget.querySelector("video"); if (!video) return; - if (on) video.play().catch(() => {}); - else { video.pause(); video.currentTime = 0; } + if (chosen === n && !video.paused) { + video.pause(); + setChosen(null); + } else { + delete video.dataset.hover; + video.play().catch(() => {}); + setChosen(n); + } }; return ( -
    +
      {days.filter((d) => d[0] === part).map(([, n, title, post, account, file, line]) => ( -
    1. play(e, true)} onMouseLeave={(e) => play(e, false)}> +
    2. hover(e, true, n)} onMouseLeave={(e) => hover(e, false, n)}> {String(n).padStart(2, "0")}
      {title}

      {rich(line)}

      -
    3. ))}
    From fadae16de284695a4fb9380d7125ff45d0d7f3dd Mon Sep 17 00:00:00 2001 From: ukimsanov Date: Mon, 26 Oct 2026 01:33:57 -0400 Subject: [PATCH 3/5] docs: 30 Days plays one chosen film at a time Choosing a lesson film pauses the one chosen before, so every button matches its film, and other rows do not preview while a film plays. --- docs/snippets/day-list.jsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/snippets/day-list.jsx b/docs/snippets/day-list.jsx index d3a75011d84..91bc6531bcd 100644 --- a/docs/snippets/day-list.jsx +++ b/docs/snippets/day-list.jsx @@ -56,9 +56,10 @@ export const DayList = ({ part }) => { query.addEventListener("change", follow); return () => query.removeEventListener("change", follow); }, []); - const hover = (event, on, n) => { + const hover = (event, on) => { const video = event.currentTarget.querySelector("video"); - if (!video || chosen === n) return; + // While a film is chosen, other rows don't preview: one film plays at a time. + if (!video || chosen !== null) return; if (on && !reduced) { video.dataset.hover = "1"; video.play().catch(() => {}); @@ -74,16 +75,18 @@ export const DayList = ({ part }) => { if (chosen === n && !video.paused) { video.pause(); setChosen(null); - } else { - delete video.dataset.hover; - video.play().catch(() => {}); - setChosen(n); + return; } + // Choosing a film stops the one chosen before, so each button always matches its film. + for (const v of listRef.current?.querySelectorAll("video") ?? []) if (v !== video) v.pause(); + delete video.dataset.hover; + video.play().catch(() => {}); + setChosen(n); }; return (
      {days.filter((d) => d[0] === part).map(([, n, title, post, account, file, line]) => ( -
    1. hover(e, true, n)} onMouseLeave={(e) => hover(e, false, n)}> +
    2. hover(e, true)} onMouseLeave={(e) => hover(e, false)}> {String(n).padStart(2, "0")}
      {title} From 76f754bb27a2763a1a58a378667c470b4d99445d Mon Sep 17 00:00:00 2001 From: ukimsanov Date: Mon, 26 Oct 2026 02:41:45 -0400 Subject: [PATCH 4/5] docs: one film at a time across the whole 30 Days page The page mounts three lesson lists, and each only paused its own films. A chosen film now plays with sound, and any unmuted start on the page makes every list stop its own films and drop its choice; previews stay muted and wait while a film with sound plays. --- docs/snippets/day-list.jsx | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/docs/snippets/day-list.jsx b/docs/snippets/day-list.jsx index 91bc6531bcd..8d2f7dbc275 100644 --- a/docs/snippets/day-list.jsx +++ b/docs/snippets/day-list.jsx @@ -56,10 +56,28 @@ export const DayList = ({ part }) => { query.addEventListener("change", follow); return () => query.removeEventListener("change", follow); }, []); + // One film at a time across the whole page, though each list (and every other player) is its own component: a film + // started with sound is a deliberate choice, so when any video on the page starts playing unmuted, this list stops + // its own films and lets go of its choice. Previews are muted and never evict anything. + useEffect(() => { + const onPlay = (event) => { + const v = event.target; + if (!(v instanceof HTMLVideoElement) || v.muted) return; + for (const own of listRef.current?.querySelectorAll("video") ?? []) { + if (own === v) continue; + own.pause(); + own.muted = true; + } + setChosen((c) => (listRef.current?.querySelector(`[data-day="${c}"] video`) === v ? c : null)); + }; + document.addEventListener("play", onPlay, true); + return () => document.removeEventListener("play", onPlay, true); + }, []); + const filmWithSound = () => [...document.querySelectorAll("video")].some((v) => !v.paused && !v.muted); const hover = (event, on) => { const video = event.currentTarget.querySelector("video"); - // While a film is chosen, other rows don't preview: one film plays at a time. - if (!video || chosen !== null) return; + // While a film plays with sound anywhere on the page, rows don't preview. + if (!video || chosen !== null || (on && filmWithSound())) return; if (on && !reduced) { video.dataset.hover = "1"; video.play().catch(() => {}); @@ -74,19 +92,20 @@ export const DayList = ({ part }) => { if (!video) return; if (chosen === n && !video.paused) { video.pause(); + video.muted = true; setChosen(null); return; } - // Choosing a film stops the one chosen before, so each button always matches its film. - for (const v of listRef.current?.querySelectorAll("video") ?? []) if (v !== video) v.pause(); + // A chosen film plays with its sound; its unmuted start stops every other film on the page (see onPlay). delete video.dataset.hover; + video.muted = false; video.play().catch(() => {}); setChosen(n); }; return (
        {days.filter((d) => d[0] === part).map(([, n, title, post, account, file, line]) => ( -
      1. hover(e, true)} onMouseLeave={(e) => hover(e, false)}> +
      2. hover(e, true)} onMouseLeave={(e) => hover(e, false)}> {String(n).padStart(2, "0")}
        {title} @@ -95,7 +114,7 @@ export const DayList = ({ part }) => {