mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-11 19:09:20 +00:00
docs: make journey pages interactive
This commit is contained in:
+596
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user