Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .fallowrc.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
{
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 19 additions & 1 deletion docs/concepts/variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
53 changes: 53 additions & 0 deletions docs/sdk/reference/composition.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,59 @@ comp.setVariableValue("brandFont", { name: "Inter", source: "https://fonts.googl
comp.setVariableValue("heroImage", { url: "/assets/hero.jpg", fit: "cover" });
```

### `declareVariable` / `updateVariableDeclaration` / `removeVariableDeclaration`

```typescript
declareVariable(declaration: CompositionVariable): void
updateVariableDeclaration(id: string, declaration: CompositionVariable): void
removeVariableDeclaration(id: string): void
```

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: "title", type: "string", label: "Title", default: "Hello" });
comp.updateVariableDeclaration("title", { id: "title", type: "string", label: "Headline", default: "Hello" });
comp.removeVariableDeclaration("title");
```

### `getVariableDeclarations` / `getVariableValues` / `validateVariableValues` / `getVariableUsage`

```typescript
getVariableDeclarations(): CompositionVariable[]
getVariableValues(overrides?: Record<string, unknown>): Record<string, unknown>
validateVariableValues(values: Record<string, unknown>): VariableValidationIssue[]
getVariableUsage(): VariableUsageReport
```

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
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
Expand Down
4 changes: 4 additions & 0 deletions packages/studio/src/components/StudioRightPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -427,6 +428,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}
Expand Down
70 changes: 70 additions & 0 deletions packages/studio/src/components/panels/VariablesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -410,6 +473,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}
Expand Down
12 changes: 12 additions & 0 deletions packages/studio/src/components/renders/useRenderQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -154,6 +162,7 @@ export function useRenderQueue(projectId: string | null) {
format: string;
resolution?: string;
composition?: string;
variables?: Record<string, unknown>;
telemetryDistinctId: string;
} = {
fps,
Expand All @@ -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`, {
Expand Down
Loading