Add condition tooltips with 5.5e descriptions
All checks were successful
CI / check (push) Successful in 1m22s
CI / build-image (push) Successful in 19s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-22 22:48:23 +01:00
parent 9def2d7c24
commit 6336dec38a
4 changed files with 165 additions and 41 deletions

View File

@@ -20,6 +20,7 @@ import {
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { cn } from "../lib/utils";
import { Tooltip } from "./ui/tooltip.js";
const ICON_MAP: Record<string, LucideIcon> = {
EyeOff,
@@ -121,8 +122,8 @@ export function ConditionPicker({
const isActive = active.has(def.id);
const colorClass = COLOR_CLASSES[def.color] ?? "text-muted-foreground";
return (
<Tooltip key={def.id} content={def.description} className="block">
<button
key={def.id}
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",
@@ -135,11 +136,14 @@ export function ConditionPicker({
className={isActive ? colorClass : "text-muted-foreground"}
/>
<span
className={isActive ? "text-foreground" : "text-muted-foreground"}
className={
isActive ? "text-foreground" : "text-muted-foreground"
}
>
{def.label}
</span>
</button>
</Tooltip>
);
})}
</div>,

View File

@@ -19,6 +19,7 @@ import {
ZapOff,
} from "lucide-react";
import { cn } from "../lib/utils.js";
import { Tooltip } from "./ui/tooltip.js";
const ICON_MAP: Record<string, LucideIcon> = {
EyeOff,
@@ -71,10 +72,9 @@ export function ConditionTags({
if (!Icon) return null;
const colorClass = COLOR_CLASSES[def.color] ?? "text-muted-foreground";
return (
<Tooltip key={condId} content={`${def.label}: ${def.description}`}>
<button
key={condId}
type="button"
title={def.label}
aria-label={`Remove ${def.label}`}
className={cn(
"inline-flex items-center rounded p-0.5 transition-colors hover:bg-hover-neutral-bg",
@@ -87,6 +87,7 @@ export function ConditionTags({
>
<Icon size={14} />
</button>
</Tooltip>
);
})}
<button

View File

@@ -0,0 +1,55 @@
import { type ReactNode, useRef, useState } from "react";
import { createPortal } from "react-dom";
interface TooltipProps {
content: string;
children: ReactNode;
className?: string;
}
export function Tooltip({
content,
children,
className,
}: Readonly<TooltipProps>) {
const ref = useRef<HTMLSpanElement>(null);
const [pos, setPos] = useState<{ top: number; left: number } | null>(null);
function show() {
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
setPos({
top: rect.top - 4,
left: rect.left + rect.width / 2,
});
}
function hide() {
setPos(null);
}
return (
<>
<span
ref={ref}
onPointerEnter={show}
onPointerLeave={hide}
className={className ?? "inline-flex"}
>
{children}
</span>
{pos !== null &&
createPortal(
<div
role="tooltip"
className="pointer-events-none fixed z-[60] max-w-64 -translate-x-1/2 -translate-y-full rounded-md border border-border bg-background px-2.5 py-1.5 text-foreground text-xs leading-snug shadow-lg"
style={{ top: pos.top, left: pos.left }}
>
{content}
</div>,
document.body,
)}
</>
);
}

View File

@@ -18,63 +18,127 @@ export type ConditionId =
export interface ConditionDefinition {
readonly id: ConditionId;
readonly label: string;
readonly description: string;
readonly iconName: string;
readonly color: string;
}
export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
{ id: "blinded", label: "Blinded", iconName: "EyeOff", color: "neutral" },
{ id: "charmed", label: "Charmed", iconName: "Heart", color: "pink" },
{ id: "deafened", label: "Deafened", iconName: "EarOff", color: "neutral" },
{
id: "blinded",
label: "Blinded",
description:
"Can't see. Auto-fail sight checks. Attacks have Disadvantage; attacks against have Advantage.",
iconName: "EyeOff",
color: "neutral",
},
{
id: "charmed",
label: "Charmed",
description:
"Can't attack or target the charmer with harmful abilities. Charmer has Advantage on social checks.",
iconName: "Heart",
color: "pink",
},
{
id: "deafened",
label: "Deafened",
description: "Can't hear. Auto-fail hearing checks.",
iconName: "EarOff",
color: "neutral",
},
{
id: "exhaustion",
label: "Exhaustion",
description:
"Subtract exhaustion level from D20 Tests and Spell save DCs. Speed reduced by 5 ft. \u00D7 level. Removed by long rest (1 level) or death at 10 levels.",
iconName: "BatteryLow",
color: "amber",
},
{
id: "frightened",
label: "Frightened",
description:
"Disadvantage on ability checks and attacks while source of fear is in line of sight. Can't willingly move closer to the source.",
iconName: "Siren",
color: "orange",
},
{ id: "grappled", label: "Grappled", iconName: "Hand", color: "neutral" },
{
id: "grappled",
label: "Grappled",
description:
"Speed is 0 and can't benefit from bonuses to speed. Ends if grappler is Incapacitated or moved out of reach.",
iconName: "Hand",
color: "neutral",
},
{
id: "incapacitated",
label: "Incapacitated",
description:
"Can't take Actions, Bonus Actions, or Reactions. Concentration is broken.",
iconName: "Ban",
color: "gray",
},
{
id: "invisible",
label: "Invisible",
description:
"Impossible to see without magic or special sense. Heavily Obscured. Attacks have Advantage; attacks against have Disadvantage.",
iconName: "Ghost",
color: "violet",
},
{
id: "paralyzed",
label: "Paralyzed",
description:
"Incapacitated. Can't move or speak. Auto-fail Str/Dex saves. Attacks against have Advantage. Hits within 5 ft. are critical.",
iconName: "ZapOff",
color: "yellow",
},
{
id: "petrified",
label: "Petrified",
description:
"Turned to stone. Weight \u00D710. Incapacitated. Can't move or speak. Attacks against have Advantage. Auto-fail Str/Dex saves. Resistant to all damage. Immune to poison and disease.",
iconName: "Gem",
color: "slate",
},
{ id: "poisoned", label: "Poisoned", iconName: "Droplet", color: "green" },
{ id: "prone", label: "Prone", iconName: "ArrowDown", color: "neutral" },
{
id: "poisoned",
label: "Poisoned",
description: "Disadvantage on attack rolls and ability checks.",
iconName: "Droplet",
color: "green",
},
{
id: "prone",
label: "Prone",
description:
"Can only crawl (costs extra movement). Disadvantage on attacks. Attacks within 5 ft. have Advantage; ranged attacks have Disadvantage. Standing up costs half movement.",
iconName: "ArrowDown",
color: "neutral",
},
{
id: "restrained",
label: "Restrained",
description:
"Speed is 0. Attacks have Disadvantage. Attacks against have Advantage. Disadvantage on Dex saves.",
iconName: "Link",
color: "neutral",
},
{ id: "stunned", label: "Stunned", iconName: "Sparkles", color: "yellow" },
{
id: "stunned",
label: "Stunned",
description:
"Incapacitated. Can't move. Can speak only falteringly. Auto-fail Str/Dex saves. Attacks against have Advantage.",
iconName: "Sparkles",
color: "yellow",
},
{
id: "unconscious",
label: "Unconscious",
description:
"Incapacitated. Can't move or speak. Unaware of surroundings. Drops held items, falls Prone. Auto-fail Str/Dex saves. Attacks against have Advantage. Hits within 5 ft. are critical.",
iconName: "Moon",
color: "indigo",
},