# Tasks: Stat Block Panel Fold/Unfold and Pin **Input**: Design documents from `/specs/035-statblock-fold-pin/` **Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md **Tests**: Acceptance scenario tests in Phase 7 (`apps/web/src/__tests__/stat-block-fold-pin.test.tsx`). **Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. ## Format: `[ID] [P?] [Story] Description` - **[P]**: Can run in parallel (different files, no dependencies) - **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3) - Include exact file paths in descriptions ## Phase 1: Setup (Shared Infrastructure) **Purpose**: CSS utilities needed by all user stories - [x] T001 Add CSS transition utility for panel slide animation (`transition: translate 200ms ease-out`) in `apps/web/src/index.css` - [x] T002 [P] Add CSS utility for vertical text (`writing-mode: vertical-rl`) in `apps/web/src/index.css` --- ## Phase 2: Foundational (Blocking Prerequisites) **Purpose**: Refactor StatBlockPanel props interface to support role-based rendering before any user story work **CRITICAL**: No user story work can begin until this phase is complete - [x] T003 Update `StatBlockPanelProps` interface in `apps/web/src/components/stat-block-panel.tsx`: add `panelRole` (`"browse" | "pinned"`), `isFolded` (boolean), `onToggleFold` (callback), `onPin` (callback), `onUnpin` (callback), `showPinButton` (boolean), `side` (`"left" | "right"`), `onDismiss` (callback for mobile backdrop). Remove `onClose` prop. - [x] T004 Update `App.tsx` to pass the new props to StatBlockPanel: set `panelRole="browse"`, `side="right"`, wire `onDismiss` to clear `selectedCreatureId`, add `isRightPanelFolded` state (default `false`), wire `onToggleFold`. File: `apps/web/src/App.tsx` **Checkpoint**: App compiles and existing panel behavior works with new prop interface (fold/pin not yet functional) --- ## Phase 3: User Story 1 - Fold and Unfold Stat Block Panel (Priority: P1) MVP **Goal**: Replace close button with fold/unfold toggle; panel collapses to slim right-edge tab with creature name and smooth animation **Independent Test**: Open a stat block, fold it, verify slim tab with creature name appears on right edge, unfold it, verify full panel returns with same creature ### Implementation for User Story 1 - [x] T005 [US1] Remove "Stat Block" heading text (`panelTitle` / `` element) from both desktop and mobile header sections in `apps/web/src/components/stat-block-panel.tsx` - [x] T006 [US1] Replace the close button (X icon) with a fold toggle button (use `PanelRightClose` / `PanelRightOpen` Lucide icons or similar chevron) in the desktop header of `apps/web/src/components/stat-block-panel.tsx`. Wire to `onToggleFold` prop. - [x] T007 [US1] Implement the folded tab view in the desktop branch of `apps/web/src/components/stat-block-panel.tsx`: when `isFolded` is true, render a 40px-wide clickable strip anchored to the right edge showing the creature name vertically (using the vertical text CSS utility from T002). Clicking the tab calls `onToggleFold`. - [x] T008 [US1] Add the CSS slide transition to the desktop panel container in `apps/web/src/components/stat-block-panel.tsx`: apply the transition utility (from T001), toggle between `translate-x-0` (expanded) and `translate-x-[calc(100%-40px)]` (folded) based on `isFolded` prop. The folded tab must remain visible at the viewport edge. - [x] T009 [US1] Update mobile drawer behavior in `apps/web/src/components/stat-block-panel.tsx`: replace X close button with fold icon matching desktop. Backdrop click calls `onDismiss` (full dismiss). Fold toggle on mobile also calls `onDismiss` (since mobile has no folded tab state — fold = dismiss on mobile). - [x] T010 [US1] Update auto-show logic in `apps/web/src/App.tsx`: when active combatant changes and `selectedCreatureId` is set, also set `isRightPanelFolded = false` to auto-unfold the panel on turn change. **Checkpoint**: Panel folds to slim tab on desktop, unfolds on click, no close button, no heading, mobile drawer preserved with fold-as-dismiss, auto-unfold on turn change works --- ## Phase 4: User Story 2 - Pin Creature to Second Panel (Priority: P2) **Goal**: Pin button copies current creature to a left-side panel; right panel stays active for browsing; pin hidden on small screens **Independent Test**: Open a stat block on xl+ viewport, click pin, verify left panel appears with same creature, select different combatant, verify right panel updates while left stays, click unpin, verify left panel removed ### Implementation for User Story 2 - [x] T011 [US2] Add `pinnedCreatureId` state and derived `pinnedCreature` (via `getCreature`) in `apps/web/src/App.tsx`. Add `isWideDesktop` state using `matchMedia("(min-width: 1280px)")` with change listener (same pattern as existing `isDesktop` in stat-block-panel.tsx). - [x] T012 [US2] Wire pin/unpin callbacks in `apps/web/src/App.tsx`: `onPin` sets `pinnedCreatureId = selectedCreatureId`, `onUnpin` sets `pinnedCreatureId = null`. Pass `showPinButton = isWideDesktop` to browse panel. - [x] T013 [US2] Render the pinned StatBlockPanel in `apps/web/src/App.tsx`: conditionally render a second `` when `pinnedCreatureId` is set and `isWideDesktop` is true, with `panelRole="pinned"`, `side="left"`, creature data from `pinnedCreature`, and `onUnpin` callback. - [x] T014 [US2] Implement pin and unpin button rendering in `apps/web/src/components/stat-block-panel.tsx`: when `panelRole="browse"` and `showPinButton` is true, render a pin icon button (use `Pin` Lucide icon) in the header that calls `onPin`. When `panelRole="pinned"`, render an unpin icon button (use `PinOff` Lucide icon) in the header that calls `onUnpin`. - [x] T015 [US2] Update panel positioning in `apps/web/src/components/stat-block-panel.tsx`: use the `side` prop to apply `left-0 border-r` for pinned panel vs `right-0 border-l` for browse panel in the desktop layout classes. **Checkpoint**: Pin button visible on xl+ screens, clicking it creates left panel with same creature, right panel browses independently, unpin removes left panel, pin button hidden below 1280px --- ## Phase 5: User Story 3 - Fold Behavior with Pinned Panel (Priority: P3) **Goal**: Right panel folds independently while pinned panel remains visible; unfolding restores last browsed creature **Independent Test**: Pin a creature, fold right panel, verify pinned panel stays visible and right panel shows folded tab, unfold right panel, verify it shows last browsed creature ### Implementation for User Story 3 - [x] T016 [US3] Verify fold/unfold independence in `apps/web/src/App.tsx`: ensure `isRightPanelFolded` state only affects the browse panel and does not hide or modify the pinned panel. The pinned panel has no fold state — it is always expanded when present. - [x] T017 [US3] Verify fold preserves `selectedCreatureId` in `apps/web/src/App.tsx`: when `isRightPanelFolded` is toggled to true, `selectedCreatureId` must remain unchanged so unfolding restores the same creature. **Checkpoint**: Both panels operate independently — fold right panel while pinned panel stays, unfold shows last creature --- ## Phase 6: Polish & Cross-Cutting Concerns **Purpose**: Edge cases, cleanup, and quality gate - [x] T018 Handle viewport resize edge case in `apps/web/src/App.tsx`: when `isWideDesktop` changes from true to false and `pinnedCreatureId` is set, the pinned panel should stop rendering (handled by conditional render). When resized back to wide, pinned panel reappears with same creature. - [x] T019 Verify bulk import mode works with fold/unfold in `apps/web/src/components/stat-block-panel.tsx`: `bulkImportMode` panels use the same fold toggle behavior. Folded tab shows "Bulk Import" as creature name fallback. - [x] T020 Run `pnpm check` (full quality gate: audit + knip + biome + typecheck + test/coverage + jscpd) and fix any issues - [x] T021 Verify pinned creature removal edge case in `apps/web/src/App.tsx`: `pinnedCreatureId` is not cleared on combatant removal — data resolved via `getCreature` independently. --- ## Phase 7: Acceptance Tests **Purpose**: Map spec acceptance scenarios to automated tests - [x] T022 [P] [US1] Test fold/unfold behavior in `apps/web/src/__tests__/stat-block-fold-pin.test.tsx`: 7 tests covering fold button, no close button, no heading, folded tab with creature name, toggle callbacks, translate classes. - [x] T023 [P] [US1] Test mobile behavior in `apps/web/src/__tests__/stat-block-fold-pin.test.tsx`: 3 tests covering fold button on mobile, backdrop dismiss, pinned panel not rendered on mobile. - [x] T024 [P] [US2] Test pin/unpin behavior in `apps/web/src/__tests__/stat-block-fold-pin.test.tsx`: 7 tests covering pin/unpin buttons, callbacks, panel positioning (left/right). - [x] T025 [P] [US3] Test fold independence with pinned panel in `apps/web/src/__tests__/stat-block-fold-pin.test.tsx`: 2 tests verifying pinned panel has no fold button and is always expanded. --- ## Dependencies & Execution Order ### Phase Dependencies - **Setup (Phase 1)**: No dependencies — can start immediately - **Foundational (Phase 2)**: Depends on Phase 1 (CSS utilities) — BLOCKS all user stories - **User Story 1 (Phase 3)**: Depends on Phase 2 completion - **User Story 2 (Phase 4)**: Depends on Phase 2 completion. Can run in parallel with US1 but integrates more cleanly after US1. - **User Story 3 (Phase 5)**: Depends on US1 and US2 being complete (verifies their interaction) - **Polish (Phase 6)**: Depends on all user stories being complete - **Acceptance Tests (Phase 7)**: Depends on all user stories being complete. All test tasks [P] can run in parallel. ### User Story Dependencies - **User Story 1 (P1)**: Can start after Foundational (Phase 2) — no dependencies on other stories - **User Story 2 (P2)**: Can start after Foundational (Phase 2) — recommended after US1 for cleaner integration - **User Story 3 (P3)**: Depends on US1 + US2 (verifies combined behavior) ### Within Each User Story - Props/state wiring before UI rendering - CSS utilities before components that use them - Core implementation before edge cases ### Parallel Opportunities - T001 and T002 can run in parallel (different CSS utilities, same file but independent additions) - T005 and T006 modify different sections of stat-block-panel.tsx header — can be done together - T011 and T014 modify different files (App.tsx vs stat-block-panel.tsx) — can run in parallel --- ## Parallel Example: User Story 1 ```bash # T005 + T006 can be done in one pass (both modify header in stat-block-panel.tsx): Task: "Remove Stat Block heading and replace close with fold toggle in apps/web/src/components/stat-block-panel.tsx" # T007 + T008 are sequential (T007 creates the tab, T008 adds animation to it) ``` ## Parallel Example: User Story 2 ```bash # These modify different files and can run in parallel: Task: "T011 — Add pinnedCreatureId state + isWideDesktop in apps/web/src/App.tsx" Task: "T014 — Add pin/unpin buttons in apps/web/src/components/stat-block-panel.tsx" ``` --- ## Implementation Strategy ### MVP First (User Story 1 Only) 1. Complete Phase 1: CSS utilities (T001-T002) 2. Complete Phase 2: Props refactor (T003-T004) 3. Complete Phase 3: Fold/unfold (T005-T010) 4. **STOP and VALIDATE**: Test fold/unfold independently on desktop and mobile 5. Deploy/demo if ready — panel folds and unfolds, no close button, no heading ### Incremental Delivery 1. Setup + Foundational → App compiles with new prop interface 2. Add User Story 1 → Test fold/unfold → Deploy/Demo (MVP!) 3. Add User Story 2 → Test pin/unpin → Deploy/Demo 4. Add User Story 3 → Verify combined behavior → Deploy/Demo 5. Polish → Run quality gate 6. Acceptance Tests → Verify all scenarios → Commit --- ## Notes - [P] tasks = different files, no dependencies - [Story] label maps task to specific user story for traceability - Each user story should be independently completable and testable - Commit after each phase or logical group of tasks - Stop at any checkpoint to validate story independently - US3 is lightweight verification — most behavior should already work from US1 + US2 wiring