Shift the dark theme from neutral gray to a richer blue-tinted palette inspired by CharBuilder-style TTRPG apps. Deeper navy background, steel-blue card surfaces, and visible blue borders create more depth and visual layering. - Update design tokens: background, card, border, input, muted colors - Add card-glow utility (radial gradient + blue box-shadow) for card surfaces - Add panel-glow utility (top-down gradient) for tall panels like stat blocks - Apply glow and rounded-lg to all card surfaces, dropdowns, dialogs, toasts - Give outline buttons a subtle fill instead of transparent background - Active combatant row now uses full border with glow instead of left accent Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
73 lines
2.0 KiB
TypeScript
73 lines
2.0 KiB
TypeScript
import { EllipsisVertical } from "lucide-react";
|
|
import { type ReactNode, useEffect, useRef, useState } from "react";
|
|
import { Button } from "./button";
|
|
|
|
export interface OverflowMenuItem {
|
|
readonly icon: ReactNode;
|
|
readonly label: string;
|
|
readonly onClick: () => void;
|
|
readonly disabled?: boolean;
|
|
}
|
|
|
|
interface OverflowMenuProps {
|
|
readonly items: readonly OverflowMenuItem[];
|
|
}
|
|
|
|
export function OverflowMenu({ items }: OverflowMenuProps) {
|
|
const [open, setOpen] = useState(false);
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
function handleMouseDown(e: MouseEvent) {
|
|
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
setOpen(false);
|
|
}
|
|
}
|
|
function handleKeyDown(e: KeyboardEvent) {
|
|
if (e.key === "Escape") setOpen(false);
|
|
}
|
|
document.addEventListener("mousedown", handleMouseDown);
|
|
document.addEventListener("keydown", handleKeyDown);
|
|
return () => {
|
|
document.removeEventListener("mousedown", handleMouseDown);
|
|
document.removeEventListener("keydown", handleKeyDown);
|
|
};
|
|
}, [open]);
|
|
|
|
return (
|
|
<div ref={ref} className="relative">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="text-muted-foreground hover:text-hover-neutral"
|
|
onClick={() => setOpen((o) => !o)}
|
|
aria-label="More actions"
|
|
title="More actions"
|
|
>
|
|
<EllipsisVertical className="h-5 w-5" />
|
|
</Button>
|
|
{!!open && (
|
|
<div className="card-glow absolute right-0 bottom-full z-50 mb-1 min-w-48 rounded-lg border border-border bg-card py-1">
|
|
{items.map((item) => (
|
|
<button
|
|
key={item.label}
|
|
type="button"
|
|
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-foreground text-sm hover:bg-hover-neutral-bg disabled:pointer-events-none disabled:opacity-50"
|
|
disabled={item.disabled}
|
|
onClick={() => {
|
|
item.onClick();
|
|
setOpen(false);
|
|
}}
|
|
>
|
|
{item.icon}
|
|
{item.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|