feat(studio): add Tooltip to player and timeline controls

This commit is contained in:
Miguel Ángel
2026-05-21 21:55:13 -04:00
parent 8ffa2eb8c1
commit 2483ab5446
2 changed files with 316 additions and 296 deletions
@@ -4,6 +4,7 @@ import {
} from "../player/components/timelineZoom";
import { getTimelineToggleTitle } from "../utils/timelineDiscovery";
import { usePlayerStore } from "../player";
import { Tooltip } from "./ui";
interface TimelineToolbarProps {
toggleTimelineVisibility: () => void;
@@ -23,6 +24,7 @@ export function TimelineToolbar({ toggleTimelineVisibility }: TimelineToolbarPro
Timeline
</div>
<div className="flex items-center gap-1">
<Tooltip label="Fit timeline to width">
<button
type="button"
onClick={() => setZoomMode("fit")}
@@ -31,24 +33,28 @@ export function TimelineToolbar({ toggleTimelineVisibility }: TimelineToolbarPro
? "border-studio-accent/30 bg-studio-accent/10 text-studio-accent"
: "border-neutral-800 text-neutral-400 hover:border-neutral-700 hover:text-neutral-200"
}`}
title="Fit timeline to width"
>
Fit
</button>
</Tooltip>
<Tooltip label="Zoom out">
<button
type="button"
onClick={() => {
setZoomMode("manual");
setManualZoomPercent(getNextTimelineZoomPercent("out", zoomMode, manualZoomPercent));
setManualZoomPercent(
getNextTimelineZoomPercent("out", zoomMode, manualZoomPercent),
);
}}
className="h-7 w-7 rounded-md border border-neutral-800 text-neutral-400 transition-colors hover:border-neutral-700 hover:text-neutral-200"
title="Zoom out"
>
-
</button>
</Tooltip>
<div className="min-w-[58px] text-center text-[10px] font-medium tabular-nums text-neutral-500">
{`${displayedTimelineZoomPercent}%`}
</div>
<Tooltip label="Zoom in">
<button
type="button"
onClick={() => {
@@ -56,15 +62,15 @@ export function TimelineToolbar({ toggleTimelineVisibility }: TimelineToolbarPro
setManualZoomPercent(getNextTimelineZoomPercent("in", zoomMode, manualZoomPercent));
}}
className="h-7 w-7 rounded-md border border-neutral-800 text-neutral-400 transition-colors hover:border-neutral-700 hover:text-neutral-200"
title="Zoom in"
>
+
</button>
</Tooltip>
<Tooltip label={getTimelineToggleTitle(true)}>
<button
type="button"
onClick={toggleTimelineVisibility}
className="ml-1 flex h-7 w-7 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-neutral-900 hover:text-neutral-200"
title={getTimelineToggleTitle(true)}
aria-label="Hide timeline editor"
>
<svg
@@ -82,6 +88,7 @@ export function TimelineToolbar({ toggleTimelineVisibility }: TimelineToolbarPro
<path d="m8 11 4 4 4-4" />
</svg>
</button>
</Tooltip>
</div>
</div>
</div>
@@ -4,6 +4,7 @@ import { formatFrameTime, frameToSeconds, stepFrameTime, formatTime } from "../l
import { shouldMutePreviewAudio } from "../lib/timelineIframeHelpers";
import { usePlayerStore, liveTime } from "../store/playerStore";
import { trackStudioEvent } from "../../utils/studioTelemetry";
import { Tooltip } from "../../components/ui";
const SPEED_OPTIONS = [0.25, 0.5, 1, 1.5, 2] as const;
const SEEK_EDGE_SNAP_PX = 8;
@@ -340,9 +341,9 @@ export const PlayerControls = memo(function PlayerControls({
}}
>
{/* Play/Pause button */}
<Tooltip label={isPlaying ? "Pause" : "Play"}>
<button
type="button"
title={isPlaying ? "Pause" : "Play"}
aria-label={isPlaying ? "Pause" : "Play"}
onClick={() => {
trackStudioEvent("playback", { action: isPlaying ? "pause" : "play" });
@@ -363,13 +364,16 @@ export const PlayerControls = memo(function PlayerControls({
</svg>
)}
</button>
</Tooltip>
{/* Time display — click to toggle time/frame mode */}
<Tooltip
label={timeDisplayMode === "time" ? "Switch to frame display" : "Switch to time display"}
>
<button
type="button"
onClick={() => 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" }}
>
@@ -381,6 +385,7 @@ export const PlayerControls = memo(function PlayerControls({
</>
) : null}
</button>
</Tooltip>
{/* Seek bar — teal progress fill */}
<div
@@ -470,6 +475,7 @@ export const PlayerControls = memo(function PlayerControls({
</div>
{/* Mute toggle */}
<Tooltip label={muteButtonLabel}>
<button
type="button"
onClick={() => {
@@ -479,7 +485,6 @@ export const PlayerControls = memo(function PlayerControls({
}
}}
disabled={controlsDisabled || audioAutoMuted}
title={muteButtonLabel}
aria-label={muteButtonLabel}
aria-pressed={effectiveAudioMuted}
className={`h-7 w-7 flex-shrink-0 flex items-center justify-center rounded-md border transition-colors disabled:pointer-events-none ${
@@ -522,19 +527,21 @@ export const PlayerControls = memo(function PlayerControls({
</svg>
)}
</button>
</Tooltip>
{/* Speed control */}
<div ref={speedMenuContainerRef} className="relative flex-shrink-0">
<Tooltip label="Playback speed">
<button
type="button"
onClick={() => setShowSpeedMenu((v) => !v)}
disabled={disabled}
title="Playback speed"
className="w-10 px-2 py-1 rounded-md text-[10px] font-mono tabular-nums transition-colors"
style={{ color: "#71717A", background: "rgba(255,255,255,0.04)" }}
>
{playbackRate === 1 ? "1x" : `${playbackRate}x`}
</button>
</Tooltip>
{showSpeedMenu && (
<div
className="absolute bottom-full right-0 mb-1.5 rounded-lg shadow-xl z-50 min-w-[56px] overflow-hidden"
@@ -568,6 +575,7 @@ export const PlayerControls = memo(function PlayerControls({
)}
</div>
<Tooltip label="Loop playback">
<button
type="button"
onClick={() => {
@@ -580,7 +588,6 @@ export const PlayerControls = memo(function PlayerControls({
? "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"
}`}
title="Loop playback"
aria-label={loopEnabled ? "Disable loop playback" : "Enable loop playback"}
aria-pressed={loopEnabled}
>
@@ -601,9 +608,11 @@ export const PlayerControls = memo(function PlayerControls({
<path d="M21 13v2a4 4 0 0 1-4 4H3" />
</svg>
</button>
</Tooltip>
{/* Fullscreen toggle */}
{onToggleFullscreen && (
<Tooltip label={isFullscreen ? "Exit fullscreen (F)" : "Enter fullscreen (F)"}>
<button
type="button"
onClick={() => {
@@ -615,7 +624,6 @@ export const PlayerControls = memo(function PlayerControls({
? "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"
}`}
title={isFullscreen ? "Exit fullscreen (F)" : "Enter fullscreen (F)"}
aria-label={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
>
{isFullscreen ? (
@@ -654,10 +662,12 @@ export const PlayerControls = memo(function PlayerControls({
</svg>
)}
</button>
</Tooltip>
)}
{/* Keyboard shortcuts + frame jump + work area — click to open panel */}
<div ref={shortcutsPanelRef} className="relative flex-shrink-0">
<Tooltip label="Shortcuts and tools">
<button
type="button"
onClick={() => setShowShortcuts((v) => !v)}
@@ -666,7 +676,6 @@ export const PlayerControls = memo(function PlayerControls({
? "border-neutral-600 text-neutral-200 bg-neutral-800"
: "border-neutral-800 text-neutral-600 hover:text-neutral-300 hover:border-neutral-600"
}`}
title="Shortcuts and tools"
aria-label="Shortcuts and tools"
aria-expanded={showShortcuts}
>
@@ -685,6 +694,7 @@ export const PlayerControls = memo(function PlayerControls({
<path d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M6 12h.01M10 12h.01M14 12h.01M18 12h.01M8 16h8" />
</svg>
</button>
</Tooltip>
{showShortcuts && (
<div
className="absolute bottom-full right-0 mb-2 z-50 rounded-lg shadow-xl min-w-[220px] overflow-y-auto"
@@ -712,14 +722,15 @@ export const PlayerControls = memo(function PlayerControls({
onKeyDown={handleJumpKeyDown}
onBlur={commitJumpFrame}
/>
<Tooltip label="Jump to frame">
<button
type="submit"
disabled={disabled}
title="Jump to frame"
className="h-6 px-2 rounded border border-neutral-700 text-[10px] text-neutral-300 transition-colors hover:border-neutral-500 hover:bg-neutral-800 disabled:opacity-40"
>
Go
</button>
</Tooltip>
</form>
</div>
<div style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }} />
@@ -745,11 +756,11 @@ export const PlayerControls = memo(function PlayerControls({
<span className="font-mono text-[10px] text-neutral-300">
{formatTime(inPoint)}
</span>
<Tooltip label="Clear in-point">
<button
type="button"
onClick={() => setInPoint(null)}
className="w-4 h-4 flex items-center justify-center rounded text-neutral-500 hover:text-neutral-200 transition-colors"
title="Clear in-point"
aria-label="Clear in-point"
>
<svg
@@ -763,6 +774,7 @@ export const PlayerControls = memo(function PlayerControls({
<path d="M18 6L6 18M6 6l12 12" />
</svg>
</button>
</Tooltip>
</>
) : (
<span className="text-[10px] text-neutral-600"></span>
@@ -785,11 +797,11 @@ export const PlayerControls = memo(function PlayerControls({
<span className="font-mono text-[10px] text-neutral-300">
{formatTime(outPoint)}
</span>
<Tooltip label="Clear out-point">
<button
type="button"
onClick={() => setOutPoint(null)}
className="w-4 h-4 flex items-center justify-center rounded text-neutral-500 hover:text-neutral-200 transition-colors"
title="Clear out-point"
aria-label="Clear out-point"
>
<svg
@@ -803,6 +815,7 @@ export const PlayerControls = memo(function PlayerControls({
<path d="M18 6L6 18M6 6l12 12" />
</svg>
</button>
</Tooltip>
</>
) : (
<span className="text-[10px] text-neutral-600"></span>