mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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 }>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user