feat(studio): full Blocks panel — browse, search, add, drag-and-drop registry items

Adds a Blocks tab to the Studio left sidebar with the full 78-item registry
catalog (58 blocks + 20 components). Users can browse by category, search by
title/description, preview CDN-hosted poster thumbnails with video-on-hover,
and install items on-demand with one click or drag-to-timeline.

Core changes:
- BlockCategory type + resolveBlockCategory() for 7 categories (Captions, VFX,
  Transitions, Effects, Social, Data, Scenes)
- Registry API routes: GET /api/registry/blocks (catalog) + POST install
- StudioApiAdapter extended with listRegistryCatalog + installRegistryBlock
- Vite adapter reads from disk; CLI adapter fetches from GitHub (24h cache)
- BlockParam interface + params on 6 blocks for future parameter controls

Studio UI:
- 4th sidebar tab "Blocks" with responsive grid, category pills, search bar
- BlockCard: CDN poster thumbnail, video autoplay on hover, duration + WebGL badges
- On-demand install: blocks append as sub-compositions on timeline; components
  overlay at start=0 spanning full duration with transparent background patching
- TIMELINE_BLOCK_MIME drag-and-drop to timeline
- BlockParamsPanel (Phase 3 scaffold) auto-opens for parameterized blocks

Registry manifests:
- All 58 blocks backfilled with preview: { video, poster } CDN URLs
- All 20 components normalized to object format + poster URLs added
- 6 blocks annotated with params (Liquid Glass/Background, Portal, Chart,
  Logo Outro, Magnetic)
- flowchart-vertical preview generated and uploaded to CDN
This commit is contained in:
Miguel Ángel
2026-05-18 21:15:15 -04:00
parent d9183ba27d
commit ffbc18ad31
111 changed files with 1800 additions and 106 deletions
@@ -0,0 +1,75 @@
import { useState, useEffect, useMemo } from "react";
import type { RegistryItem } from "@hyperframes/core/registry";
import { type BlockCategory, resolveBlockCategory } from "../utils/blockCategories";
export type CatalogItem = RegistryItem & {
category: BlockCategory;
};
export function useBlockCatalog() {
const [blocks, setBlocks] = useState<CatalogItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [search, setSearch] = useState("");
const [category, setCategory] = useState<BlockCategory | null>(null);
// fallow-ignore-next-line complexity
useEffect(() => {
const CATEGORY_ORDER: Record<BlockCategory, number> = {
captions: 0,
vfx: 1,
transitions: 2,
effects: 3,
social: 4,
data: 5,
scenes: 6,
};
let cancelled = false;
(async () => {
try {
const res = await fetch("/api/registry/blocks");
if (!res.ok) throw new Error("Failed to load catalog");
const data = (await res.json()) as RegistryItem[];
if (cancelled) return;
const items = data
.map((b) => ({ ...b, category: resolveBlockCategory(b.tags) }))
.sort((a, b) => (CATEGORY_ORDER[a.category] ?? 9) - (CATEGORY_ORDER[b.category] ?? 9));
setBlocks(items);
} catch (err) {
if (cancelled) return;
setError(err instanceof Error ? err.message : "Failed to load catalog");
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => {
cancelled = true;
};
}, []);
const filteredBlocks = useMemo(() => {
let result = blocks;
if (category) {
result = result.filter((b) => b.category === category);
}
if (search.trim()) {
const q = search.toLowerCase();
result = result.filter(
(b) => b.title.toLowerCase().includes(q) || b.description.toLowerCase().includes(q),
);
}
return result;
}, [blocks, category, search]);
return {
blocks,
loading,
error,
search,
setSearch,
category,
setCategory,
filteredBlocks,
};
}