mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
feat(studio): add styled Tooltip component to tabs and panels
Create a Tooltip component with styled popover (dark bg, border, shadow) that appears on hover with a 400ms delay. Applied to: - Left sidebar tabs: Code, Comps, Assets, Catalog - Right panel tabs: Design, Layers, Motion, Renders Replaces native title attributes with proper styled tooltips.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { Tooltip } from "./ui";
|
||||
import { PropertyPanel } from "./editor/PropertyPanel";
|
||||
import { MotionPanel } from "./editor/MotionPanel";
|
||||
import { LayersPanel } from "./editor/LayersPanel";
|
||||
@@ -106,58 +107,62 @@ export function StudioRightPanel({
|
||||
<div className="flex items-center gap-1 border-b border-neutral-800 px-3 py-2">
|
||||
{STUDIO_INSPECTOR_PANELS_ENABLED && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRightPanelTab("design")}
|
||||
title="Design"
|
||||
className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors ${
|
||||
rightPanelTab === "design"
|
||||
? "bg-neutral-800 text-white"
|
||||
: "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
Design
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRightPanelTab("layers")}
|
||||
title="Layers"
|
||||
className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors ${
|
||||
rightPanelTab === "layers"
|
||||
? "bg-neutral-800 text-white"
|
||||
: "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
Layers
|
||||
</button>
|
||||
{STUDIO_MOTION_PANEL_ENABLED && (
|
||||
<Tooltip label="Element styles and properties" side="bottom">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRightPanelTab("motion")}
|
||||
title="Motion"
|
||||
onClick={() => setRightPanelTab("design")}
|
||||
className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors ${
|
||||
rightPanelTab === "motion"
|
||||
rightPanelTab === "design"
|
||||
? "bg-neutral-800 text-white"
|
||||
: "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
Motion
|
||||
Design
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip label="Composition layer stack" side="bottom">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRightPanelTab("layers")}
|
||||
className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors ${
|
||||
rightPanelTab === "layers"
|
||||
? "bg-neutral-800 text-white"
|
||||
: "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
Layers
|
||||
</button>
|
||||
</Tooltip>
|
||||
{STUDIO_MOTION_PANEL_ENABLED && (
|
||||
<Tooltip label="Animation and motion" side="bottom">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRightPanelTab("motion")}
|
||||
className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors ${
|
||||
rightPanelTab === "motion"
|
||||
? "bg-neutral-800 text-white"
|
||||
: "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
Motion
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRightPanelTab("renders")}
|
||||
title="Renders"
|
||||
className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors ${
|
||||
rightPanelTab === "renders"
|
||||
? "bg-neutral-800 text-white"
|
||||
: "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
{renderJobs.length > 0 ? `Renders (${renderJobs.length})` : "Renders"}
|
||||
</button>
|
||||
<Tooltip label="Render queue and exports" side="bottom">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRightPanelTab("renders")}
|
||||
className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors ${
|
||||
rightPanelTab === "renders"
|
||||
? "bg-neutral-800 text-white"
|
||||
: "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
{renderJobs.length > 0 ? `Renders (${renderJobs.length})` : "Renders"}
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1">
|
||||
{rightPanelTab === "block-params" && activeBlockParams ? (
|
||||
|
||||
@@ -13,6 +13,7 @@ import { trackStudioEvent } from "../../utils/studioTelemetry";
|
||||
import { BlocksTab, type BlockPreviewInfo } from "./BlocksTab";
|
||||
import { FileTree } from "../editor/FileTree";
|
||||
import { STUDIO_BLOCKS_PANEL_ENABLED } from "../editor/manualEditingAvailability";
|
||||
import { Tooltip } from "../ui";
|
||||
|
||||
export type SidebarTab = "compositions" | "assets" | "code" | "blocks";
|
||||
|
||||
@@ -124,55 +125,59 @@ export const LeftSidebar = memo(
|
||||
: "1fr 1fr 1fr",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectTab("code")}
|
||||
title="Source code editor"
|
||||
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
|
||||
tab === "code"
|
||||
? "bg-neutral-800 text-white"
|
||||
: "text-neutral-500 hover:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
Code
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectTab("compositions")}
|
||||
title="Composition layers and sub-compositions"
|
||||
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
|
||||
tab === "compositions"
|
||||
? "bg-neutral-800 text-white"
|
||||
: "text-neutral-500 hover:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
Comps
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectTab("assets")}
|
||||
title="Media assets — videos, images, audio, fonts"
|
||||
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
|
||||
tab === "assets"
|
||||
? "bg-neutral-800 text-white"
|
||||
: "text-neutral-500 hover:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
Assets
|
||||
</button>
|
||||
{STUDIO_BLOCKS_PANEL_ENABLED && (
|
||||
<Tooltip label="Source code editor" side="bottom">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectTab("blocks")}
|
||||
title="Browse blocks, components, and templates from the registry"
|
||||
onClick={() => selectTab("code")}
|
||||
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
|
||||
tab === "blocks"
|
||||
tab === "code"
|
||||
? "bg-neutral-800 text-white"
|
||||
: "text-neutral-500 hover:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
Catalog
|
||||
Code
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip label="Compositions and sub-compositions" side="bottom">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectTab("compositions")}
|
||||
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
|
||||
tab === "compositions"
|
||||
? "bg-neutral-800 text-white"
|
||||
: "text-neutral-500 hover:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
Comps
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip label="Videos, images, audio, fonts" side="bottom">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectTab("assets")}
|
||||
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
|
||||
tab === "assets"
|
||||
? "bg-neutral-800 text-white"
|
||||
: "text-neutral-500 hover:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
Assets
|
||||
</button>
|
||||
</Tooltip>
|
||||
{STUDIO_BLOCKS_PANEL_ENABLED && (
|
||||
<Tooltip label="Browse blocks and components" side="bottom">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectTab("blocks")}
|
||||
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
|
||||
tab === "blocks"
|
||||
? "bg-neutral-800 text-white"
|
||||
: "text-neutral-500 hover:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
Catalog
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
{onToggleCollapse && (
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useState, useRef, useCallback, type ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
interface TooltipProps {
|
||||
label: string;
|
||||
children: ReactNode;
|
||||
delay?: number;
|
||||
side?: "top" | "bottom";
|
||||
}
|
||||
|
||||
export function Tooltip({ label, children, delay = 400, side = "top" }: TooltipProps) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [pos, setPos] = useState({ x: 0, y: 0 });
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const triggerRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
const show = useCallback(() => {
|
||||
timerRef.current = setTimeout(() => {
|
||||
const el = triggerRef.current;
|
||||
if (!el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
setPos({
|
||||
x: rect.left + rect.width / 2,
|
||||
y: side === "top" ? rect.top - 6 : rect.bottom + 6,
|
||||
});
|
||||
setVisible(true);
|
||||
}, delay);
|
||||
}, [delay, side]);
|
||||
|
||||
const hide = useCallback(() => {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
setVisible(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span ref={triggerRef} onPointerEnter={show} onPointerLeave={hide} className="contents">
|
||||
{children}
|
||||
</span>
|
||||
{visible &&
|
||||
createPortal(
|
||||
<div
|
||||
className="fixed z-[200] pointer-events-none"
|
||||
style={{
|
||||
left: pos.x,
|
||||
top: pos.y,
|
||||
transform: side === "top" ? "translate(-50%, -100%)" : "translate(-50%, 0)",
|
||||
}}
|
||||
>
|
||||
<div className="px-2 py-1 rounded-md bg-neutral-800 border border-neutral-700/50 text-[10px] font-medium text-neutral-200 whitespace-nowrap shadow-lg">
|
||||
{label}
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,3 +2,4 @@
|
||||
export { Button, IconButton } from "./Button";
|
||||
export { HyperframesLoader, StatusFrame } from "./HyperframesLoader";
|
||||
export type { HyperframesLoaderProps } from "./HyperframesLoader";
|
||||
export { Tooltip } from "./Tooltip";
|
||||
|
||||
Reference in New Issue
Block a user