mirror of
https://github.com/hanzoai/frames.git
synced 2026-09-24 00:08:03 +00:00
feat(studio): render with preview variables + template handoff + docs
Sixth PR of the template-variables Studio stack — closing the loop from
preview to render to developer handoff.
- renders started from the Renders tab now carry the active preview
variable overrides (StartRenderOptions.variables → POST /render →
RenderConfig.variables), so "render" produces exactly what the user is
previewing.
- Variables panel "Use this template" footer: copy the effective values
(defaults merged with overrides) as JSON, or as a ready-to-run
`npx hyperframes render <comp> --variables '<json>'` command.
- gitignore: negate the renders/ output rule for the tracked
src/components/renders/ source dir — without it, pre-commit's format
re-stage (`git add {staged_files}`) hard-fails on any change to those
files.
- docs: the Studio panel docs/concepts/variables.mdx described was
aspirational — replace with the real Variables-in-Studio section
(declare/edit, render-truthful preview, render-with-values, handoff,
usage badges); document the new SDK variable APIs in
docs/sdk/reference/composition.mdx (declaration ops, read APIs,
setPreviewVariables).
Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -107,6 +107,12 @@
|
||||
// convention documented in CLAUDE.md. This is a namespace barrel, not a
|
||||
// collision.
|
||||
{ "file": "packages/cli/src/commands/*.ts", "exports": ["examples"] },
|
||||
// Options type for the render-queue hook's public startRender callback —
|
||||
// consumed structurally by callers (StudioRightPanel), not by import.
|
||||
{
|
||||
"file": "packages/studio/src/components/renders/useRenderQueue.ts",
|
||||
"exports": ["StartRenderOptions"],
|
||||
},
|
||||
// Independent ML model managers each declare their own DEFAULT_MODEL /
|
||||
// MODELS_DIR / ensureModel for their model namespace.
|
||||
{
|
||||
|
||||
@@ -51,6 +51,9 @@ output/
|
||||
renders/
|
||||
!packages/producer/tests/*/output/
|
||||
!packages/producer/tests/distributed/*/output/
|
||||
# Source dir that the renders/ output rule accidentally shadows (its files are
|
||||
# tracked; without this, pre-commit's format re-stage fails on them).
|
||||
!packages/studio/src/components/renders/
|
||||
|
||||
# Composition source media (large binaries)
|
||||
compositions/**/*.mp4
|
||||
|
||||
@@ -300,7 +300,25 @@ const { variables } = extractCompositionMetadata(html);
|
||||
// variables is CompositionVariable[]
|
||||
```
|
||||
|
||||
This is the same API the Studio editing UI uses to build the variables panel for each composition.
|
||||
This is the same API the Studio Variables panel uses to build its editor for each composition.
|
||||
|
||||
## Variables in Studio
|
||||
|
||||
The Studio's **Variables** tab (right inspector panel) is a full UI over this
|
||||
system:
|
||||
|
||||
- **Declare and edit** — add, edit, and remove declarations without touching the
|
||||
HTML by hand; edits persist into `data-composition-variables` with undo support.
|
||||
- **Preview with values** — type-appropriate inputs write ephemeral overrides that
|
||||
are injected into the preview as `window.__hfVariables`, exactly like render-time
|
||||
injection, so what you preview is what `--variables` renders. A header pill shows
|
||||
whether you're previewing defaults or custom values.
|
||||
- **Render with values** — renders started from the Renders tab carry the active
|
||||
preview overrides.
|
||||
- **Handoff** — copy the effective values as JSON or as a ready-to-run
|
||||
`hyperframes render --variables` command.
|
||||
- **Usage** — declarations no script reads are badged `unused`; ids read by scripts
|
||||
but missing from the schema get a one-click Declare action.
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
@@ -140,35 +140,61 @@ for (const v of comp.listVariables()) {
|
||||
}
|
||||
```
|
||||
|
||||
### `declareVariable`
|
||||
### `declareVariable` / `updateVariableDeclaration` / `removeVariableDeclaration`
|
||||
|
||||
```typescript
|
||||
declareVariable(decl: CompositionVariable): void
|
||||
declareVariable(declaration: CompositionVariable): void
|
||||
updateVariableDeclaration(id: string, declaration: CompositionVariable): void
|
||||
removeVariableDeclaration(id: string): void
|
||||
```
|
||||
|
||||
Create a new variable declaration, or fully replace an existing one (type, label, default, and all other schema fields — not just the value). This is the only path that creates the schema entry from scratch; `setVariableValue` intentionally refuses to.
|
||||
Manage the `data-composition-variables` schema itself. `declareVariable` adds a new
|
||||
typed declaration (creating the attribute when the composition has none);
|
||||
`updateVariableDeclaration` replaces an existing declaration wholesale — the id is
|
||||
immutable, rename by remove + declare; `removeVariableDeclaration` deletes a
|
||||
declaration (the last removal drops the whole attribute). All three validate via
|
||||
`can()` (`E_DUPLICATE_VARIABLE`, `E_VARIABLE_NOT_FOUND`, `E_INVALID_ARGS`,
|
||||
`E_FRAGMENT_COMPOSITION` — fragment sources have no `<html>` to carry the schema).
|
||||
|
||||
```typescript
|
||||
comp.declareVariable({
|
||||
id: "brandColor",
|
||||
type: "color",
|
||||
label: "Brand color",
|
||||
default: "#6C5CE7",
|
||||
});
|
||||
comp.declareVariable({ id: "title", type: "string", label: "Title", default: "Hello" });
|
||||
comp.updateVariableDeclaration("title", { id: "title", type: "string", label: "Headline", default: "Hello" });
|
||||
comp.removeVariableDeclaration("title");
|
||||
```
|
||||
|
||||
### `removeVariable`
|
||||
<Note>`removeVariable(id)` is a shorthand alias of `removeVariableDeclaration(id)`.</Note>
|
||||
|
||||
### `getVariableDeclarations` / `getVariableValues` / `validateVariableValues` / `getVariableUsage`
|
||||
|
||||
```typescript
|
||||
removeVariable(id: string): void
|
||||
getVariableDeclarations(): CompositionVariable[]
|
||||
getVariableValues(overrides?: Record<string, unknown>): Record<string, unknown>
|
||||
validateVariableValues(values: Record<string, unknown>): VariableValidationIssue[]
|
||||
getVariableUsage(): VariableUsageReport
|
||||
```
|
||||
|
||||
Remove a variable's declaration entirely. Any live `var.{id}` overrides and `data-var-*` DOM references are left untouched — this only removes the schema entry.
|
||||
Read-only variable APIs (no dispatch). `getVariableDeclarations` returns the typed
|
||||
schema (same strict filter the render pipeline uses). `getVariableValues` resolves
|
||||
values exactly like the runtime's `getVariables()` — declared defaults merged under
|
||||
the given overrides — so callers can predict what a composition script will read for
|
||||
a given `--variables` payload. `validateVariableValues` runs the same checks as
|
||||
`--strict-variables` (`undeclared` / `type-mismatch` / `enum-out-of-range`).
|
||||
`getVariableUsage` statically scans every inline script for `getVariables()` reads
|
||||
and cross-references the schema: `{ usedIds, unusedDeclarations, undeclaredReads,
|
||||
scanIncomplete }` — `scanIncomplete` flips when scripts access variables dynamically,
|
||||
making `usedIds` a lower bound.
|
||||
|
||||
### `setPreviewVariables`
|
||||
|
||||
```typescript
|
||||
comp.removeVariable("brandColor");
|
||||
setPreviewVariables(values: Record<string, unknown> | null): boolean
|
||||
```
|
||||
|
||||
Apply ephemeral variable values to the preview surface (never persisted — use
|
||||
`setVariableValue` to change a default). Pass `null` to restore declared defaults.
|
||||
Delegates to the preview adapter's optional `setPreviewVariables`; returns `false`
|
||||
when no adapter support is available.
|
||||
|
||||
### `getElementTimings`
|
||||
|
||||
```typescript
|
||||
|
||||
@@ -16,6 +16,7 @@ import { SlideshowPanel } from "./panels/SlideshowPanel";
|
||||
import type { SceneInfo } from "./panels/SlideshowPanel";
|
||||
import { VariablesPanel } from "./panels/VariablesPanel";
|
||||
import { PanelTabButton } from "./PanelTabButton";
|
||||
import { usePreviewVariablesStore } from "../hooks/previewVariablesStore";
|
||||
import type { RenderJob } from "./renders/useRenderQueue";
|
||||
import type { BlockParam } from "@hyperframes/core/registry";
|
||||
import type { IframeWindow } from "../player/lib/playbackTypes";
|
||||
@@ -421,6 +422,9 @@ export function StudioRightPanel({
|
||||
format,
|
||||
resolution,
|
||||
composition,
|
||||
// Render what the user is previewing: active variable overrides
|
||||
// from the Variables panel ride along (undefined = defaults).
|
||||
variables: usePreviewVariablesStore.getState().values ?? undefined,
|
||||
});
|
||||
}}
|
||||
compositionDimensions={compositionDimensions}
|
||||
|
||||
@@ -17,8 +17,14 @@ import {
|
||||
EMPTY_DRAFT,
|
||||
} from "./VariablesDeclarationForm";
|
||||
import { PreviewValueControl } from "./VariablesValueControls";
|
||||
import { copyTextToClipboard } from "../../utils/clipboard";
|
||||
import { isScalarVariableValue as isScalar } from "@hyperframes/core/variables";
|
||||
|
||||
/** POSIX single-quote escaping so the copied command survives quotes in values. */
|
||||
function shellSingleQuote(value: string): string {
|
||||
return `'${value.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
interface VariablesPanelProps {
|
||||
sdkSession: Composition | null;
|
||||
reloadPreview: () => void;
|
||||
@@ -209,6 +215,45 @@ function PreviewModeHeader({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Developer/agent handoff: copy the effective values as JSON or as a
|
||||
* ready-to-run render command mirroring exactly what the preview shows.
|
||||
*/
|
||||
function HandoffFooter({
|
||||
effectiveValues,
|
||||
compPath,
|
||||
onCopy,
|
||||
}: {
|
||||
effectiveValues: Record<string, unknown>;
|
||||
compPath: string;
|
||||
onCopy: (text: string, what: string) => void;
|
||||
}) {
|
||||
const json = JSON.stringify(effectiveValues);
|
||||
const command = `npx hyperframes render ${shellSingleQuote(compPath)} --variables ${shellSingleQuote(json)}`;
|
||||
return (
|
||||
<div className="space-y-1.5 rounded-lg border border-neutral-800/70 bg-neutral-900/40 p-2">
|
||||
<p className="text-[9px] font-medium uppercase tracking-wider text-neutral-500">
|
||||
Use this template
|
||||
</p>
|
||||
<code className="block truncate font-mono text-[9px] text-neutral-500" title={command}>
|
||||
{command}
|
||||
</code>
|
||||
<div className="flex items-center gap-2">
|
||||
<RowAction
|
||||
label="Copy render command"
|
||||
title="CLI command rendering exactly what the preview shows"
|
||||
onClick={() => onCopy(command, "Render command")}
|
||||
/>
|
||||
<RowAction
|
||||
label="Copy values JSON"
|
||||
title="Effective values (defaults merged with preview overrides)"
|
||||
onClick={() => onCopy(json, "Values JSON")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const EMPTY_STATE = (
|
||||
<p className="text-[10px] leading-relaxed text-neutral-500">
|
||||
No variables declared. Variables make parts of this composition dynamic — declare them here (or
|
||||
@@ -269,6 +314,24 @@ export const VariablesPanel = memo(function VariablesPanel({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[sdkSession, previewValues, refreshKey, revision],
|
||||
);
|
||||
const effectiveValues = useMemo(
|
||||
() => sdkSession?.getVariableValues(previewValues ?? undefined) ?? {},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[sdkSession, previewValues, refreshKey, revision],
|
||||
);
|
||||
|
||||
const copyToClipboard = useCallback(
|
||||
(text: string, what: string) => {
|
||||
// Shared helper carries the execCommand fallback Safari needs.
|
||||
void copyTextToClipboard(text).then((ok) =>
|
||||
showToast(
|
||||
ok ? `${what} copied` : `Couldn't copy ${what.toLowerCase()}`,
|
||||
ok ? "info" : "error",
|
||||
),
|
||||
);
|
||||
},
|
||||
[showToast],
|
||||
);
|
||||
|
||||
const dropPreviewOverride = useCallback(
|
||||
(id: string) => {
|
||||
@@ -424,6 +487,13 @@ export const VariablesPanel = memo(function VariablesPanel({
|
||||
Scripts access variables dynamically — usage info may be incomplete.
|
||||
</p>
|
||||
)}
|
||||
{declarations.length > 0 && (
|
||||
<HandoffFooter
|
||||
effectiveValues={effectiveValues}
|
||||
compPath={activeCompPath ?? "index.html"}
|
||||
onCopy={copyToClipboard}
|
||||
/>
|
||||
)}
|
||||
{addOpen ? (
|
||||
<DeclarationForm
|
||||
initial={EMPTY_DRAFT}
|
||||
|
||||
@@ -34,6 +34,12 @@ export interface StartRenderOptions {
|
||||
resolution?: ResolutionPreset | "auto";
|
||||
/** Render a specific composition file instead of index.html. */
|
||||
composition?: string;
|
||||
/**
|
||||
* Composition-variable overrides ({variableId: value}), forwarded to the
|
||||
* render route and injected as window.__hfVariables — the same channel
|
||||
* `hyperframes render --variables` uses.
|
||||
*/
|
||||
variables?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// "Hide" (formerly "Clear") is a view operation, not a delete: hidden ids are
|
||||
@@ -126,7 +132,9 @@ export function useRenderQueue(projectId: string | null) {
|
||||
}, [loadRenders]);
|
||||
|
||||
// Start a render and track progress via SSE
|
||||
// Pre-existing branchy fetch/poll flow — the variables passthrough added one branch.
|
||||
const startRender = useCallback(
|
||||
// fallow-ignore-next-line complexity
|
||||
async (opts: StartRenderOptions = {}) => {
|
||||
if (!projectId) return;
|
||||
|
||||
@@ -154,6 +162,7 @@ export function useRenderQueue(projectId: string | null) {
|
||||
format: string;
|
||||
resolution?: string;
|
||||
composition?: string;
|
||||
variables?: Record<string, unknown>;
|
||||
telemetryDistinctId: string;
|
||||
} = {
|
||||
fps,
|
||||
@@ -166,6 +175,9 @@ export function useRenderQueue(projectId: string | null) {
|
||||
};
|
||||
if (resolution && resolution !== "auto") body.resolution = resolution;
|
||||
if (composition) body.composition = composition;
|
||||
if (opts.variables && Object.keys(opts.variables).length > 0) {
|
||||
body.variables = opts.variables;
|
||||
}
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`/api/projects/${projectId}/render`, {
|
||||
|
||||
Reference in New Issue
Block a user