mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +00:00
feat(studio): harden ui primitives — focus rings, keyboard tooltips, dialog hook (#1962)
## Summary Base of the studio UX-review stack (148 findings audited across the studio; 13 critical). This PR hardens the shared `components/ui` primitives that every later PR in the stack builds on. ## Changes - **Button / IconButton**: visible `focus-visible` outline (studio accent); `disabled:pointer-events-none` removed (replaced with `disabled:cursor-not-allowed`, hover/active gated behind `enabled:`) so disabled buttons can host explain-why tooltips. - **Tooltip**: keyboard support (`onFocus`/`onBlur` triggers), `role="tooltip"`, Escape-to-hide, viewport flip (top↔bottom) + horizontal clamping. API unchanged — all ~28 call sites unaffected. - **HyperframesLoader**: `role="status"` on the loader; determinate track is a real `role="progressbar"` with `aria-valuenow/min/max` (was `aria-hidden`). - **VideoFrameThumbnail**: error event resolves to a static fallback-label tile instead of an infinite shimmer; `motion-reduce` guard. - **NEW `useDialogBehavior`**: shared modal contract — document-level Escape, Tab focus trap, focus-first-on-open, focus-restore-on-close, `canClose()` veto for dirty-draft guards. Adopted by every modal later in the stack. - **NEW `SearchInput`**: shared search primitive with required `aria-label`, panel-input token style (kills the two-divergent-search-styles inconsistency in the sidebar). - **studio.css**: `hf-toast-in/out` + `hf-backdrop-in` keyframes with `prefers-reduced-motion` guards (the previous `animate-in fade-in` classes were dead — no tailwindcss-animate plugin exists). ## Verification - oxlint 0 errors, oxfmt clean, `tsc --noEmit` clean at stack top - Full studio suite at stack top: 1189 tests pass ## Stack PR 1/7 of the studio UX-review fixes. Merges bottom-up; the stack top is fully green (tsc + 1189 tests). Some shared-file edits span PRs, so intermediate branches may not typecheck in isolation. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -5,10 +5,19 @@ import { useState, useEffect } from "react";
|
||||
* video + canvas. Seeks to ~10% of duration to avoid black opening frames.
|
||||
* Used by AssetThumbnail (assets tab) and RenderQueueItem (renders tab).
|
||||
*/
|
||||
export function VideoFrameThumbnail({ src }: { src: string }) {
|
||||
export function VideoFrameThumbnail({
|
||||
src,
|
||||
fallbackLabel,
|
||||
}: {
|
||||
src: string;
|
||||
/** Shown instead of an endless shimmer when the video can't be decoded. */
|
||||
fallbackLabel?: string;
|
||||
}) {
|
||||
const [frame, setFrame] = useState<string | null>(null);
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setFailed(false);
|
||||
const video = document.createElement("video");
|
||||
video.crossOrigin = "anonymous";
|
||||
video.muted = true;
|
||||
@@ -35,15 +44,29 @@ export function VideoFrameThumbnail({ src }: { src: string }) {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
video.addEventListener("error", cleanup);
|
||||
video.addEventListener("error", () => {
|
||||
// Resolve the loading state — a permanent shimmer reads as "still loading".
|
||||
setFailed(true);
|
||||
cleanup();
|
||||
});
|
||||
video.src = src;
|
||||
video.load();
|
||||
|
||||
return cleanup;
|
||||
}, [src]);
|
||||
|
||||
if (failed && !frame) {
|
||||
return (
|
||||
<div className="w-full h-full bg-neutral-800 flex items-center justify-center">
|
||||
<span className="text-[9px] font-medium text-neutral-600">{fallbackLabel ?? "VIDEO"}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!frame) {
|
||||
return <div className="w-full h-full bg-neutral-800 animate-pulse" />;
|
||||
return (
|
||||
<div className="w-full h-full bg-neutral-800 animate-pulse motion-reduce:animate-none" />
|
||||
);
|
||||
}
|
||||
|
||||
return <img src={frame} alt="" draggable={false} className="w-full h-full object-contain" />;
|
||||
|
||||
Reference in New Issue
Block a user