mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 06:30:03 +00:00
feat: Persist Studio manual edits via manifest (#593)
## Summary Studio manual geometry edits now persist as a project-local manifest instead of being baked into composition source on each gesture. The manifest lives at: ```text .hyperframes/studio-manual-edits.json ``` It is the source of truth for manual drag, resize, rotation, inspector geometry edits, group moves, and selected-layer reset. ## Architecture - **Manifest-backed edits**: each edit stores a kind (`path-offset`, `box-size`, `rotation`), a source-scoped target, and the edit values. - **Source-scoped resolution**: targets include `sourceFile`, `id`, `selector`, and `selectorIndex`, so duplicate selectors in nested compositions resolve against the owning source file. - **Additive CSS layer**: move uses CSS `translate`, resize writes stable dimensions/flex sizing, and rotation uses CSS `rotate` over the authored base. - **Shared replay runtime**: Studio preview, thumbnails, frame capture, producer renders, and CLI Studio renders/thumbnails all use the same core manual-edit render script. - **Animation-safe replay**: Studio reapplies the manual layer after load, refresh, timeline seeks, player operations, playback frames, thumbnail seeks, and render seeks instead of rewriting GSAP timelines. - **History and handoff**: the manifest is a normal project file, so undo/redo and agent edits can preserve, modify, or remove manual visual edits explicitly. ## User Impact Users can move, resize, rotate, group-move, and reset supported layers from the canvas or inspector, then refresh, capture thumbnails/screenshots, play animated compositions, and render videos without manual edits drifting away from the edited state. ## Main Files - `packages/studio/src/components/editor/manualEdits.ts` - `packages/studio/src/components/editor/DomEditOverlay.tsx` - `packages/studio/src/components/editor/PropertyPanel.tsx` - `packages/studio/src/App.tsx` - `packages/core/src/studio-api/helpers/manualEditsRenderScript.ts` - `packages/studio/vite.config.ts` - `packages/cli/src/server/studioServer.ts` - `packages/core/src/compiler/htmlBundler.ts` - `packages/producer/src/services/htmlCompiler.ts` - `packages/core/src/studio-api/routes/thumbnail.ts` - `packages/producer/src/services/fileServer.ts` - `packages/producer/src/services/renderOrchestrator.ts` ## Test Plan ```bash volta run --node 22.20.0 bun run build volta run --node 22.20.0 bun run --filter @hyperframes/core test -- src/studio-api/helpers/manualEditsRenderScript.test.ts volta run --node 22.20.0 bun run --filter @hyperframes/core typecheck volta run --node 22.20.0 bun run --filter @hyperframes/studio typecheck volta run --node 22.20.0 bun run --filter @hyperframes/cli typecheck volta run --node 22.20.0 bunx oxlint <changed files> volta run --node 22.20.0 bunx oxfmt --check <changed files> git diff --check ```
This commit is contained in:
@@ -90,64 +90,71 @@ export const LeftSidebar = memo(function LeftSidebar({
|
||||
style={{ width }}
|
||||
>
|
||||
{/* Tabs — Code first */}
|
||||
<div className="flex border-b border-neutral-800/50 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectTab("code")}
|
||||
className={`flex-1 py-2 text-[11px] font-medium transition-colors ${
|
||||
tab === "code"
|
||||
? "text-neutral-200 border-b-2 border-studio-accent"
|
||||
: "text-neutral-500 hover:text-neutral-400"
|
||||
}`}
|
||||
>
|
||||
Code
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectTab("compositions")}
|
||||
className={`flex-1 py-2 text-[11px] font-medium transition-colors ${
|
||||
tab === "compositions"
|
||||
? "text-neutral-200 border-b-2 border-studio-accent"
|
||||
: "text-neutral-500 hover:text-neutral-400"
|
||||
}`}
|
||||
>
|
||||
Compositions
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectTab("assets")}
|
||||
className={`flex-1 py-2 text-[11px] font-medium transition-colors ${
|
||||
tab === "assets"
|
||||
? "text-neutral-200 border-b-2 border-studio-accent"
|
||||
: "text-neutral-500 hover:text-neutral-400"
|
||||
}`}
|
||||
>
|
||||
Assets
|
||||
</button>
|
||||
{onToggleCollapse && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleCollapse}
|
||||
className="mx-1 my-1 flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-md border border-transparent text-neutral-500 transition-colors hover:border-neutral-800 hover:bg-neutral-900 hover:text-neutral-300"
|
||||
title="Hide sidebar"
|
||||
aria-label="Hide sidebar"
|
||||
<div className="border-b border-neutral-800/50 px-3 py-3 flex-shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="grid min-w-0 flex-1 gap-1 rounded-[18px] bg-neutral-900 p-1 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]"
|
||||
style={{ gridTemplateColumns: "0.9fr 1.25fr 0.9fr" }}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectTab("code")}
|
||||
className={`rounded-[14px] px-2.5 py-2 text-[10px] font-semibold transition-all ${
|
||||
tab === "code"
|
||||
? "bg-neutral-800 text-white"
|
||||
: "text-neutral-500 hover:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
<path d="m14 7-5 5 5 5" />
|
||||
<path d="M19 4v16" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
Code
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectTab("compositions")}
|
||||
className={`rounded-[14px] px-2.5 py-2 text-[10px] font-semibold transition-all ${
|
||||
tab === "compositions"
|
||||
? "bg-neutral-800 text-white"
|
||||
: "text-neutral-500 hover:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
Compositions
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectTab("assets")}
|
||||
className={`rounded-[14px] px-2.5 py-2 text-[10px] font-semibold transition-all ${
|
||||
tab === "assets"
|
||||
? "bg-neutral-800 text-white"
|
||||
: "text-neutral-500 hover:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
Assets
|
||||
</button>
|
||||
</div>
|
||||
{onToggleCollapse && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleCollapse}
|
||||
className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-transparent text-neutral-500 transition-colors hover:border-neutral-800 hover:bg-neutral-900 hover:text-neutral-300"
|
||||
title="Hide sidebar"
|
||||
aria-label="Hide sidebar"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m14 7-5 5 5 5" />
|
||||
<path d="M19 4v16" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
|
||||
Reference in New Issue
Block a user