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
@@ -8,9 +8,11 @@ import {
} from "react";
import { CompositionsTab } from "./CompositionsTab";
import { AssetsTab } from "./AssetsTab";
import { BlocksTab } from "./BlocksTab";
import { FileTree } from "../editor/FileTree";
import { STUDIO_BLOCKS_PANEL_ENABLED } from "../editor/manualEditingAvailability";
export type SidebarTab = "compositions" | "assets" | "code";
export type SidebarTab = "compositions" | "assets" | "code" | "blocks";
export interface LeftSidebarHandle {
selectTab: (tab: SidebarTab) => void;
@@ -22,6 +24,7 @@ function getPersistedTab(): SidebarTab {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored === "assets") return "assets";
if (stored === "code") return "code";
if (stored === "blocks") return "blocks";
return "compositions";
}
@@ -48,6 +51,7 @@ interface LeftSidebarProps {
onLint?: () => void;
linting?: boolean;
onToggleCollapse?: () => void;
onAddBlock?: (blockName: string) => void;
takeoverContent?: ReactNode;
}
@@ -76,6 +80,7 @@ export const LeftSidebar = memo(
onLint,
linting,
onToggleCollapse,
onAddBlock,
takeoverContent,
},
ref,
@@ -103,7 +108,11 @@ export const LeftSidebar = memo(
<div className="flex items-center gap-2">
<div
className="grid min-w-0 flex-1 gap-0.5 rounded-[18px] bg-neutral-900 p-1 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]"
style={{ gridTemplateColumns: "1fr 1fr 1fr" }}
style={{
gridTemplateColumns: STUDIO_BLOCKS_PANEL_ENABLED
? "1fr 1fr 1fr 1fr"
: "1fr 1fr 1fr",
}}
>
<button
type="button"
@@ -138,6 +147,19 @@ export const LeftSidebar = memo(
>
Assets
</button>
{STUDIO_BLOCKS_PANEL_ENABLED && (
<button
type="button"
onClick={() => selectTab("blocks")}
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
tab === "blocks"
? "bg-neutral-800 text-white"
: "text-neutral-500 hover:text-neutral-200"
}`}
>
Blocks
</button>
)}
</div>
{onToggleCollapse && (
<button
@@ -214,6 +236,10 @@ export const LeftSidebar = memo(
</div>
)}
{STUDIO_BLOCKS_PANEL_ENABLED && tab === "blocks" && onAddBlock && (
<BlocksTab onAddBlock={onAddBlock} />
)}
{/* Lint button pinned at the bottom */}
{onLint && (
<div className="border-t border-neutral-800 p-2 flex-shrink-0">