Files
initiative/apps/web/src/components/condition-picker.tsx
Lukas 228a2603e8
All checks were successful
CI / check (push) Successful in 1m8s
CI / build-image (push) Successful in 15s
Add Sapped and Slowed conditions for 5.5e weapon mastery
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>
2026-03-25 00:31:41 +01:00

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,
);
}