Files
initiative/specs/035-statblock-fold-pin/plan.md
Lukas 460c65bf49 Implement stat block panel fold/unfold and pin-to-second-panel
Replace the close button and heading with fold/unfold controls that
collapse the panel to a slim right-edge tab showing the creature name
vertically, and add a pin button (xl+ viewports with creature loaded)
that opens the creature in a second left-side panel for simultaneous
reference. Fold state is respected on turn change. 19 acceptance tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:18:15 +01:00

7.0 KiB

Implementation Plan: Stat Block Panel Fold/Unfold and Pin

Branch: 035-statblock-fold-pin | Date: 2026-03-11 | Spec: spec.md Input: Feature specification from /specs/035-statblock-fold-pin/spec.md

Summary

Replace the stat block panel's close button and heading with fold/unfold controls that collapse the panel to a slim right-edge tab (with smooth CSS slide animation), and add a pin button that opens the creature in a second left-side panel for simultaneous reference while browsing other creatures in the right panel. All changes are UI-layer only with ephemeral state.

Technical Context

Language/Version: TypeScript 5.8 (strict mode, verbatimModuleSyntax) Primary Dependencies: React 19, Tailwind CSS v4, Lucide React (icons), class-variance-authority Storage: N/A (no persistence changes — all new state is ephemeral) Testing: Vitest + @testing-library/react Target Platform: Web (desktop + mobile browsers) Project Type: Web application (React SPA) Performance Goals: Fold/unfold animation completes in ~200ms; no layout shifts or jank Constraints: Dual panels require xl (1280px+) viewport; mobile drawer behavior preserved unchanged Scale/Scope: 3 files modified, 0 new files, ~150-200 lines changed

Constitution Check

GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.

Principle Status Notes
I. Deterministic Domain Core PASS No domain changes. All changes are in the adapter (web) layer.
II. Layered Architecture PASS Changes confined to apps/web/ (adapter layer). No imports from inner layers are added or modified.
III. Clarification-First PASS Spec is complete with no NEEDS CLARIFICATION markers. Research resolved all design decisions.
IV. Escalation Gates PASS All requirements trace to spec.md acceptance criteria. No scope creep.
V. MVP Baseline Language PASS No permanent bans introduced.
VI. No Gameplay Rules PASS Feature is UI chrome, not gameplay.

Post-Phase 1 Re-check: All gates still pass. No domain or application layer changes introduced in design. Data model is ephemeral UI state only.

Project Structure

Documentation (this feature)

specs/035-statblock-fold-pin/
├── spec.md              # Feature specification
├── plan.md              # This file
├── research.md          # Phase 0: design decisions and rationale
├── data-model.md        # Phase 1: state model and transitions
├── quickstart.md        # Phase 1: developer onboarding
└── checklists/
    └── requirements.md  # Spec quality checklist

Source Code (repository root)

apps/web/src/
├── components/
│   ├── stat-block-panel.tsx   # MODIFY: fold/unfold, pin/unpin, remove close/heading
│   └── stat-block.tsx         # NO CHANGE (display component)
├── App.tsx                    # MODIFY: new state, second panel, auto-unfold logic
└── index.css                  # MODIFY: new transition + vertical text utilities

Structure Decision: All changes fit within the existing component structure. No new files or directories needed. The StatBlockPanel component is reused for both browse and pinned panels via a prop.

Design Decisions

D-001: Panel Component Reuse

The same StatBlockPanel component renders both the browse (right) and pinned (left) panels. A new role prop ("browse" | "pinned") controls:

  • browse: Shows fold button + pin button (xl+ only). Positioned right-0.
  • pinned: Shows unpin button only. Positioned left-0. No fold behavior (always expanded when present).

This avoids duplicating the panel chrome, scrolling, stat block rendering, and bestiary fetch logic.

D-002: Fold State Architecture

isRightPanelFolded is a boolean state in App.tsx. When true:

  • The browse panel renders as a slim folded tab (40px wide, right edge)
  • The creature name is displayed vertically using writing-mode: vertical-rl
  • Clicking the tab sets isRightPanelFolded = false

The fold state is independent of selectedCreatureId — folding preserves the selection, unfolding restores it. Setting selectedCreatureId to null still fully removes the panel.

D-003: Animation Strategy

Use CSS transition: translate 200ms ease-out on the panel container. Toggle between translate-x-0 (expanded) and translate-x-[calc(100%-40px)] (folded, with tab visible). This gives smooth bidirectional animation with a single CSS rule, matching the existing slide-in-right pattern.

The folded tab is part of the panel element itself (rendered at the left edge of the 400px panel), so when the panel translates right, the tab remains visible at the viewport edge.

D-004: Fold State Respected on Turn Change

The existing auto-show logic sets selectedCreatureId when the active combatant changes, but does NOT change isRightPanelFolded. If the user folded the panel, advancing turns updates which creature is selected internally — unfolding later shows the current active creature. This respects the user's deliberate fold action.

D-005: Viewport-Responsive Pin Button

The pin button visibility requires: (1) wide desktop viewport (min-width: 1280px / xl breakpoint), AND (2) a resolved creature is displayed (hidden during source fetch prompts and bulk import mode). A new isWideDesktop state (1280px) controls the viewport condition. Same matchMedia listener pattern as existing code.

D-006: Mobile Behavior

On mobile (< 1024px), the drawer/backdrop pattern is preserved. The close button (X) is replaced with a fold icon, but tapping the backdrop still fully dismisses the panel (sets selectedCreatureId = null). The fold toggle on mobile slides the drawer off-screen with the same animation. No pin button on mobile.

Component Props Changes

StatBlockPanel — New Props

panelRole: "browse" | "pinned"     — controls header buttons and positioning (named panelRole to avoid ARIA role conflict)
isFolded: boolean                  — whether panel is in folded tab state (browse only)
onToggleFold: () => void           — callback to toggle fold state (browse only)
onPin: () => void                  — callback to pin current creature (browse only, xl+)
onUnpin: () => void                — callback to unpin (pinned only, rendered top-right for consistency)
showPinButton: boolean             — whether pin button is visible (viewport + creature loaded)
side: "left" | "right"             — controls fixed positioning (left-0 vs right-0)
onDismiss: () => void              — callback for mobile backdrop dismiss

StatBlockPanel — Removed Props

onClose: () => void                — replaced by fold/unfold; backdrop dismiss handled internally

Note: On mobile, backdrop click still needs to fully dismiss. This will be handled by the parent passing an onDismiss callback or by the fold action on mobile mapping to dismiss behavior.

Complexity Tracking

No constitution violations. No complexity justifications needed.