Merge pull request #947 from heygen-com/feat/studio-blocks-panel

feat(studio): full Blocks panel — browse, search, add, drag-and-drop registry items
This commit is contained in:
Miguel Ángel
2026-05-19 03:58:25 +02:00
committed by GitHub
111 changed files with 1800 additions and 106 deletions
+5
View File
@@ -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,
+52
View File
@@ -77,6 +77,17 @@ 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";
@@ -84,6 +95,8 @@ export interface BlockItem extends RegistryItemBase {
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. */
@@ -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 {
@@ -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.
@@ -26,6 +27,7 @@ export function createStudioApi(adapter: StudioApiAdapter): Hono {
registerThumbnailRoutes(api, adapter);
registerWaveformRoutes(api, adapter);
registerFontRoutes(api);
registerRegistryRoutes(api, adapter);
return api;
}
@@ -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
View File
@@ -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 {
@@ -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 }>;
}