Add jsinspect-plus (AST-based structural duplication detector) to pnpm check with threshold 50 / min 3 instances. Fix all findings: - Extract condition icon/color maps to shared condition-styles.ts - Extract useClickOutside hook (5 components) - Extract dispatchAction + resolveAndRename in use-encounter - Extract runEncounterAction in application layer (13 use cases) - Extract findCombatant helper in domain (9 functions) - Extract TraitSection in stat-block (4 trait rendering blocks) - Extract DialogHeader in dialog.tsx (4 dialogs) Net result: -263 lines across 40 files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
59 lines
1.6 KiB
TypeScript
59 lines
1.6 KiB
TypeScript
import { EllipsisVertical } from "lucide-react";
|
|
import { type ReactNode, useRef, useState } from "react";
|
|
import { useClickOutside } from "../../hooks/use-click-outside.js";
|
|
import { Button } from "./button";
|
|
|
|
export interface OverflowMenuItem {
|
|
readonly icon: ReactNode;
|
|
readonly label: string;
|
|
readonly onClick: () => void;
|
|
readonly disabled?: boolean;
|
|
readonly keepOpen?: boolean;
|
|
}
|
|
|
|
interface OverflowMenuProps {
|
|
readonly items: readonly OverflowMenuItem[];
|
|
}
|
|
|
|
export function OverflowMenu({ items }: OverflowMenuProps) {
|
|
const [open, setOpen] = useState(false);
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
useClickOutside(ref, () => setOpen(false), 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();
|
|
if (!item.keepOpen) setOpen(false);
|
|
}}
|
|
>
|
|
{item.icon}
|
|
{item.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|