From c0c3b8dea9e1624ae5539d0b4e218e13738e7bc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 13 May 2026 15:19:23 -0700 Subject: [PATCH 1/7] feat(studio): header logo, playbar cleanup, and I/O work-area markers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Hyperframes icon mark to the studio header (left of project name) - Remove m:ss toggle button — click the timecode directly to switch modes - Remove frame jump input from controls bar — moved into ⌨ shortcuts panel - Replace Loop text button with a repeat icon - Collapse J/K/L shortcut badges into a single ⌨ icon that opens a panel - Shortcuts panel: Jump to frame, Work area I/O display, shortcuts reference - Implement I/O work-area markers (closes #807): - I / Shift+I: set / clear in-point at playhead - O / Shift+O: set / clear out-point at playhead - A: jump to in-point (or start); E: jump to out-point (or end) - Loop respects in/out boundaries for both forward and backward playback - Teal work-area band + tick markers rendered on the seek bar --- .filesize-allowlist | 1 + .../studio/src/components/StudioHeader.tsx | 49 ++- .../src/player/components/PlayerControls.tsx | 342 +++++++++++++++--- .../src/player/hooks/usePlaybackKeyboard.ts | 26 +- .../src/player/hooks/useTimelinePlayer.ts | 24 +- .../studio/src/player/store/playerStore.ts | 12 + 6 files changed, 387 insertions(+), 67 deletions(-) diff --git a/.filesize-allowlist b/.filesize-allowlist index 693d19295..a9261f39f 100644 --- a/.filesize-allowlist +++ b/.filesize-allowlist @@ -1,2 +1,3 @@ packages/studio/src/player/hooks/useTimelinePlayer.ts packages/studio/src/hooks/useManifestPersistence.ts +packages/studio/src/player/components/PlayerControls.tsx diff --git a/packages/studio/src/components/StudioHeader.tsx b/packages/studio/src/components/StudioHeader.tsx index 8315e1377..881ebdac0 100644 --- a/packages/studio/src/components/StudioHeader.tsx +++ b/packages/studio/src/components/StudioHeader.tsx @@ -18,6 +18,52 @@ export interface StudioHeaderProps { inspectorPanelActive: boolean; } +function HyperframesIcon({ size = 18 }: { size?: number }) { + return ( + + ); +} + export function StudioHeader({ captureFrameHref, captureFrameFilename, @@ -32,8 +78,9 @@ export function StudioHeader({ return (
- {/* Left: project name */} + {/* Left: logo + project name */}
+ {projectId}
{/* Right: toolbar buttons */} diff --git a/packages/studio/src/player/components/PlayerControls.tsx b/packages/studio/src/player/components/PlayerControls.tsx index ed4d85538..9594a91ba 100644 --- a/packages/studio/src/player/components/PlayerControls.tsx +++ b/packages/studio/src/player/components/PlayerControls.tsx @@ -6,11 +6,29 @@ import { usePlayerStore, liveTime } from "../store/playerStore"; const SPEED_OPTIONS = [0.25, 0.5, 1, 1.5, 2] as const; const SEEK_EDGE_SNAP_PX = 8; type TimeDisplayMode = "time" | "frame"; -const SHORTCUT_HINTS = [ - { key: "J", label: "Play backward" }, - { key: "K", label: "Stop playback" }, - { key: "L", label: "Play forward" }, - { key: "←/→", label: "Step one frame backward or forward" }, +const SHORTCUT_SECTIONS = [ + { + title: "Playback", + hints: [ + { key: "Space", label: "Play / Pause" }, + { key: "J", label: "Play backward" }, + { key: "K", label: "Stop" }, + { key: "L", label: "Play forward" }, + { key: "←/→", label: "Step 1 frame" }, + { key: "⇧←/⇧→", label: "Step 10 frames" }, + ], + }, + { + title: "Work area", + hints: [ + { key: "I", label: "Set in-point" }, + { key: "⇧I", label: "Clear in-point" }, + { key: "O", label: "Set out-point" }, + { key: "⇧O", label: "Clear out-point" }, + { key: "A", label: "Jump to in-point" }, + { key: "E", label: "Jump to out-point" }, + ], + }, ] as const; export function resolveSeekPercent(clientX: number, rectLeft: number, rectWidth: number): number { @@ -42,7 +60,12 @@ export const PlayerControls = memo(function PlayerControls({ const loopEnabled = usePlayerStore((s) => s.loopEnabled); const setPlaybackRate = usePlayerStore.getState().setPlaybackRate; const setLoopEnabled = usePlayerStore.getState().setLoopEnabled; + const inPoint = usePlayerStore((s) => s.inPoint); + const outPoint = usePlayerStore((s) => s.outPoint); + const setInPoint = usePlayerStore.getState().setInPoint; + const setOutPoint = usePlayerStore.getState().setOutPoint; const [showSpeedMenu, setShowSpeedMenu] = useState(false); + const [showShortcuts, setShowShortcuts] = useState(false); const [timeDisplayMode, setTimeDisplayMode] = useState("time"); const [jumpFrame, setJumpFrame] = useState(""); @@ -52,6 +75,7 @@ export const PlayerControls = memo(function PlayerControls({ const seekBarRef = useRef(null); const sliderRef = useRef(null); const speedMenuContainerRef = useRef(null); + const shortcutsPanelRef = useRef(null); const isDraggingRef = useRef(false); const currentTimeRef = useRef(0); const timeDisplayModeRef = useRef(timeDisplayMode); @@ -116,6 +140,19 @@ export const PlayerControls = memo(function PlayerControls({ }; }, [showSpeedMenu]); + useEffect(() => { + if (!showShortcuts) return; + const handleMouseDown = (e: MouseEvent) => { + if (shortcutsPanelRef.current && !shortcutsPanelRef.current.contains(e.target as Node)) { + setShowShortcuts(false); + } + }; + document.addEventListener("mousedown", handleMouseDown); + return () => { + document.removeEventListener("mousedown", handleMouseDown); + }; + }, [showShortcuts]); + const seekFromClientX = useCallback( (clientX: number) => { if (disabled) return; @@ -278,10 +315,14 @@ export const PlayerControls = memo(function PlayerControls({ )} - {/* Time display */} - setTimeDisplayMode((m) => (m === "time" ? "frame" : "time"))} + disabled={disabled} + title={timeDisplayMode === "time" ? "Switch to frame display" : "Switch to time display"} + className="font-mono text-[11px] tabular-nums flex-shrink-0 w-[118px] text-left transition-colors disabled:pointer-events-none hover:opacity-80" + style={{ color: "#A1A1AA", cursor: "pointer" }} > {formatTime(0)} {timeDisplayMode === "time" ? ( @@ -290,7 +331,7 @@ export const PlayerControls = memo(function PlayerControls({ {formatTime(duration)} ) : null} - + {/* Seek bar — teal progress fill */}
+ {/* Work-area band between in/out points */} + {(inPoint !== null || outPoint !== null) && duration > 0 && ( +
+ )} {/* Progress fill — width is controlled imperatively via ref to avoid React re-render resets */}
+ {/* In-point marker */} + {inPoint !== null && duration > 0 && ( +
+ )} + {/* Out-point marker */} + {outPoint !== null && duration > 0 && ( +
+ )} {/* Playhead thumb — left is controlled imperatively via ref */}
setLoopEnabled(!loopEnabled)} disabled={disabled} - className={`h-7 w-14 rounded-md border px-2 text-[10px] font-medium transition-colors ${ + className={`h-7 w-7 flex items-center justify-center rounded-md border transition-colors ${ loopEnabled ? "text-studio-accent bg-studio-accent/10 border-studio-accent/30" : "border-neutral-700 text-neutral-400 hover:border-neutral-500 hover:bg-neutral-800" @@ -394,53 +476,201 @@ export const PlayerControls = memo(function PlayerControls({ aria-label={loopEnabled ? "Disable loop playback" : "Enable loop playback"} aria-pressed={loopEnabled} > - Loop - - - -
- setJumpFrame(e.target.value)} - disabled={disabled} - inputMode="numeric" - pattern="[0-9]*" - aria-label="Jump to frame" - placeholder="frame" - className="h-7 w-[58px] rounded-md border border-neutral-700 bg-neutral-900 px-2 text-[10px] font-mono tabular-nums text-neutral-200 outline-none transition-colors placeholder:text-neutral-600 focus:border-studio-accent/60" - onKeyDown={handleJumpKeyDown} - onBlur={commitJumpFrame} - /> -
- -
- {SHORTCUT_HINTS.map((shortcut) => ( - + + {showShortcuts && ( +
+ {/* Frame jump */} +
+

+ Jump to frame +

+
+ setJumpFrame(e.target.value)} + disabled={disabled} + inputMode="numeric" + pattern="[0-9]*" + aria-label="Jump to frame" + placeholder="frame number" + className="h-6 flex-1 rounded border border-neutral-700 bg-neutral-900 px-2 text-[10px] font-mono tabular-nums text-neutral-200 outline-none transition-colors placeholder:text-neutral-600 focus:border-studio-accent/60" + onKeyDown={handleJumpKeyDown} + onBlur={commitJumpFrame} + /> + +
+
+
+ {/* Work area */} +
+

+ Work area +

+
+
+
+ + I + + In-point +
+
+ {inPoint !== null ? ( + <> + + {formatTime(inPoint)} + + + + ) : ( + + )} +
+
+
+
+ + O + + Out-point +
+
+ {outPoint !== null ? ( + <> + + {formatTime(outPoint)} + + + + ) : ( + + )} +
+
+
+
+
+ {/* Shortcuts */} +
+ {SHORTCUT_SECTIONS.map((section) => ( +
+

+ {section.title} +

+
+ {section.hints.map((hint) => ( +
+ + {hint.key} + + {hint.label} +
+ ))} +
+
+ ))} +
+
+ )}
); diff --git a/packages/studio/src/player/hooks/usePlaybackKeyboard.ts b/packages/studio/src/player/hooks/usePlaybackKeyboard.ts index f4b18e4f7..d7adf8098 100644 --- a/packages/studio/src/player/hooks/usePlaybackKeyboard.ts +++ b/packages/studio/src/player/hooks/usePlaybackKeyboard.ts @@ -128,9 +128,33 @@ export function usePlaybackKeyboard({ return; } shuttle("forward"); + return; + } + if (e.code === "KeyI") { + e.preventDefault(); + const t = getAdapter()?.getTime() ?? usePlayerStore.getState().currentTime; + usePlayerStore.getState().setInPoint(e.shiftKey ? null : t); + return; + } + if (e.code === "KeyO") { + e.preventDefault(); + const t = getAdapter()?.getTime() ?? usePlayerStore.getState().currentTime; + usePlayerStore.getState().setOutPoint(e.shiftKey ? null : t); + return; + } + if (e.code === "KeyA") { + e.preventDefault(); + seek(usePlayerStore.getState().inPoint ?? 0); + return; + } + if (e.code === "KeyE") { + e.preventDefault(); + const { outPoint } = usePlayerStore.getState(); + seek(outPoint ?? getAdapter()?.getDuration() ?? usePlayerStore.getState().duration); + return; } }, - [pause, shuttle, stepFrames, togglePlay], + [pause, shuttle, stepFrames, togglePlay, getAdapter, seek], ); const handlePlaybackKeyUp = useCallback((e: KeyboardEvent) => { diff --git a/packages/studio/src/player/hooks/useTimelinePlayer.ts b/packages/studio/src/player/hooks/useTimelinePlayer.ts index 727f17a58..1d46e4302 100644 --- a/packages/studio/src/player/hooks/useTimelinePlayer.ts +++ b/packages/studio/src/player/hooks/useTimelinePlayer.ts @@ -185,10 +185,13 @@ export function useTimelinePlayer() { const time = adapter.getTime(); const dur = adapter.getDuration(); liveTime.notify(time); // direct DOM updates, no React re-render - if (time >= dur && !adapter.isPlaying()) { + const { inPoint, outPoint } = usePlayerStore.getState(); + const loopEnd = outPoint !== null ? outPoint : dur; + const loopStart = inPoint !== null ? inPoint : 0; + if (time >= loopEnd && !adapter.isPlaying()) { if (usePlayerStore.getState().loopEnabled && dur > 0) { - adapter.seek(0); - liveTime.notify(0); + adapter.seek(loopStart); + liveTime.notify(loopStart); adapter.play(); setIsPlaying(true); rafRef.current = requestAnimationFrame(tick); @@ -269,15 +272,18 @@ export function useTimelinePlayer() { const tick = (now: number) => { const elapsed = ((now - startedAt) / 1000) * speed; let nextTime = startTime - elapsed; - if (nextTime <= 0) { + const { inPoint, outPoint } = usePlayerStore.getState(); + const loopEnd = outPoint !== null ? outPoint : duration; + const loopStart = inPoint !== null ? inPoint : 0; + if (nextTime <= loopStart) { if (usePlayerStore.getState().loopEnabled && duration > 0) { - startTime = duration; + startTime = loopEnd; startedAt = now; - nextTime = duration; + nextTime = loopEnd; } else { - adapter.seek(0); - liveTime.notify(0); - setCurrentTime(0); + adapter.seek(loopStart); + liveTime.notify(loopStart); + setCurrentTime(loopStart); setIsPlaying(false); shuttleDirectionRef.current = null; reverseRafRef.current = 0; diff --git a/packages/studio/src/player/store/playerStore.ts b/packages/studio/src/player/store/playerStore.ts index 2fce59381..71d8d2d72 100644 --- a/packages/studio/src/player/store/playerStore.ts +++ b/packages/studio/src/player/store/playerStore.ts @@ -43,6 +43,10 @@ interface PlayerState { zoomMode: ZoomMode; /** Timeline zoom percent relative to the fit width when in manual mode */ manualZoomPercent: number; + /** Work-area in-point (seconds). When set, loop starts here and A jumps here. */ + inPoint: number | null; + /** Work-area out-point (seconds). When set, loop ends here and E jumps here. */ + outPoint: number | null; setIsPlaying: (playing: boolean) => void; setCurrentTime: (time: number) => void; @@ -58,6 +62,8 @@ interface PlayerState { ) => void; setZoomMode: (mode: ZoomMode) => void; setManualZoomPercent: (percent: number) => void; + setInPoint: (time: number | null) => void; + setOutPoint: (time: number | null) => void; reset: () => void; /** @@ -93,6 +99,8 @@ export const usePlayerStore = create((set) => ({ loopEnabled: false, zoomMode: "fit", manualZoomPercent: 100, + inPoint: null, + outPoint: null, requestedSeekTime: null, requestSeek: (time) => set({ requestedSeekTime: time }), @@ -105,6 +113,8 @@ export const usePlayerStore = create((set) => ({ }, setLoopEnabled: (enabled) => set({ loopEnabled: enabled }), setZoomMode: (mode) => set({ zoomMode: mode }), + setInPoint: (time) => set({ inPoint: time !== null && Number.isFinite(time) ? time : null }), + setOutPoint: (time) => set({ outPoint: time !== null && Number.isFinite(time) ? time : null }), setManualZoomPercent: (percent) => set({ manualZoomPercent: Math.max(10, Math.min(2000, Math.round(percent))) }), setCurrentTime: (time) => set({ currentTime: Number.isFinite(time) ? time : 0 }), @@ -129,5 +139,7 @@ export const usePlayerStore = create((set) => ({ timelineReady: false, elements: [], selectedElementId: null, + inPoint: null, + outPoint: null, }), })); From c7eefdfa1ac9c5a967783e14a1f788680a6f8446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 13 May 2026 15:29:36 -0700 Subject: [PATCH 2/7] fix(studio): guard against inverted in/out work-area points in loop ticks If the user sets out-point before in-point (outPoint < inPoint), rawLoopStart >= rawLoopEnd caused the loop guard to fire immediately on every tick, creating a tight infinite seek loop. Both the forward RAF tick and the reverse RAF tick now fall back to the full composition range when the work area is invalid. --- .../studio/src/player/hooks/useTimelinePlayer.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/studio/src/player/hooks/useTimelinePlayer.ts b/packages/studio/src/player/hooks/useTimelinePlayer.ts index 1d46e4302..93e1c46fb 100644 --- a/packages/studio/src/player/hooks/useTimelinePlayer.ts +++ b/packages/studio/src/player/hooks/useTimelinePlayer.ts @@ -186,8 +186,10 @@ export function useTimelinePlayer() { const dur = adapter.getDuration(); liveTime.notify(time); // direct DOM updates, no React re-render const { inPoint, outPoint } = usePlayerStore.getState(); - const loopEnd = outPoint !== null ? outPoint : dur; - const loopStart = inPoint !== null ? inPoint : 0; + const rawLoopEnd = outPoint !== null ? outPoint : dur; + const rawLoopStart = inPoint !== null ? inPoint : 0; + const loopEnd = rawLoopStart < rawLoopEnd ? rawLoopEnd : dur; + const loopStart = rawLoopStart < rawLoopEnd ? rawLoopStart : 0; if (time >= loopEnd && !adapter.isPlaying()) { if (usePlayerStore.getState().loopEnabled && dur > 0) { adapter.seek(loopStart); @@ -273,8 +275,10 @@ export function useTimelinePlayer() { const elapsed = ((now - startedAt) / 1000) * speed; let nextTime = startTime - elapsed; const { inPoint, outPoint } = usePlayerStore.getState(); - const loopEnd = outPoint !== null ? outPoint : duration; - const loopStart = inPoint !== null ? inPoint : 0; + const rawLoopEnd = outPoint !== null ? outPoint : duration; + const rawLoopStart = inPoint !== null ? inPoint : 0; + const loopEnd = rawLoopStart < rawLoopEnd ? rawLoopEnd : duration; + const loopStart = rawLoopStart < rawLoopEnd ? rawLoopStart : 0; if (nextTime <= loopStart) { if (usePlayerStore.getState().loopEnabled && duration > 0) { startTime = loopEnd; From e6de8a08591b2ef2dc39588865ffc2db34e2c4e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 13 May 2026 15:32:33 -0700 Subject: [PATCH 3/7] fix(studio): address work-area edge cases from review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - setInPoint/setOutPoint now cross-clear the opposite marker when setting one would produce an inverted range (in >= out), preventing the invalid state rather than correcting it at tick time - Forward tick no longer gates on !adapter.isPlaying() — outPoint crossing fires even while the adapter is running; explicitly pauses on the non-loop path so playback stops at out-point rather than sailing to dur - play() end-of-stream reset seeks to inPoint (if set) instead of hardcoded 0 --- .../src/player/hooks/useTimelinePlayer.ts | 5 +++-- .../studio/src/player/store/playerStore.ts | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/studio/src/player/hooks/useTimelinePlayer.ts b/packages/studio/src/player/hooks/useTimelinePlayer.ts index 93e1c46fb..ff2d3a026 100644 --- a/packages/studio/src/player/hooks/useTimelinePlayer.ts +++ b/packages/studio/src/player/hooks/useTimelinePlayer.ts @@ -190,7 +190,7 @@ export function useTimelinePlayer() { const rawLoopStart = inPoint !== null ? inPoint : 0; const loopEnd = rawLoopStart < rawLoopEnd ? rawLoopEnd : dur; const loopStart = rawLoopStart < rawLoopEnd ? rawLoopStart : 0; - if (time >= loopEnd && !adapter.isPlaying()) { + if (time >= loopEnd) { if (usePlayerStore.getState().loopEnabled && dur > 0) { adapter.seek(loopStart); liveTime.notify(loopStart); @@ -199,6 +199,7 @@ export function useTimelinePlayer() { rafRef.current = requestAnimationFrame(tick); return; } + if (adapter.isPlaying()) adapter.pause(); setCurrentTime(time); // sync Zustand once at end setIsPlaying(false); cancelAnimationFrame(rafRef.current); @@ -246,7 +247,7 @@ export function useTimelinePlayer() { const adapter = getAdapter(); if (!adapter) return; if (adapter.getTime() >= adapter.getDuration()) { - adapter.seek(0); + adapter.seek(usePlayerStore.getState().inPoint ?? 0); } unmutePreviewMedia(iframeRef.current); applyPlaybackRate(usePlayerStore.getState().playbackRate); diff --git a/packages/studio/src/player/store/playerStore.ts b/packages/studio/src/player/store/playerStore.ts index 71d8d2d72..4b96c2634 100644 --- a/packages/studio/src/player/store/playerStore.ts +++ b/packages/studio/src/player/store/playerStore.ts @@ -113,8 +113,23 @@ export const usePlayerStore = create((set) => ({ }, setLoopEnabled: (enabled) => set({ loopEnabled: enabled }), setZoomMode: (mode) => set({ zoomMode: mode }), - setInPoint: (time) => set({ inPoint: time !== null && Number.isFinite(time) ? time : null }), - setOutPoint: (time) => set({ outPoint: time !== null && Number.isFinite(time) ? time : null }), + setInPoint: (time) => + set((state) => { + const t = time !== null && Number.isFinite(time) ? time : null; + return { + inPoint: t, + outPoint: + t !== null && state.outPoint !== null && t >= state.outPoint ? null : state.outPoint, + }; + }), + setOutPoint: (time) => + set((state) => { + const t = time !== null && Number.isFinite(time) ? time : null; + return { + outPoint: t, + inPoint: t !== null && state.inPoint !== null && t <= state.inPoint ? null : state.inPoint, + }; + }), setManualZoomPercent: (percent) => set({ manualZoomPercent: Math.max(10, Math.min(2000, Math.round(percent))) }), setCurrentTime: (time) => set({ currentTime: Number.isFinite(time) ? time : 0 }), From a2815fc7d0ea96928d22870bcd3345423772fa87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 13 May 2026 15:43:06 -0700 Subject: [PATCH 4/7] feat(studio): use full Hyperframes wordmark logo in header Replace the standalone icon mark with the complete logo from logo-dark.svg (icon mark + Hyperframes wordmark), with all black text fills inverted to white for the dark header background. Project name is shown next to the logo separated by a middot. --- .../studio/src/components/StudioHeader.tsx | 79 ++++++++++++++++--- 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/packages/studio/src/components/StudioHeader.tsx b/packages/studio/src/components/StudioHeader.tsx index 881ebdac0..06fc6f225 100644 --- a/packages/studio/src/components/StudioHeader.tsx +++ b/packages/studio/src/components/StudioHeader.tsx @@ -18,19 +18,25 @@ export interface StudioHeaderProps { inspectorPanelActive: boolean; } -function HyperframesIcon({ size = 18 }: { size?: number }) { +function HyperframesLogo() { + // Derived from logo-dark.svg (263×79). Paths 8–9 = gradient icon mark; + // paths 10–20 = Hyperframes wordmark, black → white for dark background. + const W = 263; + const H = 79; + const height = 30; + const width = Math.round(height * (W / H)); return ( ); @@ -79,9 +131,12 @@ export function StudioHeader({ return (
{/* Left: logo + project name */} -
- - {projectId} +
+ + + {projectId}
{/* Right: toolbar buttons */}
From 6f5a9e7c4746503f5aeabbdd09528da21fa27b89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 13 May 2026 15:45:30 -0700 Subject: [PATCH 5/7] Revert "feat(studio): use full Hyperframes wordmark logo in header" This reverts commit a2815fc7d0ea96928d22870bcd3345423772fa87. --- .../studio/src/components/StudioHeader.tsx | 79 +++---------------- 1 file changed, 12 insertions(+), 67 deletions(-) diff --git a/packages/studio/src/components/StudioHeader.tsx b/packages/studio/src/components/StudioHeader.tsx index 06fc6f225..881ebdac0 100644 --- a/packages/studio/src/components/StudioHeader.tsx +++ b/packages/studio/src/components/StudioHeader.tsx @@ -18,25 +18,19 @@ export interface StudioHeaderProps { inspectorPanelActive: boolean; } -function HyperframesLogo() { - // Derived from logo-dark.svg (263×79). Paths 8–9 = gradient icon mark; - // paths 10–20 = Hyperframes wordmark, black → white for dark background. - const W = 263; - const H = 79; - const height = 30; - const width = Math.round(height * (W / H)); +function HyperframesIcon({ size = 18 }: { size?: number }) { return ( ); @@ -131,12 +79,9 @@ export function StudioHeader({ return (
{/* Left: logo + project name */} -
- - - {projectId} +
+ + {projectId}
{/* Right: toolbar buttons */}
From f2685e4c20e6200ce1dc82b666433f390194f08a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 13 May 2026 15:58:37 -0700 Subject: [PATCH 6/7] feat(studio): show full HeyGen/Hyperframes logo in header Replace the standalone chevron icon with the complete logo from logo-dark.svg: heygen label + gradient mark + hyperframes wordmark, all white fills on dark background. Project name follows after a middot separator. --- .../studio/src/components/StudioHeader.tsx | 102 +++++++++++++++--- 1 file changed, 90 insertions(+), 12 deletions(-) diff --git a/packages/studio/src/components/StudioHeader.tsx b/packages/studio/src/components/StudioHeader.tsx index 881ebdac0..8fecab8f5 100644 --- a/packages/studio/src/components/StudioHeader.tsx +++ b/packages/studio/src/components/StudioHeader.tsx @@ -18,19 +18,23 @@ export interface StudioHeaderProps { inspectorPanelActive: boolean; } -function HyperframesIcon({ size = 18 }: { size?: number }) { +function HyperframesLogo() { + // Full logo from logo-dark.svg (263×79): heygen label + gradient mark + hyperframes wordmark. + // All fill="black" paths inverted to white for the dark header. + const height = 28; + const width = Math.round(height * (263 / 79)); return ( ); @@ -79,9 +154,12 @@ export function StudioHeader({ return (
{/* Left: logo + project name */} -
- - {projectId} +
+ + + {projectId}
{/* Right: toolbar buttons */}
From 444e15b281e1183083fe2f7f81eea8a1a7550a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 13 May 2026 16:05:52 -0700 Subject: [PATCH 7/7] =?UTF-8?q?fix(studio):=20use=20|=20instead=20of=20?= =?UTF-8?q?=C2=B7=20as=20logo/project=20separator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/studio/src/components/StudioHeader.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/studio/src/components/StudioHeader.tsx b/packages/studio/src/components/StudioHeader.tsx index 8fecab8f5..ee4c5ea66 100644 --- a/packages/studio/src/components/StudioHeader.tsx +++ b/packages/studio/src/components/StudioHeader.tsx @@ -156,8 +156,8 @@ export function StudioHeader({ {/* Left: logo + project name */}
-