docs: make journey pages interactive

This commit is contained in:
ukimsanov
2026-08-03 15:02:57 -07:00
parent eee496b4a8
commit ead05f5ec7
11 changed files with 1430 additions and 184 deletions
+596
View File
@@ -760,6 +760,10 @@ a:not([class]):hover {
/* Full narrated films use a quiet custom control layer over the native video
* engine. Preview loops remain plain muted videos. */
.hf-docs-video-block {
width: 100%;
}
.hf-docs-video {
position: relative;
aspect-ratio: 16 / 9;
@@ -771,6 +775,390 @@ a:not([class]):hover {
outline: none;
}
.hf-docs-video-guide {
margin-top: 0.75rem;
padding: 0.85rem;
border: 1px solid rgba(128, 128, 128, 0.24);
border-radius: 0.7rem;
background: rgba(128, 128, 128, 0.045);
}
.hf-docs-video-guide-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.55rem;
font-size: 0.8rem;
}
.hf-docs-video-guide-head strong {
font-weight: 650;
}
.hf-docs-video-guide-head button,
.hf-docs-video-chapters button,
.hf-docs-video-transcript button {
border: 0;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
}
.hf-docs-video-guide-head button {
padding: 0.2rem 0;
opacity: 0.62;
font-size: 0.73rem;
}
.hf-docs-video-guide-head button:hover,
.hf-docs-video-guide-head button:focus-visible {
opacity: 1;
}
.hf-docs-video-chapters {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
gap: 0.35rem;
}
.hf-docs-video-chapters button {
display: flex;
min-width: 0;
align-items: baseline;
gap: 0.45rem;
padding: 0.48rem 0.55rem;
border-radius: 0.45rem;
text-align: left;
font-size: 0.76rem;
line-height: 1.3;
}
.hf-docs-video-chapters button:hover,
.hf-docs-video-chapters button:focus-visible,
.hf-docs-video-chapters button[data-active="true"] {
background: rgba(128, 128, 128, 0.14);
}
.hf-docs-video-chapters button[data-active="true"] {
box-shadow: inset 2px 0 #6ba864;
}
.hf-docs-video-chapters span,
.hf-docs-video-transcript button span {
flex: 0 0 auto;
opacity: 0.58;
font-variant-numeric: tabular-nums;
}
.hf-docs-video-transcript {
margin-top: 0.55rem;
border-top: 1px solid rgba(128, 128, 128, 0.2);
}
.hf-docs-video-transcript summary {
width: fit-content;
padding: 0.65rem 0 0.1rem;
font-size: 0.76rem;
font-weight: 600;
cursor: pointer;
}
.hf-docs-video-transcript > div {
display: grid;
gap: 0.15rem;
max-height: 15rem;
margin-top: 0.45rem;
overflow-y: auto;
}
.hf-docs-video-transcript button {
display: grid;
grid-template-columns: 2.4rem 1fr;
gap: 0.5rem;
padding: 0.4rem 0.45rem;
border-radius: 0.35rem;
text-align: left;
font-size: 0.75rem;
line-height: 1.45;
}
.hf-docs-video-transcript button:hover,
.hf-docs-video-transcript button:focus-visible {
background: rgba(128, 128, 128, 0.12);
}
.hf-agent-action {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.25rem;
margin: 2rem 0 1.5rem;
padding: 1rem 1.05rem;
border: 1px solid rgba(128, 128, 128, 0.26);
border-radius: 0.7rem;
background: rgba(128, 128, 128, 0.05);
}
.hf-agent-action > div {
display: grid;
gap: 0.16rem;
}
.hf-agent-action strong {
font-size: 0.86rem;
font-weight: 650;
}
.hf-agent-action span {
opacity: 0.64;
font-size: 0.76rem;
}
.hf-agent-action button {
flex: 0 0 auto;
padding: 0.52rem 0.78rem;
border: 1px solid rgba(128, 128, 128, 0.38);
border-radius: 0.48rem;
background: transparent;
color: inherit;
font: inherit;
font-size: 0.76rem;
font-weight: 620;
cursor: pointer;
}
.hf-agent-action button:hover,
.hf-agent-action button:focus-visible {
border-color: #6ba864;
background: rgba(107, 168, 100, 0.1);
}
.hf-live-reference-start {
display: flex;
width: 100%;
align-items: center;
gap: 0.8rem;
margin: 1rem 0 2rem;
padding: 1rem;
border: 1px solid rgba(128, 128, 128, 0.28);
border-radius: 0.75rem;
background: rgba(128, 128, 128, 0.05);
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
}
.hf-live-reference-start > span:first-child {
display: grid;
width: 2.4rem;
height: 2.4rem;
flex: 0 0 auto;
place-items: center;
border-radius: 50%;
background: #6ba864;
color: #071008;
font-size: 0.78rem;
}
.hf-live-reference-start > span:last-child {
display: grid;
gap: 0.14rem;
}
.hf-live-reference-start strong {
font-size: 0.86rem;
}
.hf-live-reference-start small {
opacity: 0.65;
font-size: 0.75rem;
}
.hf-live-reference-start:hover,
.hf-live-reference-start:focus-visible {
border-color: #6ba864;
background: rgba(107, 168, 100, 0.08);
}
.hf-live-reference {
margin: 1rem 0 2rem;
overflow: hidden;
border: 1px solid rgba(128, 128, 128, 0.28);
border-radius: 0.75rem;
}
.hf-live-reference-stage {
position: relative;
display: grid;
min-height: 18rem;
place-items: center;
aspect-ratio: 16 / 9;
background: #080808;
}
.hf-live-reference-preview {
position: absolute;
inset: 0;
display: grid;
width: 100%;
height: 100%;
padding: 0;
overflow: hidden;
place-items: center;
border: 0;
background-color: #e9e9e7;
background-position: center;
background-size: cover;
cursor: pointer;
z-index: 4;
}
.hf-live-reference-preview span {
position: relative;
display: grid;
width: 3.5rem;
height: 3.5rem;
place-items: center;
border: 1px solid rgba(255, 255, 255, 0.68);
border-radius: 999px;
background: rgba(10, 10, 10, 0.84);
color: #fff;
font-size: 1rem;
line-height: 1;
box-shadow: 0 0.75rem 2rem rgba(10, 10, 10, 0.2);
transform: translateZ(0);
}
.hf-live-reference-preview:hover span,
.hf-live-reference-preview:focus-visible span {
transform: scale(1.05);
}
.hf-live-reference-player-host,
.hf-live-reference-player-host > * {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
}
.hf-live-reference-loading {
color: rgba(255, 255, 255, 0.7);
font-size: 0.78rem;
}
.hf-live-reference-controls {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr) auto auto;
gap: 0.65rem;
align-items: end;
padding: 0.85rem;
background: rgba(128, 128, 128, 0.055);
}
.hf-live-reference-controls label,
.hf-live-reference-controls fieldset {
display: grid;
min-width: 0;
gap: 0.3rem;
margin: 0;
padding: 0;
border: 0;
}
.hf-live-reference-controls label > span,
.hf-live-reference-controls legend {
padding: 0;
opacity: 0.62;
font-size: 0.68rem;
font-weight: 600;
}
.hf-live-reference-controls input {
width: 100%;
min-width: 0;
height: 2.15rem;
padding: 0 0.58rem;
border: 1px solid rgba(128, 128, 128, 0.3);
border-radius: 0.42rem;
background: rgba(128, 128, 128, 0.06);
color: inherit;
font: inherit;
font-size: 0.72rem;
}
.hf-live-reference-controls fieldset > div {
display: flex;
height: 2.15rem;
align-items: center;
gap: 0.28rem;
}
.hf-live-reference-controls fieldset button {
width: 1.35rem;
height: 1.35rem;
padding: 0;
border: 2px solid transparent;
border-radius: 50%;
background: var(--hf-live-accent);
box-shadow: 0 0 0 1px rgba(128, 128, 128, 0.34);
cursor: pointer;
}
.hf-live-reference-controls fieldset button[aria-pressed="true"] {
border-color: currentColor;
box-shadow: 0 0 0 2px rgba(107, 168, 100, 0.4);
}
.hf-live-reference-reset {
height: 2.15rem;
padding: 0 0.65rem;
border: 1px solid rgba(128, 128, 128, 0.3);
border-radius: 0.42rem;
background: transparent;
color: inherit;
font: inherit;
font-size: 0.7rem;
cursor: pointer;
}
@media (max-width: 720px) {
.hf-live-reference-controls {
grid-template-columns: 1fr 1fr;
}
.hf-live-reference-controls label:nth-child(2) {
grid-column: 1 / -1;
grid-row: 2;
}
}
@media (max-width: 520px) {
.hf-agent-action {
align-items: stretch;
flex-direction: column;
}
.hf-agent-action button {
width: 100%;
}
.hf-live-reference-stage {
min-height: 0;
}
.hf-live-reference-controls {
grid-template-columns: 1fr;
}
.hf-live-reference-controls label:nth-child(2) {
grid-column: auto;
grid-row: auto;
}
}
.hf-docs-video:focus-visible {
box-shadow: 0 0 0 3px rgba(245, 244, 92, 0.78);
}
@@ -1135,3 +1523,211 @@ a:not([class]):hover {
.eyebrow {
display: none;
}
/* Reference project — a visible artifact chain, not another route chooser. */
.hf-reference-story {
margin: 2.25rem 0 3.5rem;
overflow: hidden;
border: 1px solid var(--hf-border-color);
border-radius: 1.25rem;
background: color-mix(in srgb, var(--hf-background-light) 92%, transparent);
}
.hf-reference-story__header {
padding: 1.75rem 1.75rem 1.5rem;
border-bottom: 1px solid var(--hf-border-color);
background:
linear-gradient(
135deg,
color-mix(in srgb, var(--hf-accent-green) 10%, transparent),
transparent 55%
),
var(--hf-background-light);
}
.hf-reference-story__header > span:first-child,
.hf-reference-story__revision span {
color: var(--hf-accent-green);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.hf-reference-story__header h2 {
margin: 0.35rem 0 0.45rem;
font-size: clamp(1.6rem, 4vw, 2.2rem);
line-height: 1.05;
letter-spacing: -0.035em;
}
.hf-reference-story__header :where(p, [data-as="p"]),
.hf-reference-story__body :where(p, [data-as="p"]) {
margin: 0;
color: var(--hf-text-secondary);
}
.hf-reference-story__flow {
margin: 0;
padding: 0;
list-style: none;
}
.hf-reference-story__flow > li {
display: grid;
grid-template-columns: 3.25rem minmax(0, 1fr);
gap: 0.5rem;
padding: 1.5rem 1.75rem;
border-bottom: 1px solid var(--hf-border-color);
}
.hf-reference-story__flow > li:last-child {
border-bottom: 0;
}
.hf-reference-story__step {
position: relative;
display: grid;
width: 1.75rem;
height: 1.75rem;
place-items: center;
border: 1px solid color-mix(in srgb, var(--hf-accent-green) 42%, var(--hf-border-color));
border-radius: 999px;
color: var(--hf-accent-green);
font-size: 0.75rem;
font-weight: 750;
}
.hf-reference-story__flow > li:not(:last-child) .hf-reference-story__step::after {
position: absolute;
top: 1.75rem;
left: 50%;
width: 1px;
height: calc(100% + 1.75rem);
background: var(--hf-border-color);
content: "";
}
.hf-reference-story__body h3 {
margin: 0 0 0.65rem;
font-size: 1rem;
}
.hf-reference-story__body blockquote {
margin: 0.8rem 0;
padding: 0.8rem 1rem;
border-left: 2px solid var(--hf-accent-green);
background: color-mix(in srgb, var(--hf-accent-green) 7%, transparent);
color: var(--hf-text);
font-weight: 650;
}
.hf-reference-story__request {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.9rem 1rem;
border: 1px solid var(--hf-border-color);
border-radius: 0.8rem;
background: var(--hf-code-bg);
}
.hf-reference-story__request code {
flex: 1;
min-width: 0;
overflow-wrap: anywhere;
color: var(--hf-text);
font-size: 0.78rem;
white-space: normal;
}
.hf-reference-story__request button,
.hf-reference-story__body a {
border: 0;
color: var(--hf-accent-green);
font-size: 0.78rem;
font-weight: 700;
white-space: nowrap;
}
.hf-reference-story__request button {
padding: 0.4rem 0.55rem;
border-radius: 0.45rem;
background: color-mix(in srgb, var(--hf-accent-green) 11%, transparent);
cursor: pointer;
}
.hf-reference-story__files {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.55rem;
}
.hf-reference-story__files > span {
display: flex;
flex-direction: column;
gap: 0.2rem;
padding: 0.8rem;
border: 1px solid var(--hf-border-color);
border-radius: 0.75rem;
}
.hf-reference-story__files code {
color: var(--hf-text);
font-size: 0.8rem;
}
.hf-reference-story__files small {
color: var(--hf-text-secondary);
font-size: 0.72rem;
}
.hf-reference-story__revision {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
gap: 0.9rem;
}
.hf-reference-story__revision > div:not(:nth-child(2)) {
min-height: 7.5rem;
padding: 1rem;
border: 1px solid var(--hf-border-color);
border-radius: 0.8rem;
}
.hf-reference-story__revision p {
margin-top: 0.5rem;
font-size: 0.85rem;
}
.hf-reference-story__body > a {
display: inline-block;
margin-top: 0.75rem;
text-decoration: none;
}
@media (max-width: 640px) {
.hf-reference-story__header,
.hf-reference-story__flow > li {
padding-right: 1rem;
padding-left: 1rem;
}
.hf-reference-story__flow > li {
grid-template-columns: 2.5rem minmax(0, 1fr);
}
.hf-reference-story__files,
.hf-reference-story__revision {
grid-template-columns: 1fr;
}
.hf-reference-story__revision > div:nth-child(2) {
transform: rotate(90deg);
text-align: center;
}
.hf-reference-story__request {
align-items: flex-start;
}
}
+45 -2
View File
@@ -11,6 +11,35 @@ import { DocsVideo } from "/snippets/docs-video.jsx";
title="A HyperFrames composition shown through the CLI, SDK, Player, and rendering infrastructure."
src="/images/showcase/journey-developers-v3.mp4"
poster="/images/showcase/journey-developers-v3.jpg"
chapters={[
{ time: 0, label: "What you can build" },
{ time: 5.2, label: "The HTML contract" },
{ time: 13.2, label: "CLI automation" },
{ time: 20, label: "SDK inspection and editing" },
{ time: 27.4, label: "Player embedding" },
{ time: 32.2, label: "Rendering layers" },
{ time: 40.2, label: "The mental model" },
]}
transcript={[
{ time: 0.5, text: "What can you build on top of HyperFrames?" },
{
time: 5.6,
text: "Start with the contract. A composition is just HTML.",
},
{ time: 9.6, text: "Every clip declares its own timing." },
{ time: 13.6, text: "The command line treats that file as input." },
{ time: 16, text: "One composition, three rows, three renders." },
{
time: 20.4,
text: "The SDK opens the same HTML with no browser at all.",
},
{ time: 23.9, text: "Query it, retime a clip, write it back." },
{ time: 27.8, text: "The player embeds it in your page, and seeks it." },
{ time: 32.6, text: "Underneath, the engine seeks Chrome frame by frame." },
{ time: 35.7, text: "The producer encodes and mixes." },
{ time: 37.6, text: "The cloud does both without your machine." },
{ time: 40.8, text: "One composition. Inspect or edit. Play or render." },
]}
/>
</Frame>
@@ -28,14 +57,28 @@ cd my-video
npx hyperframes preview
```
The project is ordinary HTML. A timed element can be as small as:
The project is ordinary HTML. The Reference Project used in the beginner path
starts with this real composition root:
```html
<h1 class="clip" data-start="0" data-duration="3" data-track-index="0">Hello from HyperFrames</h1>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="10"
data-width="1920"
data-height="1080"
>
<div class="clip" data-start="0" data-duration="10" data-track-index="1">
<!-- Ordinary HTML, CSS, images, and timed media. -->
</div>
</div>
```
The project template supplies the composition root, viewport, and duration.
HTML and CSS control what appears; `data-*` attributes control when it appears.
You can [inspect the complete project](https://github.com/heygen-com/hyperframes/tree/main/examples/docs-reference-project)
before choosing an integration surface.
When the project is ready:
```bash
+6 -19
View File
@@ -4,16 +4,16 @@ description: "Watch finished HyperFrames videos, inspect real production project
---
import { DocsVideo } from "/snippets/docs-video.jsx";
import { ReferenceProjectStory } from "/snippets/reference-project-story.jsx";
Use examples in three different ways:
Start with one complete, small project. Then use the larger productions for range and the templates
for a working starting point.
- **Watch** a finished result to understand what HyperFrames can make.
- **Inspect** a production project to learn how story, media, scenes, and motion fit together.
- **Start** from a template when you want working structure immediately.
<ReferenceProjectStory sourceHref="https://github.com/heygen-com/hyperframes/tree/main/examples/docs-reference-project" />
## Finished production videos
## See how far it can go
These are real HyperFrames projects with public source.
These larger films are also real HyperFrames projects with public source.
<div className="not-prose grid grid-cols-1 md:grid-cols-2 gap-5 my-8">
<div className="overflow-hidden rounded-xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-950">
@@ -99,19 +99,6 @@ These are real HyperFrames projects with public source.
</div>
</div>
## What to learn from the source
The [hyperframes-launches repository](https://github.com/heygen-com/hyperframes-launches) contains complete project folders. Depending on the project, you may find:
- `BRIEF.md`, `STORYBOARD.md`, or design notes;
- the main `index.html`;
- separate scene compositions;
- local media and fonts;
- narration, captions, music, and sound effects;
- a finished render to compare against the source.
These projects are useful references, not universal templates. Copy the production thinking and patterns that fit your work; do not copy an entire visual identity by default.
## Start from a template
Create a project with a working example:
+60 -3
View File
@@ -12,19 +12,76 @@ import { DocsVideo } from "/snippets/docs-video.jsx";
title="An existing HyperFrames project is revised with an agent, edited in Studio, enriched, checked, and rendered."
src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/journey-go-further-v2.mp4"
poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/journey-go-further-v2.jpg"
chapters={[
{ time: 0, label: "Start from a working project" },
{ time: 6.3, label: "Ask for a story change" },
{ time: 13.85, label: "Let the agent rewrite it" },
{ time: 22.6, label: "Make a precise Studio edit" },
{ time: 30.1, label: "Add reusable detail" },
{ time: 42.1, label: "Check and render" },
{ time: 50.6, label: "Review the result" },
]}
transcript={[
{ time: 0.45, text: "This project already works." },
{
time: 2.55,
text: "Going further means changing it without starting over.",
},
{ time: 6.9, text: "Some changes are story-sized." },
{
time: 9.15,
text: "Beat four is where this piece turns — and it was the shortest scene on the timeline.",
},
{
time: 14.3,
text: "Say that in plain language, and the timeline rewrites itself.",
},
{
time: 17.9,
text: "The turn gains a second and a half; the ending comes in to meet it.",
},
{
time: 23.2,
text: "Some changes are pixel-sized. Those you type yourself.",
},
{ time: 26.9, text: "A title size, set by hand, in the panel." },
{ time: 30.9, text: "Then make the change worth keeping." },
{
time: 33.3,
text: "A Catalog lower third fills the room the agent made,",
},
{
time: 36.6,
text: "and its lines become variables — so the next listing is a flag, not an edit.",
},
{
time: 42.9,
text: "Check it for real: the two findings that mattered are gone.",
},
{ time: 47.3, text: "Render." },
{
time: 51.9,
text: "One project, two hands. The agent moves the story; you move the frame.",
},
]}
/>
</Frame>
The project does not change format as you gain control. Your agent, Studio, the
SDK, the Player, and the renderer all work from the same HTML project.
The [Reference Project](https://github.com/heygen-com/hyperframes/tree/main/examples/docs-reference-project)
continues the first-video example. Its first version is a real page capture on a
10-second timeline. The revised version keeps that source and adds editable
variables, narration, fixed captions, music, and sound.
## Choose the surface by the size of the change
| The change | Best first move | Example |
| ----------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------- |
| Story, structure, or several scenes | Ask the agent | “Make the opening reveal the product by three seconds.” |
| One visible detail or moment | Edit in Studio | Change a line, crop, position, clip boundary, or keyframe. |
| The same design needs new content | Use variables or nested compositions | Produce customer, language, or format variants without duplicating the layout. |
| Story, structure, or several scenes | Ask the agent | “Keep the real page. Add a short spoken explanation.” |
| One visible detail or moment | Edit in Studio | Change the accent or supporting line. |
| The same design needs new content | Use variables or nested compositions | Make the title, accent, and supporting line reusable. |
| The project must finish reliably | Check, render, and watch the file | Run the browser gate, render through your preferred surface, then review the actual output. |
Use the highest-level tool that gives you enough control. A one-line agent
+45
View File
@@ -5,12 +5,47 @@ description: "See what an AI agent can make with ordinary web technology."
---
import { DocsVideo, ShowcaseWall } from "/snippets/docs-video.jsx";
import { LiveReferenceProject } from "/snippets/live-reference-project.jsx";
<Frame>
<DocsVideo
title="What is HyperFrames?"
src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/journey-introduction-v5.mp4"
poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/journey-introduction-v5.jpg"
chapters={[
{ time: 0, label: "What it is" },
{ time: 6.6, label: "Why agents can build it" },
{ time: 16, label: "Real films, made as web pages" },
{ time: 31.7, label: "The editable project" },
{ time: 40.8, label: "Studio and render" },
{ time: 48.8, label: "The whole idea" },
]}
transcript={[
{ time: 0.45, text: "So — what exactly is HyperFrames?" },
{ time: 3.96, text: "It turns HTML into video." },
{
time: 6.61,
text: "And that matters, because your agent already writes HTML, CSS and JavaScript. There's no new language for it to learn.",
},
{ time: 16, text: "Every film you're watching right now is a web page." },
{
time: 19.67,
text: "The layouts, the typography, the interfaces, the animations, the transitions, the effects — all of it authored as HTML, CSS and JavaScript by an AI agent.",
},
{
time: 31.76,
text: "The footage, the images, the narration, the music, the sound — those are real files, assembled into that same editable project.",
},
{ time: 40.83, text: "Open it in Studio. Move a clip. Change a word." },
{
time: 44.56,
text: "Then one command renders every frame, and hands you a file.",
},
{
time: 48.89,
text: "That's the whole idea. A request. An agent. An editable project. A video.",
},
]}
/>
</Frame>
@@ -32,6 +67,16 @@ Every tile below is a finished HyperFrames film. Open one to watch it with sound
depending on live playback, so a slow machine does not drop moments from the
finished video.
## Change the project yourself
This is the same real HTML project used in the first-video walkthrough. Change
its words or accent, then press Play.
<LiveReferenceProject
src="https://cdn.jsdelivr.net/gh/heygen-com/hyperframes@51f14079e0660434c7e622013e8f5e20621ce304/examples/docs-reference-project/live/index.html"
poster="https://cdn.jsdelivr.net/gh/heygen-com/hyperframes@51f14079e0660434c7e622013e8f5e20621ce304/examples/docs-reference-project/live/poster.jpg"
/>
## Make one
<Card title="Make your first video" icon="play" href="/quickstart" horizontal arrow>
+32
View File
@@ -5,12 +5,42 @@ description: "Install the HyperFrames skills, make one short request, and contin
import { QuickstartContinuationGrid } from "/snippets/quickstart-continuation-grid.jsx";
import { DocsVideo } from "/snippets/docs-video.jsx";
import { AgentAction } from "/snippets/agent-action.jsx";
<Frame>
<DocsVideo
title="A beginner creates a first HyperFrames video from one short request, then continues with an agent, Studio, or the CLI."
src="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/journey-quickstart-v5.mp4"
poster="https://static.heygen.ai/hyperframes-oss/docs/images/showcase/journey-quickstart-v5.jpg"
chapters={[
{ time: 0, label: "Make the request" },
{ time: 8.4, label: "Confirm the intent" },
{ time: 17, label: "Get a real project" },
{ time: 22.8, label: "Watch the first version" },
{ time: 27.8, label: "Choose how to continue" },
{ time: 35.6, label: "The result" },
]}
transcript={[
{ time: 0.2, text: "Let's make one." },
{ time: 2.2, text: "This is the whole request." },
{ time: 5.55, text: "One sentence, and a link." },
{ time: 9.2, text: "Your agent reads it back," },
{
time: 10.75,
text: "confirms what matters, and asks what's missing.",
},
{ time: 14.55, text: "You never had to write a specification." },
{ time: 17.6, text: "Then it becomes a real project." },
{ time: 19.85, text: "Scenes, media, timing. Files you own." },
{ time: 23.5, text: "And a first version you can watch." },
{ time: 28.45, text: "From here, three doors into the same project." },
{
time: 31.3,
text: "Ask the agent. Open Studio. Or use the command line.",
},
{ time: 36.55, text: "Ten seconds, from one sentence." },
{ time: 40.1, text: "Now make yours." },
]}
/>
</Frame>
@@ -50,6 +80,8 @@ the same project.
<QuickstartContinuationGrid />
<AgentAction request="Using /hyperframes, make a 10-second product intro for https://example.com." />
## What next?
<CardGroup cols={2}>
+55
View File
@@ -0,0 +1,55 @@
export const AgentAction = ({ request }) => {
const [copied, setCopied] = useState(false);
const resetTimerRef = useRef(null);
const writeClipboard = async (text) => {
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
} catch {
// Fall through to the browser's older synchronous copy path.
}
const field = document.createElement("textarea");
field.value = text;
field.setAttribute("readonly", "");
field.style.position = "fixed";
field.style.opacity = "0";
document.body.appendChild(field);
field.select();
const copied = document.execCommand("copy");
field.remove();
return copied;
};
const copyRequest = async () => {
if (await writeClipboard(request)) {
setCopied(true);
if (resetTimerRef.current) window.clearTimeout(resetTimerRef.current);
resetTimerRef.current = window.setTimeout(() => setCopied(false), 1800);
} else {
setCopied(false);
}
};
useEffect(
() => () => {
if (resetTimerRef.current) window.clearTimeout(resetTimerRef.current);
},
[],
);
return (
<div className="hf-agent-action">
<div>
<strong>Use this with your agent</strong>
<span>Replace the URL with the page you want to show.</span>
</div>
<button type="button" onClick={copyRequest}>
{copied ? "Copied" : "Copy request"}
</button>
</div>
);
};
+277 -159
View File
@@ -1,10 +1,19 @@
export const DocsVideo = ({ src, poster, title, autoPlay = false }) => {
export const DocsVideo = ({
src,
poster,
title,
autoPlay = false,
chapters = [],
transcript = [],
}) => {
const videoRef = useRef(null);
const previewVideoRef = useRef(null);
const playerRef = useRef(null);
const hideTimerRef = useRef(null);
const progressFrameRef = useRef(null);
const previewSeekFrameRef = useRef(null);
const initialMomentAppliedRef = useRef(false);
const copyTimerRef = useRef(null);
const [enhanced, setEnhanced] = useState(false);
const [playing, setPlaying] = useState(false);
const [waiting, setWaiting] = useState(false);
@@ -19,6 +28,29 @@ export const DocsVideo = ({ src, poster, title, autoPlay = false }) => {
const [scrubbing, setScrubbing] = useState(false);
const [previewTime, setPreviewTime] = useState(0);
const [previewPosition, setPreviewPosition] = useState(0);
const [momentCopied, setMomentCopied] = useState(false);
const writeClipboard = async (text) => {
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
} catch {
// Fall through to the browser's older synchronous copy path.
}
const field = document.createElement("textarea");
field.value = text;
field.setAttribute("readonly", "");
field.style.position = "fixed";
field.style.opacity = "0";
document.body.appendChild(field);
field.select();
const copied = document.execCommand("copy");
field.remove();
return copied;
};
const formatTime = (seconds) => {
if (!Number.isFinite(seconds) || seconds < 0) return "0:00";
@@ -75,6 +107,28 @@ export const DocsVideo = ({ src, poster, title, autoPlay = false }) => {
setCurrentTime(nextTime);
};
const seekTo = (seconds) => {
const video = videoRef.current;
if (!video) return;
const nextTime = Math.min(duration || video.duration || seconds, Math.max(0, seconds));
video.currentTime = nextTime;
setCurrentTime(nextTime);
revealControls();
};
const copyMomentLink = async () => {
if (typeof window === "undefined") return;
const url = new URL(window.location.href);
url.searchParams.set("t", String(Math.max(0, Math.floor(currentTime))));
if (await writeClipboard(url.toString())) {
setMomentCopied(true);
if (copyTimerRef.current) window.clearTimeout(copyTimerRef.current);
copyTimerRef.current = window.setTimeout(() => setMomentCopied(false), 1800);
} else {
setMomentCopied(false);
}
};
const updateScrubPreview = (event, seekMainVideo = false) => {
if (!duration) return;
const rect = event.currentTarget.getBoundingClientRect();
@@ -157,7 +211,10 @@ export const DocsVideo = ({ src, poster, title, autoPlay = false }) => {
setFullscreenSupported(
Boolean(playerRef.current?.requestFullscreen || videoRef.current?.webkitEnterFullscreen),
);
return clearHideTimer;
return () => {
clearHideTimer();
if (copyTimerRef.current) window.clearTimeout(copyTimerRef.current);
};
}, []);
useEffect(() => {
@@ -200,191 +257,252 @@ export const DocsVideo = ({ src, poster, title, autoPlay = false }) => {
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
const replaying = duration > 0 && currentTime >= duration - 0.15;
const activeChapter = chapters.reduce(
(active, chapter, index) => (currentTime >= chapter.time ? index : active),
0,
);
return (
<div
ref={playerRef}
className="hf-docs-video"
role="region"
aria-label={title}
tabIndex={0}
onKeyDown={handleKeyboard}
onPointerMove={revealControls}
onPointerLeave={() => playing && setControlsVisible(false)}
onFocus={revealControls}
>
<video
ref={videoRef}
<div className="hf-docs-video-block">
<div
ref={playerRef}
className="hf-docs-video"
role="region"
aria-label={title}
src={src}
poster={poster}
autoPlay={autoPlay}
playsInline
preload="metadata"
controls={!enhanced}
onClick={togglePlayback}
onDoubleClick={toggleFullscreen}
onLoadedMetadata={(event) => {
setDuration(event.currentTarget.duration || 0);
setMuted(event.currentTarget.muted);
}}
onDurationChange={(event) => setDuration(event.currentTarget.duration || 0)}
onTimeUpdate={(event) => setCurrentTime(event.currentTarget.currentTime)}
onPlay={() => setPlaying(true)}
onPause={() => setPlaying(false)}
onPlaying={() => setWaiting(false)}
onWaiting={() => setWaiting(true)}
onCanPlay={() => setWaiting(false)}
onEnded={() => {
setPlaying(false);
setControlsVisible(true);
}}
onVolumeChange={(event) => setMuted(event.currentTarget.muted)}
/>
tabIndex={0}
onKeyDown={handleKeyboard}
onPointerMove={revealControls}
onPointerLeave={() => playing && setControlsVisible(false)}
onFocus={revealControls}
>
<video
ref={videoRef}
aria-label={title}
src={src}
poster={poster}
autoPlay={autoPlay}
playsInline
preload="metadata"
controls={!enhanced}
onClick={togglePlayback}
onDoubleClick={toggleFullscreen}
onLoadedMetadata={(event) => {
const nextDuration = event.currentTarget.duration || 0;
setDuration(nextDuration);
setMuted(event.currentTarget.muted);
if (!initialMomentAppliedRef.current && typeof window !== "undefined") {
initialMomentAppliedRef.current = true;
const requestedTime = Number(new URL(window.location.href).searchParams.get("t"));
if (Number.isFinite(requestedTime) && requestedTime > 0) {
const nextTime = Math.min(nextDuration, requestedTime);
event.currentTarget.currentTime = nextTime;
setCurrentTime(nextTime);
}
}
}}
onDurationChange={(event) => setDuration(event.currentTarget.duration || 0)}
onTimeUpdate={(event) => setCurrentTime(event.currentTarget.currentTime)}
onPlay={() => setPlaying(true)}
onPause={() => setPlaying(false)}
onPlaying={() => setWaiting(false)}
onWaiting={() => setWaiting(true)}
onCanPlay={() => setWaiting(false)}
onEnded={() => {
setPlaying(false);
setControlsVisible(true);
}}
onVolumeChange={(event) => setMuted(event.currentTarget.muted)}
/>
{enhanced && (
<>
{!playing && (
<button
type="button"
className="hf-docs-video-hero-play"
onClick={togglePlayback}
aria-label={replaying ? "Replay video" : "Play video"}
>
<span className="hf-docs-video-hero-icon" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path d="M8 5.5v13l10-6.5z" />
</svg>
</span>
</button>
)}
{waiting && playing && <span className="hf-docs-video-spinner" aria-label="Loading" />}
<div
className="hf-docs-video-controls"
data-visible={controlsVisible || !playing ? "true" : "false"}
>
<div
className="hf-docs-video-scrub-preview"
data-visible={previewing ? "true" : "false"}
style={{ "--hf-video-preview-x": `${previewPosition}%` }}
aria-hidden="true"
>
<video
ref={previewVideoRef}
src={src}
muted
playsInline
preload="metadata"
tabIndex={-1}
aria-hidden="true"
/>
<span>{formatTime(previewTime)}</span>
</div>
<input
className="hf-docs-video-progress"
type="range"
min="0"
max={duration || 0}
step="0.01"
value={Math.min(currentTime, duration || 0)}
aria-label="Video progress"
aria-valuetext={`${formatTime(currentTime)} of ${formatTime(duration)}`}
onChange={seek}
onPointerEnter={updateScrubPreview}
onPointerMove={(event) => updateScrubPreview(event, scrubbing || event.buttons === 1)}
onPointerDown={(event) => {
setScrubbing(true);
event.currentTarget.setPointerCapture?.(event.pointerId);
updateScrubPreview(event, true);
}}
onPointerUp={(event) => {
setScrubbing(false);
if (event.pointerType !== "mouse") setPreviewing(false);
}}
onPointerCancel={() => {
setScrubbing(false);
setPreviewing(false);
}}
onPointerLeave={() => {
if (!scrubbing) setPreviewing(false);
}}
style={{ "--hf-video-progress": `${progress}%` }}
/>
<div className="hf-docs-video-control-row">
{enhanced && (
<>
{!playing && (
<button
type="button"
className="hf-docs-video-control"
className="hf-docs-video-hero-play"
onClick={togglePlayback}
aria-label={playing ? "Pause video" : "Play video"}
aria-label={replaying ? "Replay video" : "Play video"}
>
{playing ? (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M7 5h4v14H7zm6 0h4v14h-4z" />
</svg>
) : (
<svg viewBox="0 0 24 24" aria-hidden="true">
<span className="hf-docs-video-hero-icon" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path d="M8 5.5v13l10-6.5z" />
</svg>
)}
</span>
</button>
)}
<button
type="button"
className="hf-docs-video-control"
onClick={toggleMute}
aria-label={muted ? "Unmute video" : "Mute video"}
{waiting && playing && <span className="hf-docs-video-spinner" aria-label="Loading" />}
<div
className="hf-docs-video-controls"
data-visible={controlsVisible || !playing ? "true" : "false"}
>
<div
className="hf-docs-video-scrub-preview"
data-visible={previewing ? "true" : "false"}
style={{ "--hf-video-preview-x": `${previewPosition}%` }}
aria-hidden="true"
>
{muted ? (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 9v6h4l5 4V5L8 9zm11.5 1.1 1.4-1.4 1.6 1.6 1.6-1.6 1.4 1.4-1.6 1.6 1.6 1.6-1.4 1.4-1.6-1.6-1.6 1.6-1.4-1.4 1.6-1.6z" />
</svg>
) : (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 9v6h4l5 4V5L8 9zm11 1.2v3.6c1-.5 1.7-1.5 1.7-2.8S16 10.7 15 10.2zm0-4v2.1c2.2.6 3.7 2.5 3.7 4.7s-1.5 4.1-3.7 4.7v2.1c3.3-.7 5.7-3.5 5.7-6.8S18.3 6.9 15 6.2z" />
</svg>
)}
</button>
<video
ref={previewVideoRef}
src={src}
muted
playsInline
preload="metadata"
tabIndex={-1}
aria-hidden="true"
/>
<span>{formatTime(previewTime)}</span>
</div>
<span className="hf-docs-video-time" aria-hidden="true">
{formatTime(currentTime)} <span>/</span> {formatTime(duration)}
</span>
<input
className="hf-docs-video-progress"
type="range"
min="0"
max={duration || 0}
step="0.01"
value={Math.min(currentTime, duration || 0)}
aria-label="Video progress"
aria-valuetext={`${formatTime(currentTime)} of ${formatTime(duration)}`}
onChange={seek}
onPointerEnter={updateScrubPreview}
onPointerMove={(event) =>
updateScrubPreview(event, scrubbing || event.buttons === 1)
}
onPointerDown={(event) => {
setScrubbing(true);
event.currentTarget.setPointerCapture?.(event.pointerId);
updateScrubPreview(event, true);
}}
onPointerUp={(event) => {
setScrubbing(false);
if (event.pointerType !== "mouse") setPreviewing(false);
}}
onPointerCancel={() => {
setScrubbing(false);
setPreviewing(false);
}}
onPointerLeave={() => {
if (!scrubbing) setPreviewing(false);
}}
style={{ "--hf-video-progress": `${progress}%` }}
/>
<span className="hf-docs-video-spacer" />
<button
type="button"
className="hf-docs-video-rate"
onClick={cyclePlaybackRate}
aria-label={`Playback speed ${playbackRate} times`}
>
{playbackRate}×
</button>
{fullscreenSupported && (
<div className="hf-docs-video-control-row">
<button
type="button"
className="hf-docs-video-control"
onClick={toggleFullscreen}
aria-label={fullscreen ? "Exit fullscreen" : "Enter fullscreen"}
onClick={togglePlayback}
aria-label={playing ? "Pause video" : "Play video"}
>
{fullscreen ? (
{playing ? (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M8 3H6v3H3v2h5zm8 0v5h5V6h-3V3zM3 16v2h3v3h2v-5zm13 0v5h2v-3h3v-2z" />
<path d="M7 5h4v14H7zm6 0h4v14h-4z" />
</svg>
) : (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M3 8h2V5h3V3H3zm13-5v2h3v3h2V3zM5 16H3v5h5v-2H5zm14 3h-3v2h5v-5h-2z" />
<path d="M8 5.5v13l10-6.5z" />
</svg>
)}
</button>
)}
<button
type="button"
className="hf-docs-video-control"
onClick={toggleMute}
aria-label={muted ? "Unmute video" : "Mute video"}
>
{muted ? (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 9v6h4l5 4V5L8 9zm11.5 1.1 1.4-1.4 1.6 1.6 1.6-1.6 1.4 1.4-1.6 1.6 1.6 1.6-1.4 1.4-1.6-1.6-1.6 1.6-1.4-1.4 1.6-1.6z" />
</svg>
) : (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 9v6h4l5 4V5L8 9zm11 1.2v3.6c1-.5 1.7-1.5 1.7-2.8S16 10.7 15 10.2zm0-4v2.1c2.2.6 3.7 2.5 3.7 4.7s-1.5 4.1-3.7 4.7v2.1c3.3-.7 5.7-3.5 5.7-6.8S18.3 6.9 15 6.2z" />
</svg>
)}
</button>
<span className="hf-docs-video-time" aria-hidden="true">
{formatTime(currentTime)} <span>/</span> {formatTime(duration)}
</span>
<span className="hf-docs-video-spacer" />
<button
type="button"
className="hf-docs-video-rate"
onClick={cyclePlaybackRate}
aria-label={`Playback speed ${playbackRate} times`}
>
{playbackRate}×
</button>
{fullscreenSupported && (
<button
type="button"
className="hf-docs-video-control"
onClick={toggleFullscreen}
aria-label={fullscreen ? "Exit fullscreen" : "Enter fullscreen"}
>
{fullscreen ? (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M8 3H6v3H3v2h5zm8 0v5h5V6h-3V3zM3 16v2h3v3h2v-5zm13 0v5h2v-3h3v-2z" />
</svg>
) : (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M3 8h2V5h3V3H3zm13-5v2h3v3h2V3zM5 16H3v5h5v-2H5zm14 3h-3v2h5v-5h-2z" />
</svg>
)}
</button>
)}
</div>
</div>
</>
)}
</div>
{chapters.length > 0 && (
<div className="hf-docs-video-guide">
<div className="hf-docs-video-guide-head">
<strong>Chapters</strong>
<button type="button" onClick={copyMomentLink}>
{momentCopied ? "Link copied" : `Copy ${formatTime(currentTime)} link`}
</button>
</div>
</>
<div className="hf-docs-video-chapters" aria-label={`${title} chapters`}>
{chapters.map((chapter, index) => (
<button
type="button"
key={`${chapter.time}-${chapter.label}`}
data-active={activeChapter === index ? "true" : "false"}
onClick={() => seekTo(chapter.time)}
aria-current={activeChapter === index ? "true" : undefined}
>
<span>{formatTime(chapter.time)}</span>
{chapter.label}
</button>
))}
</div>
{transcript.length > 0 && (
<details className="hf-docs-video-transcript">
<summary>Read transcript</summary>
<div>
{transcript.map((line) => (
<button
type="button"
key={`${line.time}-${line.text}`}
onClick={() => seekTo(line.time)}
>
<span>{formatTime(line.time)}</span>
{line.text}
</button>
))}
</div>
</details>
)}
</div>
)}
</div>
);
+200
View File
@@ -0,0 +1,200 @@
export const LiveReferenceProject = ({ src, poster }) => {
const playerRef = useRef(null);
const playerHostRef = useRef(null);
const [mounted, setMounted] = useState(false);
const [playerReady, setPlayerReady] = useState(false);
const [compositionSrc, setCompositionSrc] = useState(null);
const [previewVisible, setPreviewVisible] = useState(true);
const [loadFailed, setLoadFailed] = useState(false);
const [title, setTitle] = useState("Example Domain");
const [supportingLine, setSupportingLine] = useState(
"For use in documentation examples without needing permission.",
);
const [accent, setAccent] = useState("#334488");
useEffect(() => {
if (!mounted || typeof window === "undefined") return undefined;
let cancelled = false;
const markReady = () => {
if (!cancelled) setPlayerReady(true);
};
if (window.customElements?.get("hyperframes-player")) {
markReady();
return () => {
cancelled = true;
};
}
let script = document.querySelector("script[data-hf-docs-player]");
if (!script) {
script = document.createElement("script");
script.type = "module";
script.src = "https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7.90";
script.dataset.hfDocsPlayer = "true";
script.addEventListener("error", () => !cancelled && setLoadFailed(true), { once: true });
document.head.appendChild(script);
}
window.customElements
?.whenDefined("hyperframes-player")
.then(markReady)
.catch(() => !cancelled && setLoadFailed(true));
return () => {
cancelled = true;
};
}, [mounted]);
useEffect(() => {
if (!playerReady || typeof window === "undefined") return undefined;
const controller = new AbortController();
let objectUrl;
fetch(src, { signal: controller.signal })
.then((response) => {
if (!response.ok) throw new Error(`Composition request failed: ${response.status}`);
return response.text();
})
.then((html) => {
const assetBase = new URL("../", src).toString();
const preparedHtml = html.includes('<base href="../">')
? html.replace('<base href="../">', `<base href="${assetBase}">`)
: html.replace("<head>", `<head><base href="${assetBase}">`);
objectUrl = URL.createObjectURL(new Blob([preparedHtml], { type: "text/html" }));
setCompositionSrc(objectUrl);
})
.catch((error) => {
if (error.name !== "AbortError") setLoadFailed(true);
});
return () => {
controller.abort();
if (objectUrl) URL.revokeObjectURL(objectUrl);
};
}, [playerReady, src]);
useEffect(() => {
if (!playerReady || !compositionSrc || !playerHostRef.current) return undefined;
const player = document.createElement("hyperframes-player");
player.setAttribute("src", compositionSrc);
player.setAttribute("controls", "");
player.setAttribute("width", "1920");
player.setAttribute("height", "1080");
playerHostRef.current.replaceChildren(player);
playerRef.current = player;
return () => {
playerRef.current = null;
player.remove();
};
}, [compositionSrc, playerReady]);
useEffect(() => {
if (!playerReady) return undefined;
const player = playerRef.current;
if (!player) return undefined;
const sendVariables = () => {
player.iframeElement?.contentWindow?.postMessage(
{
type: "hyperframes-docs:variables",
variables: { title, supportingLine, accent },
},
"*",
);
};
player.addEventListener("ready", sendVariables);
sendVariables();
return () => player.removeEventListener("ready", sendVariables);
}, [playerReady, title, supportingLine, accent]);
const reset = () => {
setTitle("Example Domain");
setSupportingLine("For use in documentation examples without needing permission.");
setAccent("#334488");
};
const playFromStart = () => {
const player = playerRef.current;
if (!player) return;
setPreviewVisible(false);
player.seek(0);
player.play();
};
if (!mounted) {
return (
<button type="button" className="hf-live-reference-start" onClick={() => setMounted(true)}>
<span aria-hidden="true">▶</span>
<span>
<strong>Load the live composition</strong>
<small>Runs the actual editable HTML project in this page.</small>
</span>
</button>
);
}
return (
<div className="hf-live-reference">
<div className="hf-live-reference-stage">
{playerReady && compositionSrc ? (
<>
<div ref={playerHostRef} className="hf-live-reference-player-host" />
{poster && previewVisible && (
<button
type="button"
className="hf-live-reference-preview"
onClick={playFromStart}
aria-label="Play the live Reference Project"
style={{ backgroundImage: `url(${poster})` }}
>
<span aria-hidden="true">▶</span>
</button>
)}
</>
) : (
<div className="hf-live-reference-loading" role="status">
{loadFailed ? "The live project could not load." : "Loading the composition…"}
</div>
)}
</div>
<div className="hf-live-reference-controls" aria-label="Change the live composition">
<label>
<span>Headline</span>
<input value={title} onChange={(event) => setTitle(event.target.value)} maxLength={42} />
</label>
<label>
<span>Supporting line</span>
<input
value={supportingLine}
onChange={(event) => setSupportingLine(event.target.value)}
maxLength={74}
/>
</label>
<fieldset>
<legend>Accent</legend>
<div>
{["#334488", "#2f7d4f", "#a44b35", "#6b4fa1"].map((color) => (
<button
type="button"
key={color}
aria-label={`Use accent ${color}`}
aria-pressed={accent === color}
onClick={() => setAccent(color)}
style={{ "--hf-live-accent": color }}
/>
))}
</div>
</fieldset>
<button type="button" className="hf-live-reference-reset" onClick={reset}>
Reset
</button>
</div>
</div>
);
};
+112
View File
@@ -0,0 +1,112 @@
export const ReferenceProjectStory = ({ sourceHref }) => {
const request = "Using /hyperframes, make a 10-second product intro for https://example.com.";
const copyRequest = async () => {
try {
await navigator.clipboard.writeText(request);
} catch {
const field = document.createElement("textarea");
field.value = request;
field.style.position = "fixed";
field.style.opacity = "0";
document.body.appendChild(field);
field.select();
document.execCommand("copy");
field.remove();
}
};
return (
<section className="hf-reference-story" aria-label="A HyperFrames project, opened up">
<header className="hf-reference-story__header">
<span>Reference project</span>
<h2>One request. Every artifact.</h2>
<p>
Follow the same small project through the beginner, editing, Studio, and developer docs.
</p>
</header>
<ol className="hf-reference-story__flow">
<li>
<div className="hf-reference-story__step">1</div>
<div className="hf-reference-story__body">
<h3>Request</h3>
<div className="hf-reference-story__request">
<code>{request}</code>
<button type="button" onClick={copyRequest} aria-label="Copy the reference request">
Copy
</button>
</div>
</div>
</li>
<li>
<div className="hf-reference-story__step">2</div>
<div className="hf-reference-story__body">
<h3>Brief</h3>
<p>The request settles the source, length, and format. The agent asks one question:</p>
<blockquote>Sell it, or show it as it is?</blockquote>
<p>
The answer is “show it.” The real page and its own words become the source of truth.
</p>
</div>
</li>
<li>
<div className="hf-reference-story__step">3</div>
<div className="hf-reference-story__body">
<h3>Source</h3>
<div className="hf-reference-story__files" role="list" aria-label="Project files">
<span role="listitem">
<code>BRIEF.md</code>
<small>intent and facts</small>
</span>
<span role="listitem">
<code>index.html</code>
<small>composition and motion</small>
</span>
<span role="listitem">
<code>captions.html</code>
<small>timed phrases</small>
</span>
<span role="listitem">
<code>assets/</code>
<small>capture, voice, music, SFX</small>
</span>
</div>
</div>
</li>
<li>
<div className="hf-reference-story__step">4</div>
<div className="hf-reference-story__body">
<h3>Revision</h3>
<div className="hf-reference-story__revision">
<div>
<span>First version</span>
<p>A real capture, a 10-second timeline, and music.</p>
</div>
<div aria-hidden="true">→</div>
<div>
<span>Controlled version</span>
<p>Editable variables, narration, captions, and purposeful sound.</p>
</div>
</div>
</div>
</li>
<li>
<div className="hf-reference-story__step">5</div>
<div className="hf-reference-story__body">
<h3>Render</h3>
<p>
The browser preview and final MP4 come from the same HTML project and the same
timeline.
</p>
<a href={sourceHref}>Open the complete project →</a>
</div>
</li>
</ol>
</section>
);
};
+2 -1
View File
@@ -15,7 +15,8 @@ import { DocsVideo } from "/snippets/docs-video.jsx";
</Frame>
Studio is the visual editor for the same HTML project your agent and the CLI
use. Open the project folder, then run:
use. The Reference Project from the first-video guide opens here without an
import or conversion. Open any project folder, then run:
```bash
npx hyperframes preview