feat(studio): keyframe system — parser, runtime, timeline UI, design panel, gesture recording (#1311)

* feat(studio): runtime hooks — global time compiler + keyframe runtime

Add the runtime bridge layer: global time compilation (tween % → clip %),
soft reload after mutations, runtime keyframe preview, and keyframe
commit helper.

* feat(studio): runtime hooks — global time compiler + keyframe runtime

Add the runtime bridge layer: global time compilation (tween % → clip %),
soft reload after mutations, runtime keyframe preview, and keyframe
commit helper.

* feat(studio): keyframe cache + commit hooks

Add hooks for keyframe cache population (tween → clip-relative %),
mutation dispatch, keyframe snapping, and audio beat detection.

* feat(studio): timeline UI — dopesheet diamonds + keyboard nav

Add dopesheet strip with diamond keyframe indicators, timeline property
rows, keyboard navigation (J/Shift+J/Delete/K), and feature gate
(STUDIO_KEYFRAMES_ENABLED defaults to false).

* feat(studio): design panel — arc controls + ease curve + stagger

Add arc path controls (curviness slider, auto-rotate), motion path SVG
overlay, ease curve visualization, stagger controls, and expanded
animation card. Includes border-radius editor dependency from #1217.

* feat(studio): gesture recording core

Add gesture recording engine with RAF sampling, modifier key property
mapping (Shift→rotationXY, Alt→rotation, Cmd→opacity),
Ramer-Douglas-Peucker simplification, and ghost trail SVG overlay.

* fix(studio): keyframe drag + recording bug bash

21 fixes: capture GSAP base at drag start, translate:none before
gsap.set, skip reapplyPathOffsets for GSAP elements, clamp recording
seek, _auto flag for 100% keyframes, overlay flash fix, block edits
during recording.

* feat(studio): keyframe integration wiring + docs

Wire App.tsx recording orchestration, TimelineToolbar K/R buttons,
PropertyPanel per-property diamonds, shortcuts panel, toast
notifications, and keyframes guide documentation. All gated on
STUDIO_KEYFRAMES_ENABLED (default false).
This commit is contained in:
Miguel Ángel
2026-06-09 18:30:23 -04:00
committed by GitHub
parent 96b8d617d8
commit a468550f82
72 changed files with 4421 additions and 621 deletions
+47 -7
View File
@@ -1,18 +1,58 @@
interface StudioToastProps {
message: string;
tone?: "error" | "info";
onDismiss?: () => void;
}
export function StudioToast({ message, tone }: StudioToastProps) {
export function StudioToast({ message, tone, onDismiss }: StudioToastProps) {
const isError = tone === "error";
return (
<div
className={`absolute bottom-6 left-1/2 -translate-x-1/2 z-[91] px-4 py-2 rounded-lg border text-sm shadow-lg animate-in fade-in slide-in-from-bottom-2 ${
tone === "error"
? "bg-red-900/90 border-red-700/50 text-red-200"
: "bg-neutral-900/95 border-neutral-700/60 text-neutral-100"
}`}
className="absolute bottom-6 right-6 z-[91] animate-in fade-in slide-in-from-bottom-2"
onClick={onDismiss}
role={onDismiss ? "button" : undefined}
style={onDismiss ? { cursor: "pointer" } : undefined}
>
{message}
<div
className="relative flex items-center gap-3 overflow-hidden rounded-2xl pl-4 pr-2 py-3 text-[12px]"
style={{
background: isError
? "linear-gradient(135deg, rgba(127,29,29,0.55), rgba(80,10,10,0.45))"
: "linear-gradient(135deg, rgba(38,38,38,0.55), rgba(23,23,23,0.45))",
backdropFilter: "blur(16px) saturate(1.6)",
WebkitBackdropFilter: "blur(16px) saturate(1.6)",
border: `1px solid ${isError ? "rgba(239,68,68,0.18)" : "rgba(255,255,255,0.08)"}`,
boxShadow: [
"0 8px 32px rgba(0,0,0,0.35)",
`inset 0 1px 0 ${isError ? "rgba(239,68,68,0.12)" : "rgba(255,255,255,0.06)"}`,
`inset 0 -1px 0 rgba(0,0,0,0.15)`,
].join(", "),
}}
>
<span className={isError ? "text-red-200" : "text-neutral-200"}>{message}</span>
{onDismiss && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onDismiss();
}}
className="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-white/10 hover:text-neutral-300"
aria-label="Dismiss"
>
<svg
width="10"
height="10"
viewBox="0 0 10 10"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
>
<path d="M2 2l6 6M8 2l-6 6" />
</svg>
</button>
)}
</div>
</div>
);
}