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

53 KiB
Raw Blame History

phase, slug, status, shadcn_initialized, preset, created, revised, reviewed_at
phase slug status shadcn_initialized preset created revised reviewed_at
1 level-up-pf2e-regelkonform approved false none 2026-04-27 2026-04-27 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.
  • 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-labels. 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:

<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.tsxPROFICIENCY_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