diff --git a/apps/web/src/builder/steps/identity.tsx b/apps/web/src/builder/steps/identity.tsx index b1f00bb..38c341a 100644 --- a/apps/web/src/builder/steps/identity.tsx +++ b/apps/web/src/builder/steps/identity.tsx @@ -1,9 +1,13 @@ -import { MonoLabel, Panel, TextInput } from "../../components/ui.tsx"; +import { createSignal, Show } from "solid-js"; +import { NarrativeFields } from "../../components/narrative-fields.tsx"; +import { Button, MonoLabel, Panel, TextInput } from "../../components/ui.tsx"; import type { BuilderStore } from "../store.ts"; export function IdentityStep(props: { store: BuilderStore }) { + const [showFile, setShowFile] = createSignal(false); + return ( - +

DECLARE IDENTITY

+ +
+
+

// PERSONNEL FILE

+ OPTIONAL — AUTHOR: PLAYER + +
+ + // epithet, appearance, traits, backstory — make the file yours (editable later on the + dossier) +

+ } + > +
+ props.store.setDraft("narrative", field, value)} + /> +
+
+
); } diff --git a/apps/web/src/builder/store.ts b/apps/web/src/builder/store.ts index 0aa163d..d8070c4 100644 --- a/apps/web/src/builder/store.ts +++ b/apps/web/src/builder/store.ts @@ -16,10 +16,13 @@ import { } from "@riftforge/rules"; import { createMemo, type Accessor } from "solid-js"; import { createStore, type SetStoreFunction } from "solid-js/store"; +import { emptyNarrativeForm, toNarrative, type NarrativeForm } from "../lib/narrative.ts"; /** Everything the player has chosen so far. */ export interface Draft { name: string; + /** Optional player-authored identity (raw form strings). */ + narrative: NarrativeForm; attributes?: Record; occId?: string; alignmentId?: string; @@ -52,6 +55,7 @@ export interface BuilderStore { export function createBuilderStore(): BuilderStore { const [draft, setDraft] = createStore({ name: "", + narrative: { ...emptyNarrativeForm }, psychicClass: "ordinary", occChoices: {}, related: [], @@ -94,6 +98,7 @@ export function createBuilderStore(): BuilderStore { const attributes = attributeTotals(); const skills = assembled(); if (!draft.occId || !attributes || !skills) return undefined; + const narrative = toNarrative(draft.narrative); return { name: draft.name.trim(), occId: draft.occId, @@ -104,6 +109,7 @@ export function createBuilderStore(): BuilderStore { psychicClass: draft.psychicClass, skills: skills.skills, spellIds: draft.spellIds, + ...(narrative !== undefined ? { narrative } : {}), }; }); diff --git a/apps/web/src/components/narrative-fields.tsx b/apps/web/src/components/narrative-fields.tsx new file mode 100644 index 0000000..6d24706 --- /dev/null +++ b/apps/web/src/components/narrative-fields.tsx @@ -0,0 +1,77 @@ +import { For } from "solid-js"; +import type { NarrativeForm } from "../lib/narrative.ts"; +import { MonoLabel, TextInput } from "./ui.tsx"; + +// maxLength mirrors the rules-layer schema caps so writes can't be rejected +// for length (appearanceSchema / narrativeSchema in @riftforge/rules). +const APPEARANCE_FIELDS = [ + ["height", "HEIGHT", 40], + ["weight", "WEIGHT", 40], + ["age", "AGE", 40], + ["eyes", "EYES", 80], + ["origin", "ORIGIN", 120], + ["disposition", "DISPOSITION", 120], +] as const satisfies readonly (readonly [keyof NarrativeForm, string, number])[]; + +/** 12 traits × 60 chars + separators — the schema's worst legal case. */ +const TRAITS_INPUT_MAX = 12 * 60 + 11 * 2; + +/** + * The player-authored identity fields — shared by the wizard's identity step + * and the dossier's edit panel. All optional: story, not mechanics. + */ +export function NarrativeFields(props: { + form: NarrativeForm; + onChange: (field: K, value: string) => void; +}) { + return ( +
+ +
+ + {([field, label, max]) => ( + + )} + +
+ +