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;
|
font-style: italic;
|
||||||
grid-column: 1 / -1;
|
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 ThreeFireOverlay from "../components/ThreeFireOverlay";
|
||||||
import type { AtmosphereState } from "../lib/atmosphereTypes";
|
import type { AtmosphereState } from "../lib/atmosphereTypes";
|
||||||
import { defaultAtmosphere } 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";
|
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() {
|
export default function CampaignView() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const campaignId = Number(id);
|
const campaignId = Number(id);
|
||||||
|
|
@ -43,12 +41,6 @@ export default function CampaignView() {
|
||||||
const [characters, setCharacters] = useState<Character[]>([]);
|
const [characters, setCharacters] = useState<Character[]>([]);
|
||||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||||
const [showCreate, setShowCreate] = useState(false);
|
const [showCreate, setShowCreate] = useState(false);
|
||||||
const [newChar, setNewChar] = useState({
|
|
||||||
name: "",
|
|
||||||
class: "Fighter",
|
|
||||||
ancestry: "Human",
|
|
||||||
hp_max: 1,
|
|
||||||
});
|
|
||||||
const [campaignName, setCampaignName] = useState("");
|
const [campaignName, setCampaignName] = useState("");
|
||||||
const [rolls, setRolls] = useState<RollResult[]>([]);
|
const [rolls, setRolls] = useState<RollResult[]>([]);
|
||||||
const [freshIds, setFreshIds] = useState<Set<number>>(new Set());
|
const [freshIds, setFreshIds] = useState<Set<number>>(new Set());
|
||||||
|
|
@ -293,12 +285,9 @@ export default function CampaignView() {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function handleCreate(e: React.FormEvent) {
|
async function handleCreate(data: CreateCharacterData) {
|
||||||
e.preventDefault();
|
|
||||||
if (!newChar.name.trim()) return;
|
|
||||||
try {
|
try {
|
||||||
await createCharacter(campaignId, newChar);
|
await createCharacter(campaignId, data);
|
||||||
setNewChar({ name: "", class: "Fighter", ancestry: "Human", hp_max: 1 });
|
|
||||||
setShowCreate(false);
|
setShowCreate(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to create character:", err);
|
console.error("Failed to create character:", err);
|
||||||
|
|
@ -450,73 +439,11 @@ export default function CampaignView() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showCreate && (
|
{showCreate && (
|
||||||
<div
|
<CharacterWizard
|
||||||
className={styles.createModal}
|
campaignId={campaignId}
|
||||||
onClick={() => setShowCreate(false)}
|
|
||||||
>
|
|
||||||
<form
|
|
||||||
className={styles.createForm}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
onSubmit={handleCreate}
|
onSubmit={handleCreate}
|
||||||
>
|
onClose={() => setShowCreate(false)}
|
||||||
<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>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<RollLog campaignId={campaignId} rolls={rolls} freshIds={freshIds} onUndoRoll={handleUndoRoll} />
|
<RollLog campaignId={campaignId} rolls={rolls} freshIds={freshIds} onUndoRoll={handleUndoRoll} />
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue