Shift the dark theme from neutral gray to a richer blue-tinted palette inspired by CharBuilder-style TTRPG apps. Deeper navy background, steel-blue card surfaces, and visible blue borders create more depth and visual layering. - Update design tokens: background, card, border, input, muted colors - Add card-glow utility (radial gradient + blue box-shadow) for card surfaces - Add panel-glow utility (top-down gradient) for tall panels like stat blocks - Apply glow and rounded-lg to all card surfaces, dropdowns, dialogs, toasts - Give outline buttons a subtle fill instead of transparent background - Active combatant row now uses full border with glow instead of left accent Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
134 lines
3.6 KiB
TypeScript
134 lines
3.6 KiB
TypeScript
import type { PlayerCharacter, PlayerCharacterId } from "@initiative/domain";
|
|
import { Pencil, Plus, Trash2, X } from "lucide-react";
|
|
import { useEffect, useRef } from "react";
|
|
import { PLAYER_COLOR_HEX, PLAYER_ICON_MAP } from "./player-icon-map";
|
|
import { Button } from "./ui/button";
|
|
import { ConfirmButton } from "./ui/confirm-button";
|
|
|
|
interface PlayerManagementProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
characters: readonly PlayerCharacter[];
|
|
onEdit: (pc: PlayerCharacter) => void;
|
|
onDelete: (id: PlayerCharacterId) => void;
|
|
onCreate: () => void;
|
|
}
|
|
|
|
export function PlayerManagement({
|
|
open,
|
|
onClose,
|
|
characters,
|
|
onEdit,
|
|
onDelete,
|
|
onCreate,
|
|
}: Readonly<PlayerManagementProps>) {
|
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
|
|
useEffect(() => {
|
|
const dialog = dialogRef.current;
|
|
if (!dialog) return;
|
|
if (open && !dialog.open) {
|
|
dialog.showModal();
|
|
} else if (!open && dialog.open) {
|
|
dialog.close();
|
|
}
|
|
}, [open]);
|
|
|
|
useEffect(() => {
|
|
const dialog = dialogRef.current;
|
|
if (!dialog) return;
|
|
function handleCancel(e: Event) {
|
|
e.preventDefault();
|
|
onClose();
|
|
}
|
|
function handleBackdropClick(e: MouseEvent) {
|
|
if (e.target === dialog) onClose();
|
|
}
|
|
dialog.addEventListener("cancel", handleCancel);
|
|
dialog.addEventListener("mousedown", handleBackdropClick);
|
|
return () => {
|
|
dialog.removeEventListener("cancel", handleCancel);
|
|
dialog.removeEventListener("mousedown", handleBackdropClick);
|
|
};
|
|
}, [onClose]);
|
|
|
|
return (
|
|
<dialog
|
|
ref={dialogRef}
|
|
className="card-glow m-auto w-full max-w-md rounded-lg border border-border bg-card p-6 backdrop:bg-black/50"
|
|
>
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<h2 className="font-semibold text-foreground text-lg">
|
|
Player Characters
|
|
</h2>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={onClose}
|
|
className="text-muted-foreground"
|
|
>
|
|
<X size={20} />
|
|
</Button>
|
|
</div>
|
|
|
|
{characters.length === 0 ? (
|
|
<div className="flex flex-col items-center gap-3 py-8 text-center">
|
|
<p className="text-muted-foreground">No player characters yet</p>
|
|
<Button onClick={onCreate}>
|
|
<Plus size={16} />
|
|
Create your first player character
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col gap-1">
|
|
{characters.map((pc) => {
|
|
const Icon = pc.icon ? PLAYER_ICON_MAP[pc.icon] : undefined;
|
|
const color = pc.color ? PLAYER_COLOR_HEX[pc.color] : undefined;
|
|
return (
|
|
<div
|
|
key={pc.id}
|
|
className="group flex items-center gap-3 rounded-md px-3 py-2 hover:bg-hover-neutral-bg"
|
|
>
|
|
{!!Icon && (
|
|
<Icon size={18} style={{ color }} className="shrink-0" />
|
|
)}
|
|
<span className="flex-1 truncate text-foreground text-sm">
|
|
{pc.name}
|
|
</span>
|
|
<span className="text-muted-foreground text-xs tabular-nums">
|
|
AC {pc.ac}
|
|
</span>
|
|
<span className="text-muted-foreground text-xs tabular-nums">
|
|
HP {pc.maxHp}
|
|
</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
onClick={() => onEdit(pc)}
|
|
className="text-muted-foreground"
|
|
title="Edit"
|
|
>
|
|
<Pencil size={14} />
|
|
</Button>
|
|
<ConfirmButton
|
|
icon={<Trash2 size={14} />}
|
|
label="Delete player character"
|
|
onConfirm={() => onDelete(pc.id)}
|
|
size="icon-sm"
|
|
className="text-muted-foreground"
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
<div className="mt-2 flex justify-end">
|
|
<Button onClick={onCreate} variant="ghost">
|
|
<Plus size={16} />
|
|
Add
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</dialog>
|
|
);
|
|
}
|