These D&D 2024 weapon mastery conditions are edition-gated: they only appear in the condition picker when 5.5e rules are selected. Applied conditions still render correctly regardless of edition setting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
169 lines
3.9 KiB
TypeScript
169 lines
3.9 KiB
TypeScript
import {
|
|
type ConditionId,
|
|
getConditionDescription,
|
|
getConditionsForEdition,
|
|
} from "@initiative/domain";
|
|
import type { LucideIcon } from "lucide-react";
|
|
import {
|
|
ArrowDown,
|
|
Ban,
|
|
BatteryLow,
|
|
Droplet,
|
|
EarOff,
|
|
EyeOff,
|
|
Gem,
|
|
Ghost,
|
|
Hand,
|
|
Heart,
|
|
Link,
|
|
Moon,
|
|
ShieldMinus,
|
|
Siren,
|
|
Snail,
|
|
Sparkles,
|
|
ZapOff,
|
|
} from "lucide-react";
|
|
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
import { createPortal } from "react-dom";
|
|
import { useRulesEditionContext } from "../contexts/rules-edition-context.js";
|
|
import { cn } from "../lib/utils";
|
|
import { Tooltip } from "./ui/tooltip.js";
|
|
|
|
const ICON_MAP: Record<string, LucideIcon> = {
|
|
EyeOff,
|
|
Heart,
|
|
EarOff,
|
|
BatteryLow,
|
|
Siren,
|
|
Hand,
|
|
Ban,
|
|
Ghost,
|
|
ZapOff,
|
|
Gem,
|
|
Droplet,
|
|
ArrowDown,
|
|
Link,
|
|
ShieldMinus,
|
|
Snail,
|
|
Sparkles,
|
|
Moon,
|
|
};
|
|
|
|
const COLOR_CLASSES: Record<string, string> = {
|
|
neutral: "text-muted-foreground",
|
|
pink: "text-pink-400",
|
|
amber: "text-amber-400",
|
|
orange: "text-orange-400",
|
|
gray: "text-gray-400",
|
|
violet: "text-violet-400",
|
|
yellow: "text-yellow-400",
|
|
slate: "text-slate-400",
|
|
green: "text-green-400",
|
|
indigo: "text-indigo-400",
|
|
sky: "text-sky-400",
|
|
};
|
|
|
|
interface ConditionPickerProps {
|
|
anchorRef: React.RefObject<HTMLElement | null>;
|
|
activeConditions: readonly ConditionId[] | undefined;
|
|
onToggle: (conditionId: ConditionId) => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function ConditionPicker({
|
|
anchorRef,
|
|
activeConditions,
|
|
onToggle,
|
|
onClose,
|
|
}: Readonly<ConditionPickerProps>) {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const [pos, setPos] = useState<{
|
|
top: number;
|
|
left: number;
|
|
maxHeight: number;
|
|
} | null>(null);
|
|
|
|
useLayoutEffect(() => {
|
|
const anchor = anchorRef.current;
|
|
const el = ref.current;
|
|
if (!anchor || !el) return;
|
|
|
|
const anchorRect = anchor.getBoundingClientRect();
|
|
const menuHeight = el.scrollHeight;
|
|
const pad = 8;
|
|
|
|
const spaceBelow = window.innerHeight - anchorRect.bottom - pad;
|
|
const spaceAbove = anchorRect.top - pad;
|
|
const openBelow = spaceBelow >= menuHeight || spaceBelow >= spaceAbove;
|
|
|
|
const top = openBelow
|
|
? anchorRect.bottom + 4
|
|
: Math.max(pad, anchorRect.top - Math.min(menuHeight, spaceAbove) - 4);
|
|
const maxHeight = openBelow ? spaceBelow : Math.min(menuHeight, spaceAbove);
|
|
|
|
setPos({ top, left: anchorRect.left, maxHeight });
|
|
}, [anchorRef]);
|
|
|
|
useEffect(() => {
|
|
function handleClickOutside(e: MouseEvent) {
|
|
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
onClose();
|
|
}
|
|
}
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
}, [onClose]);
|
|
|
|
const { edition } = useRulesEditionContext();
|
|
const conditions = getConditionsForEdition(edition);
|
|
const active = new Set(activeConditions ?? []);
|
|
|
|
return createPortal(
|
|
<div
|
|
ref={ref}
|
|
className="card-glow fixed z-50 w-fit overflow-y-auto rounded-lg border border-border bg-background p-1"
|
|
style={
|
|
pos
|
|
? { top: pos.top, left: pos.left, maxHeight: pos.maxHeight }
|
|
: { visibility: "hidden" as const }
|
|
}
|
|
>
|
|
{conditions.map((def) => {
|
|
const Icon = ICON_MAP[def.iconName];
|
|
if (!Icon) return null;
|
|
const isActive = active.has(def.id);
|
|
const colorClass = COLOR_CLASSES[def.color] ?? "text-muted-foreground";
|
|
return (
|
|
<Tooltip
|
|
key={def.id}
|
|
content={getConditionDescription(def, edition)}
|
|
className="block"
|
|
>
|
|
<button
|
|
type="button"
|
|
className={cn(
|
|
"flex w-full items-center gap-2 rounded px-2 py-1 text-sm transition-colors hover:bg-hover-neutral-bg",
|
|
isActive && "bg-card/50",
|
|
)}
|
|
onClick={() => onToggle(def.id)}
|
|
>
|
|
<Icon
|
|
size={14}
|
|
className={isActive ? colorClass : "text-muted-foreground"}
|
|
/>
|
|
<span
|
|
className={
|
|
isActive ? "text-foreground" : "text-muted-foreground"
|
|
}
|
|
>
|
|
{def.label}
|
|
</span>
|
|
</button>
|
|
</Tooltip>
|
|
);
|
|
})}
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|