docs(01): UI design contract approved

This commit is contained in:
2026-04-27 11:17:33 +02:00
parent 96c81fa29d
commit e3064332cc

View File

@@ -1,10 +1,12 @@
---
phase: 1
slug: level-up-pf2e-regelkonform
status: draft
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
@@ -26,7 +28,7 @@ created: 2026-04-27
| 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). |
| 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). |
@@ -49,26 +51,36 @@ Declared values (all multiples of 4, all already present in Tailwind v4 default
Touch-target minimums (mobile-first, CLAUDE.md):
- All interactive buttons: 44px height — `h-11` (`Button` `default`), `h-11 w-11` (`Button` `icon`).
- 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).
- Stepper-dot buttons: 32px tap-target (`h-8 w-8`) but padded to a 44px hit-zone via `p-1.5` wrapper.
- 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: none.
**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 **3 sizes + display** and **2 weights** to satisfy the 3-4-size, 2-weight contract.
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 |
|------|------|----------------|--------|-------------|-------|
| 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) |
| 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`. |
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).
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`).
@@ -83,15 +95,16 @@ The 60/30/10 split maps directly to existing tokens in `client/src/index.css`:
| 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). |
| 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``Button variant="default"` (which is `bg-primary-500`).
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.
@@ -99,12 +112,14 @@ Semantic color usage (already established by existing modals — `rest-modal.tsx
| 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). |
| 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)"). |
Source-tag colors for talent badges (existing convention in `feat-detail-modal.tsx:22-29`, copy verbatim):
### 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 |
|--------|---------------|
@@ -129,8 +144,10 @@ All copy is **German** (CLAUDE.md / PROJECT.md). German imperative tone, du-Form
| 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) |
| 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`)
@@ -160,13 +177,14 @@ All copy is **German** (CLAUDE.md / PROJECT.md). German imperative tone, du-Form
| 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 `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". |
| **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`). |
@@ -227,21 +245,44 @@ The wizard reuses the project's modal pattern (canonical: `add-feat-modal.tsx:24
- 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.
- 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 (no labels on mobile, labels visible on `sm:` and up):
Mobile-first compact dot-stepper with **always-visible progress label**:
- 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).
- 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)
@@ -253,12 +294,13 @@ Mobile-first compact dot-stepper (no labels on mobile, labels visible on `sm:` a
### 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`):
- 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: `<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>`
- 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).
---
@@ -298,7 +340,7 @@ Inner structure (top row → meta row → optional description):
- 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`.
- **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`.
@@ -319,29 +361,63 @@ Inner structure (top row → meta row → optional description):
## 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.
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.
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>
**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">
<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 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>
</button>
</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 row shows a small chip `+1 (Cap bei 18)` next to the new-score line in `text-warning-500`. Otherwise `+2`.
- **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.
@@ -397,7 +473,7 @@ Top of the step body shows a read-only info strip describing what's automatic:
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.
- 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.
---
@@ -409,7 +485,7 @@ Layout: a vertical stack of mutually-exclusive choice-cards (canonical Choice-Ca
- 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`).
- 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`).
---
@@ -428,11 +504,33 @@ A `Card`-wrapped list of every choice the user made, grouped by step. Each row:
<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>
<button
onClick={() => goToStep(step, { revisionMode: true })}
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.
#### `Ä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)
@@ -476,9 +574,9 @@ A **secondary modal layered over the wizard** at `z-60` (wizard sits at `z-50`,
- 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"`).
- 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.
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.
---
@@ -577,45 +675,44 @@ 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 |
| 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), committing (`isLoading` on Bestätigen) |
| 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).
- All buttons have `aria-label` for icon-only variants (close X, +/-, stepper dots).
- 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 and `aria-label="{stepLabel}, abgeschlossen"` for completed.
- 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).
---
## 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` | 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 |
@@ -623,7 +720,7 @@ The wizard introduces **zero new design tokens**. All colors, fonts, radii, shad
| `--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-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 |