Skip to content

feat: add studio timeline editing#390

Merged
miguel-heygen merged 3 commits intomainfrom
feat/studio-timeline-editing
Apr 21, 2026
Merged

feat: add studio timeline editing#390
miguel-heygen merged 3 commits intomainfrom
feat/studio-timeline-editing

Conversation

@miguel-heygen
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen commented Apr 21, 2026

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

result.mp4 (uploaded via Graphite)

Copy link
Copy Markdown
Collaborator Author

miguel-heygen commented Apr 21, 2026

@miguel-heygen miguel-heygen marked this pull request as ready for review April 21, 2026 20:02
Comment thread packages/studio/src/player/components/Timeline.tsx Outdated
@jrusso1020
Copy link
Copy Markdown
Collaborator

One additional edge case I found spans the base PR and this one: the standalone root-composition fallback clip is editable now, but it is created without selector / sourceFile metadata in the preview-runtime foundation layer. In this PR, move/resize persistence falls back to { id: element.id } plus the active file path. That works only if the root composition host also has a matching DOM id, which is not guaranteed in the fallback case because the source object is derived from data-composition-id. Net effect: the UI can expose drag/resize affordances for some standalone composition clips that cannot actually be patched back to source reliably.

@miguel-heygen
Copy link
Copy Markdown
Collaborator Author

Addressed in 32b280bd.

What changed:

  • standalone root-composition fallback clips are now built with the same patch metadata as normal timeline elements
  • the fallback entry now carries selector and sourceFile instead of relying on an id-only best effort
  • added a regression test covering the standalone root fallback metadata path

Concretely:

  • buildStandaloneRootTimelineElement(...) now derives compositionSrc/sourceFile from the iframe preview URL and keeps the root selector
  • onIframeLoad uses that helper for the final standalone-composition timeline fallback

Verification:

  • bun test packages/studio/src/player/hooks/useTimelinePlayer.test.ts
  • bunx oxlint packages/studio/src/player/hooks/useTimelinePlayer.ts packages/studio/src/player/hooks/useTimelinePlayer.test.ts
  • bunx oxfmt --check packages/studio/src/player/hooks/useTimelinePlayer.ts packages/studio/src/player/hooks/useTimelinePlayer.test.ts
  • bun run --filter @hyperframes/studio typecheck

@miguel-heygen miguel-heygen force-pushed the feat/studio-timeline-editing branch 4 times, most recently from 3c945a9 to d383e94 Compare April 21, 2026 23:07
Copy link
Copy Markdown
Collaborator

@jrusso1020 jrusso1020 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full +2236 with a focus on the drag/resize state machine, source patcher, and pointer event lifecycle. Editing layer is well-architected: pure math in timelineEditing.ts with solid edge cases tested, refs-for-handlers pattern avoids stale-closure bugs on pointer events, useMountEffect cleanup removes window listeners plus cancels the auto-scroll RAF, and stopPropagation on the resize handles correctly prevents drag/resize double-activation.

Optimistic updates with rollback on rejected persist is the right shape. updateElement(key ?? id, ...) on mount gives instant feedback, and the .catch branch resets to the pre-move state if the save fails. Nice.

Non-blocking items for a followup pass:

  1. sourcePatcher doesn't escape attribute values. patchAttribute writes ${attr}="${value}" into the HTML verbatim. If a user's value contained a " (or a timeline-editing operation produced one via some future path), the HTML would silently corrupt. The author-as-user threat model makes this alpha-acceptable — but worth escaping "" before the release that lets non-authors drive edits, and worth a test fixture that asserts "patch with a quote character round-trips cleanly" even if it just documents the current limitation.

  2. patchTextContent regex (<[^>]*\\bid="${elementId}"[^>]*>)([\\s\\S]*?)(<\\/[a-z]+>) matches the first closing tag. On <div id="x"><span>hi</span></div>, the non-greedy match stops at </span> and would clip the inner element. Not exercised by the current timeline editing path (which only calls inline-style and attribute), but it's a landmine if text-content patching gets used later.

  3. handleTimelineElementMove re-patches z-index on every element in the target file after a track change. Correct for the "track reorder shouldn't flip visual stacking" invariant, but on a file with many clips this writes a lot of inline z-index churn to the source. Not a blocker — just a patch-count growth curve to keep in mind once compositions get dense.

  4. fetch → patch → save is not atomic. If a user edits the file via the code editor between the fetch and the save, their changes get clobbered. The setRefreshKey at the end means they'd notice immediately, but realistically someone will lose a keystroke. Alpha-acceptable.

Approved for alpha release. Nice work — this is a lot of surface area to land cleanly.

Rames Jusso

@miguel-heygen miguel-heygen changed the base branch from fix/studio-preview-runtime-foundation to graphite-base/390 April 21, 2026 23:42
@miguel-heygen miguel-heygen force-pushed the feat/studio-timeline-editing branch from d383e94 to cb7f3dd Compare April 21, 2026 23:43
@graphite-app graphite-app Bot changed the base branch from graphite-base/390 to main April 21, 2026 23:43
@miguel-heygen miguel-heygen force-pushed the feat/studio-timeline-editing branch from cb7f3dd to 5590cdc Compare April 21, 2026 23:43
@miguel-heygen miguel-heygen merged commit 0ba56f9 into main Apr 21, 2026
17 checks passed
Copy link
Copy Markdown
Collaborator Author

Merge activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants