Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .filesize-allowlist
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ packages/studio/src/components/editor/manualEdits.test.ts
packages/studio/src/player/hooks/useTimelinePlayer.test.ts
packages/studio/src/components/editor/manualEditsDom.ts
packages/studio/src/utils/sourcePatcher.ts
packages/studio/src/App.tsx
packages/studio/src/player/components/Timeline.tsx
2 changes: 1 addition & 1 deletion packages/aws-lambda/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hyperframes/aws-lambda",
"version": "0.6.22",
"version": "0.6.23",
"description": "AWS Lambda adapter for HyperFrames distributed rendering — handler, client-side SDK, and CDK construct.",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hyperframes/cli",
"version": "0.6.22",
"version": "0.6.23",
"description": "HyperFrames CLI — create, preview, and render HTML video compositions",
"repository": {
"type": "git",
Expand Down
21 changes: 21 additions & 0 deletions packages/cli/src/server/studioServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,27 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
await page?.close().catch(() => {});
}
},

async listRegistryCatalog() {
const { listRegistryItems, loadAllItems } = await import("../registry/resolver.js");
const entries = await listRegistryItems();
const blockAndComponentEntries = entries.filter(
(e) => e.type === "hyperframes:block" || e.type === "hyperframes:component",
);
return loadAllItems(blockAndComponentEntries);
},

async installRegistryBlock(opts) {
const { resolveItem } = await import("../registry/resolver.js");
const { installItem } = await import("../registry/installer.js");
const item = await resolveItem(opts.blockName);
const { written } = await installItem(item, { destDir: opts.project.dir });
const relativePaths = written.map((abs) => {
const rel = abs.startsWith(opts.project.dir) ? abs.slice(opts.project.dir.length + 1) : abs;
return rel;
});
return { written: relativePaths, block: item };
},
};

