From e3064332cc1637a7ab8b83143391f7299e52b58b Mon Sep 17 00:00:00 2001 From: Alexander Zielonka Date: Mon, 27 Apr 2026 11:17:33 +0200 Subject: [PATCH] docs(01): UI design contract approved --- .../01-UI-SPEC.md | 237 ++++++++++++------ 1 file changed, 167 insertions(+), 70 deletions(-) diff --git a/.planning/phases/01-level-up-pf2e-regelkonform/01-UI-SPEC.md b/.planning/phases/01-level-up-pf2e-regelkonform/01-UI-SPEC.md index d88a24e..b45c810 100644 --- a/.planning/phases/01-level-up-pf2e-regelkonform/01-UI-SPEC.md +++ b/.planning/phases/01-level-up-pf2e-regelkonform/01-UI-SPEC.md @@ -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 `` triggering the same backdrop logic. +- Right: close 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 ` + ``` + - The `-m-1` negative margin on the wrapper compensates for the 44px hit-zone so the perceived gap between dots stays at 8–12px 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 `` 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: `` — 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 `` 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`): - `` - - On non-final steps: `` - - On Review step: `` + - On non-final steps in the normal forward flow: `` + - On the Review step: `` + - When the user is on a step they re-entered via Review's `Ändern` link, the primary right button changes to `` (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): -``` - - {count} - +
+ + + {count} + +
- + ``` +- 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: `

{boostsSpent} von 4 Boosts gewählt. Du musst genau 4 verschiedene Attribute boosten.

` — 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: {stepLabel} {choiceLabel} - + ``` -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 `` (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: `

Voraussetzung nicht prüfbar

` - Body: `

Voraussetzung:

„{prereqText}"

Dimension47 kann diese Bedingung nicht automatisch prüfen. Erfüllst du sie?

` -- Footer: `
` with `Abbrechen` (`Button variant="ghost"`) + `Trotzdem wählen` (`Button variant="default"`). +- Footer: `
` 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 `
`), `+/-` 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 `