darkwatch/client/src/components/InfoPanel.tsx
Aaron Wood 768c55c6b9 fix: dice overlay no longer blocks clicks, fog alpha wired to intensity, title auto-updates from class/alignment/level
- DiceTray: remove pointer-events:auto from .active so UI remains clickable during dice animation
- FogOverlay: accept intensity prop, map 0-100 to 0.15-1.0 opacity
- CampaignView: pass fog intensity to FogOverlay
- InfoPanel: auto-derive Shadowdark title when class/alignment/level changes
- Add shadowdark-titles.ts utility with full title lookup table from Player Quickstart
- Add CLAUDE.md with project instructions and pre-approved permissions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 03:53:00 -04:00

191 lines
6.6 KiB
TypeScript

import { useRef, useEffect } from "react";
import type { Character } from "../types";
import TalentList from "./TalentList";
import SelectDropdown from "./SelectDropdown";
import { getShadowdarkTitle } from "../utils/shadowdark-titles.js";
import styles from "./InfoPanel.module.css";
const CLASSES = ["Fighter", "Priest", "Thief", "Wizard"];
const ANCESTRIES = ["Human", "Elf", "Dwarf", "Halfling", "Goblin", "Half-Orc"];
const ALIGNMENTS = ["Lawful", "Neutral", "Chaotic"];
interface InfoPanelProps {
character: Character;
mode: "view" | "edit";
onUpdate: (id: number, data: Partial<Character>) => void;
onAddTalent: (
characterId: number,
data: {
name: string;
description: string;
effect?: Record<string, unknown>;
game_talent_id?: number | null;
},
) => void;
onRemoveTalent: (characterId: number, talentId: number) => void;
}
export default function InfoPanel({
character,
mode,
onUpdate,
onAddTalent,
onRemoveTalent,
}: InfoPanelProps) {
const debounceRef = useRef<ReturnType<typeof setTimeout>>();
useEffect(() => {
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current);
};
}, []);
function handleField(field: string, value: string | number) {
const update: Partial<Character> = { [field]: value };
// Auto-update title when class, alignment, or level changes
if (field === "class" || field === "alignment" || field === "level") {
const newClass = field === "class" ? String(value) : character.class;
const newAlignment = field === "alignment" ? String(value) : character.alignment;
const newLevel = field === "level" ? Number(value) : character.level;
const derived = getShadowdarkTitle(newClass, newAlignment, newLevel);
if (derived) update.title = derived;
}
if (typeof value === "string" && field !== "class" && field !== "alignment") {
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => {
onUpdate(character.id, update);
}, 400);
} else {
onUpdate(character.id, update);
}
}
return (
<div className={styles.panel}>
<TalentList
talents={character.talents}
onAdd={(data) => onAddTalent(character.id, data)}
onRemove={(id) => onRemoveTalent(character.id, id)}
mode={mode}
/>
<div className={styles.sectionTitle} style={{ marginTop: "0.75rem" }}>
Info
</div>
{mode === "view" ? (
<div className={styles.infoGrid}>
{character.background && (
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Background</span>
<span className={styles.infoValue}>{character.background}</span>
</div>
)}
{character.deity && (
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Deity</span>
<span className={styles.infoValue}>{character.deity}</span>
</div>
)}
{character.languages && (
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Languages</span>
<span className={styles.infoValue}>{character.languages}</span>
</div>
)}
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Alignment</span>
<span className={styles.infoValue}>{character.alignment}</span>
</div>
{character.notes && (
<>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Notes</span>
</div>
<div className={styles.notes}>{character.notes}</div>
</>
)}
</div>
) : (
<div className={styles.infoGrid}>
<div className={styles.editRow}>
<div className={styles.field}>
<label className={styles.fieldLabel}>Class</label>
<SelectDropdown
value={character.class}
options={CLASSES}
onChange={(v) => handleField("class", v)}
/>
</div>
<div className={styles.field}>
<label className={styles.fieldLabel}>Ancestry</label>
<SelectDropdown
value={character.ancestry}
options={ANCESTRIES}
onChange={(v) => handleField("ancestry", v)}
/>
</div>
</div>
<div className={styles.editRow}>
<div className={styles.field}>
<label className={styles.fieldLabel}>Level</label>
<input
className={styles.editField}
type="number"
min={0}
value={character.level}
onChange={(e) => handleField("level", Number(e.target.value))}
/>
</div>
<div className={styles.field}>
<label className={styles.fieldLabel}>Alignment</label>
<SelectDropdown
value={character.alignment}
options={ALIGNMENTS}
onChange={(v) => handleField("alignment", v)}
/>
</div>
</div>
<div className={styles.field}>
<label className={styles.fieldLabel}>Background</label>
<input
className={styles.editField}
value={character.background}
placeholder="Urchin..."
onChange={(e) => handleField("background", e.target.value)}
/>
</div>
<div className={styles.field}>
<label className={styles.fieldLabel}>Deity</label>
<input
className={styles.editField}
value={character.deity}
placeholder="None..."
onChange={(e) => handleField("deity", e.target.value)}
/>
</div>
<div className={styles.field}>
<label className={styles.fieldLabel}>Languages</label>
<input
className={styles.editField}
value={character.languages}
placeholder="Common, Elvish..."
onChange={(e) => handleField("languages", e.target.value)}
/>
</div>
<div className={styles.field}>
<label className={styles.fieldLabel}>Notes</label>
<textarea
className={styles.notesEdit}
value={character.notes}
onChange={(e) => handleField("notes", e.target.value)}
placeholder="Freeform notes..."
/>
</div>
</div>
)}
</div>
);
}