mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
fix: gate studio timeline actions by capability (#415)
## Summary - gate timeline actions to clips Studio can control deterministically - disable direct move/trim for generic GSAP-timed DOM clips - add an in-clip `Copy to Agent` fallback for unsupported edits ## Why Studio should only advertise timeline actions it can round-trip to source HTML with deterministic meaning. This PR now follows that stricter rule: - direct move/end-trim are only exposed for clips with a deterministic timeline window - start trim is only exposed for clips with a real content-offset model - unsupported motion clips now offer `Copy to Agent` so users still have a fast path to request source-level timing changes In practice this means generic GSAP-authored DOM clips no longer pretend Studio can rewrite their visible timing just by patching `data-start` / `data-duration`. ## What changed - added `hasPatchableTimelineTarget()` and `getTimelineEditCapabilities()` in `timelineEditing.ts` - tightened deterministic-window detection so only media, images, and composition hosts keep direct move/end-trim controls - kept wrapped media clips editable by recognizing real media metadata even when the host tag is a `div` - updated `TimelineClip` / `Timeline` to guard interactions with the shared capability model - added `buildTimelineElementAgentPrompt()` and a `Copy to Agent` fallback button for unsupported clips - added focused tests for capability derivation and the agent-prompt helper ## Verification ### Automated - `bun test packages/studio/src/player/components/timelineEditing.test.ts packages/studio/src/player/components/Timeline.test.ts packages/studio/src/player/store/playerStore.test.ts packages/studio/src/utils/sourcePatcher.test.ts` - `bun run --filter @hyperframes/studio typecheck` - `bunx oxlint packages/studio/src/player/components/Timeline.tsx packages/studio/src/player/components/timelineEditing.ts packages/studio/src/player/components/timelineEditing.test.ts packages/studio/src/player/components/TimelineClip.tsx` - `bunx oxfmt --check packages/studio/src/player/components/Timeline.tsx packages/studio/src/player/components/timelineEditing.ts packages/studio/src/player/components/timelineEditing.test.ts packages/studio/src/player/components/TimelineClip.tsx` ### Browser Verified in Studio with live browser automation against `http://127.0.0.1:4175/#project/timeline-edit-playground`: - generic GSAP-timed clips (`feature-card`, `title-card`, `prompt-card`) show `Copy to Agent` and no direct move/trim affordances - wrapped media (`media-card`) still exposes direct controls and remains draggable - the local playground timings were realigned to match the authored GSAP positions, so preview visibility now matches the timeline windows during manual testing Recording artifacts used during verification: - `/tmp/timeline-capabilities-proof/capabilities-flow.webm` - `/tmp/timeline-capabilities-proof/capabilities-agent-flow.webm`
This commit is contained in:
@@ -4,7 +4,7 @@ import type { TimelineTrackStyle } from "./timelineTheme";
|
||||
import { memo, type ReactNode } from "react";
|
||||
import type { TimelineElement } from "../store/playerStore";
|
||||
import { defaultTimelineTheme, getClipHandleOpacity, type TimelineTheme } from "./timelineTheme";
|
||||
import { canOffsetTrimClipStart } from "./timelineEditing";
|
||||
import { getTimelineEditCapabilities } from "./timelineEditing";
|
||||
|
||||
interface TimelineClipProps {
|
||||
el: TimelineElement;
|
||||
@@ -60,7 +60,7 @@ export const TimelineClip = memo(function TimelineClip({
|
||||
: isHovered
|
||||
? theme.clipShadowHover
|
||||
: theme.clipShadow;
|
||||
const canTrimStart = canOffsetTrimClipStart(el);
|
||||
const capabilities = getTimelineEditCapabilities(el);
|
||||
const showHandles = handleOpacity > 0.01;
|
||||
|
||||
return (
|
||||
@@ -87,7 +87,7 @@ export const TimelineClip = memo(function TimelineClip({
|
||||
transition:
|
||||
"border-color 120ms ease-out, box-shadow 140ms ease-out, background 140ms ease-out",
|
||||
zIndex: isDragging ? 20 : isSelected ? 10 : isHovered ? 5 : 1,
|
||||
cursor: "grab",
|
||||
cursor: capabilities.canMove ? "grab" : "default",
|
||||
transform: isDragging ? "translateY(-1px)" : undefined,
|
||||
}}
|
||||
title={
|
||||
@@ -111,13 +111,13 @@ export const TimelineClip = memo(function TimelineClip({
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: 18,
|
||||
opacity: showHandles && canTrimStart ? 1 : 0,
|
||||
pointerEvents: onResizeStart && canTrimStart ? "auto" : "none",
|
||||
opacity: showHandles && capabilities.canTrimStart ? 1 : 0,
|
||||
pointerEvents: onResizeStart && capabilities.canTrimStart ? "auto" : "none",
|
||||
zIndex: 4,
|
||||
transition: "opacity 120ms ease-out",
|
||||
cursor: "col-resize",
|
||||
background:
|
||||
showHandles && canTrimStart
|
||||
showHandles && capabilities.canTrimStart
|
||||
? `linear-gradient(90deg, ${trackStyle.accent}4d 0%, ${trackStyle.accent}22 42%, transparent 100%)`
|
||||
: "transparent",
|
||||
}}
|
||||
@@ -148,13 +148,14 @@ export const TimelineClip = memo(function TimelineClip({
|
||||
bottom: 0,
|
||||
width: 18,
|
||||
opacity: showHandles ? 1 : 0,
|
||||
pointerEvents: onResizeStart ? "auto" : "none",
|
||||
pointerEvents: onResizeStart && capabilities.canTrimEnd ? "auto" : "none",
|
||||
zIndex: 4,
|
||||
transition: "opacity 120ms ease-out",
|
||||
cursor: "col-resize",
|
||||
background: showHandles
|
||||
? `linear-gradient(270deg, ${trackStyle.accent}4d 0%, ${trackStyle.accent}22 42%, transparent 100%)`
|
||||
: "transparent",
|
||||
background:
|
||||
showHandles && capabilities.canTrimEnd
|
||||
? `linear-gradient(270deg, ${trackStyle.accent}4d 0%, ${trackStyle.accent}22 42%, transparent 100%)`
|
||||
: "transparent",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user