f4fb69dbc7
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>
75 lines
1.9 KiB
TypeScript
75 lines
1.9 KiB
TypeScript
import type { RollMode } from "@initiative/domain";
|
|
import { ChevronsDown, ChevronsUp } from "lucide-react";
|
|
import { useLayoutEffect, useRef, useState } from "react";
|
|
import { useClickOutside } from "../hooks/use-click-outside.js";
|
|
|
|
interface RollModeMenuProps {
|
|
readonly position: { x: number; y: number };
|
|
readonly onSelect: (mode: RollMode) => void;
|
|
readonly onClose: () => void;
|
|
}
|
|
|
|
export function RollModeMenu({
|
|
position,
|
|
onSelect,
|
|
onClose,
|
|
}: RollModeMenuProps) {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const [pos, setPos] = useState<{ top: number; left: number } | null>(null);
|
|
|
|
useLayoutEffect(() => {
|
|
const el = ref.current;
|
|
if (!el) return;
|
|
const rect = el.getBoundingClientRect();
|
|
const vw = document.documentElement.clientWidth;
|
|
const vh = document.documentElement.clientHeight;
|
|
|
|
let left = position.x;
|
|
let top = position.y;
|
|
|
|
if (left + rect.width > vw) left = vw - rect.width - 8;
|
|
if (left < 8) left = 8;
|
|
if (top + rect.height > vh) top = position.y - rect.height;
|
|
if (top < 8) top = 8;
|
|
|
|
setPos({ top, left });
|
|
}, [position.x, position.y]);
|
|
|
|
useClickOutside(ref, onClose);
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className="card-glow fixed z-50 min-w-40 rounded-lg border border-border bg-card py-1"
|
|
style={
|
|
pos
|
|
? { top: pos.top, left: pos.left }
|
|
: { visibility: "hidden" as const }
|
|
}
|
|
>
|
|
<button
|
|
type="button"
|
|
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-emerald-400 text-sm hover:bg-hover-neutral-bg"
|
|
onClick={() => {
|
|
onSelect("advantage");
|
|
onClose();
|
|
}}
|
|
>
|
|
<ChevronsUp className="h-4 w-4" />
|
|
Advantage
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-red-400 text-sm hover:bg-hover-neutral-bg"
|
|
onClick={() => {
|
|
onSelect("disadvantage");
|
|
onClose();
|
|
}}
|
|
>
|
|
<ChevronsDown className="h-4 w-4" />
|
|
Disadvantage
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|