Files
Dimension-47/.planning/phases/01-level-up-pf2e-regelkonform/01-UI-SPEC.md

775 lines
53 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 1
slug: level-up-pf2e-regelkonform
status: approved
shadcn_initialized: false
preset: none
created: 2026-04-27
revised: 2026-04-27
reviewed_at: 2026-04-27
---
# Phase 1 — UI Design Contract
> Visual and interaction contract for the Level-Up Wizard. Generated by gsd-ui-researcher,
> verified by gsd-ui-checker.
>
> Most decisions are inherited from CONTEXT.md, the project's existing design tokens
> (`client/src/index.css`), and the established modal/component patterns in
> `client/src/features/characters/components/`. Where CONTEXT.md locks a behavior,
> the relevant `D-XX` decision ID is referenced inline.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | none (hand-built primitives) |
| Preset | not applicable |
| Component library | hand-built shadcn-style primitives in `client/src/shared/components/ui/` (`Button`, `Input`, `Card`, `Spinner`, `ActionIcon`); reused for the wizard. |
| Icon library | `lucide-react` 0.562.0 (`ChevronLeft`, `ChevronRight`, `X`, `AlertTriangle`, `Sparkles`, `Swords`, `Star`, `BookOpen`, `Shield`, `Heart`, `Eye`, `Footprints`, `Wand2`, `Check`, `Lock`, `Info`, `RotateCcw`, `Minus`, `Plus`, `Trash2`, `ArrowLeft`) — never emojis (CLAUDE.md). |
| Font | Inter (sans), JetBrains Mono (mono) — declared in `--font-sans`, `--font-mono` (`client/src/index.css:66-67`). |
| Theme | Tailwind v4 `@theme` block in `client/src/index.css`. All tokens already exist; UI-SPEC adds **no new tokens**. |
| Animation lib | Framer Motion 12.26.2 is installed but unused today. Phase 1 introduces it for the wizard step transition (see "Motion" section). |
---
## Spacing Scale
Declared values (all multiples of 4, all already present in Tailwind v4 default scale used by the codebase):
| Token | Value | Usage in Phase 1 |
|-------|-------|-------------------|
| xs | 4px (`p-1`, `gap-1`) | Icon gap inside choice-card chips, badge padding |
| sm | 8px (`p-2`, `gap-2`) | Stepper-dot gap, footer button gap, badge stacks |
| md | 16px (`p-4`, `gap-4`) | Wizard body padding (mobile), step-section padding, choice-card inner padding |
| lg | 24px (`p-6`, `gap-6`) | Wizard body padding (`sm:` desktop), Review-step section padding |
| xl | 32px (`p-8`) | Reserved for review-step major section breaks on desktop |
| 2xl | 48px | Not used in Phase 1 |
| 3xl | 64px | Not used in Phase 1 |
Touch-target minimums (mobile-first, CLAUDE.md):
- All interactive buttons: 44px height minimum — `h-11` (`Button` `default`), `h-11 w-11` (`Button` `icon`).
- Choice-cards: minimum 64px tap height (so an entire card is a single touch target including its title row + meta row — exceeds 44px).
- Boost-step `+/-` controls: `h-11 w-11` (44×44px) — see Boost-Step contract.
- Stepper-dot buttons: visible dot is `h-2 w-2` / `h-2.5 w-2.5` but the wrapping `<button>` is `h-11 w-11` with `-m-1` to keep the visual gap at 812px while the hit-zone is a genuine 44×44 — see Stepper subsection for the honest math.
**Exceptions (micro-utilities for inline chip / badge / icon-stack patterns only — NOT promoted to the layout grid):**
- Chip / badge padding: `px-1.5 py-0.5` (6×2px) — used inside source-tag badges, source-cap chips, level chips, delta chips. These are inline glyph-sized elements, never standalone surfaces.
- Inline icon offset: `mt-0.5` (2px) — used to optically align an icon's baseline with its sibling text on chip/banner heads (e.g. `RotateCcw` in DRAFT-resume banner, `AlertTriangle` in violations banner).
- Footer button gap: `gap-3` (12px) — used in the dialog footer row only when two buttons sit at the right edge with a clear visual separation; layout-grid gaps remain on the 4/8/16/24 ladder.
These micro-values stay as Tailwind utilities and never appear on layout-level surfaces (modals, cards, sections, body padding).
---
## Typography
The codebase has no custom font-size tokens — sizes come from Tailwind defaults. Phase 1 declares **4 sizes** (12 / 14 / 18 / 24) and **2 weights** to satisfy the 3-4-size, 2-weight contract.
| Role | Size | Tailwind class | Weight | Line Height | Usage |
|------|------|----------------|--------|-------------|-------|
| Auxiliary / Chrome | 12px | `text-xs` | 400 (`font-normal`); 600 (`font-semibold`) inside chips | 1.4 | Stepper labels (current/completed), meta-row chips on choice-cards (level, source, rarity, cap-erreicht, action-cost), banner sub-lines (`Zuletzt bearbeitet …`), card sub-lines (English-name under German-name), positive/negative delta chips in Review, footer help text in Boost-step. Two weights coexist within this size: chip *labels* (e.g. badge text) are 600; banner sub-lines and English-name fallbacks are 400. |
| Body | 14px | `text-sm` | 400 (`font-normal`) | 1.5 (`leading-normal`) | Choice-card descriptions, prereq text, banner body, dialog body, tooltip body, Spellcaster info-strip body, Skill-row text. |
| Label | 14px | `text-sm` | 600 (`font-semibold`) | 1.4 | Choice-card titles, step-section labels, button labels, banner headings, dialog headings (when ≤ `text-base`). |
| Heading | 18px | `text-lg` | 600 (`font-semibold`) | 1.3 | Wizard header title (`Stufenaufstieg — Stufe X`), step-screen heading (`Boost setzen`, `Klassentalent wählen`, …), modal H2. |
| Display | 24px | `text-2xl` | 600 (`font-semibold`) | 1.2 | Review-step "before/after" stat numbers (HP-Max, AC, DC, Wahrnehmung, Saves) in `font-mono`. |
Total: **4 sizes** (12 / 14 / 18 / 24) — within the 3-4-size limit. **2 weights**: 400 (normal) and 600 (semibold). No `font-medium` (500), no `font-bold` (700).
This matches the existing `add-condition-modal.tsx`, `rest-modal.tsx`, `add-feat-modal.tsx` patterns (`text-lg font-semibold` for headers, `text-sm` for body, `text-xs` for chips).
Mono font (JetBrains Mono) is reserved for stat numbers and dice notation in the Review step (`font-mono`).
---
## Color
The 60/30/10 split maps directly to existing tokens in `client/src/index.css`:
| Role | Value | Token | Usage |
|------|-------|-------|-------|
| Dominant (60%) | `#0f0f12` | `bg-bg-primary` | Page background behind the wizard backdrop |
| Secondary (30%) | `#1a1a1f` / `#242429` | `bg-bg-secondary` (modal panel), `bg-bg-tertiary` (choice-cards, info-strips, stepper rail) | Wizard surface, choice-card surfaces, step-section containers, stepper inactive state |
| Accent (10%) | `#c26dbc` | `primary-500` (`text-primary-500`, `bg-primary-500`, `border-primary-500`) | **EXACTLY** these elements only (see reserved-for list below) |
| Destructive | `#ef4444` | `error-500` (`text-error-500`, `bg-error-500`) | "Verwerfen" (DRAFT discard) confirm-dialog button only — **not** generic cancel surfaces. |
Accent reserved-for list (the only places `primary-500` may be applied in this phase):
1. **Primary CTA buttons in the wizard footer**: `Weiter`, `Bestätigen`, `Zurück zur Übersicht` (after `Ändern` in Review) — `Button variant="default"` (which is `bg-primary-500`).
2. **Active stepper dot**: the dot for the current step uses `bg-primary-500`; completed dots use `bg-primary-500/40`; future dots use `bg-bg-tertiary`.
3. **Selected choice-card border + checkmark**: the chosen card (boost target, talent, skill, doctrine, …) gets `border-primary-500 ring-1 ring-primary-500/40`, and a `Check` icon in `text-primary-500` appears in the top-right corner.
4. **DRAFT-resume banner accent stripe and the "Fortsetzen" button**: the resume banner has a left-border `border-l-4 border-primary-500`, body text `text-text-primary`, and the "Fortsetzen" CTA is `Button variant="default"`.
5. **"Stufe steigen"-Button on the character-sheet header** (when enabled — i.e. character is below cap and no foreign DRAFT exists): `Button variant="default"` with `Sparkles` icon.
6. **`Ändern` link in Review-step Section A**: `text-xs text-primary-500 hover:underline` — the only inline-link use of accent in the wizard.
`primary-500` is **never** used for: choice-card backgrounds (those stay `bg-bg-tertiary`), step-headings (those use `text-text-primary`), tab/section dividers (`border-border`), tooltip backgrounds, banner backgrounds.
Semantic color usage (already established by existing modals — `rest-modal.tsx` is the canonical reference):
| Semantic | Token | Reserved For |
|----------|-------|--------------|
| Warning (`#f59e0b`, `warning-500` / `yellow-400`) | `text-warning-500` `bg-warning-500/10` `border-warning-500/20` | (a) `AlertTriangle` icon at the top-right of any **choice-card whose prereq is non-evaluable** (D-03). (b) Pathbuilder-import-violation banner background (D-06). (c) "Cap erreicht" indicator on a skill that cannot be increased further. (d) "+1 (Cap bei 18)" chip in Boost-step. |
| Error (`#ef4444`, `error-500` / `red-400`) | `text-error-500` `bg-error-500/10` `border-error-500/20` | (a) "Verwerfen" button in the DRAFT-resume banner and the destructive-confirm dialog. (b) Inline form-validation errors (e.g. boost target invalid). |
| Success (`#22c55e`, `success-500` / `green-400`) | `text-success-500` | Checkmarks in completed stepper dots; "+X HP", "+1 Save" deltas in the Review step (positive change). |
| Info (`#3b82f6`, `info-500` / `blue-400`) | `text-info-500` `bg-info-500/10` | Read-only info strips in the wizard (e.g. "Free Archetype: aktiv", "Spellcaster-Slot +1 (automatisch)"). |
### Inherited Exceptions — Source-tag chroma palette
The 6 raw-hue source badges below are an **inherited convention** from `feat-detail-modal.tsx:22-29` (existing `featSourceColors` map) and are intentionally exempt from the single-accent rule. They are used **only for talent-source identification badges** in the meta row of choice-cards — never for layout, surfaces, banner backgrounds, CTAs, or interactive accents. The selection accent (`primary-500`) remains the only interactive accent across the wizard.
| Source | Token classes |
|--------|---------------|
| Klassentalent | `bg-red-500/20 text-red-400` |
| Abstammungstalent | `bg-blue-500/20 text-blue-400` |
| Allgemeines Talent | `bg-yellow-500/20 text-yellow-400` |
| Fertigkeitstalent | `bg-green-500/20 text-green-400` |
| Archetypentalent | `bg-purple-500/20 text-purple-400` |
| Bonustalent | `bg-cyan-500/20 text-cyan-400` |
---
## Copywriting Contract
All copy is **German** (CLAUDE.md / PROJECT.md). German imperative tone, du-Form, no emojis, no exclamation marks except in genuine warnings.
### Primary CTAs
| Element | Copy |
|---------|------|
| Character-sheet button to start the wizard | `Stufe steigen` (icon: `Sparkles`) |
| Wizard footer — go to next step | `Weiter` (icon: `ChevronRight` on right) |
| Wizard footer — go to previous step | `Zurück` (icon: `ChevronLeft` on left, `Button variant="outline"`) |
| Wizard footer — final commit on Review step | `Bestätigen` (replaces `Weiter`; icon: `Check` on right) |
| Wizard footer — return-to-Review after `Ändern` revision | `Zurück zur Übersicht` (icon: `ArrowLeft` on left, `Button variant="default"`, replaces `Weiter` while user is revising a step entered via `Ändern` from Review — see Review-step contract for re-validation behavior) |
| Choice-card click result label | `Wählen` (only used inside the prereq-confirm dialog as "Trotzdem wählen") |
| Stepper progress label (always visible — mobile and desktop) | `Schritt {n} von {m} — {currentStepLabel}` (e.g. `Schritt 3 von 7 — Klassentalent`) |
| Confirm-dialog secondary action (cancel within a destructive-confirm dialog only) | `Abbrechen` (`Button variant="ghost"`) — used **only inside** the destructive/prereq confirm dialogs. The wizard footer itself has no `Abbrechen` button; mid-flow exit happens via the header `X` close button or backdrop click. |
### Wizard-step screen headings (one per step, `text-lg font-semibold`)
| Step | Heading | Sub-line (`text-sm text-text-secondary`) |
|------|---------|--------------------------------------------|
| 0 — Klassenmerkmale (auto) | `Klassenmerkmale auf Stufe {N}` | `Diese Merkmale werden automatisch übernommen.` |
| 0a — Klassenmerkmal-Wahl (`choiceType`) | `{Merkmalname} wählen` (z.B. `Lehre wählen`, `Schule wählen`, `Waffenmeisterschaft wählen`) | `Wähle eine Option — diese Wahl ist endgültig nach Bestätigung.` |
| 1 — Boost-Set | `Attributs-Boosts setzen` | `Wähle vier verschiedene Attribute. Werte über 18 erhalten +1, sonst +2.` |
| 2 — Skill-Increase | `Fertigkeits-Erhöhung` | `Wähle eine Fertigkeit, um sie um eine Stufe zu erhöhen.` |
| 3 — Klassentalent | `Klassentalent wählen` | `Talente, deren Voraussetzungen du erfüllst.` |
| 4 — Fertigkeitstalent | `Fertigkeitstalent wählen` | `Talente, deren Voraussetzungen du erfüllst.` |
| 5 — Allgemein-Talent | `Allgemeines Talent wählen` | `Allgemein- oder Fertigkeitstalente sind erlaubt.` |
| 6 — Ancestry-Talent | `Abstammungstalent wählen` | `Talente deiner Abstammung und deines Erbes.` |
| 7 — Free-Archetype-Slot | `Archetypentalent (Free Archetype)` | (vor Dedication) `Wähle eine Dedication.` / (nach Dedication) `Talente jedes Archetyps sind erlaubt (Pathbuilder-Verhalten).` |
| 8 — Spellcaster | `Zauberprogression` | (spontan) `Repertoire um {N} Zauber erweitern.` / (vorbereitet) `Slot-Erhöhung wird automatisch angewendet.` |
| 9 — Review | `Übersicht & Bestätigen` | `Vergleiche Vorher/Nachher. Erst nach „Bestätigen" wird der Charakter geändert.` |
### Empty / Loading / Error states
| State | Copy |
|-------|------|
| Loading talent list (Spinner + label) | `Talente werden geladen …` |
| Loading any list (boost options, skills, formulas) | `Wird geladen …` |
| Empty: no talents meet prerequisites at this slot | Heading: `Keine erfüllbaren Talente gefunden` — Body: `Alle Talente dieser Kategorie haben Voraussetzungen, die du derzeit nicht erfüllst. Du kannst trotzdem ein Talent mit Warnung wählen — aktiviere "Auch nicht erfüllbare anzeigen".` (toggle button text: `Auch nicht erfüllbare anzeigen` / `Nur erfüllbare anzeigen`). |
| Empty: no skills can be increased (cap at all) | Heading: `Keine Fertigkeit erhöhbar` — Body: `Alle Fertigkeiten haben das für deine Stufe erlaubte Maximum erreicht. Diesen Schritt überspringen?` (button: `Schritt überspringen`). |
| Empty: no spells available for repertoire (impossible at L2+, but defensive) | `Keine Zauber verfügbar.` |
| Error: API failure during step (e.g. talent search) | Heading: `Talente konnten nicht geladen werden` — Body: `Versuche es erneut oder wähle ein anderes Filter.` — Action: `Erneut versuchen` (`Button variant="outline"`). |
| Error: commit transaction failed (rollback) | Heading: `Stufenaufstieg konnte nicht gespeichert werden` — Body: `Deine Wahlen sind sicher als Entwurf gespeichert. Bitte versuche es erneut. Wenn der Fehler bleibt, lade die Seite neu.` — Action: `Erneut versuchen` (`Button variant="default"`) + `Schließen` (`Button variant="ghost"`). DRAFT bleibt erhalten. |
| Error: prereq evaluator returns "non-evaluable" | (Inline am Talent — kein eigener State; siehe Prereq-Confirm-Dialog) |
| Review revalidation after `Ändern` cleared a later choice | Inline note in the affected Review row: `Diese Wahl wurde durch eine frühere Änderung zurückgesetzt — bitte erneut treffen.` (`text-xs text-warning-500`) — the row's `Ändern` link is replaced by `Wählen` (`text-xs text-primary-500`) which jumps directly to that step. |
### Destructive confirmations
| Action | Trigger | Confirm-Dialog |
|--------|---------|----------------|
| **DRAFT verwerfen** | Click `Verwerfen` in DRAFT-resume banner (D-14) | Heading: `Entwurf verwerfen?` — Body: `Deine bisherigen Wahlen für Stufe {N} werden gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.` — Buttons: `Abbrechen` (ghost) + `Verwerfen` (`destructive`, `Trash2` icon). |
| **Wizard mid-flow abbrechen** mit ungesicherten Änderungen | Click X (close) in header or backdrop click after at least one choice was made | Heading: `Wizard abbrechen?` — Body: `Deine bisherigen Wahlen werden als Entwurf gespeichert und du kannst später fortsetzen.` — Buttons: `Weiter bearbeiten` (default) + `Als Entwurf speichern und schließen` (outline). **Note:** discard is NOT offered here — only commit-or-save. Discard happens only via the resume-banner "Verwerfen". |
| **Talent mit nicht erfüllter Voraussetzung wählen** (D-03) | Click `Wählen` on a choice-card that carries the yellow `AlertTriangle` (non-evaluable prereq) | Heading: `Voraussetzung nicht prüfbar` — Body: `Voraussetzung: „{prereqText}". Dimension47 kann diese Bedingung nicht automatisch prüfen. Erfüllst du sie?` — Buttons: `Abbrechen` (ghost) + `Trotzdem wählen` (`Button variant="default"`, no destructive coloring — this is informed user choice, not a destructive op). |
| **Bestätigen (Final Commit)** in Review step | Click `Bestätigen` | No additional dialog — the Review step IS the confirmation surface. The button is `Button variant="default"` with `isLoading` spinner during commit. After success: wizard closes with a 2-second toast `Stufenaufstieg bestätigt — Stufe {N}.` (info-tone, `bg-success-500/10`). |
### Banner copy (D-06: Pathbuilder-Import-Violations)
Position: directly below the character-sheet header, above the tab navigation. Background `bg-warning-500/10`, border `border-l-4 border-warning-500`, padding `p-4`.
- Heading (`text-sm font-semibold text-warning-500`): `{N} Talente mit nicht erfüllter Voraussetzung`
- Body (`text-sm text-text-secondary`): `Beim Import wurde festgestellt, dass folgende Talente Voraussetzungen haben, die der Charakter nicht erfüllt. Liste prüfen und ggf. nachträglich anpassen.`
- Action: `Liste anzeigen` (`Button variant="ghost"` size sm, expand-collapse — when expanded, shows a `<ul>` of `{TalentName} — Voraussetzung: {prereqText}` items).
- Dismiss: not dismissible (the banner stays until the violations are resolved by GM/Player editing — Phase 1 ships read-only listing only; resolution UI is out of scope, deferred to v2 retrain).
### Banner copy (D-14: DRAFT-Resume-Banner)
Position: at the top of the character-sheet page (above the avatar header) AND inside the wizard if user clicks "Stufe steigen" while a DRAFT exists. Background `bg-bg-tertiary`, border `border-l-4 border-primary-500`, padding `p-4`.
- Heading (`text-sm font-semibold text-text-primary`): `Du hast eine offene Stufenaufstiegs-Session — Stufe {N}.`
- Body (`text-sm text-text-secondary`): `Zuletzt bearbeitet: {relativeDate}.` (z.B. `vor 3 Tagen`).
- Actions: `Fortsetzen` (`Button variant="default"`, icon `RotateCcw`) + `Verwerfen` (`Button variant="ghost"`, `text-error-500`, icon `Trash2`).
### "Stufe steigen"-Button on character-sheet header
Position: in the right-side header button cluster of `character-sheet-page.tsx` (lines 1607-1626 — alongside Download, Edit, Delete). Order: **first** in the cluster (left of Download).
| Character state | Button rendering |
|-----------------|------------------|
| Eligible, no DRAFT | `Button variant="default"` size `sm` — label `Stufe steigen`, icon `Sparkles`, fully enabled. |
| Eligible, DRAFT exists | `Button variant="default"` size `sm` — label `Stufe fortsetzen`, icon `RotateCcw`, fully enabled. (Consistent with D-14 banner; both routes open the same wizard, the banner is just an upper-page reminder.) |
| Character at level cap (level 20) | Hidden entirely. (No disabled state to avoid confusion — the button just isn't there.) |
| User is neither owner nor GM | Hidden entirely. |
---
## Component Contract — Wizard Chrome
### Container
The wizard reuses the project's modal pattern (canonical: `add-feat-modal.tsx:246-260`):
```
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
<div className="absolute inset-0 bg-black/60" onClick={handleBackdropClick} />
<div className="relative w-full sm:max-w-2xl max-h-[90vh] bg-bg-secondary
rounded-t-2xl sm:rounded-2xl flex flex-col overflow-hidden">
{/* Header */}
{/* Stepper */}
{/* Body (current step) */}
{/* Footer */}
</div>
</div>
```
- Mobile: full-width bottom-sheet (`items-end`, `rounded-t-2xl`).
- Desktop (`sm:` ≥ 640px): centered modal, max-width `2xl` (672px), `rounded-2xl`.
- Backdrop click: opens the "Wizard mid-flow abbrechen" confirm if any choice has been made; closes silently if no choice yet.
### Header (always visible)
- Container: `flex items-center justify-between p-4 border-b border-border`.
- Left: `<h2 className="text-lg font-semibold text-text-primary">Stufenaufstieg — Stufe {N}</h2>` + sub-line `<p className="text-xs text-text-secondary">{characterName}</p>`.
- Right: close button `<Button variant="ghost" size="icon" aria-label="Schließen"><X className="h-5 w-5" /></Button>` triggering the same backdrop-confirm logic as the backdrop click. **This is the only cancel surface in the wizard chrome** (the footer carries no Abbrechen button).
### Stepper (always visible, between Header and Body)
Mobile-first compact dot-stepper with **always-visible progress label**:
- Outer container: `<nav role="navigation" aria-label="Wizard-Schritte" className="px-4 py-3 border-b border-border">`.
- **Progress label (always visible, mobile and desktop)**: rendered as the first child of the stepper, above the dot row:
```
<div className="text-xs text-text-secondary pb-2">
Schritt {currentIdx+1} von {totalSteps} — {currentStepLabel}
</div>
```
This guarantees that on mobile (where dot-row labels are hidden) the user always knows which step they are on. With up to 11 conditional steps this label is essential, not optional.
- Dot row: `<ol className="flex items-center gap-2 overflow-x-auto">`.
- Each dot is wrapped in a real button so the touch target is genuine 44×44:
```
<button
type="button"
onClick={() => goToStep(idx)}
disabled={idx > currentIdx}
aria-label={`${stepLabel}${idx === currentIdx ? ', aktuell' : idx < currentIdx ? ', abgeschlossen' : ', noch nicht verfügbar'}`}
aria-current={idx === currentIdx ? 'step' : undefined}
className="h-11 w-11 -m-1 flex items-center justify-center disabled:cursor-not-allowed"
>
{/* visible dot */}
<span className={cn('rounded-full transition-colors duration-150', dotState)} />
</button>
```
- The `-m-1` negative margin on the wrapper compensates for the 44px hit-zone so the perceived gap between dots stays at 812px while the tappable area is 44×44 — honest math, not a 6px-padding-claims-44px trick.
- Visible-dot states (rendered inside the wrapping button):
- **Future**: `h-2 w-2 rounded-full bg-bg-tertiary`.
- **Current**: `h-2.5 w-2.5 rounded-full bg-primary-500`.
- **Completed**: `h-2 w-2 rounded-full bg-primary-500/40`.
- Step labels next to dots (visible **`sm:` and up only** — desktop bonus): each completed/current dot shows its step label `text-xs font-semibold text-text-primary` to its right; completed dots additionally render a `Check` icon at `h-3 w-3 text-success-500` overlaid on the dot. Mobile users rely on the always-visible progress label above the row instead.
- Step-label vocabulary (one of, depending on which steps apply this level): `Merkmale`, `Wahl`, `Boost`, `Skill`, `Klasse`, `Fertigkeit`, `Allgemein`, `Abstammung`, `Archetyp`, `Zauber`, `Übersicht`. Conditional steps not shown.
- Connector between dots: `h-px w-4 bg-bg-tertiary` (`bg-primary-500/40` between completed pairs). The connector sits inside its own non-interactive `<span>` between the dot buttons.
- Future steps: dot button is `disabled` — no jumping forward to incomplete future steps. Completed and current steps are interactive (back-navigation only).
### Body (per-step content area)
- Container: `flex-1 overflow-y-auto p-4 sm:p-6`.
- Top of body shows the step heading + sub-line (see Copywriting table above).
- Below: the step's content (Choice-Cards, Boost-Picker, Skill-Picker, Review-Diff, …).
- Mobile: one wahlpunkt per scroll-screen — choice-cards stack vertically, full-width.
- Desktop: choice-cards may form a 2-column grid (`grid grid-cols-1 sm:grid-cols-2 gap-3`) for talent picks; boost picker stays single-column for clarity.
### Footer (always visible)
- Container: `flex items-center justify-between gap-3 p-4 border-t border-border bg-bg-secondary`.
- **Left side** (contextual hint, optional): a `<span className="text-xs text-text-secondary">` may render `Schritt {n}/{m}` as a redundant footer hint, or be empty. The footer carries **no Abbrechen button** — mid-flow exit happens exclusively via the header `X` or backdrop click.
- **Right cluster** (`flex items-center gap-2`):
- `<Button variant="outline" onClick={prev} disabled={isFirstStep}><ChevronLeft className="h-4 w-4" /> Zurück</Button>`
- On non-final steps in the normal forward flow: `<Button variant="default" onClick={next} disabled={!stepValid}>Weiter <ChevronRight className="h-4 w-4" /></Button>`
- On the Review step: `<Button variant="default" onClick={commit} isLoading={isCommitting}><Check className="h-4 w-4" /> Bestätigen</Button>`
- When the user is on a step they re-entered via Review's `Ändern` link, the primary right button changes to `<Button variant="default" onClick={returnToReview}><ArrowLeft className="h-4 w-4" /> Zurück zur Übersicht</Button>` (replaces `Weiter` until the user returns to Review). See Review-step contract for the re-validation contract.
- Footer is sticky at the bottom of the modal (the body scrolls, the footer does not).
---
## Component Contract — Choice-Card
The choice-card is the primary interactive element across most steps (talent picks, boost-target picks, skill-increase picks, doctrine/school picks). Single canonical layout, used everywhere.
### Layout (per card)
Container:
```
<button
onClick={() => onSelect(option)}
disabled={isLocked}
className={cn(
'w-full text-left p-4 rounded-xl border transition-colors',
'min-h-[64px]', // touch-target floor
!isSelected && !isLocked && 'bg-bg-tertiary border-border hover:border-border-hover',
isSelected && 'bg-bg-tertiary border-primary-500 ring-1 ring-primary-500/40',
isLocked && 'bg-bg-tertiary/60 border-border opacity-60 cursor-not-allowed',
)}
>
```
Inner structure (top row → meta row → optional description):
1. **Top row** (`flex items-start justify-between gap-2`):
- Left: `<h4 className="text-sm font-semibold text-text-primary">{germanName ?? englishName}</h4>` + sub-line `text-xs text-text-muted` showing the English name if a German name exists.
- Right (icons stack): in this order, only if applicable:
- `Lock` icon (`h-4 w-4 text-text-muted`) if locked (already chosen / cap reached / not eligible at this slot).
- `AlertTriangle` icon (`h-4 w-4 text-warning-500`) if prereq is **non-evaluable** (D-03) — wrapped in a tooltip showing the raw prereq string.
- `Check` icon (`h-4 w-4 text-primary-500`) if currently selected.
2. **Meta row** (`mt-2 flex flex-wrap gap-2 items-center text-xs`):
- Source badge (existing color map — Klassen-/Abstammungs-/etc., see Color section).
- Level chip: `text-text-secondary` `Stufe {n}+`.
- Action-cost badge if applicable (uses existing `ActionIcon` component from `client/src/shared/components/ui/action-icon.tsx`).
- Rarity chip if not `Common` (existing convention from `add-feat-modal.tsx:294-302`).
- **Cap-erreicht** chip on Skill-Increase cards: `bg-warning-500/10 text-warning-500 px-1.5 py-0.5 rounded` text `Cap erreicht`.
3. **Description (truncated)**: `mt-2 text-sm text-text-secondary line-clamp-2`. A `Mehr` link beneath the truncation opens the existing `FeatDetailModal` (or a similar detail modal for non-talent options) **as a layered modal** at `z-60`.
### Locked / unavailable states
- **Already chosen this Level-Up** (e.g. cannot pick the same skill twice): `disabled` + `Lock` icon + opacity 60%. No click effect.
- **Prereq fails (evaluable)**: hidden by default. A toggle in the step body (`Auch nicht erfüllbare anzeigen`) shows them grayed (`opacity-60`) with a `bg-error-500/10 text-error-500 text-xs` chip `Voraussetzung nicht erfüllt: {parsed reason}`. Click still enabled but routes through the prereq-confirm dialog.
- **Prereq non-evaluable (D-03)**: shown by default (it's only a warning, not a block). Yellow `AlertTriangle` icon. Click triggers the prereq-confirm dialog with the raw `prerequisites` string.
- **Cap reached** (skill-increase): `disabled` + warning chip `Cap erreicht` + tooltip `Diese Fertigkeit ist auf deiner Stufe nicht weiter erhöhbar.`
### Selected state
- Border `border-primary-500` + halo `ring-1 ring-primary-500/40`.
- Top-right `Check` icon `text-primary-500`.
- Background remains `bg-bg-tertiary` — the accent is **only** the border + checkmark, not the surface fill (60/30/10 discipline).
---
## Component Contract — Boost-Set Step
Special layout: 6 attribute rows (STR, DEX, CON, INT, WIS, CHA) with a count of how many of the 4 boosts are spent on each. Mobile: vertical stack of 6 attribute rows. Desktop: 2-column grid.
**The row container is a `<div>`, not a `<button>`.** Tapping the row body itself has no behavior. Only the inner `+/-` controls are interactive — they are real `<button>` elements with proper `aria-label`s. This avoids the invalid HTML pattern of nesting `<button>` inside `<button>` and keeps a11y/event propagation clean. The row's hover state (`hover:border-border-hover`) is purely a decorative styling cue, not a clickable affordance.
Per attribute row:
```tsx
<li className="w-full p-4 rounded-xl border border-border bg-bg-tertiary
flex items-center justify-between min-h-[64px]
hover:border-border-hover">
<div className="flex-1 min-w-0">
<h4 className="text-sm font-semibold text-text-primary">
{abbreviation} — {germanName}
</h4>
<p className="text-xs text-text-secondary">
Aktuell {currentScore}{capReached ? ' (Cap)' : ''} → wird {newScore}
</p>
{currentScore >= 18 && count >= 1 && (
<span className="inline-block mt-1 text-xs text-warning-500
bg-warning-500/10 px-1.5 py-0.5 rounded">
+1 (Cap bei 18)
</span>
)}
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<button
type="button"
onClick={dec}
disabled={count === 0}
aria-label={`${abbreviation} Boost verringern`}
className="h-11 w-11 rounded-lg bg-bg-elevated flex items-center justify-center
disabled:opacity-40 disabled:cursor-not-allowed
hover:bg-bg-elevated/80 transition-colors"
>
<Minus className="h-5 w-5" />
</button>
<span className="text-lg font-semibold text-text-primary tabular-nums w-8 text-center">
{count}
</span>
<button
type="button"
onClick={inc}
disabled={!canIncrement}
aria-label={`${abbreviation} Boost erhöhen`}
className="h-11 w-11 rounded-lg bg-bg-elevated flex items-center justify-center
disabled:opacity-40 disabled:cursor-not-allowed
hover:bg-bg-elevated/80 transition-colors"
>
<Plus className="h-5 w-5" />
</button>
</div>
</li>
```
- The `+/-` controls are `h-11 w-11` (44×44px) — meeting the 44px touch-target floor declared in the Spacing Scale. Lucide `Minus` / `Plus` icons render at `h-5 w-5` to keep visual weight balanced inside the larger button.
- **Responsive note for narrow viewports (`<360px`)**: the row layout falls back from `flex-row` to `flex-col` so the `+/-` cluster sits below the attribute label rather than to its right. Implementation: wrap the row in a container with `flex-col @[360px]:flex-row @[360px]:items-center @[360px]:justify-between` (Tailwind container queries) — or, if container queries are unavailable, accept the cramped layout and ensure the row has `flex-wrap gap-3` so the cluster wraps under the label rather than overlapping it.
- "wird {newScore}" reflects the +2 / +1 cap-bei-18 rule live (not the full character recompute — that's review-only per D-12, but this single-attribute preview stays correct because the math is local).
- **Cap visualisation**: when `currentScore >= 18` AND `count >= 1`, the inline chip `+1 (Cap bei 18)` renders below the score line in `text-warning-500`. Otherwise the row implies +2.
- **Footer-helper** below the 6 rows: `<p className="text-xs text-text-secondary mt-4">{boostsSpent} von 4 Boosts gewählt. Du musst genau 4 verschiedene Attribute boosten.</p>` — when `boostsSpent === 4`, the `Weiter` button enables.
- A boost cannot be doubled-up: max `count` per attribute is 1 (PF2e rule on level-up boosts). The +/- controls enforce this.
---
## Component Contract — Skill-Increase Step
A scrollable list of 16+ skills (or all the character's trained-or-higher skills, plus untrained ones to allow new training).
Per skill row (compact):
```
<button className="w-full p-3 rounded-lg border border-border bg-bg-tertiary
flex items-center justify-between min-h-[56px]
hover:border-border-hover disabled:opacity-60">
<div className="flex items-center gap-3">
<span className="text-sm font-semibold text-text-primary">{germanSkillName}</span>
<span className="text-xs text-text-secondary">{abilityAbbreviation}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-text-secondary">{germanCurrentProf}</span>
<ChevronRight className="h-3 w-3 text-text-muted" />
<span className={cn('text-xs font-semibold', proficiencyColors[newProf])}>{germanNewProf}</span>
</div>
</button>
```
- Reuses the existing proficiency color map (`feat-detail-modal.tsx` → `PROFICIENCY_COLORS` in `character-sheet-page.tsx`): `TRAINED text-blue-500`, `EXPERT text-purple-500`, `MASTER text-orange-500`, `LEGENDARY text-red-500`.
- **Cap-erreicht skills** (e.g. trying to push to MASTER before L7) are rendered with `disabled` + warning chip `Cap erreicht`.
- **Already trained skills (UNTRAINED case is rare, only for first-time training)** are shown normally.
- Mobile: one column. Desktop: still one column for clarity (16 rows × 56px ≈ 900px which fits in the modal scroll body).
---
## Component Contract — Free-Archetype-Slot Step (D-08)
Top of the step body shows the read-only toggle status:
- If FA is **enabled** for this character: info strip `bg-info-500/10 border border-info-500/20 text-info-500 p-3 rounded-lg flex items-center gap-2` with `<Sparkles className="h-4 w-4" />` icon and copy `Free Archetype: aktiv` + sub-line `text-xs text-text-secondary` `Du erhältst zusätzlich einen Archetypen-Talent-Slot.`
- If FA is **disabled**: the entire step is skipped (not rendered in the stepper).
- Below the strip: choice-cards filtered by archetype rules:
- Vor erster Dedication: cards filtered to `featType === 'Archetype'` AND trait includes `Dedication`.
- Nach erster Dedication (D-07 — Pathbuilder behavior): all `featType === 'Archetype'` cards (any archetype).
- A small text below the filter-toggle row reads either `Wähle eine Dedication.` or `Talente jedes Archetyps sind erlaubt (Pathbuilder-Verhalten).` (matches sub-line copy table above).
---
## Component Contract — Spellcaster Step
Top of the step body shows a read-only info strip describing what's automatic:
- Vorbereiteter Caster: `<info strip> Slot-Erhöhung: +{N} Slot auf Grad {M} (automatisch)` — and the body has **no choice-cards** beyond this strip; the user can press `Weiter` immediately.
- Spontaner Caster (Bard, Sorcerer, Oracle, Witch with Patron-Variant, Summoner spontaneously cast — D-18): info strip + a **repertoire-pick** sub-section. The repertoire-pick is a list of choice-cards (one per spell), filtered by tradition and by the user's known spell-levels. Cap message: `Wähle {N} Zauber für dein Repertoire.` `{spellsPicked} von {N} gewählt.` `Weiter` enables when `spellsPicked === N`.
Spell choice-cards reuse the canonical Choice-Card layout, with these adaptations:
- Top row title: `{germanSpellName}` + sub-line englishName.
- Meta row: tradition chip (`bg-primary-500/10 text-primary-400 px-1.5 py-0.5 rounded text-xs`), spell-level chip `Grad {n}`, traits.
---
## Component Contract — Choice-Klassenmerkmal Sub-Step (D-19)
Triggered when the `ClassProgression` row for `(class, targetLevel)` carries `choiceType` (e.g. Cleric Doctrine, Wizard School, Fighter Weapon Mastery, Champion Cause, Sorcerer Bloodline if not at L1, …).
Layout: a vertical stack of mutually-exclusive choice-cards (canonical Choice-Card layout), single-column on mobile and desktop. Selecting one card auto-deselects the previous (radio-group semantics, but using the same visual Choice-Card).
- Header sub-line is dynamic based on the choice key (see Copywriting table — `Lehre wählen`, `Schule wählen`, …).
- The `choiceOptionsRef` from `ClassProgression` resolves to a list of options. Each option's description is rendered in the truncated description area; `Mehr` opens a layered detail-modal at `z-60`.
- After picking, the meta row shows the badge `Klassenmerkmal-Wahl` (`bg-secondary-500/20 text-secondary-300 text-xs px-1.5 py-0.5 rounded`).
---
## Component Contract — Review Step (D-12)
Two-section layout, mobile-first vertically stacked, desktop two-column where helpful.
### Section A — Wahlen-Zusammenfassung
A `Card`-wrapped list of every choice the user made, grouped by step. Each row:
```
<div className="flex items-start justify-between gap-3 py-2 border-b border-border last:border-0">
<div className="flex items-center gap-2">
{stepIcon} {/* e.g. Star for talent, Sparkles for skill, Wand2 for repertoire */}
<span className="text-xs text-text-secondary">{stepLabel}</span>
<span className="text-sm font-semibold text-text-primary">{choiceLabel}</span>
</div>
<button
onClick={() => goToStep(step, { revisionMode: true })}
className="text-xs text-primary-500 hover:underline"
>
Ändern
</button>
</div>
```
#### `Ändern` revision contract
When the user clicks `Ändern` on a Review row:
1. Wizard navigates to that step (back-navigation).
2. The wizard footer's primary right button changes from `Weiter` to `<Button variant="default"><ArrowLeft className="h-4 w-4" /> Zurück zur Übersicht</Button>` (icon `ArrowLeft`). `Zurück` (left button) still works as normal back-nav, as does direct stepper-dot tapping.
3. The user revises their choice on that step.
4. When the user clicks `Zurück zur Übersicht`, the wizard runs **chain re-validation**:
- Walk forward through every step that comes after the revised one.
- For each later step, check whether the previously-saved choice is still valid given the new earlier choice (e.g. a Skill-Increase that depended on a now-removed boost; a Talent whose evaluable prereq now fails because of a swapped earlier feat).
- Any step whose saved choice is now invalid has its choice **cleared** (set back to "not yet picked").
5. Wizard returns to the Review step.
6. Cleared choices appear in Review Section A with the inline note `Diese Wahl wurde durch eine frühere Änderung zurückgesetzt — bitte erneut treffen.` (`text-xs text-warning-500`), and the `Ändern` link on that row is replaced by a `Wählen` link (`text-xs text-primary-500`) that jumps to the cleared step.
7. The `Bestätigen` button is disabled until every Review row has a valid choice.
This contract avoids the "click Weiter through 6 steps again" trap: users always get a single explicit return path (`Zurück zur Übersicht`) and only re-enter steps that the chain actually invalidated.
Direct stepper-dot taps continue to work as conventional back-nav (no re-validation triggered) so users can browse without committing to a revision.
### Section B — Stat-Diff (Vorher / Nachher)
Two-column "Vorher / Nachher" layout. Mobile: stacked (Vorher card on top, Nachher card below). Desktop: side-by-side `grid grid-cols-1 sm:grid-cols-2 gap-4`.
Each card uses `Card` + `CardHeader` + `CardContent`:
- Card title (`CardTitle`): `Vorher (Stufe {N-1})` / `Nachher (Stufe {N})` — for "Nachher", a `Sparkles` icon `text-primary-500` is rendered next to the title.
- Card content: a 5-row `<dl>`:
| Stat | Vorher | Nachher |
|------|--------|---------|
| HP-Max | `{old}` | `{new}` |
| RK | `{old}` | `{new}` |
| Klassen-DC | `{old}` | `{new}` |
| Wahrnehmung | `{old}` | `{new}` |
| Rettungswürfe | `Fort {old} / Ref {old} / Wille {old}` | `Fort {new} / Ref {new} / Wille {new}` |
Numbers in the Nachher card use `font-mono text-2xl font-semibold text-text-primary`. Deltas are rendered inline as small chips:
- Positive change: `text-xs px-1.5 py-0.5 rounded bg-success-500/10 text-success-500` (`+{delta}`).
- No change: omitted (no chip).
- Negative change (rare — only if a class transition causes it; defensive): `bg-error-500/10 text-error-500` (`{delta}`).
### Section C — Spellcaster-Diff (only if applicable)
Shown below B when the character is a caster: a small table of spell-slot increments and (for spontaneous) repertoire deltas.
```
<dl className="grid grid-cols-2 gap-2 text-sm">
<dt className="text-text-secondary">Slots Grad 3</dt><dd className="text-text-primary">3 → 4 (<span className="text-success-500">+1</span>)</dd>
<dt className="text-text-secondary">Repertoire</dt><dd className="text-text-primary">+{N} Zauber gewählt</dd>
</dl>
```
---
## Component Contract — Prereq-Confirm Dialog (D-03)
A **secondary modal layered over the wizard** at `z-60` (wizard sits at `z-50`, dialog at `z-60` — both share the same fixed-positioned root, dialog gets its own backdrop `bg-black/40`).
- Container: `relative w-full sm:max-w-md p-6 bg-bg-secondary rounded-2xl border border-warning-500/30`.
- Header: `<div className="flex items-center gap-3 mb-3"> <AlertTriangle className="h-5 w-5 text-warning-500" /> <h3 className="text-base font-semibold text-text-primary">Voraussetzung nicht prüfbar</h3> </div>`
- Body: `<p className="text-sm text-text-secondary mb-2">Voraussetzung:</p> <p className="text-sm font-semibold text-text-primary mb-4 p-3 rounded-lg bg-bg-tertiary border-l-4 border-warning-500">„{prereqText}"</p> <p className="text-sm text-text-secondary">Dimension47 kann diese Bedingung nicht automatisch prüfen. Erfüllst du sie?</p>`
- Footer: `<div className="flex gap-3 mt-4 justify-end">` with `Abbrechen` (`Button variant="ghost"`) + `Trotzdem wählen` (`Button variant="default"`).
The same dialog shape is reused for "Wizard mid-flow abbrechen", "DRAFT verwerfen" — each with their own copy + heading + appropriate button variant. Note: the `Abbrechen` ghost button **only** appears inside these confirm dialogs. The wizard chrome itself never shows a top-level Abbrechen button.
---
## Component Contract — DRAFT-Resume Banner (D-14)
Two surfaces share this banner:
1. **Character-sheet page** (when a DRAFT exists for this character) — at the very top of the page, above the avatar header. Persistent until DRAFT is committed or discarded.
2. **Wizard entry** — when the user clicks `Stufe steigen` and a DRAFT exists, the wizard opens directly to the last step the user was on, with a small inline strip at the top of the body `Entwurf wiederhergestellt — letzte Bearbeitung: {relativeDate}.` (`text-xs text-info-500 bg-info-500/10 p-2 rounded mb-4`).
Banner layout (surface 1):
```
<div className="bg-bg-tertiary border-l-4 border-primary-500 p-4 rounded-r-lg flex items-start justify-between gap-3">
<div className="flex items-start gap-3">
<RotateCcw className="h-5 w-5 text-primary-500 flex-shrink-0 mt-0.5" />
<div>
<h3 className="text-sm font-semibold text-text-primary">
Du hast eine offene Stufenaufstiegs-Session — Stufe {N}.
</h3>
<p className="text-xs text-text-secondary mt-0.5">
Zuletzt bearbeitet: {relativeDate}.
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" onClick={discard} className="text-error-500">
<Trash2 className="h-4 w-4" /> Verwerfen
</Button>
<Button variant="default" size="sm" onClick={resume}>
<RotateCcw className="h-4 w-4" /> Fortsetzen
</Button>
</div>
</div>
```
---
## Component Contract — Pathbuilder-Import-Violations Banner (D-06)
Surface: directly below the avatar header, above the tab-navigation row. Persistent — Phase 1 ships listing only; user-driven resolution UI is v2.
```
<div className="bg-warning-500/10 border-l-4 border-warning-500 p-4 rounded-r-lg">
<div className="flex items-start gap-3">
<AlertTriangle className="h-5 w-5 text-warning-500 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<h3 className="text-sm font-semibold text-warning-500">
{N} Talente mit nicht erfüllter Voraussetzung
</h3>
<p className="text-sm text-text-secondary mt-1">
Beim Import wurde festgestellt, dass folgende Talente Voraussetzungen haben,
die der Charakter nicht erfüllt. Liste prüfen und ggf. nachträglich anpassen.
</p>
<Button variant="ghost" size="sm" onClick={toggleList} className="mt-2 -ml-2">
{expanded ? <ChevronDown /> : <ChevronRight />} Liste {expanded ? 'verbergen' : 'anzeigen'}
</Button>
{expanded && (
<ul className="mt-2 space-y-1 text-sm text-text-secondary">
{violations.map(v => (
<li key={v.featId}>
<span className="font-semibold text-text-primary">{v.featName}</span>
<span className="text-text-muted"> — Voraussetzung: {v.prereqText}</span>
</li>
))}
</ul>
)}
</div>
</div>
</div>
```
---
## Motion
Phase 1 is the first feature in the codebase to use `framer-motion`. Two motion contracts:
1. **Step transitions** (slide horizontal — direction = next/prev):
- Wrapper: `<AnimatePresence mode="wait">` around the step body.
- Each step `<motion.div key={stepId} initial={{ opacity: 0, x: direction === 'forward' ? 24 : -24 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: direction === 'forward' ? -24 : 24 }} transition={{ duration: 0.2, ease: 'easeOut' }}>`.
- Direction is derived from the previous step index vs. current step index.
- Reduced-motion: respects `prefers-reduced-motion: reduce` — when set, the wrapper falls back to no `x` translation and a 0.1s opacity-only fade. (Implemented via `useReducedMotion()` hook from framer-motion.)
2. **Modal entry/exit**: existing CSS animations from `index.css` (`@keyframes slide-up` / `fade-in`) are sufficient for the wizard container itself. No framer-motion on the outer modal.
**Stepper dot transitions**: pure CSS `transition-colors duration-150 ease-out` on `bg-primary-500` / `bg-bg-tertiary` swaps. No framer-motion needed.
**Confirm-dialog (prereq, abort, discard)**: existing `slide-up 0.3s ease-out` from `index.css` applied to the dialog panel. No framer-motion.
---
## States Inventory
For each interactive surface, the executor must implement:
| Surface | States required |
|---------|-----------------|
| Choice-card | default, hover (`border-border-hover`), focus (`ring-2 ring-primary-500/40`), selected, locked (already chosen / cap), prereq-warning (yellow icon), prereq-fail (hidden by default) |
| Boost-attribute row | default, hover (decorative only — row is a `<div>`), `+/-` disabled when count=0 / cap reached / 4 boosts spent |
| Skill row | default, hover, cap-reached (disabled + warning chip) |
| Talent list | loading (`Spinner` + label), empty (heading + body + show-unavailable toggle), error (heading + body + retry button), populated |
| Wizard footer | first-step (Zurück disabled), middle-step (both enabled when `stepValid`), final-step (Bestätigen replaces Weiter), revision-mode (Zurück zur Übersicht replaces Weiter), committing (`isLoading` on Bestätigen) |
| Stepper dot button | future (disabled), current (`aria-current="step"`), completed (interactive, back-nav), all wrapped at 44×44 hit-zone |
| DRAFT-resume banner | character-sheet surface (always visible until resolved), wizard inline (one-shot info strip on entry) |
| Pathbuilder-violations banner | collapsed (default), expanded (list visible) |
| `Stufe steigen` button on character header | enabled-no-draft (label `Stufe steigen`), enabled-with-draft (label `Stufe fortsetzen`, icon `RotateCcw`), hidden (cap or no permission) |
| Prereq-confirm dialog | default, accepting click closes dialog and selects card |
| Review row | normal (with `Ändern` link), revalidation-cleared (with warning note + `Wählen` link), all-valid (Bestätigen enabled) |
---
## Accessibility
- All interactive elements have a 44×44px tap-target floor (CLAUDE.md mobile-first). The boost `+/-` controls and the stepper-dot wrappers each meet this floor explicitly (`h-11 w-11`).
- All buttons have `aria-label` for icon-only variants:
- Header close button: `aria-label="Schließen"`.
- Boost `+`: `aria-label="{abbreviation} Boost erhöhen"` (e.g. `STR Boost erhöhen`).
- Boost `-`: `aria-label="{abbreviation} Boost verringern"`.
- Stepper dot buttons: `aria-label="{stepLabel}, {state}"` where state ∈ `aktuell` / `abgeschlossen` / `noch nicht verfügbar`.
- Modals use `role="dialog"` + `aria-modal="true"` + `aria-labelledby` pointing at the header H2.
- The stepper uses `role="navigation"` + each completed/current dot has `aria-current="step"` for current.
- Choice-cards are native `<button>` — keyboard focus + Enter/Space activates `onSelect`.
- The Boost-step row is a `<li>` within a `<ul>`, never a `<button>` — the only interactive children are the `+/-` buttons. No nested-button HTML.
- Prereq-confirm dialog traps focus (focus-lock) — first focusable element is `Abbrechen`, Esc closes the dialog.
- All text passes WCAG AA contrast on `bg-bg-secondary` (`text-text-primary` `#f5f5f7` on `#1a1a1f` is 14:1; `text-text-secondary` `#a1a1a6` is 7.4:1; `text-warning-500` on `bg-warning-500/10` is 4.5:1).
- `prefers-reduced-motion` honored (see Motion section).
---
## Design Tokens — Inventory (for executor reference)
The wizard introduces **zero new design tokens**. All colors, fonts, radii, shadows are read from the existing `@theme` block in `client/src/index.css`:
| Token name | Used by Phase 1 |
|------------|-----------------|
| `--color-primary-500` | wizard accent (CTA, selected card, stepper, banner stripe, button, `Ändern` link, `Zurück zur Übersicht`) |
| `--color-primary-500/40`, `--color-primary-500/20`, `--color-primary-500/10` | halos, badges, info strips |
| `--color-bg-primary` | page background |
| `--color-bg-secondary` | wizard modal surface |
| `--color-bg-tertiary` | choice-card surface, banner background, info-strip background |
| `--color-bg-elevated` | +/- step buttons in boost picker |
| `--color-text-primary`, `--color-text-secondary`, `--color-text-muted` | text hierarchy |
| `--color-border`, `--color-border-hover`, `--color-border-focus` | card outlines, hover, focus |
| `--color-warning-500`, `--color-warning-500/10`, `--color-warning-500/20` | non-evaluable prereq icon, violations banner, "+1 (Cap bei 18)" chip, revalidation-cleared note |
| `--color-error-500`, `--color-error-500/10` | DRAFT-discard button, prereq-fail chip |
| `--color-success-500`, `--color-success-500/10` | completed-step check, positive Δ chips |
| `--color-info-500`, `--color-info-500/10` | "Free Archetype: aktiv" strip, "automatisch"-info strips |
| `--radius-lg` (8px), `--radius-xl` (12px), `--radius-2xl` (16px) | rounded corners on cards / modals |
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| (none — hand-built primitives in `client/src/shared/components/ui/`) | `Button`, `Input`, `Card`, `Spinner`, `ActionIcon` (existing) | not applicable — code is owned in-repo, not pulled from a registry |
| (no third-party shadcn registry declared) | — | not applicable |
No `npx shadcn` add operations occur in Phase 1. All wizard components are written from scratch under `client/src/features/characters/components/level-up/` (planner discretion on exact filenames; UI-SPEC dictates the component contracts above).
---
## Component File Plan (advisory — final filenames at planner's discretion)
The following kebab-case files implement the contracts above. The planner may merge or split, but each contract section above must be implemented somewhere:
- `level-up-wizard.tsx` — outer container, stepper, header, footer, motion orchestration
- `level-up-step-class-features.tsx` — step 0 (auto-class-features summary)
- `level-up-step-class-feature-choice.tsx` — step 0a (Cleric Doctrine, Wizard School, …)
- `level-up-step-boost.tsx` — step 1 (4 attribute boosts)
- `level-up-step-skill-increase.tsx` — step 2
- `level-up-step-feat-class.tsx` — step 3
- `level-up-step-feat-skill.tsx` — step 4
- `level-up-step-feat-general.tsx` — step 5
- `level-up-step-feat-ancestry.tsx` — step 6
- `level-up-step-feat-archetype.tsx` — step 7 (Free Archetype slot)
- `level-up-step-spellcaster.tsx` — step 8
- `level-up-step-review.tsx` — step 9
- `level-up-choice-card.tsx` — shared choice-card primitive
- `level-up-prereq-confirm-dialog.tsx` — secondary z-60 dialog for D-03 prereq + abort + discard
- `level-up-resume-banner.tsx` — D-14 banner on character-sheet
- `level-up-violations-banner.tsx` — D-06 banner on character-sheet
---
## Checker Sign-Off
- [ ] Dimension 1 Copywriting: PASS
- [ ] Dimension 2 Visuals: PASS
- [ ] Dimension 3 Color: PASS
- [ ] Dimension 4 Typography: PASS
- [ ] Dimension 5 Spacing: PASS
- [ ] Dimension 6 Registry Safety: PASS
**Approval:** pending