docs(01): UI design contract for level-up wizard

This commit is contained in:
2026-04-27 11:02:09 +02:00
parent 24a9562bf3
commit 96c81fa29d

View File

@@ -0,0 +1,677 @@
---
phase: 1
slug: level-up-pf2e-regelkonform
status: draft
shadcn_initialized: false
preset: none
created: 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`) — 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 — `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).
- Stepper-dot buttons: 32px tap-target (`h-8 w-8`) but padded to a 44px hit-zone via `p-1.5` wrapper.
Exceptions: none.
---
## Typography
The codebase has no custom font-size tokens — sizes come from Tailwind defaults. Phase 1 declares **3 sizes + display** and **2 weights** to satisfy the 3-4-size, 2-weight contract.
| Role | Size | Tailwind class | Weight | Line Height | Usage |
|------|------|----------------|--------|-------------|-------|
| Body | 14px | `text-sm` | 400 (`font-normal`) | 1.5 (`leading-normal`) | Choice-card descriptions, prereq text, banner body, footer help text, tooltip body |
| Label | 14px | `text-sm` | 600 (`font-semibold`) | 1.4 | Choice-card titles, step-section labels, button labels, badge text |
| 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) |
Weights: only **400 (normal)** for body and **600 (semibold)** for everything else. 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).
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** Abbrechen (Abbrechen = `outline`, see Copywriting). |
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``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.
`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. |
| 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 Abbrechen-with-unsaved-changes confirm. (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)"). |
Source-tag colors for talent badges (existing convention in `feat-detail-modal.tsx:22-29`, copy verbatim):
| 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 — abort wizard mid-flow | `Abbrechen` (`Button variant="ghost"`, secondary placement) |
| Choice-card click result label | `Wählen` (only used inside the prereq-confirm dialog as "Trotzdem wählen") |
### 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) |
### 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 `Abbrechen` button 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"><X className="h-5 w-5" /></Button>` triggering the same backdrop logic.
### Stepper (always visible, between Header and Body)
Mobile-first compact dot-stepper (no labels on mobile, labels visible on `sm:` and up):
- Container: `flex items-center gap-2 px-4 py-3 border-b border-border overflow-x-auto`.
- Each step is a clickable dot **only for steps already completed or the current step** (no jumping forward to incomplete future steps).
- Dot states:
- **Future**: `h-2 w-2 rounded-full bg-bg-tertiary` (no label).
- **Current**: `h-2.5 w-2.5 rounded-full bg-primary-500` + step label visible on `sm:` (`text-xs font-semibold text-text-primary`).
- **Completed**: `h-2 w-2 rounded-full bg-primary-500/40` + on hover/click navigates back; on `sm:` shows `Check` icon at `h-3 w-3 text-success-500` overlay.
- Tap-target: each dot is wrapped in `<button className="p-1.5 -m-1.5">` to extend the hit-zone to 44px.
- Step labels (visible `sm:` and up): one of `Merkmale`, `Wahl`, `Boost`, `Skill`, `Klasse`, `Fertigkeit`, `Allgemein`, `Abstammung`, `Archetyp`, `Zauber`, `Übersicht` — exactly the steps that apply to this level. Conditional steps not shown.
- Connector between dots: `h-px w-4 bg-bg-tertiary` (`bg-primary-500/40` between completed pairs).
### 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-2 p-4 border-t border-border bg-bg-secondary`.
- Left: `<Button variant="ghost" onClick={handleAbort}>Abbrechen</Button>` — always visible, always live.
- 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: `<Button variant="default" onClick={next} disabled={!stepValid}>Weiter <ChevronRight className="h-4 w-4" /></Button>`
- On Review step: `<Button variant="default" onClick={commit} isLoading={isCommitting}><Check className="h-4 w-4" /> Bestätigen</Button>`
- 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-2 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 "buttons" (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.
Per attribute row (button-style, full-width):
```
<button 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>
<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>
</div>
<div className="flex items-center gap-2">
<button onClick={dec} disabled={count === 0} className="h-9 w-9 rounded-lg bg-bg-elevated"><Minus /></button>
<span className="text-lg font-semibold text-text-primary tabular-nums w-8 text-center">{count}</span>
<button onClick={inc} disabled={!canIncrement} className="h-9 w-9 rounded-lg bg-bg-elevated"><Plus /></button>
</div>
</button>
```
- "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 row shows a small chip `+1 (Cap bei 18)` next to the new-score line in `text-warning-500`. Otherwise `+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-2 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-2 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)} className="text-xs text-primary-500 hover:underline">Ändern</button>
</div>
```
Tap on `Ändern` returns to that step (back-navigation in the stepper); user can revise then re-enter Review.
### 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-2 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.
---
## 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, +/- 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), committing (`isLoading` on Bestätigen) |
| 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 |
---
## Accessibility
- All interactive elements have a 44×44px tap-target floor (CLAUDE.md mobile-first).
- All buttons have `aria-label` for icon-only variants (close X, +/-, stepper dots).
- 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 and `aria-label="{stepLabel}, abgeschlossen"` for completed.
- Choice-cards are native `<button>` — keyboard focus + Enter/Space activates `onSelect`.
- 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).
---
## Spacing Scale
(Re-stated for the checker — see top of file for the canonical table.)
All spacing is `4 / 8 / 16 / 24` for Phase 1; `32` reserved for Review-step desktop. No values outside this set.
---
## 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) |
| `--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 |
| `--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