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:
parent
a6218c72d4
commit
2e1fff4c2d
2 changed files with 9 additions and 214 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue