mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
feat: add studio timeline editing (#390)
## Summary Add the actual Studio timeline editing layer on top of the preview/runtime foundation. This PR includes: - drag-to-move clips across time and tracks - left/right resize handles with media-aware trim persistence - edge auto-scroll and edge track creation while dragging - selector-based source patching for `data-start`, `data-duration`, `data-track-index`, `z-index`, and media trim attributes - timeline UI cleanup, theming, hover/drag states, and the `Copy Prompt` action ## Why This PR Is Separate This is the user-facing editing behavior. It depends on the preview/runtime fixes in the base PR, but it is much easier to review once that plumbing is isolated. ## Verification - `bun run --filter @hyperframes/studio test` - `bun run --filter @hyperframes/studio typecheck` - `bunx oxlint packages/studio/src/App.tsx packages/studio/src/components/nle/NLELayout.tsx packages/studio/src/player/components/EditModal.tsx packages/studio/src/player/components/Timeline.tsx packages/studio/src/player/components/TimelineClip.tsx packages/studio/src/player/components/timelineEditing.ts packages/studio/src/player/components/timelineEditing.test.ts packages/studio/src/player/components/timelineTheme.ts packages/studio/src/player/components/timelineTheme.test.ts packages/studio/src/utils/sourcePatcher.ts packages/studio/src/utils/sourcePatcher.test.ts` - `bunx oxfmt --check packages/studio/src/App.tsx packages/studio/src/components/nle/NLELayout.tsx packages/studio/src/player/components/EditModal.tsx packages/studio/src/player/components/Timeline.tsx packages/studio/src/player/components/TimelineClip.tsx packages/studio/src/player/components/timelineEditing.ts packages/studio/src/player/components/timelineEditing.test.ts packages/studio/src/player/components/timelineTheme.ts packages/studio/src/player/components/timelineTheme.test.ts packages/studio/src/utils/sourcePatcher.ts packages/studio/src/utils/sourcePatcher.test.ts` ## Browser Proof - verified timeline drag / resize / trim flows in Studio with `agent-browser` - verified preview hot-refresh behavior without iframe remount flashes ## Stack - depends on #389 - followed by `fix: smooth scrubber end seeking` [result.mp4 <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.com/user-attachments/thumbnails/ca71c177-5042-468d-906f-b353938f40f8.mp4" />](https://app.graphite.com/user-attachments/video/ca71c177-5042-468d-906f-b353938f40f8.mp4)
This commit is contained in:
@@ -132,6 +132,19 @@ describe("usePlayerStore", () => {
|
||||
usePlayerStore.getState().updateElement("nonexistent", { start: 10 });
|
||||
expect(usePlayerStore.getState().elements[0].start).toBe(0);
|
||||
});
|
||||
|
||||
it("prefers the stable element key when duplicate ids exist", () => {
|
||||
usePlayerStore.getState().setElements([
|
||||
{ id: "headline", key: "a", tag: "div", start: 0, duration: 5, track: 0 },
|
||||
{ id: "headline", key: "b", tag: "div", start: 5, duration: 5, track: 1 },
|
||||
]);
|
||||
|
||||
usePlayerStore.getState().updateElement("b", { start: 9 });
|
||||
|
||||
const elements = usePlayerStore.getState().elements;
|
||||
expect(elements[0].start).toBe(0);
|
||||
expect(elements[1].start).toBe(9);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setZoomMode", () => {
|
||||
|
||||
@@ -2,12 +2,16 @@ import { create } from "zustand";
|
||||
|
||||
export interface TimelineElement {
|
||||
id: string;
|
||||
key?: string;
|
||||
tag: string;
|
||||
start: number;
|
||||
duration: number;
|
||||
track: number;
|
||||
domId?: string;
|
||||
/** Best-effort selector used when patching source HTML back from timeline edits */
|
||||
selector?: string;
|
||||
/** Zero-based occurrence index for non-unique selectors */
|
||||
selectorIndex?: number;
|
||||
/** Source composition file that owns this element, when known */
|
||||
sourceFile?: string;
|
||||
src?: string;
|
||||
@@ -86,7 +90,9 @@ export const usePlayerStore = create<PlayerState>((set) => ({
|
||||
setSelectedElementId: (id) => set({ selectedElementId: id }),
|
||||
updateElement: (elementId, updates) =>
|
||||
set((state) => ({
|
||||
elements: state.elements.map((el) => (el.id === elementId ? { ...el, ...updates } : el)),
|
||||
elements: state.elements.map((el) =>
|
||||
(el.key ?? el.id) === elementId ? { ...el, ...updates } : el,
|
||||
),
|
||||
})),
|
||||
// Resets project-specific state when switching compositions.
|
||||
// playbackRate, zoomMode, and pixelsPerSecond are intentionally preserved
|
||||
|
||||
Reference in New Issue
Block a user