// ── Build the Hono app ─────────────────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hyperframes/core",
"version": "0.6.22",
"version": "0.6.23",
"description": "",
"repository": {
"type": "git",
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/registry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ export type {
ComponentItem,
RegistryManifestEntry,
RegistryManifest,
BlockCategory,
BlockCategoryMeta,
BlockParam,
} from "./types.js";

export {
ITEM_TYPES,
FILE_TYPES,
ITEM_TYPE_DIRS,
BLOCK_CATEGORIES,
resolveBlockCategory,
isExampleItem,
isBlockItem,
isComponentItem,
Expand Down
52 changes: 52 additions & 0 deletions packages/core/src/registry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,26 @@ export interface ExampleItem extends RegistryItemBase {
duration: number;
}

export interface BlockParam {
key: string;
label: string;
type: "color" | "text" | "number" | "select";
default: string;
options?: { label: string; value: string }[];
min?: number;
max?: number;
step?: number;
}

/** Sub-composition block — installed by `hyperframes add <name>`. */
export interface BlockItem extends RegistryItemBase {
type: "hyperframes:block";
/** Canvas dimensions (required for blocks — they are standalone compositions). */
dimensions: RegistryItemDimensions;
/** Duration in seconds (required for blocks). */
duration: number;
/** Customizable parameters with CSS variable mapping. */
params?: BlockParam[];
}

/** Effect / snippet — merged into an existing composition. */
Expand Down Expand Up @@ -159,6 +172,45 @@ const _fileTypesExhaustive: _AssertFileTypesExhaustive = true;
void _itemTypesExhaustive;
void _fileTypesExhaustive;

// ── Block categories ───────────────────────────────────────────────────────

export type BlockCategory =
| "vfx"
| "transitions"
| "social"
| "data"
| "scenes"
| "captions"
| "effects";

export interface BlockCategoryMeta {
id: BlockCategory;
label: string;
color: string;
}

export const BLOCK_CATEGORIES: BlockCategoryMeta[] = [
{ id: "captions", label: "Captions", color: "cyan" },
{ id: "vfx", label: "VFX", color: "purple" },
{ id: "transitions", label: "Transitions", color: "blue" },
{ id: "effects", label: "Effects", color: "rose" },
{ id: "social", label: "Social", color: "pink" },
{ id: "data", label: "Data", color: "green" },
{ id: "scenes", label: "Scenes", color: "amber" },
];

export function resolveBlockCategory(tags: string[] | undefined): BlockCategory {
if (!tags || tags.length === 0) return "scenes";
const set = new Set(tags);
if (set.has("captions") || set.has("caption-style")) return "captions";
if (set.has("transition")) return "transitions";
if (set.has("social") || set.has("overlay")) return "social";
if (set.has("data") || set.has("chart") || set.has("map")) return "data";
if (set.has("html-in-canvas") || set.has("webgl") || set.has("shader")) return "vfx";
if (set.has("effect") || set.has("grain") || set.has("vignette")) return "effects";
return "scenes";
}

// ── Type guards ─────────────────────────────────────────────────────────────

export function isExampleItem(item: RegistryItem): item is ExampleItem {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/studio-api/createStudioApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { registerRenderRoutes } from "./routes/render.js";
import { registerThumbnailRoutes } from "./routes/thumbnail.js";
import { registerWaveformRoutes } from "./routes/waveform.js";
import { registerFontRoutes } from "./routes/fonts.js";
import { registerRegistryRoutes } from "./routes/registry.js";

/**
* Create a Hono sub-app with all studio API routes.
Expand All @@ -26,6 +27,7 @@ export function createStudioApi(adapter: StudioApiAdapter): Hono {
registerThumbnailRoutes(api, adapter);
registerWaveformRoutes(api, adapter);
registerFontRoutes(api);
registerRegistryRoutes(api, adapter);

return api;
}
34 changes: 34 additions & 0 deletions packages/core/src/studio-api/routes/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Hono } from "hono";
import type { StudioApiAdapter } from "../types.js";

export function registerRegistryRoutes(api: Hono, adapter: StudioApiAdapter): void {
api.get("/registry/blocks", async (c) => {
if (!adapter.listRegistryCatalog) {
return c.json({ error: "Registry not available" }, 501);
}
const items = await adapter.listRegistryCatalog();
return c.json(items);
});

// fallow-ignore-next-line complexity
api.post("/projects/:id/registry/install", async (c) => {
if (!adapter.installRegistryBlock) {
return c.json({ error: "Registry install not available" }, 501);
}
const project = await adapter.resolveProject(c.req.param("id"));
if (!project) return c.json({ error: "Project not found" }, 404);

const body = await c.req.json<{ blockName?: string }>().catch(() => null);
if (!body?.blockName) {
return c.json({ error: "blockName is required" }, 400);
}

try {
const result = await adapter.installRegistryBlock({ project, blockName: body.blockName });
return c.json(result);
} catch (err) {
const message = err instanceof Error ? err.message : "Install failed";
return c.json({ error: message }, 500);
}
});
}
10 changes: 10 additions & 0 deletions packages/core/src/studio-api/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { CanvasResolution } from "../core.types.js";
import type { RegistryItem } from "../registry/types.js";

/** Resolved info about a single project. */
export interface ResolvedProject {
Expand Down Expand Up @@ -107,4 +108,13 @@ export interface StudioApiAdapter {

/** Optional: resolve session ID to project (multi-project mode). */
resolveSession?: (sessionId: string) => Promise<{ projectId: string; title: string } | null>;

/** Optional: list all registry items (blocks + components) for the catalog. */
listRegistryCatalog?(): Promise<RegistryItem[]>;

/** Optional: install a registry item into a project directory. */
installRegistryBlock?(opts: {
project: ResolvedProject;
blockName: string;
}): Promise<{ written: string[]; block: RegistryItem }>;
}
2 changes: 1 addition & 1 deletion packages/engine/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hyperframes/engine",
"version": "0.6.22",
"version": "0.6.23",
"description": "Seekable web page to video rendering engine (Puppeteer + FFmpeg)",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion packages/player/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hyperframes/player",
"version": "0.6.22",
"version": "0.6.23",
"description": "Embeddable web component for HyperFrames compositions",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion packages/producer/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hyperframes/producer",
"version": "0.6.22",
"version": "0.6.23",
"description": "HTML-to-video rendering engine using Chrome's BeginFrame API",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion packages/shader-transitions/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hyperframes/shader-transitions",
"version": "0.6.22",
"version": "0.6.23",
"description": "WebGL shader transitions for HyperFrames compositions",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion packages/studio/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hyperframes/studio",
"version": "0.6.22",
"version": "0.6.23",
"description": "",
"repository": {
"type": "git",
Expand Down
88 changes: 88 additions & 0 deletions packages/studio/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { usePanelLayout } from "./hooks/usePanelLayout";
import { useFileManager } from "./hooks/useFileManager";
import { useManifestPersistence } from "./hooks/useManifestPersistence";
import { useTimelineEditing } from "./hooks/useTimelineEditing";
import { addBlockToProject } from "./utils/blockInstaller";
import type { BlockParam } from "@hyperframes/core/registry";
import { useDomEditSession } from "./hooks/useDomEditSession";
import { useAppHotkeys } from "./hooks/useAppHotkeys";
import { useClipboard } from "./hooks/useClipboard";
Expand Down Expand Up @@ -59,6 +61,12 @@ export function StudioApp() {
const [compositionLoading, setCompositionLoading] = useState(true);
const [refreshKey, setRefreshKey] = useState(0);
const [, setPreviewDocumentVersion] = useState(0);
const [activeBlockParams, setActiveBlockParams] = useState<{
blockName: string;
blockTitle: string;
params: BlockParam[];
compositionPath: string;
} | null>(null);

const previewIframeRef = useRef<HTMLIFrameElement | null>(null);
const activeCompPathRef = useRef(activeCompPath);
Expand Down Expand Up @@ -161,6 +169,79 @@ export function StudioApp() {
uploadProjectFiles: fileManager.uploadProjectFiles,
});

const handleAddBlock = useCallback(
(blockName: string) => {
if (!projectId) return;
void (async () => {
const result = await addBlockToProject({
projectId,
blockName,
activeCompPath,
timelineElements,
readProjectFile: fileManager.readProjectFile,
writeProjectFile: fileManager.writeProjectFile,
recordEdit: editHistory.recordEdit,
refreshFileTree: fileManager.refreshFileTree,
reloadPreview,
showToast,
});
const params = result?.block.type === "hyperframes:block" ? result.block.params : undefined;
if (params?.length) {
setActiveBlockParams({
blockName: result!.block.name,
blockTitle: result!.block.title,
params,
compositionPath: result!.compositionPath,
});
panelLayout.setRightCollapsed(false);
panelLayout.setRightPanelTab("block-params");
}
})();
},
[
projectId,
activeCompPath,
timelineElements,
fileManager.readProjectFile,
fileManager.writeProjectFile,
fileManager.refreshFileTree,
editHistory.recordEdit,
reloadPreview,
showToast,
panelLayout,
],
);

const handleTimelineBlockDrop = useCallback(
(blockName: string, placement: { start: number; track: number }) => {
if (!projectId) return;
void addBlockToProject({
projectId,
blockName,
activeCompPath,
placement,
timelineElements,
readProjectFile: fileManager.readProjectFile,
writeProjectFile: fileManager.writeProjectFile,
recordEdit: editHistory.recordEdit,
refreshFileTree: fileManager.refreshFileTree,
reloadPreview,
showToast,
});
},
[
projectId,
activeCompPath,
timelineElements,
fileManager.readProjectFile,
fileManager.writeProjectFile,
fileManager.refreshFileTree,
editHistory.recordEdit,
reloadPreview,
showToast,
],
);

const clearDomSelectionRef = useRef<() => void>(() => {});
const domEditSelectionBridgeRef = useRef<DomEditSelection | null>(null);
const handleDomEditElementDeleteRef = useRef<(s: DomEditSelection) => Promise<void>>(
Expand Down Expand Up @@ -427,6 +508,7 @@ export function StudioApp() {
<StudioLeftSidebar
leftSidebarRef={leftSidebarRef}
onSelectComposition={handleSelectComposition}
onAddBlock={handleAddBlock}
onLint={handleLint}
linting={linting}
/>
Expand All @@ -435,6 +517,7 @@ export function StudioApp() {
renderClipContent={renderClipContent}
handleTimelineElementDelete={timelineEditing.handleTimelineElementDelete}
handleTimelineAssetDrop={timelineEditing.handleTimelineAssetDrop}
handleTimelineBlockDrop={handleTimelineBlockDrop}
handleTimelineFileDrop={timelineEditing.handleTimelineFileDrop}
handleTimelineElementMove={timelineEditing.handleTimelineElementMove}
handleTimelineElementResize={timelineEditing.handleTimelineElementResize}
Expand All @@ -449,6 +532,11 @@ export function StudioApp() {
selectedStudioMotion={selectedStudioMotion}
designPanelActive={designPanelActive}
motionPanelActive={motionPanelActive}
activeBlockParams={activeBlockParams}
onCloseBlockParams={() => {
setActiveBlockParams(null);
panelLayout.setRightPanelTab("design");
}}
/>
)}
</div>
Expand Down
Loading
Loading