feat(studio): header logo, playbar cleanup, and I/O work-area markers (#811)

* feat(studio): header logo, playbar cleanup, and I/O work-area markers

- 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

* 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.

* fix(studio): address work-area edge cases from review

- 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

* 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.

* Revert "feat(studio): use full Hyperframes wordmark logo in header"

This reverts commit a2815fc7d0.

* 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.

* fix(studio): use | instead of · as logo/project separator
This commit is contained in:
Miguel Ángel
2026-05-14 01:06:55 +02:00
committed by GitHub
parent c08e8b2322
commit 1caeb28658
6 changed files with 488 additions and 70 deletions
@@ -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<PlayerState>((set) => ({
loopEnabled: false,
zoomMode: "fit",
manualZoomPercent: 100,
inPoint: null,
outPoint: null,
requestedSeekTime: null,
requestSeek: (time) => set({ requestedSeekTime: time }),
@@ -105,6 +113,23 @@ export const usePlayerStore = create<PlayerState>((set) => ({
},
setLoopEnabled: (enabled) => set({ loopEnabled: enabled }),
setZoomMode: (mode) => set({ zoomMode: mode }),
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 }),
@@ -129,5 +154,7 @@ export const usePlayerStore = create<PlayerState>((set) => ({
timelineReady: false,
elements: [],
selectedElementId: null,
inPoint: null,
outPoint: null,
}),
}));