From 51f6651d0151eddcec2720e73715e6f12e6387c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 26 Mar 2026 14:16:13 -0400 Subject: [PATCH 1/2] refactor(studio): update layout, build config, and clean up exports - NLELayout: add toolbar slot, composition breadcrumb, improved resizing - App: update toolbar wiring, remove split/delete, add edit modal - Remove AgentActivityTrack (unused) - Update vite config with improved dev server and build settings - Update package.json dependencies - Simplify htmlEditor.ts (keep parseStyleString, mergeStyleIntoTag, findElementBlock) - Update exports in index.ts --- packages/studio/src/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/studio/src/index.ts b/packages/studio/src/index.ts index 9c7b9be4..2d4e1f2e 100644 --- a/packages/studio/src/index.ts +++ b/packages/studio/src/index.ts @@ -27,6 +27,9 @@ export { FileTree } from "./components/editor/FileTree"; // App export { StudioApp } from "./App"; +// Timeline toolbar +export { TimelineToolbar } from "./components/timeline/TimelineToolbar"; + // Hooks export { useCodeEditor } from "./hooks/useCodeEditor"; export type { OpenFile, UseCodeEditorReturn } from "./hooks/useCodeEditor"; From b372691412d3888d3ce29a34eff06f7e301906f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 26 Mar 2026 16:20:42 -0400 Subject: [PATCH 2/2] feat(studio): add Edit Range toolbar with Copy to Agent - Add TimelineToolbar with Edit button (replaces Split/Delete) - Add EditModal with time range selection, element list, prompt textarea - "Copy to Agent" copies structured context to clipboard - Wire toolbar and modal into App.tsx --- .../src/components/timeline/EditModal.tsx | 164 ++++++++++++++++++ packages/studio/src/index.ts | 3 - .../studio/src/player/components/Timeline.tsx | 122 ++++++++++++- 3 files changed, 282 insertions(+), 7 deletions(-) create mode 100644 packages/studio/src/components/timeline/EditModal.tsx diff --git a/packages/studio/src/components/timeline/EditModal.tsx b/packages/studio/src/components/timeline/EditModal.tsx new file mode 100644 index 00000000..b5523a26 --- /dev/null +++ b/packages/studio/src/components/timeline/EditModal.tsx @@ -0,0 +1,164 @@ +import { useState, useCallback, useMemo, useEffect, useRef } from "react"; +import { usePlayerStore } from "../../player/store/playerStore"; +import { formatTime } from "../../player/lib/time"; + +interface EditPopoverProps { + rangeStart: number; + rangeEnd: number; + anchorX: number; + anchorY: number; + onClose: () => void; +} + +export function EditPopover({ rangeStart, rangeEnd, anchorX, anchorY, onClose }: EditPopoverProps) { + const elements = usePlayerStore((s) => s.elements); + const [prompt, setPrompt] = useState(""); + const [copied, setCopied] = useState(false); + const popoverRef = useRef(null); + const textareaRef = useRef(null); + + const start = Math.min(rangeStart, rangeEnd); + const end = Math.max(rangeStart, rangeEnd); + + const elementsInRange = useMemo(() => { + return elements.filter((el) => { + const elEnd = el.start + el.duration; + return el.start < end && elEnd > start; + }); + }, [elements, start, end]); + + useEffect(() => { + setTimeout(() => textareaRef.current?.focus(), 50); + }, []); + + useEffect(() => { + const handleKey = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + window.addEventListener("keydown", handleKey); + return () => window.removeEventListener("keydown", handleKey); + }, [onClose]); + + useEffect(() => { + const handleClick = (e: MouseEvent) => { + if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) { + onClose(); + } + }; + setTimeout(() => window.addEventListener("mousedown", handleClick), 100); + return () => window.removeEventListener("mousedown", handleClick); + }, [onClose]); + + const buildClipboardText = useCallback(() => { + const elementLines = elementsInRange + .map( + (el) => + `- #${el.id} (${el.tag}) — ${formatTime(el.start)} to ${formatTime(el.start + el.duration)}, track ${el.track}`, + ) + .join("\n"); + + return `Edit the following HyperFrames composition: + +Time range: ${formatTime(start)} — ${formatTime(end)} + +Elements in range: +${elementLines || "(none)"} + +User request: +${prompt.trim() || "(no prompt provided)"} + +Instructions: +Modify only the elements listed above within the specified time range. +The composition uses HyperFrames data attributes (data-start, data-duration, data-track-index) and GSAP for animations. +Preserve all other elements and timing outside this range.`; + }, [start, end, elementsInRange, prompt]); + + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(buildClipboardText()); + } catch { + const ta = document.createElement("textarea"); + ta.value = buildClipboardText(); + document.body.appendChild(ta); + ta.select(); + document.execCommand("copy"); + document.body.removeChild(ta); + } + setCopied(true); + setTimeout(() => { + setCopied(false); + onClose(); + }, 800); + }, [buildClipboardText, onClose]); + + const style: React.CSSProperties = { + position: "fixed", + left: Math.max(8, Math.min(anchorX - 160, window.innerWidth - 336)), + top: Math.max(8, anchorY - 280), + zIndex: 200, + }; + + return ( +
+
+ {/* Header */} +
+
+
+ + {formatTime(start)} — {formatTime(end)} + +
+ + {elementsInRange.length} element{elementsInRange.length !== 1 ? "s" : ""} + +
+ + {/* Elements */} + {elementsInRange.length > 0 && ( +
+ {elementsInRange.map((el) => ( +
+ #{el.id} + {el.tag} +
+ ))} +
+ )} + + {/* Prompt */} +
+