More text improvements.

This commit is contained in:
2026-04-25 20:25:34 -04:00
parent 3be9b0a921
commit 1e0df31cd4
4 changed files with 129 additions and 126 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700;900&family=EB+Garamond:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700;900&family=Alegreya+Sans:ital,wght@0,400;0,500;0,700;1,400&display=swap" rel="stylesheet">
<title>TTRPG Initiative Tracker</title> <title>TTRPG Initiative Tracker</title>
</head> </head>
<body> <body>
+126 -123
View File
@@ -44,7 +44,7 @@ if (typeof document !== 'undefined') {
// CONSTANTS // CONSTANTS
// ============================================================================ // ============================================================================
const APP_VERSION = 'v0.2.2'; const APP_VERSION = 'v0.2.4';
const DEFAULT_MAX_HP = 10; const DEFAULT_MAX_HP = 10;
const DEFAULT_INIT_MOD = 0; const DEFAULT_INIT_MOD = 0;
const MONSTER_DEFAULT_INIT_MOD = 2; const MONSTER_DEFAULT_INIT_MOD = 2;
@@ -225,10 +225,10 @@ function Modal({ onClose, title, children }) {
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center p-4 z-50"> <div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center p-4 z-50">
<div className="bg-slate-800 p-6 rounded-lg shadow-xl w-full max-w-md"> <div className="bg-stone-900 p-6 rounded-lg shadow-xl w-full max-w-md">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-amber-300 font-cinzel tracking-wide">{title}</h2> <h2 className="text-xl font-semibold text-amber-300 font-cinzel tracking-wide">{title}</h2>
<button onClick={onClose} className="text-slate-400 hover:text-slate-200"> <button onClick={onClose} className="text-stone-400 hover:text-stone-200">
<XCircle size={24} /> <XCircle size={24} />
</button> </button>
</div> </div>
@@ -243,16 +243,16 @@ function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) {
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center p-4 z-50"> <div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center p-4 z-50">
<div className="bg-slate-800 p-6 rounded-lg shadow-xl w-full max-w-md"> <div className="bg-stone-900 p-6 rounded-lg shadow-xl w-full max-w-md">
<div className="flex items-center mb-4"> <div className="flex items-center mb-4">
<AlertTriangle size={24} className="text-yellow-400 mr-3 flex-shrink-0" /> <AlertTriangle size={24} className="text-yellow-400 mr-3 flex-shrink-0" />
<h2 className="text-xl font-semibold text-yellow-300">{title || "Confirm Action"}</h2> <h2 className="text-xl font-semibold text-yellow-300">{title || "Confirm Action"}</h2>
</div> </div>
<p className="text-slate-300 mb-6">{message || "Are you sure you want to proceed?"}</p> <p className="text-stone-300 mb-6">{message || "Are you sure you want to proceed?"}</p>
<div className="flex justify-end space-x-3"> <div className="flex justify-end space-x-3">
<button <button
onClick={onClose} onClick={onClose}
className="px-4 py-2 text-sm font-medium text-slate-300 bg-slate-600 hover:bg-slate-500 rounded-md transition-colors" className="px-4 py-2 text-sm font-medium text-stone-300 bg-stone-700 hover:bg-stone-600 rounded-md transition-colors"
> >
Cancel Cancel
</button> </button>
@@ -270,7 +270,7 @@ function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) {
function LoadingSpinner({ message = "Loading..." }) { function LoadingSpinner({ message = "Loading..." }) {
return ( return (
<div className="min-h-screen bg-slate-900 text-white flex flex-col items-center justify-center p-4"> <div className="min-h-screen bg-stone-950 text-white flex flex-col items-center justify-center p-4">
<div className="animate-spin rounded-full h-16 w-16 border-t-4 border-amber-500 border-solid"></div> <div className="animate-spin rounded-full h-16 w-16 border-t-4 border-amber-500 border-solid"></div>
<p className="mt-4 text-xl">{message}</p> <p className="mt-4 text-xl">{message}</p>
</div> </div>
@@ -279,7 +279,7 @@ function LoadingSpinner({ message = "Loading..." }) {
function ErrorDisplay({ message, critical = false }) { function ErrorDisplay({ message, critical = false }) {
return ( return (
<div className={`min-h-screen ${critical ? 'bg-red-900' : 'bg-slate-900'} text-white flex flex-col items-center justify-center p-4`}> <div className={`min-h-screen ${critical ? 'bg-red-900' : 'bg-stone-950'} text-white flex flex-col items-center justify-center p-4`}>
<h1 className="text-3xl font-bold mb-4"> <h1 className="text-3xl font-bold mb-4">
{critical ? 'Configuration Error' : 'Error'} {critical ? 'Configuration Error' : 'Error'}
</h1> </h1>
@@ -306,7 +306,7 @@ function CreateCampaignForm({ onCreate, onCancel }) {
return ( return (
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<label htmlFor="campaignName" className="block text-sm font-medium text-slate-300"> <label htmlFor="campaignName" className="block text-sm font-medium text-stone-300">
Campaign Name Campaign Name
</label> </label>
<input <input
@@ -314,12 +314,12 @@ function CreateCampaignForm({ onCreate, onCancel }) {
id="campaignName" id="campaignName"
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
className="mt-1 block w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white" className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
required required
/> />
</div> </div>
<div> <div>
<label htmlFor="backgroundUrl" className="block text-sm font-medium text-slate-300"> <label htmlFor="backgroundUrl" className="block text-sm font-medium text-stone-300">
Player Display Background URL (Optional) Player Display Background URL (Optional)
</label> </label>
<input <input
@@ -328,20 +328,20 @@ function CreateCampaignForm({ onCreate, onCancel }) {
value={backgroundUrl} value={backgroundUrl}
onChange={(e) => setBackgroundUrl(e.target.value)} onChange={(e) => setBackgroundUrl(e.target.value)}
placeholder="https://example.com/image.jpg" placeholder="https://example.com/image.jpg"
className="mt-1 block w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white" className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/> />
</div> </div>
<div className="flex justify-end space-x-3"> <div className="flex justify-end space-x-3">
<button <button
type="button" type="button"
onClick={onCancel} onClick={onCancel}
className="px-4 py-2 text-sm font-medium text-slate-300 bg-slate-600 hover:bg-slate-500 rounded-md transition-colors" className="px-4 py-2 text-sm font-medium text-stone-300 bg-stone-700 hover:bg-stone-600 rounded-md transition-colors"
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
className="px-4 py-2 text-sm font-medium text-white bg-green-500 hover:bg-green-600 rounded-md transition-colors" className="px-4 py-2 text-sm font-medium text-white bg-red-700 hover:bg-red-800 rounded-md transition-colors"
> >
Create Create
</button> </button>
@@ -363,7 +363,7 @@ function CreateEncounterForm({ onCreate, onCancel }) {
return ( return (
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<label htmlFor="encounterName" className="block text-sm font-medium text-slate-300"> <label htmlFor="encounterName" className="block text-sm font-medium text-stone-300">
Encounter Name Encounter Name
</label> </label>
<input <input
@@ -371,7 +371,7 @@ function CreateEncounterForm({ onCreate, onCancel }) {
id="encounterName" id="encounterName"
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
className="mt-1 block w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white" className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
required required
/> />
</div> </div>
@@ -379,13 +379,13 @@ function CreateEncounterForm({ onCreate, onCancel }) {
<button <button
type="button" type="button"
onClick={onCancel} onClick={onCancel}
className="px-4 py-2 text-sm font-medium text-slate-300 bg-slate-600 hover:bg-slate-500 rounded-md transition-colors" className="px-4 py-2 text-sm font-medium text-stone-300 bg-stone-700 hover:bg-stone-600 rounded-md transition-colors"
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
className="px-4 py-2 text-sm font-medium text-white bg-orange-500 hover:bg-orange-600 rounded-md transition-colors" className="px-4 py-2 text-sm font-medium text-white bg-amber-700 hover:bg-amber-800 rounded-md transition-colors"
> >
Create Create
</button> </button>
@@ -416,40 +416,40 @@ function EditParticipantModal({ participant, onClose, onSave }) {
<Modal onClose={onClose} title={`Edit ${participant.name}`}> <Modal onClose={onClose} title={`Edit ${participant.name}`}>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-slate-300">Name</label> <label className="block text-sm font-medium text-stone-300">Name</label>
<input <input
type="text" type="text"
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
className="mt-1 block w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white" className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-slate-300">Initiative</label> <label className="block text-sm font-medium text-stone-300">Initiative</label>
<input <input
type="number" type="number"
value={initiative} value={initiative}
onChange={(e) => setInitiative(e.target.value)} onChange={(e) => setInitiative(e.target.value)}
className="mt-1 block w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white" className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/> />
</div> </div>
<div className="flex gap-4"> <div className="flex gap-4">
<div className="flex-1"> <div className="flex-1">
<label className="block text-sm font-medium text-slate-300">Current HP</label> <label className="block text-sm font-medium text-stone-300">Current HP</label>
<input <input
type="number" type="number"
value={currentHp} value={currentHp}
onChange={(e) => setCurrentHp(e.target.value)} onChange={(e) => setCurrentHp(e.target.value)}
className="mt-1 block w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white" className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/> />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<label className="block text-sm font-medium text-slate-300">Max HP</label> <label className="block text-sm font-medium text-stone-300">Max HP</label>
<input <input
type="number" type="number"
value={maxHp} value={maxHp}
onChange={(e) => setMaxHp(e.target.value)} onChange={(e) => setMaxHp(e.target.value)}
className="mt-1 block w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white" className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/> />
</div> </div>
</div> </div>
@@ -460,9 +460,9 @@ function EditParticipantModal({ participant, onClose, onSave }) {
id="editIsNpc" id="editIsNpc"
checked={isNpc} checked={isNpc}
onChange={(e) => setIsNpc(e.target.checked)} onChange={(e) => setIsNpc(e.target.checked)}
className="h-4 w-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500" className="h-4 w-4 text-violet-600 border-stone-400 rounded focus:ring-violet-500"
/> />
<label htmlFor="editIsNpc" className="ml-2 block text-sm text-slate-300"> <label htmlFor="editIsNpc" className="ml-2 block text-sm text-stone-300">
Is NPC? Is NPC?
</label> </label>
</div> </div>
@@ -471,7 +471,7 @@ function EditParticipantModal({ participant, onClose, onSave }) {
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}
className="px-4 py-2 text-sm font-medium text-slate-300 bg-slate-600 hover:bg-slate-500 rounded-md transition-colors" className="px-4 py-2 text-sm font-medium text-stone-300 bg-stone-700 hover:bg-stone-600 rounded-md transition-colors"
> >
Cancel Cancel
</button> </button>
@@ -590,14 +590,14 @@ function CharacterManager({ campaignId, campaignCharacters }) {
return ( return (
<> <>
<div className="p-4 bg-slate-800 rounded-lg shadow"> <div className="p-4 bg-stone-900 rounded-lg shadow">
<div className="flex justify-between items-center mb-3"> <div className="flex justify-between items-center mb-3">
<h3 className="text-xl font-semibold text-amber-300 font-cinzel tracking-wide flex items-center"> <h3 className="text-xl font-semibold text-amber-300 font-cinzel tracking-wide flex items-center">
<Users size={24} className="mr-2" /> Campaign Characters <Users size={24} className="mr-2" /> Campaign Characters
</h3> </h3>
<button <button
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
className="p-1 text-slate-400 hover:text-slate-200" className="p-1 text-stone-400 hover:text-stone-200"
aria-label={isOpen ? "Collapse" : "Expand"} aria-label={isOpen ? "Collapse" : "Expand"}
> >
{isOpen ? <ChevronUp size={20} /> : <ChevronDown size={20} />} {isOpen ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
@@ -608,7 +608,7 @@ function CharacterManager({ campaignId, campaignCharacters }) {
<> <>
<form onSubmit={(e) => { e.preventDefault(); handleAddCharacter(); }} className="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-4 items-end"> <form onSubmit={(e) => { e.preventDefault(); handleAddCharacter(); }} className="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-4 items-end">
<div className="sm:col-span-1"> <div className="sm:col-span-1">
<label htmlFor="characterName" className="block text-xs font-medium text-slate-400"> <label htmlFor="characterName" className="block text-xs font-medium text-stone-400">
Name Name
</label> </label>
<input <input
@@ -617,11 +617,11 @@ function CharacterManager({ campaignId, campaignCharacters }) {
value={characterName} value={characterName}
onChange={(e) => setCharacterName(e.target.value)} onChange={(e) => setCharacterName(e.target.value)}
placeholder="Character name" placeholder="Character name"
className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white" className="w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/> />
</div> </div>
<div className="w-full sm:w-auto"> <div className="w-full sm:w-auto">
<label htmlFor="defaultMaxHp" className="block text-xs font-medium text-slate-400"> <label htmlFor="defaultMaxHp" className="block text-xs font-medium text-stone-400">
Default HP Default HP
</label> </label>
<input <input
@@ -629,11 +629,11 @@ function CharacterManager({ campaignId, campaignCharacters }) {
id="defaultMaxHp" id="defaultMaxHp"
value={defaultMaxHp} value={defaultMaxHp}
onChange={(e) => setDefaultMaxHp(e.target.value)} onChange={(e) => setDefaultMaxHp(e.target.value)}
className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white" className="w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/> />
</div> </div>
<div className="w-full sm:w-auto"> <div className="w-full sm:w-auto">
<label htmlFor="defaultInitMod" className="block text-xs font-medium text-slate-400"> <label htmlFor="defaultInitMod" className="block text-xs font-medium text-stone-400">
Init Mod Init Mod
</label> </label>
<input <input
@@ -641,7 +641,7 @@ function CharacterManager({ campaignId, campaignCharacters }) {
id="defaultInitMod" id="defaultInitMod"
value={defaultInitMod} value={defaultInitMod}
onChange={(e) => setDefaultInitMod(e.target.value)} onChange={(e) => setDefaultInitMod(e.target.value)}
className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white" className="w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/> />
</div> </div>
<button <button
@@ -653,12 +653,12 @@ function CharacterManager({ campaignId, campaignCharacters }) {
</form> </form>
{campaignCharacters.length === 0 && ( {campaignCharacters.length === 0 && (
<p className="text-sm text-slate-400">No characters added yet.</p> <p className="text-sm text-stone-400">No characters added yet.</p>
)} )}
<ul className="space-y-2"> <ul className="space-y-2">
{campaignCharacters.map(character => ( {campaignCharacters.map(character => (
<li key={character.id} className="flex justify-between items-center p-3 bg-slate-700 rounded-md"> <li key={character.id} className="flex justify-between items-center p-3 bg-stone-800 rounded-md">
{editingCharacter && editingCharacter.id === character.id ? ( {editingCharacter && editingCharacter.id === character.id ? (
<form <form
onSubmit={(e) => { onSubmit={(e) => {
@@ -676,20 +676,20 @@ function CharacterManager({ campaignId, campaignCharacters }) {
type="text" type="text"
value={editingCharacter.name} value={editingCharacter.name}
onChange={(e) => setEditingCharacter({ ...editingCharacter, name: e.target.value })} onChange={(e) => setEditingCharacter({ ...editingCharacter, name: e.target.value })}
className="flex-grow min-w-[100px] px-2 py-1 bg-slate-600 border border-slate-500 rounded-md text-white" className="flex-grow min-w-[100px] px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
/> />
<input <input
type="number" type="number"
value={editingCharacter.defaultMaxHp} value={editingCharacter.defaultMaxHp}
onChange={(e) => setEditingCharacter({ ...editingCharacter, defaultMaxHp: e.target.value })} onChange={(e) => setEditingCharacter({ ...editingCharacter, defaultMaxHp: e.target.value })}
className="w-20 px-2 py-1 bg-slate-600 border border-slate-500 rounded-md text-white" className="w-20 px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
title="Default Max HP" title="Default Max HP"
/> />
<input <input
type="number" type="number"
value={editingCharacter.defaultInitMod} value={editingCharacter.defaultInitMod}
onChange={(e) => setEditingCharacter({ ...editingCharacter, defaultInitMod: e.target.value })} onChange={(e) => setEditingCharacter({ ...editingCharacter, defaultInitMod: e.target.value })}
className="w-20 px-2 py-1 bg-slate-600 border border-slate-500 rounded-md text-white" className="w-20 px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
title="Default Init Mod" title="Default Init Mod"
/> />
<button type="submit" className="p-1 text-green-400 hover:text-green-300"> <button type="submit" className="p-1 text-green-400 hover:text-green-300">
@@ -698,16 +698,16 @@ function CharacterManager({ campaignId, campaignCharacters }) {
<button <button
type="button" type="button"
onClick={() => setEditingCharacter(null)} onClick={() => setEditingCharacter(null)}
className="p-1 text-slate-400 hover:text-slate-200" className="p-1 text-stone-400 hover:text-stone-200"
> >
<XCircle size={18} /> <XCircle size={18} />
</button> </button>
</form> </form>
) : ( ) : (
<> <>
<span className="text-slate-100"> <span className="text-stone-100">
{character.name}{' '} {character.name}{' '}
<span className="text-xs text-slate-400"> <span className="text-xs text-stone-400">
(HP: {character.defaultMaxHp || 'N/A'}, Init Mod: {formatInitMod(character.defaultInitMod)}) (HP: {character.defaultMaxHp || 'N/A'}, Init Mod: {formatInitMod(character.defaultInitMod)})
</span> </span>
</span> </span>
@@ -719,14 +719,14 @@ function CharacterManager({ campaignId, campaignCharacters }) {
defaultMaxHp: character.defaultMaxHp || DEFAULT_MAX_HP, defaultMaxHp: character.defaultMaxHp || DEFAULT_MAX_HP,
defaultInitMod: character.defaultInitMod || DEFAULT_INIT_MOD defaultInitMod: character.defaultInitMod || DEFAULT_INIT_MOD
})} })}
className="p-1 rounded transition-colors text-yellow-400 hover:text-yellow-300 bg-slate-600 hover:bg-slate-500" className="p-1 rounded transition-colors text-yellow-400 hover:text-yellow-300 bg-stone-700 hover:bg-stone-600"
aria-label="Edit character" aria-label="Edit character"
> >
<Edit3 size={18} /> <Edit3 size={18} />
</button> </button>
<button <button
onClick={() => requestDeleteCharacter(character.id, character.name)} onClick={() => requestDeleteCharacter(character.id, character.name)}
className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-slate-600 hover:bg-slate-500" className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-stone-700 hover:bg-stone-600"
aria-label="Delete character" aria-label="Delete character"
> >
<Trash2 size={18} /> <Trash2 size={18} />
@@ -1118,12 +1118,12 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
return ( return (
<> <>
<div className="p-3 bg-slate-800 rounded-md mt-4"> <div className="p-3 bg-stone-900 rounded-md mt-4">
<div className="flex justify-between items-center mb-3"> <div className="flex justify-between items-center mb-3">
<h4 className="text-lg font-medium text-amber-200 font-cinzel tracking-wide">Add Participants</h4> <h4 className="text-lg font-medium text-amber-200 font-cinzel tracking-wide">Add Participants</h4>
<button <button
onClick={handleAddAllCampaignCharacters} onClick={handleAddAllCampaignCharacters}
className="px-3 py-1.5 text-xs font-medium text-white bg-indigo-600 hover:bg-indigo-700 rounded-md transition-colors flex items-center" className="px-3 py-1.5 text-xs font-medium text-white bg-violet-700 hover:bg-violet-800 rounded-md transition-colors flex items-center"
disabled={!campaignCharacters || campaignCharacters.length === 0 || (encounter.isStarted && !encounter.isPaused)} disabled={!campaignCharacters || campaignCharacters.length === 0 || (encounter.isStarted && !encounter.isPaused)}
> >
<Users2 size={16} className="mr-1.5" /> <Users2 size={16} className="mr-1.5" />
@@ -1138,7 +1138,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
<div> <div>
<p className="text-yellow-300 font-semibold text-sm">Combat is Running</p> <p className="text-yellow-300 font-semibold text-sm">Combat is Running</p>
<p className="text-yellow-200 text-xs mt-1"> <p className="text-yellow-200 text-xs mt-1">
Pause combat first to add participants. Participants added during active combat won't be included in the turn order until you pause and resume. Pause combat to add or remove participants. The turn order will be recalculated when combat is resumed.
</p> </p>
</div> </div>
</div> </div>
@@ -1149,10 +1149,10 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
e.preventDefault(); e.preventDefault();
handleAddParticipant(); handleAddParticipant();
}} }}
className="grid grid-cols-1 md:grid-cols-6 gap-x-4 gap-y-2 mb-4 p-3 bg-slate-700 rounded items-end" className="grid grid-cols-1 md:grid-cols-6 gap-x-4 gap-y-2 mb-4 p-3 bg-stone-800 rounded items-end"
> >
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-sm font-medium text-slate-300">Type</label> <label className="block text-sm font-medium text-stone-300">Type</label>
<select <select
value={participantType} value={participantType}
onChange={(e) => { onChange={(e) => {
@@ -1160,7 +1160,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
setSelectedCharacterId(''); setSelectedCharacterId('');
setIsNpc(false); setIsNpc(false);
}} }}
className="mt-1 w-full px-3 py-2 bg-slate-600 border-slate-500 rounded text-white" className="mt-1 w-full px-3 py-2 bg-stone-700 border-stone-600 rounded text-white"
> >
<option value="monster">Monster</option> <option value="monster">Monster</option>
<option value="character">Character</option> <option value="character">Character</option>
@@ -1170,7 +1170,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
{participantType === 'monster' ? ( {participantType === 'monster' ? (
<> <>
<div className="md:col-span-4"> <div className="md:col-span-4">
<label htmlFor="monsterName" className="block text-sm font-medium text-slate-300"> <label htmlFor="monsterName" className="block text-sm font-medium text-stone-300">
Monster Name Monster Name
</label> </label>
<input <input
@@ -1179,11 +1179,11 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
value={participantName} value={participantName}
onChange={(e) => setParticipantName(e.target.value)} onChange={(e) => setParticipantName(e.target.value)}
placeholder="e.g., Dire Wolf" placeholder="e.g., Dire Wolf"
className="mt-1 w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white" className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/> />
</div> </div>
<div className="md:col-span-2"> <div className="md:col-span-2">
<label htmlFor="monsterInitMod" className="block text-sm font-medium text-slate-300"> <label htmlFor="monsterInitMod" className="block text-sm font-medium text-stone-300">
Init Mod Init Mod
</label> </label>
<input <input
@@ -1191,11 +1191,11 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
id="monsterInitMod" id="monsterInitMod"
value={monsterInitMod} value={monsterInitMod}
onChange={(e) => setMonsterInitMod(e.target.value)} onChange={(e) => setMonsterInitMod(e.target.value)}
className="mt-1 w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white" className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/> />
</div> </div>
<div className="md:col-span-2"> <div className="md:col-span-2">
<label htmlFor="monsterMaxHp" className="block text-sm font-medium text-slate-300"> <label htmlFor="monsterMaxHp" className="block text-sm font-medium text-stone-300">
Max HP Max HP
</label> </label>
<input <input
@@ -1203,7 +1203,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
id="monsterMaxHp" id="monsterMaxHp"
value={maxHp} value={maxHp}
onChange={(e) => setMaxHp(e.target.value)} onChange={(e) => setMaxHp(e.target.value)}
className="mt-1 w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white" className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/> />
</div> </div>
<div className="md:col-span-2 flex items-center pt-5"> <div className="md:col-span-2 flex items-center pt-5">
@@ -1212,9 +1212,9 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
id="isNpc" id="isNpc"
checked={isNpc} checked={isNpc}
onChange={(e) => setIsNpc(e.target.checked)} onChange={(e) => setIsNpc(e.target.checked)}
className="h-4 w-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500" className="h-4 w-4 text-violet-600 border-stone-400 rounded focus:ring-violet-500"
/> />
<label htmlFor="isNpc" className="ml-2 block text-sm text-slate-300"> <label htmlFor="isNpc" className="ml-2 block text-sm text-stone-300">
Is NPC? Is NPC?
</label> </label>
</div> </div>
@@ -1222,11 +1222,11 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
) : ( ) : (
<> <>
<div className="md:col-span-4"> <div className="md:col-span-4">
<label className="block text-sm font-medium text-slate-300">Select Character</label> <label className="block text-sm font-medium text-stone-300">Select Character</label>
<select <select
value={selectedCharacterId} value={selectedCharacterId}
onChange={(e) => setSelectedCharacterId(e.target.value)} onChange={(e) => setSelectedCharacterId(e.target.value)}
className="mt-1 w-full px-3 py-2 bg-slate-600 border-slate-500 rounded text-white" className="mt-1 w-full px-3 py-2 bg-stone-700 border-stone-600 rounded text-white"
> >
<option value="">-- Select from Campaign --</option> <option value="">-- Select from Campaign --</option>
{campaignCharacters.map(c => ( {campaignCharacters.map(c => (
@@ -1237,12 +1237,12 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
</select> </select>
</div> </div>
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-sm font-medium text-slate-300">Max HP (Encounter)</label> <label className="block text-sm font-medium text-stone-300">Max HP (Encounter)</label>
<input <input
type="number" type="number"
value={maxHp} value={maxHp}
onChange={(e) => setMaxHp(e.target.value)} onChange={(e) => setMaxHp(e.target.value)}
className="mt-1 w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white" className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/> />
</div> </div>
</> </>
@@ -1252,7 +1252,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
<button <button
type="submit" type="submit"
disabled={encounter.isStarted && !encounter.isPaused} disabled={encounter.isStarted && !encounter.isPaused}
className={`px-4 py-2 text-sm font-medium text-white rounded-md transition-colors flex items-center ${encounter.isStarted && !encounter.isPaused ? 'bg-slate-500 cursor-not-allowed opacity-50' : 'bg-green-500 hover:bg-green-600'}`} className={`px-4 py-2 text-sm font-medium text-white rounded-md transition-colors flex items-center ${encounter.isStarted && !encounter.isPaused ? 'bg-stone-600 cursor-not-allowed opacity-50' : 'bg-red-700 hover:bg-red-800'}`}
title={encounter.isStarted && !encounter.isPaused ? 'Pause combat to add participants' : 'Add participant and roll initiative'} title={encounter.isStarted && !encounter.isPaused ? 'Pause combat to add participants' : 'Add participant and roll initiative'}
> >
<Dices size={18} className="mr-1.5" /> Add to Encounter (Roll Init) <Dices size={18} className="mr-1.5" /> Add to Encounter (Roll Init)
@@ -1262,12 +1262,12 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
{lastRollDetails && ( {lastRollDetails && (
<p className="text-sm text-green-400 mt-2 mb-2 text-center"> <p className="text-sm text-green-400 mt-2 mb-2 text-center">
{lastRollDetails.name} ({lastRollDetails.type === 'character' ? 'Character' : lastRollDetails.type}) {lastRollDetails.name} ({lastRollDetails.type === 'character' ? 'Character' : lastRollDetails.type === 'monster' ? 'Monster' : lastRollDetails.type})
: Rolled D20 ({lastRollDetails.roll}) {formatInitMod(lastRollDetails.mod)} = {lastRollDetails.total} Initiative : Rolled d20 ({lastRollDetails.roll}) {formatInitMod(lastRollDetails.mod)} = {lastRollDetails.total} Initiative
</p> </p>
)} )}
{participants.length === 0 && <p className="text-sm text-slate-400">No participants added yet.</p>} {participants.length === 0 && <p className="text-sm text-stone-400">No participants added yet.</p>}
<ul className="space-y-2"> <ul className="space-y-2">
{sortedParticipants.map((p) => { {sortedParticipants.map((p) => {
@@ -1275,7 +1275,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
const isDraggable = (!encounter.isStarted || encounter.isPaused) && tiedInitiatives.includes(Number(p.initiative)); const isDraggable = (!encounter.isStarted || encounter.isPaused) && tiedInitiatives.includes(Number(p.initiative));
const participantDisplayType = p.type === 'monster' ? (p.isNpc ? 'NPC' : 'Monster') : 'Character'; const participantDisplayType = p.type === 'monster' ? (p.isNpc ? 'NPC' : 'Monster') : 'Character';
let bgColor = p.type === 'character' ? 'bg-blue-950' : (p.isNpc ? 'bg-slate-600' : 'bg-[#8e351c]'); let bgColor = p.type === 'character' ? 'bg-indigo-950' : (p.isNpc ? 'bg-stone-700' : 'bg-[#8e351c]');
if (isCurrentTurn && !encounter.isPaused) bgColor = 'bg-green-600'; if (isCurrentTurn && !encounter.isPaused) bgColor = 'bg-green-600';
const isDead = p.currentHp === 0; const isDead = p.currentHp === 0;
@@ -1294,7 +1294,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
{isDraggable && ( {isDraggable && (
<ChevronsUpDown <ChevronsUpDown
size={18} size={18}
className="mr-2 text-slate-400 flex-shrink-0" className="mr-2 text-stone-400 flex-shrink-0"
title="Drag to reorder in tie" title="Drag to reorder in tie"
/> />
)} )}
@@ -1307,21 +1307,21 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
<Zap size={12} className="mr-1" /> CURRENT <Zap size={12} className="mr-1" /> CURRENT
</span> </span>
)} )}
{isDead && <span className="ml-2 text-xs text-red-300 font-semibold">(Unconscious)</span>} {isDead && <span className="ml-2 text-xs text-red-300 font-semibold">{p.type === 'character' ? '(Unconscious)' : '(Dead)'}</span>}
</p> </p>
<p className={`text-sm ${isCurrentTurn && !encounter.isPaused ? 'text-green-100' : 'text-slate-200'}`}> <p className={`text-sm ${isCurrentTurn && !encounter.isPaused ? 'text-green-100' : 'text-stone-200'}`}>
Init: {p.initiative} | HP: {p.currentHp}/{p.maxHp} Init: {p.initiative} | HP: {p.currentHp}/{p.maxHp}
</p> </p>
{/* Death Saves */} {/* Death Saves - only player characters make death saving throws */}
{isDead && encounter.isStarted && ( {isDead && encounter.isStarted && p.type === 'character' && (
<div className="mt-2 flex items-center space-x-2"> <div className="mt-2 flex items-center space-x-2">
<span className="text-xs text-red-300 font-medium">Death Saves:</span> <span className="text-xs text-red-300 font-medium">Death Saves:</span>
{[1, 2, 3].map(saveNum => ( {[1, 2, 3].map(saveNum => (
<button <button
key={saveNum} key={saveNum}
onClick={() => handleDeathSaveChange(p.id, saveNum)} onClick={() => handleDeathSaveChange(p.id, saveNum)}
className={`w-6 h-6 rounded border-2 transition-all ${(p.deathSaves || 0) >= saveNum ? 'bg-red-600 border-red-500' : 'bg-slate-700 border-slate-500 hover:border-red-400'} ${saveNum === 3 && (p.deathSaves || 0) === 3 ? 'animate-pulse' : ''}`} className={`w-6 h-6 rounded border-2 transition-all ${(p.deathSaves || 0) >= saveNum ? 'bg-red-600 border-red-500' : 'bg-stone-800 border-stone-600 hover:border-red-400'} ${saveNum === 3 && (p.deathSaves || 0) === 3 ? 'animate-pulse' : ''}`}
title={`Death save ${saveNum}`} title={`Death save ${saveNum}`}
> >
{(p.deathSaves || 0) >= saveNum && <span className="text-white text-sm"></span>} {(p.deathSaves || 0) >= saveNum && <span className="text-white text-sm"></span>}
@@ -1334,13 +1334,13 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
<div className="flex flex-wrap items-center space-x-2 mt-2 sm:mt-0"> <div className="flex flex-wrap items-center space-x-2 mt-2 sm:mt-0">
{encounter.isStarted && ( {encounter.isStarted && (
<div className="flex items-center space-x-1 bg-slate-700 p-1 rounded-md"> <div className="flex items-center space-x-1 bg-stone-800 p-1 rounded-md">
<input <input
type="number" type="number"
placeholder="HP" placeholder="HP"
value={hpChangeValues[p.id] || ''} value={hpChangeValues[p.id] || ''}
onChange={(e) => setHpChangeValues(prev => ({ ...prev, [p.id]: e.target.value }))} onChange={(e) => setHpChangeValues(prev => ({ ...prev, [p.id]: e.target.value }))}
className="w-16 p-1 text-sm bg-slate-600 border border-slate-500 rounded-md text-white focus:ring-amber-600 focus:border-amber-600" className="w-16 p-1 text-sm bg-stone-700 border border-stone-600 rounded-md text-white focus:ring-amber-600 focus:border-amber-600"
aria-label={`HP change for ${p.name}`} aria-label={`HP change for ${p.name}`}
/> />
{!isDead && ( {!isDead && (
@@ -1354,7 +1354,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
)} )}
<button <button
onClick={() => applyHpChange(p.id, 'heal')} onClick={() => applyHpChange(p.id, 'heal')}
className="p-1 bg-green-500 hover:bg-green-600 text-white rounded-md text-xs" className="p-1 bg-emerald-600 hover:bg-emerald-700 text-white rounded-md text-xs"
title={isDead ? "Heal / Revive" : "Heal"} title={isDead ? "Heal / Revive" : "Heal"}
> >
<HeartPulse size={16} /> <HeartPulse size={16} />
@@ -1364,7 +1364,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
{!isDead && ( {!isDead && (
<button <button
onClick={() => toggleParticipantActive(p.id)} onClick={() => toggleParticipantActive(p.id)}
className={`p-1 rounded transition-colors ${p.isActive ? 'text-yellow-400 hover:text-yellow-300' : 'text-slate-400 hover:text-slate-300'} bg-slate-600 hover:bg-slate-500`} className={`p-1 rounded transition-colors ${p.isActive ? 'text-yellow-400 hover:text-yellow-300' : 'text-stone-400 hover:text-stone-300'} bg-stone-700 hover:bg-stone-600`}
title={p.isActive ? "Mark Inactive" : "Mark Active"} title={p.isActive ? "Mark Inactive" : "Mark Active"}
> >
{p.isActive ? <UserCheck size={18} /> : <UserX size={18} />} {p.isActive ? <UserCheck size={18} /> : <UserX size={18} />}
@@ -1372,14 +1372,14 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
)} )}
<button <button
onClick={() => setEditingParticipant(p)} onClick={() => setEditingParticipant(p)}
className="p-1 rounded transition-colors text-yellow-400 hover:text-yellow-300 bg-slate-600 hover:bg-slate-500" className="p-1 rounded transition-colors text-yellow-400 hover:text-yellow-300 bg-stone-700 hover:bg-stone-600"
title="Edit" title="Edit"
> >
<Edit3 size={18} /> <Edit3 size={18} />
</button> </button>
<button <button
onClick={() => requestDeleteParticipant(p.id, p.name)} onClick={() => requestDeleteParticipant(p.id, p.name)}
className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-slate-600 hover:bg-slate-500" className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-stone-700 hover:bg-stone-600"
title="Remove" title="Remove"
> >
<Trash2 size={18} /> <Trash2 size={18} />
@@ -1540,13 +1540,13 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
return ( return (
<> <>
<div className="lg:sticky lg:top-4 p-4 bg-slate-800 rounded-md shadow-lg"> <div className="lg:sticky lg:top-4 p-4 bg-stone-900 rounded-md shadow-lg">
<h4 className="text-lg font-medium text-amber-200 mb-4 text-center font-cinzel tracking-wide">Combat Controls</h4> <h4 className="text-lg font-medium text-amber-200 mb-4 text-center font-cinzel tracking-wide">Combat Controls</h4>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{!encounter.isStarted ? ( {!encounter.isStarted ? (
<button <button
onClick={handleStartEncounter} onClick={handleStartEncounter}
className="w-full px-4 py-3 text-sm font-medium text-white bg-green-600 hover:bg-green-700 rounded-md transition-colors flex items-center justify-center" className="w-full px-4 py-3 text-sm font-medium text-white bg-red-700 hover:bg-red-800 rounded-md transition-colors flex items-center justify-center"
disabled={!encounter.participants || encounter.participants.filter(p => p.isActive).length === 0} disabled={!encounter.participants || encounter.participants.filter(p => p.isActive).length === 0}
> >
<PlayIcon size={18} className="mr-2" /> Start Combat <PlayIcon size={18} className="mr-2" /> Start Combat
@@ -1555,14 +1555,17 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
<> <>
<button <button
onClick={handleTogglePause} onClick={handleTogglePause}
className={`w-full px-4 py-3 text-sm font-medium text-white rounded-md transition-colors flex items-center justify-center ${encounter.isPaused ? 'bg-green-500 hover:bg-green-600' : 'bg-yellow-500 hover:bg-yellow-600'}`} className={`w-full px-4 py-3 text-sm font-medium text-white rounded-md transition-colors flex items-center justify-center ${encounter.isPaused ? 'bg-red-700 hover:bg-red-800' : 'bg-amber-600 hover:bg-amber-700'}`}
title={encounter.isPaused
? 'Resume combat from the current turn. The initiative order will be recalculated to include any participants added while paused.'
: 'Pause combat to freeze the turn order. While paused, you can add or remove participants, adjust HP, and edit initiative values. The turn order will be recalculated when you resume.'}
> >
{encounter.isPaused ? <PlayIcon size={18} className="mr-2" /> : <PauseIcon size={18} className="mr-2" />} {encounter.isPaused ? <PlayIcon size={18} className="mr-2" /> : <PauseIcon size={18} className="mr-2" />}
{encounter.isPaused ? 'Resume' : 'Pause'} {encounter.isPaused ? 'Resume Combat' : 'Pause Combat'}
</button> </button>
<button <button
onClick={handleNextTurn} onClick={handleNextTurn}
className="w-full px-4 py-3 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors flex items-center justify-center" className="w-full px-4 py-3 text-sm font-medium text-white bg-purple-700 hover:bg-purple-800 rounded-md transition-colors flex items-center justify-center"
disabled={!encounter.currentTurnParticipantId || encounter.isPaused} disabled={!encounter.currentTurnParticipantId || encounter.isPaused}
> >
<SkipForwardIcon size={18} className="mr-2" /> Next Turn <SkipForwardIcon size={18} className="mr-2" /> Next Turn
@@ -1575,7 +1578,7 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
</button> </button>
{/* Round Counter */} {/* Round Counter */}
<div className="mt-2 pt-3 border-t border-slate-600"> <div className="mt-2 pt-3 border-t border-stone-700">
<p className="text-center text-lg font-semibold text-amber-300 font-cinzel">Round: {encounter.round}</p> <p className="text-center text-lg font-semibold text-amber-300 font-cinzel">Round: {encounter.round}</p>
{encounter.isPaused && ( {encounter.isPaused && (
<p className="text-center text-sm text-yellow-400 font-semibold mt-1">(Paused)</p> <p className="text-center text-sm text-yellow-400 font-semibold mt-1">(Paused)</p>
@@ -1730,26 +1733,26 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
const selectedEncounter = encounters?.find(e => e.id === selectedEncounterId); const selectedEncounter = encounters?.find(e => e.id === selectedEncounterId);
if (isLoadingEncounters && campaignId) { if (isLoadingEncounters && campaignId) {
return <p className="text-center text-slate-300 mt-4">Loading encounters...</p>; return <p className="text-center text-stone-300 mt-4">Loading encounters...</p>;
} }
return ( return (
<> <>
<div className="mt-6 p-4 bg-slate-800 rounded-lg shadow"> <div className="mt-6 p-4 bg-stone-900 rounded-lg shadow">
<div className="flex justify-between items-center mb-3"> <div className="flex justify-between items-center mb-3">
<h3 className="text-xl font-semibold text-amber-300 font-cinzel tracking-wide flex items-center"> <h3 className="text-xl font-semibold text-amber-300 font-cinzel tracking-wide flex items-center">
<Swords size={24} className="mr-2" /> Encounters <Swords size={24} className="mr-2" /> Encounters
</h3> </h3>
<button <button
onClick={() => setShowCreateModal(true)} onClick={() => setShowCreateModal(true)}
className="px-4 py-2 text-sm font-medium text-white bg-orange-500 hover:bg-orange-600 rounded-md transition-colors flex items-center" className="px-4 py-2 text-sm font-medium text-white bg-amber-700 hover:bg-amber-800 rounded-md transition-colors flex items-center"
> >
<PlusCircle size={18} className="mr-1" /> Create Encounter <PlusCircle size={18} className="mr-1" /> Create Encounter
</button> </button>
</div> </div>
{(!encounters || encounters.length === 0) && ( {(!encounters || encounters.length === 0) && (
<p className="text-sm text-slate-400">No encounters yet.</p> <p className="text-sm text-stone-400">No encounters yet.</p>
)} )}
<div className="space-y-3"> <div className="space-y-3">
@@ -1761,12 +1764,12 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
return ( return (
<div <div
key={encounter.id} key={encounter.id}
className={`p-3 rounded-md shadow transition-all ${selectedEncounterId === encounter.id ? 'bg-amber-900 ring-2 ring-amber-500' : 'bg-slate-700 hover:bg-slate-600'} ${isLive ? 'ring-2 ring-green-500 shadow-md shadow-green-500/30' : ''}`} className={`p-3 rounded-md shadow transition-all ${selectedEncounterId === encounter.id ? 'bg-amber-900 ring-2 ring-amber-500' : 'bg-stone-800 hover:bg-stone-700'} ${isLive ? 'ring-2 ring-green-500 shadow-md shadow-green-500/30' : ''}`}
> >
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div onClick={() => setSelectedEncounterId(encounter.id)} className="cursor-pointer flex-grow"> <div onClick={() => setSelectedEncounterId(encounter.id)} className="cursor-pointer flex-grow">
<h4 className="font-medium text-white">{encounter.name}</h4> <h4 className="font-medium text-white">{encounter.name}</h4>
<p className="text-xs text-slate-300"> <p className="text-xs text-stone-300">
Participants: {encounter.participants?.length || 0} Participants: {encounter.participants?.length || 0}
</p> </p>
{isLive && ( {isLive && (
@@ -1778,7 +1781,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<button <button
onClick={() => handleTogglePlayerDisplay(encounter.id)} onClick={() => handleTogglePlayerDisplay(encounter.id)}
className={`p-1 rounded transition-colors ${isLive ? 'bg-red-500 hover:bg-red-600 text-white' : 'text-amber-400 hover:text-amber-300 bg-slate-600 hover:bg-slate-500'}`} className={`p-1 rounded transition-colors ${isLive ? 'bg-red-500 hover:bg-red-600 text-white' : 'text-amber-400 hover:text-amber-300 bg-stone-700 hover:bg-stone-600'}`}
title={isLive ? "Deactivate for Player Display" : "Activate for Player Display"} title={isLive ? "Deactivate for Player Display" : "Activate for Player Display"}
> >
{isLive ? <EyeOff size={18} /> : <Eye size={18} />} {isLive ? <EyeOff size={18} /> : <Eye size={18} />}
@@ -1788,7 +1791,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
e.stopPropagation(); e.stopPropagation();
requestDeleteEncounter(encounter.id, encounter.name); requestDeleteEncounter(encounter.id, encounter.name);
}} }}
className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-slate-600 hover:bg-slate-500" className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-stone-700 hover:bg-stone-600"
title="Delete Encounter" title="Delete Encounter"
> >
<Trash2 size={18} /> <Trash2 size={18} />
@@ -1810,7 +1813,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
)} )}
{selectedEncounter && ( {selectedEncounter && (
<div className="mt-6 p-4 bg-slate-800 rounded-lg shadow-inner"> <div className="mt-6 p-4 bg-stone-900 rounded-lg shadow-inner">
<h3 className="text-xl font-semibold text-amber-300 mb-3 font-cinzel tracking-wide"> <h3 className="text-xl font-semibold text-amber-300 mb-3 font-cinzel tracking-wide">
Managing Encounter: {selectedEncounter.name} Managing Encounter: {selectedEncounter.name}
</h3> </h3>
@@ -1975,7 +1978,7 @@ function AdminView({ userId }) {
const selectedCampaign = campaignsWithDetails.find(c => c.id === selectedCampaignId); const selectedCampaign = campaignsWithDetails.find(c => c.id === selectedCampaignId);
if (isLoadingCampaigns) { if (isLoadingCampaigns) {
return <p className="text-center text-slate-300">Loading campaigns...</p>; return <p className="text-center text-stone-300">Loading campaigns...</p>;
} }
if (campaignsError) { if (campaignsError) {
@@ -1994,14 +1997,14 @@ function AdminView({ userId }) {
<h2 className="text-2xl font-semibold text-amber-300 font-cinzel tracking-wide">Campaigns</h2> <h2 className="text-2xl font-semibold text-amber-300 font-cinzel tracking-wide">Campaigns</h2>
<button <button
onClick={() => setShowCreateModal(true)} onClick={() => setShowCreateModal(true)}
className="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded-lg flex items-center transition-colors" className="bg-red-700 hover:bg-red-800 text-white font-bold py-2 px-4 rounded-lg flex items-center transition-colors"
> >
<PlusCircle size={20} className="mr-2" /> Create Campaign <PlusCircle size={20} className="mr-2" /> Create Campaign
</button> </button>
</div> </div>
{campaignsWithDetails.length === 0 && !isLoadingCampaigns && ( {campaignsWithDetails.length === 0 && !isLoadingCampaigns && (
<p className="text-slate-400">No campaigns yet. Create one to get started!</p> <p className="text-stone-400">No campaigns yet. Create one to get started!</p>
)} )}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
@@ -2010,7 +2013,7 @@ function AdminView({ userId }) {
? { backgroundImage: `url(${campaign.playerDisplayBackgroundUrl})` } ? { backgroundImage: `url(${campaign.playerDisplayBackgroundUrl})` }
: {}; : {};
const cardClasses = `h-40 flex flex-col justify-between rounded-lg shadow-md cursor-pointer transition-all relative overflow-hidden bg-cover bg-center ${selectedCampaignId === campaign.id ? 'ring-4 ring-amber-500' : ''} ${!campaign.playerDisplayBackgroundUrl ? 'bg-slate-700 hover:bg-slate-600' : 'hover:shadow-xl'}`; const cardClasses = `h-40 flex flex-col justify-between rounded-lg shadow-md cursor-pointer transition-all relative overflow-hidden bg-cover bg-center ${selectedCampaignId === campaign.id ? 'ring-4 ring-amber-500' : ''} ${!campaign.playerDisplayBackgroundUrl ? 'bg-stone-800 hover:bg-stone-700' : 'hover:shadow-xl'}`;
return ( return (
<div <div
@@ -2024,7 +2027,7 @@ function AdminView({ userId }) {
> >
<div> <div>
<h3 className="text-xl font-semibold text-white">{campaign.name}</h3> <h3 className="text-xl font-semibold text-white">{campaign.name}</h3>
<div className="text-xs text-slate-100 mt-1 space-x-3"> <div className="text-xs text-stone-100 mt-1 space-x-3">
<span className="inline-flex items-center"> <span className="inline-flex items-center">
<Users size={12} className="mr-1" /> {campaign.characters?.length || 0} Characters <Users size={12} className="mr-1" /> {campaign.characters?.length || 0} Characters
</span> </span>
@@ -2059,7 +2062,7 @@ function AdminView({ userId }) {
)} )}
{selectedCampaign && ( {selectedCampaign && (
<div className="mt-6 p-6 bg-slate-800 rounded-lg shadow-xl"> <div className="mt-6 p-6 bg-stone-900 rounded-lg shadow-xl">
<h2 className="text-2xl font-semibold text-amber-300 mb-4 font-cinzel tracking-wide"> <h2 className="text-2xl font-semibold text-amber-300 mb-4 font-cinzel tracking-wide">
Managing: {selectedCampaign.name} Managing: {selectedCampaign.name}
</h2> </h2>
@@ -2067,7 +2070,7 @@ function AdminView({ userId }) {
campaignId={selectedCampaignId} campaignId={selectedCampaignId}
campaignCharacters={selectedCampaign.characters || []} campaignCharacters={selectedCampaign.characters || []}
/> />
<hr className="my-6 border-slate-600" /> <hr className="my-6 border-stone-700" />
<EncounterManager <EncounterManager
campaignId={selectedCampaignId} campaignId={selectedCampaignId}
initialActiveEncounterId={ initialActiveEncounterId={
@@ -2189,7 +2192,7 @@ function DisplayView() {
}, [activeEncounterData?.currentTurnParticipantId, activeEncounterData?.isStarted, activeEncounterData?.isPaused]); }, [activeEncounterData?.currentTurnParticipantId, activeEncounterData?.isStarted, activeEncounterData?.isPaused]);
if (isLoadingActiveDisplay || (isPlayerDisplayActive && isLoadingEncounter)) { if (isLoadingActiveDisplay || (isPlayerDisplayActive && isLoadingEncounter)) {
return <div className="text-center py-10 text-2xl text-slate-300">Loading Player Display...</div>; return <div className="text-center py-10 text-2xl text-stone-300">Loading Player Display...</div>;
} }
if (activeDisplayError || (isPlayerDisplayActive && encounterError)) { if (activeDisplayError || (isPlayerDisplayActive && encounterError)) {
@@ -2198,8 +2201,8 @@ function DisplayView() {
if (!isPlayerDisplayActive || !activeEncounterData) { if (!isPlayerDisplayActive || !activeEncounterData) {
return ( return (
<div className="min-h-screen bg-black text-slate-400 flex flex-col items-center justify-center p-4 text-center"> <div className="min-h-screen bg-black text-stone-400 flex flex-col items-center justify-center p-4 text-center">
<EyeOff size={64} className="mb-4 text-slate-500" /> <EyeOff size={64} className="mb-4 text-stone-500" />
<h2 className="text-3xl font-semibold font-cinzel tracking-wide">Game Session Paused</h2> <h2 className="text-3xl font-semibold font-cinzel tracking-wide">Game Session Paused</h2>
<p className="text-xl mt-2">The Dungeon Master has not activated an encounter for display.</p> <p className="text-xl mt-2">The Dungeon Master has not activated an encounter for display.</p>
</div> </div>
@@ -2226,10 +2229,10 @@ function DisplayView() {
return ( return (
<div <div
className={`p-4 md:p-8 rounded-xl shadow-2xl ${!campaignBackgroundUrl ? 'bg-slate-900' : ''}`} className={`p-4 md:p-8 rounded-xl shadow-2xl ${!campaignBackgroundUrl ? 'bg-stone-950' : ''}`}
style={displayStyles} style={displayStyles}
> >
<div className={campaignBackgroundUrl ? 'bg-slate-900 bg-opacity-75 p-4 md:p-6 rounded-lg' : ''}> <div className={campaignBackgroundUrl ? 'bg-stone-950 bg-opacity-75 p-4 md:p-6 rounded-lg' : ''}>
<h2 className="text-4xl md:text-5xl font-bold text-center text-amber-400 mb-2 font-cinzel tracking-wide">{name}</h2> <h2 className="text-4xl md:text-5xl font-bold text-center text-amber-400 mb-2 font-cinzel tracking-wide">{name}</h2>
{isStarted && <p className="text-2xl text-center text-amber-300 mb-1 font-cinzel">Round: {round}</p>} {isStarted && <p className="text-2xl text-center text-amber-300 mb-1 font-cinzel">Round: {round}</p>}
@@ -2239,15 +2242,15 @@ function DisplayView() {
)} )}
{!isStarted && participants?.length > 0 && ( {!isStarted && participants?.length > 0 && (
<p className="text-2xl text-center text-slate-400 mb-6">Awaiting Start</p> <p className="text-2xl text-center text-stone-400 mb-6">Awaiting Start</p>
)} )}
{!isStarted && (!participants || participants.length === 0) && ( {!isStarted && (!participants || participants.length === 0) && (
<p className="text-2xl text-slate-500 mb-6">No participants.</p> <p className="text-2xl text-stone-500 mb-6">No participants.</p>
)} )}
{participantsToRender.length === 0 && isStarted && ( {participantsToRender.length === 0 && isStarted && (
<p className="text-xl text-slate-400">No active participants.</p> <p className="text-xl text-stone-400">No active participants.</p>
)} )}
<div className="space-y-4 max-w-3xl mx-auto"> <div className="space-y-4 max-w-3xl mx-auto">
@@ -2255,8 +2258,8 @@ function DisplayView() {
const isDead = p.currentHp === 0; const isDead = p.currentHp === 0;
const isDying = p.isDying || false; const isDying = p.isDying || false;
let participantBgColor = p.type === 'monster' let participantBgColor = p.type === 'monster'
? (p.isNpc ? 'bg-slate-700' : 'bg-[#8e351c]') ? (p.isNpc ? 'bg-stone-800' : 'bg-[#8e351c]')
: 'bg-blue-950'; : 'bg-indigo-950';
const isCurrentTurn = p.id === currentTurnParticipantId && isStarted && !isPaused; const isCurrentTurn = p.id === currentTurnParticipantId && isStarted && !isPaused;
@@ -2274,24 +2277,24 @@ function DisplayView() {
> >
<div className="flex justify-between items-center mb-2"> <div className="flex justify-between items-center mb-2">
<h3 <h3
className={`text-2xl md:text-3xl font-bold font-cinzel ${isCurrentTurn ? 'text-white' : (p.type === 'character' ? 'text-amber-100' : (p.isNpc ? 'text-slate-100' : 'text-white'))}`} className={`text-2xl md:text-3xl font-bold font-cinzel ${isCurrentTurn ? 'text-white' : (p.type === 'character' ? 'text-amber-100' : (p.isNpc ? 'text-stone-100' : 'text-white'))}`}
> >
{isDead && <span className="mr-2"></span>} {isDead && <span className="mr-2"></span>}
{p.name} {p.name}
{isCurrentTurn && ( {isCurrentTurn && (
<span className="text-yellow-300 animate-pulse ml-2">(Current)</span> <span className="text-yellow-300 animate-pulse ml-2">(Current)</span>
)} )}
{isDead && <span className="text-red-300 text-lg ml-2">(Unconscious)</span>} {isDead && <span className="text-red-300 text-lg ml-2">{p.type === 'character' ? '(Unconscious)' : '(Dead)'}</span>}
</h3> </h3>
<span <span
className={`text-xl md:text-2xl font-semibold ${isCurrentTurn ? 'text-green-200' : 'text-slate-200'}`} className={`text-xl md:text-2xl font-semibold ${isCurrentTurn ? 'text-green-200' : 'text-stone-200'}`}
> >
Init: {p.initiative} Init: {p.initiative}
</span> </span>
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="w-full bg-slate-600 rounded-full h-6 md:h-8 relative overflow-hidden border-2 border-slate-500"> <div className="w-full bg-stone-700 rounded-full h-6 md:h-8 relative overflow-hidden border-2 border-stone-600">
<div <div
className={`h-full rounded-full transition-all ${isDead ? 'bg-red-900' : (p.currentHp <= p.maxHp / 4 ? 'bg-red-500' : (p.currentHp <= p.maxHp / 2 ? 'bg-yellow-500' : 'bg-green-500'))}`} className={`h-full rounded-full transition-all ${isDead ? 'bg-red-900' : (p.currentHp <= p.maxHp / 4 ? 'bg-red-500' : (p.currentHp <= p.maxHp / 2 ? 'bg-yellow-500' : 'bg-green-500'))}`}
style={{ width: `${Math.max(0, (p.currentHp / p.maxHp) * 100)}%` }} style={{ width: `${Math.max(0, (p.currentHp / p.maxHp) * 100)}%` }}
@@ -2309,7 +2312,7 @@ function DisplayView() {
)} )}
{!p.isActive && !isDead && ( {!p.isActive && !isDead && (
<p className="text-center text-lg font-semibold text-slate-300 mt-2">(Inactive)</p> <p className="text-center text-lg font-semibold text-stone-300 mt-2">(Inactive)</p>
)} )}
</div> </div>
); );
@@ -2393,7 +2396,7 @@ function App() {
if (isPlayerViewOnlyMode) { if (isPlayerViewOnlyMode) {
return ( return (
<div className="min-h-screen bg-slate-900 text-slate-100 font-garamond"> <div className="min-h-screen bg-stone-950 text-stone-100 font-garamond">
{isAuthReady && <DisplayView />} {isAuthReady && <DisplayView />}
{!isAuthReady && !error && <p>Authenticating for Player Display...</p>} {!isAuthReady && !error && <p>Authenticating for Player Display...</p>}
</div> </div>
@@ -2401,8 +2404,8 @@ function App() {
} }
return ( return (
<div className="min-h-screen bg-slate-900 text-slate-100 font-garamond"> <div className="min-h-screen bg-stone-950 text-stone-100 font-garamond">
<header className="bg-slate-900 p-4 shadow-lg border-b border-amber-900"> <header className="bg-stone-950 p-4 shadow-lg border-b border-amber-900">
<div className="container mx-auto flex justify-between items-center"> <div className="container mx-auto flex justify-between items-center">
<h1 className="text-3xl font-bold text-amber-400 font-cinzel tracking-wide">TTRPG Initiative Tracker</h1> <h1 className="text-3xl font-bold text-amber-400 font-cinzel tracking-wide">TTRPG Initiative Tracker</h1>
<button <button
@@ -2419,7 +2422,7 @@ function App() {
{!isAuthReady && !error && <p>Authenticating...</p>} {!isAuthReady && !error && <p>Authenticating...</p>}
</main> </main>
<footer className="bg-slate-900 p-4 text-center text-sm text-slate-400 mt-8"> <footer className="bg-stone-950 p-4 text-center text-sm text-stone-400 mt-8">
TTRPG Initiative Tracker {APP_VERSION} TTRPG Initiative Tracker {APP_VERSION}
</footer> </footer>
</div> </div>
+1 -1
View File
@@ -7,7 +7,7 @@
body { body {
margin: 0; margin: 0;
font-family: 'EB Garamond', Georgia, 'Times New Roman', serif; font-family: 'Alegreya Sans', system-ui, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
+1 -1
View File
@@ -9,7 +9,7 @@ module.exports = {
extend: { extend: {
fontFamily: { fontFamily: {
cinzel: ['Cinzel', 'serif'], cinzel: ['Cinzel', 'serif'],
garamond: ['"EB Garamond"', 'Georgia', 'serif'], garamond: ['"Alegreya Sans"', 'system-ui', 'sans-serif'],
}, },
}, },
}, },