feat: replace inline create form with CharacterWizard multi-step modal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Aaron Wood 2026-04-11 11:55:43 -04:00
parent a6218c72d4
commit 2e1fff4c2d
2 changed files with 9 additions and 214 deletions

View file

@ -165,135 +165,3 @@
font-style: italic;
grid-column: 1 / -1;
}
.createModal {
position: fixed;
inset: 0;
background: var(--bg-overlay);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
padding: 1rem;
}
.createForm {
background-color: var(--bg-modal);
background-image: var(--texture-surface), var(--texture-speckle);
background-size:
256px 256px,
128px 128px;
background-repeat: repeat, repeat;
border: 2px solid rgba(var(--gold-rgb), 0.3);
border-radius: 4px;
padding: 1.5rem;
width: 100%;
max-width: 400px;
box-shadow:
0 8px 40px rgba(var(--shadow-rgb), 0.7),
0 2px 8px rgba(var(--shadow-rgb), 0.5),
inset 0 1px 0 rgba(var(--gold-rgb), 0.1),
inset 0 0 60px rgba(var(--shadow-rgb), 0.2);
}
.createTitle {
font-family: "Cinzel", Georgia, serif;
font-size: 1.2rem;
font-weight: 700;
margin-bottom: 1rem;
color: var(--gold);
letter-spacing: 0.05em;
text-shadow: 0 1px 2px rgba(var(--shadow-rgb), 0.3);
}
.formField {
display: flex;
flex-direction: column;
gap: 0.25rem;
margin-bottom: 0.75rem;
}
.formLabel {
font-family: "Cinzel", Georgia, serif;
font-size: 0.75rem;
color: var(--text-secondary);
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.05em;
}
.formInput {
padding: 0.5rem 0.75rem;
background: var(--bg-inset);
border: 1px solid rgba(var(--gold-rgb), 0.15);
border-radius: 4px;
color: var(--text-primary);
font-size: 0.9rem;
font-family: "Alegreya", Georgia, serif;
}
.formInput:focus {
outline: none;
border-color: var(--gold);
}
.formSelect {
padding: 0.5rem 0.75rem;
background: var(--bg-inset);
border: 1px solid rgba(var(--gold-rgb), 0.15);
border-radius: 4px;
color: var(--text-primary);
font-size: 0.9rem;
font-family: "Alegreya", Georgia, serif;
}
.formSelect option {
background: var(--bg-modal);
color: var(--text-primary);
}
.formActions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
margin-top: 1rem;
}
.formBtn {
padding: 0.5rem 1rem;
border-radius: 4px;
font-weight: 600;
cursor: pointer;
font-size: 0.9rem;
}
.formBtnPrimary {
background: var(--btn-gold-bg);
color: var(--btn-active-text);
border: none;
font-family: "Cinzel", Georgia, serif;
box-shadow:
0 2px 4px rgba(var(--shadow-rgb), 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
text-shadow: 0 1px 1px rgba(var(--shadow-rgb), 0.2);
}
.formBtnPrimary:hover {
background: linear-gradient(
180deg,
var(--gold-bright),
var(--gold-hover) 40%,
var(--gold)
);
}
.formBtnSecondary {
background: transparent;
color: var(--text-secondary);
border: 1px solid rgba(var(--gold-rgb), 0.2);
}
.formBtnSecondary:hover {
border-color: rgba(var(--gold-rgb), 0.4);
color: var(--text-primary);
}

View file

@ -29,12 +29,10 @@ import ParticleOverlay from "../components/ParticleOverlay";
import ThreeFireOverlay from "../components/ThreeFireOverlay";
import type { AtmosphereState } from "../lib/atmosphereTypes";
import { defaultAtmosphere } from "../lib/atmosphereTypes";
import SelectDropdown from "../components/SelectDropdown";
import CharacterWizard from "../components/CharacterWizard";
import type { CreateCharacterData } from "../api";
import styles from "./CampaignView.module.css";
const CLASSES = ["Fighter", "Priest", "Thief", "Wizard"];
const ANCESTRIES = ["Human", "Elf", "Dwarf", "Halfling", "Goblin", "Half-Orc"];
export default function CampaignView() {
const { id } = useParams<{ id: string }>();
const campaignId = Number(id);
@ -43,12 +41,6 @@ export default function CampaignView() {
const [characters, setCharacters] = useState<Character[]>([]);
const [selectedId, setSelectedId] = useState<number | null>(null);
const [showCreate, setShowCreate] = useState(false);
const [newChar, setNewChar] = useState({
name: "",
class: "Fighter",
ancestry: "Human",
hp_max: 1,
});
const [campaignName, setCampaignName] = useState("");
const [rolls, setRolls] = useState<RollResult[]>([]);
const [freshIds, setFreshIds] = useState<Set<number>>(new Set());
@ -293,12 +285,9 @@ export default function CampaignView() {
};
}, []);
async function handleCreate(e: React.FormEvent) {
e.preventDefault();
if (!newChar.name.trim()) return;
async function handleCreate(data: CreateCharacterData) {
try {
await createCharacter(campaignId, newChar);
setNewChar({ name: "", class: "Fighter", ancestry: "Human", hp_max: 1 });
await createCharacter(campaignId, data);
setShowCreate(false);
} catch (err) {
console.error("Failed to create character:", err);
@ -450,73 +439,11 @@ export default function CampaignView() {
)}
{showCreate && (
<div
className={styles.createModal}
onClick={() => setShowCreate(false)}
>
<form
className={styles.createForm}
onClick={(e) => e.stopPropagation()}
onSubmit={handleCreate}
>
<div className={styles.createTitle}>New Character</div>
<div className={styles.formField}>
<label className={styles.formLabel}>Name</label>
<input
className={styles.formInput}
type="text"
value={newChar.name}
onChange={(e) =>
setNewChar({ ...newChar, name: e.target.value })
}
autoFocus
/>
</div>
<div className={styles.formField}>
<label className={styles.formLabel}>Class</label>
<SelectDropdown
value={newChar.class}
options={CLASSES}
onChange={(v) => setNewChar({ ...newChar, class: v })}
/>
</div>
<div className={styles.formField}>
<label className={styles.formLabel}>Ancestry</label>
<SelectDropdown
value={newChar.ancestry}
options={ANCESTRIES}
onChange={(v) => setNewChar({ ...newChar, ancestry: v })}
/>
</div>
<div className={styles.formField}>
<label className={styles.formLabel}>Max HP</label>
<input
className={styles.formInput}
type="number"
min={1}
value={newChar.hp_max}
onChange={(e) =>
setNewChar({ ...newChar, hp_max: Number(e.target.value) })
}
/>
</div>
<div className={styles.formActions}>
<button
type="button"
className={`${styles.formBtn} ${styles.formBtnSecondary}`}
onClick={() => setShowCreate(false)}
>
Cancel
</button>
<button
type="submit"
className={`${styles.formBtn} ${styles.formBtnPrimary}`}
>
Create
</button>
</div>
</form>
</div>
<CharacterWizard
campaignId={campaignId}
onSubmit={handleCreate}
onClose={() => setShowCreate(false)}
/>
)}
</div>
<RollLog campaignId={campaignId} rolls={rolls} freshIds={freshIds} onUndoRoll={handleUndoRoll} />