Add player character management feature
All checks were successful
CI / check (push) Successful in 45s
CI / build-image (push) Successful in 18s

Persistent player character templates (name, AC, HP, color, icon) with
full CRUD, bestiary-style search to add PCs to encounters with pre-filled
stats, and color/icon visual distinction in combatant rows. Also stops
the stat block panel from auto-opening when adding a creature.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-12 18:11:08 +01:00
parent 768e7a390f
commit 91703ddebc
38 changed files with 3055 additions and 96 deletions

View File

@@ -2,6 +2,7 @@ import {
type CombatantId,
type ConditionId,
deriveHpStatus,
type PlayerIcon,
} from "@initiative/domain";
import { Brain, X } from "lucide-react";
import { type Ref, useCallback, useEffect, useRef, useState } from "react";
@@ -11,6 +12,7 @@ import { ConditionPicker } from "./condition-picker";
import { ConditionTags } from "./condition-tags";
import { D20Icon } from "./d20-icon";
import { HpAdjustPopover } from "./hp-adjust-popover";
import { PLAYER_COLOR_HEX, PLAYER_ICON_MAP } from "./player-icon-map";
import { ConfirmButton } from "./ui/confirm-button";
import { Input } from "./ui/input";
@@ -23,6 +25,8 @@ interface Combatant {
readonly ac?: number;
readonly conditions?: readonly ConditionId[];
readonly isConcentrating?: boolean;
readonly color?: string;
readonly icon?: string;
}
interface CombatantRowProps {
@@ -478,6 +482,11 @@ export function CombatantRow({
}
}, [combatant.isConcentrating]);
const pcColor =
combatant.color && !isActive && !combatant.isConcentrating
? PLAYER_COLOR_HEX[combatant.color as keyof typeof PLAYER_COLOR_HEX]
: undefined;
return (
/* biome-ignore lint/a11y/noStaticElementInteractions: role="button" is set conditionally when onShowStatBlock exists */
<div
@@ -490,6 +499,7 @@ export function CombatantRow({
isPulsing && "animate-concentration-pulse",
onShowStatBlock && "cursor-pointer",
)}
style={pcColor ? { borderLeftColor: pcColor } : undefined}
onClick={onShowStatBlock}
onKeyDown={
onShowStatBlock ? activateOnKeyDown(onShowStatBlock) : undefined
@@ -535,6 +545,22 @@ export function CombatantRow({
dimmed && "opacity-50",
)}
>
{combatant.icon &&
combatant.color &&
(() => {
const PcIcon = PLAYER_ICON_MAP[combatant.icon as PlayerIcon];
const pcColor =
PLAYER_COLOR_HEX[
combatant.color as keyof typeof PLAYER_COLOR_HEX
];
return PcIcon ? (
<PcIcon
size={14}
style={{ color: pcColor }}
className="shrink-0"
/>
) : null;
})()}
<EditableName
name={name}
combatantId={id}