From c75d148d1e5791ecc946e76c090f0fcc3e0baadd Mon Sep 17 00:00:00 2001 From: Lukas Date: Sat, 14 Mar 2026 11:13:11 +0100 Subject: [PATCH] Fix high-severity undici vulnerability via pnpm override Override undici to >=7.24.0 to resolve GHSA-v9p9-hfj2-hcw8 (WebSocket 64-bit length overflow). The vulnerable version was pulled in transitively via jsdom@28.1.0. Co-Authored-By: Claude Opus 4.6 --- .../plans/2026-03-13-declutter-action-bars.md | 536 ------------------ package.json | 5 + pnpm-lock.yaml | 11 +- 3 files changed, 12 insertions(+), 540 deletions(-) delete mode 100644 docs/agents/plans/2026-03-13-declutter-action-bars.md diff --git a/docs/agents/plans/2026-03-13-declutter-action-bars.md b/docs/agents/plans/2026-03-13-declutter-action-bars.md deleted file mode 100644 index 0b6a57d..0000000 --- a/docs/agents/plans/2026-03-13-declutter-action-bars.md +++ /dev/null @@ -1,536 +0,0 @@ ---- -date: "2026-03-13T14:58:42.882813+00:00" -git_commit: 75778884bd1be7d135b2f5ea9b8a8e77a0149f7b -branch: main -topic: "Declutter Action Bars" -tags: [plan, turn-navigation, action-bar, overflow-menu, ux] -status: draft ---- - -# Declutter Action Bars — Implementation Plan - -## Overview - -Reorganize buttons across the top bar (TurnNavigation) and bottom bar (ActionBar) to reduce visual clutter and improve UX. Each bar gets a clear purpose: the top bar is for turn navigation + encounter lifecycle, the bottom bar is for adding combatants + setup actions. - -## Current State Analysis - -**Top bar** (`turn-navigation.tsx`) has 5 buttons + center info: -``` -[ Prev ] | [ R1 Dwarf ] | [ D20 Library Trash ] [ Next ] -``` -The D20 (roll all initiative) and Library (manage sources) buttons are unrelated to turn navigation — they're setup/utility actions that add noise. - -**Bottom bar** (`action-bar.tsx`) has an input, Add button, and 3 icon buttons: -``` -[ + Add combatants... ] [ Add ] [ Users Eye Import ] -``` -The icon cluster (Users, Eye, Import) is cryptic — three ghost icon buttons with no labels, requiring hover to discover purpose. The Eye button opens a separate search dropdown for browsing stat blocks, which duplicates the existing search input. - -### Key Discoveries: -- `rollAllInitiativeUseCase` (`packages/application/src/roll-all-initiative-use-case.ts`) applies to combatants with `creatureId` AND no `initiative` set — this defines the conditional visibility logic -- `Combatant.initiative` is `number | undefined` and `Combatant.creatureId` is `CreatureId | undefined` (`packages/domain/src/types.ts`) -- No existing dropdown/menu UI component — the overflow menu needs a new component -- Lucide provides `EllipsisVertical` for the kebab menu trigger -- The stat block viewer already has its own search input, results list, and keyboard navigation (`action-bar.tsx:65-236`) — in browse mode, we reuse the main input for this instead - -## Desired End State - -### UI Mockups - -**Top bar (after):** -``` -[ Prev ] [ R1 Dwarf ] [ Trash ] [ Next ] -``` -4 elements. Clean, focused on turn flow + encounter lifecycle. - -**Bottom bar — add mode (default):** -``` -[ + Add combatants... 👁 ] [ Add ] [ D20? ] [ ⋮ ] -``` -The Eye icon sits inside/beside the input as a toggle. D20 appears conditionally. Kebab menu holds infrequent actions. - -**Bottom bar — browse mode (Eye toggled on):** -``` -[ 🔍 Search stat blocks... 👁 ] [ ⋮ ] -``` -The input switches purpose: placeholder changes, typing searches stat blocks instead of adding combatants. The Add button and D20 hide (irrelevant in browse mode). Eye icon stays as the toggle to switch back. Selecting a result opens the stat block panel and exits browse mode. - -**Overflow menu (⋮ clicked):** -``` -┌──────────────────────┐ -│ 👥 Player Characters │ -│ 📚 Manage Sources │ -│ 📥 Bulk Import │ -└──────────────────────┘ -``` -Labeled items with icons — discoverable without hover. - -### Key Discoveries: -- `sourceManagerOpen` state lives in App.tsx:116 — the overflow menu's "Manage Sources" item needs the same toggle callback -- The stat block viewer state (viewerOpen, viewerQuery, viewerResults, viewerIndex) in action-bar.tsx:66-71 gets replaced by a `browseMode` boolean that repurposes the main input -- The viewer's separate input, dropdown, and keyboard handling (action-bar.tsx:188-248) can be removed — browse mode reuses the existing input and suggestion dropdown infrastructure - -## What We're NOT Doing - -- Changing domain logic or use cases -- Modifying ConfirmButton behavior -- Changing the stat block panel itself -- Altering animation logic (useActionBarAnimation) -- Modifying combatant row buttons -- Changing how SourceManager works (just moving where the trigger lives) - -## Implementation Approach - -Four phases, each independently testable. Phase 1 simplifies the top bar (pure removal). Phase 2 adds the overflow menu component. Phase 3 reworks the ActionBar (browse toggle + conditional D20 + overflow integration). Phase 4 wires everything together in App.tsx. - ---- - -## Phase 1: Simplify TurnNavigation - -### Overview -Strip TurnNavigation down to just turn controls + clear encounter. Remove Roll All Initiative and Manage Sources buttons and their associated props. - -### Changes Required: - -#### [x] 1. Update TurnNavigation component -**File**: `apps/web/src/components/turn-navigation.tsx` -**Changes**: -- Remove `onRollAllInitiative` and `onOpenSourceManager` from props interface -- Remove the D20 button (lines 53-62) -- Remove the Library button (lines 63-72) -- Remove the inner `gap-0` div wrapper (lines 52, 80) since only the ConfirmButton remains -- Remove unused imports: `Library` from lucide-react, `D20Icon` -- Adjust layout: ConfirmButton + Next button grouped with `gap-3` - -Result: -```tsx -interface TurnNavigationProps { - encounter: Encounter; - onAdvanceTurn: () => void; - onRetreatTurn: () => void; - onClearEncounter: () => void; -} - -// Layout becomes: -// [ Prev ] | [ R1 Name ] | [ Trash ] [ Next ] -``` - -#### [x] 2. Update TurnNavigation usage in App.tsx -**File**: `apps/web/src/App.tsx` -**Changes**: -- Remove `onRollAllInitiative` and `onOpenSourceManager` props from the `` call (lines 256-257) - -### Success Criteria: - -#### Automated Verification: -- [x] `pnpm check` passes (typecheck catches removed props, lint catches unused imports) - -#### Manual Verification: -- [ ] Top bar shows only: Prev, round badge + name, trash, Next -- [ ] Prev/Next/Clear buttons still work as before -- [ ] Top bar animation (slide in/out) unchanged - -**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase. - ---- - -## Phase 2: Create Overflow Menu Component - -### Overview -Build a reusable overflow menu (kebab menu) component with click-outside and Escape handling, following the same patterns as ConfirmButton and the existing viewer dropdown. - -### Changes Required: - -#### [x] 1. Create OverflowMenu component -**File**: `apps/web/src/components/ui/overflow-menu.tsx` (new file) -**Changes**: Create a dropdown menu triggered by an EllipsisVertical icon button. Features: -- Toggle open/close on button click -- Close on click outside (document mousedown listener, same pattern as confirm-button.tsx:44-67) -- Close on Escape key -- Renders above the trigger (bottom-full positioning, same as action-bar suggestion dropdown) -- Each item: icon + label, full-width clickable row -- Clicking an item calls its action and closes the menu - -```tsx -import { EllipsisVertical } from "lucide-react"; -import { type ReactNode, useEffect, useRef, useState } from "react"; -import { Button } from "./button"; - -export interface OverflowMenuItem { - readonly icon: ReactNode; - readonly label: string; - readonly onClick: () => void; - readonly disabled?: boolean; -} - -interface OverflowMenuProps { - readonly items: readonly OverflowMenuItem[]; -} - -export function OverflowMenu({ items }: OverflowMenuProps) { - const [open, setOpen] = useState(false); - const ref = useRef(null); - - useEffect(() => { - if (!open) return; - function handleMouseDown(e: MouseEvent) { - if (ref.current && !ref.current.contains(e.target as Node)) { - setOpen(false); - } - } - function handleKeyDown(e: KeyboardEvent) { - if (e.key === "Escape") setOpen(false); - } - document.addEventListener("mousedown", handleMouseDown); - document.addEventListener("keydown", handleKeyDown); - return () => { - document.removeEventListener("mousedown", handleMouseDown); - document.removeEventListener("keydown", handleKeyDown); - }; - }, [open]); - - return ( -
- - {open && ( -
- {items.map((item) => ( - - ))} -
- )} -
- ); -} -``` - -### Success Criteria: - -#### Automated Verification: -- [x] `pnpm check` passes (new file compiles, no unused exports yet — will be used in phase 3) - -#### Manual Verification: -- [ ] N/A — component not yet wired into the UI - -**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase. - ---- - -## Phase 3: Rework ActionBar - -### Overview -Replace the icon button cluster with: (1) an Eye toggle on the input that switches between add mode and browse mode, (2) a conditional Roll All Initiative button, and (3) the overflow menu for infrequent actions. - -### Changes Required: - -#### [x] 1. Update ActionBarProps -**File**: `apps/web/src/components/action-bar.tsx` -**Changes**: Add new props, keep existing ones needed for overflow menu items: -```tsx -interface ActionBarProps { - // ... existing props stay ... - onRollAllInitiative?: () => void; // new — moved from top bar - showRollAllInitiative?: boolean; // new — conditional visibility - onOpenSourceManager?: () => void; // new — moved from top bar -} -``` - -#### [x] 2. Add browse mode state -**File**: `apps/web/src/components/action-bar.tsx` -**Changes**: Replace the separate viewer state (viewerOpen, viewerQuery, viewerResults, viewerIndex, viewerRef, viewerInputRef — lines 66-71) with a single `browseMode` boolean: - -```tsx -const [browseMode, setBrowseMode] = useState(false); -``` - -Remove all viewer-specific state variables and handlers: -- `viewerOpen`, `viewerQuery`, `viewerResults`, `viewerIndex` (lines 66-69) -- `viewerRef`, `viewerInputRef` (lines 70-71) -- `openViewer`, `closeViewer` (lines 189-202) -- `handleViewerQueryChange`, `handleViewerSelect`, `handleViewerKeyDown` (lines 204-236) -- The viewer click-outside effect (lines 239-248) - -#### [x] 3. Rework the input area with Eye toggle -**File**: `apps/web/src/components/action-bar.tsx` -**Changes**: Add an Eye icon button inside the input wrapper that toggles browse mode. When browse mode is active: -- Placeholder changes to "Search stat blocks..." -- Typing calls `bestiarySearch` but selecting a result calls `onViewStatBlock` instead of queuing/adding -- The suggestion dropdown shows results but clicking opens stat block panel instead of adding -- Add button and custom fields (Init/AC/MaxHP) are hidden -- D20 button is hidden - -When toggling browse mode off, clear the input and suggestions. - -The Eye icon sits to the right of the input inside the `relative flex-1` wrapper: -```tsx -
- handleNameChange(e.target.value)} - onKeyDown={browseMode ? handleBrowseKeyDown : handleKeyDown} - placeholder={browseMode ? "Search stat blocks..." : "+ Add combatants"} - className="max-w-xs pr-8" - autoFocus={autoFocus} - /> - {bestiaryLoaded && onViewStatBlock && ( - - )} - {/* suggestion dropdown — behavior changes based on browseMode */} -
-``` - -Import `cn` from `../../lib/utils` (already used by other components). - -#### [x] 4. Update suggestion dropdown for browse mode -**File**: `apps/web/src/components/action-bar.tsx` -**Changes**: In browse mode, the suggestion dropdown behaves differently: -- No "Add as custom" row at the top -- No player character matches section -- No queuing (plus/minus/confirm) — clicking a result calls `onViewStatBlock` and exits browse mode -- Keyboard Enter on a highlighted result calls `onViewStatBlock` and exits browse mode - -Add a `handleBrowseKeyDown` handler: -```tsx -const handleBrowseKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Escape") { - setBrowseMode(false); - setNameInput(""); - setSuggestions([]); - setSuggestionIndex(-1); - return; - } - if (suggestions.length === 0) return; - if (e.key === "ArrowDown") { - e.preventDefault(); - setSuggestionIndex((i) => (i < suggestions.length - 1 ? i + 1 : 0)); - } else if (e.key === "ArrowUp") { - e.preventDefault(); - setSuggestionIndex((i) => (i > 0 ? i - 1 : suggestions.length - 1)); - } else if (e.key === "Enter" && suggestionIndex >= 0) { - e.preventDefault(); - onViewStatBlock?.(suggestions[suggestionIndex]); - setBrowseMode(false); - setNameInput(""); - setSuggestions([]); - setSuggestionIndex(-1); - } -}; -``` - -In the suggestion dropdown JSX, conditionally render based on `browseMode`: -- Browse mode: simple list of creature results, click → `onViewStatBlock` + exit browse mode -- Add mode: existing behavior (custom row, PC matches, queuing) - -#### [x] 5. Replace icon button cluster with D20 + overflow menu -**File**: `apps/web/src/components/action-bar.tsx` -**Changes**: Replace the `div.flex.items-center.gap-0` block (lines 443-529) containing Users, Eye, and Import buttons with: - -```tsx -{!browseMode && ( - <> - - {showRollAllInitiative && onRollAllInitiative && ( - - )} - -)} - -``` - -Build the `overflowItems` array from props: -```tsx -const overflowItems: OverflowMenuItem[] = []; -if (onManagePlayers) { - overflowItems.push({ - icon: , - label: "Player Characters", - onClick: onManagePlayers, - }); -} -if (onOpenSourceManager) { - overflowItems.push({ - icon: , - label: "Manage Sources", - onClick: onOpenSourceManager, - }); -} -if (bestiaryLoaded && onBulkImport) { - overflowItems.push({ - icon: , - label: "Bulk Import", - onClick: onBulkImport, - disabled: bulkImportDisabled, - }); -} -``` - -#### [x] 6. Clean up imports -**File**: `apps/web/src/components/action-bar.tsx` -**Changes**: -- Add imports: `D20Icon`, `OverflowMenu` + `OverflowMenuItem`, `Library` from lucide-react, `cn` from utils -- Remove imports that are no longer needed after removing the standalone viewer: check which of `Eye`, `Import`, `Users` are still used (Eye stays for the toggle, Users and Import stay for overflow item icons, Library is new) -- The `Check`, `Minus`, `Plus` imports stay (used in queuing UI) - -### Success Criteria: - -#### Automated Verification: -- [x] `pnpm check` passes - -#### Manual Verification: -- [ ] Bottom bar shows: input with Eye toggle, Add button, (conditional D20), kebab menu -- [ ] Eye toggle switches input between "add" and "browse" modes -- [ ] In browse mode: typing shows bestiary results, clicking one opens stat block panel, exits browse mode -- [ ] In browse mode: Add button and D20 are hidden, overflow menu stays visible -- [ ] In add mode: existing behavior works (search, queue, custom fields, PC matches) -- [ ] Overflow menu opens/closes on click, closes on Escape and click-outside -- [ ] Overflow menu items (Player Characters, Manage Sources, Bulk Import) trigger correct actions -- [ ] D20 button appears only when bestiary combatants lack initiative, disappears when all have values - -**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase. - ---- - -## Phase 4: Wire Up App.tsx - -### Overview -Pass the new props to ActionBar — roll all initiative handler, conditional visibility flag, and source manager toggle. Remove the now-unused `onOpenSourceManager` callback from the TurnNavigation call (already removed in Phase 1) and ensure sourceManagerOpen toggle is routed through the overflow menu. - -### Changes Required: - -#### [x] 1. Compute showRollAllInitiative flag -**File**: `apps/web/src/App.tsx` -**Changes**: Add a derived boolean that checks if any combatant with a `creatureId` lacks an `initiative` value: - -```tsx -const showRollAllInitiative = encounter.combatants.some( - (c) => c.creatureId != null && c.initiative == null, -); -``` - -Place this near `const isEmpty = ...` (line 241). - -#### [x] 2. Pass new props to both ActionBar instances -**File**: `apps/web/src/App.tsx` -**Changes**: Add to both `` calls (empty state at ~line 269 and populated state at ~line 328): - -```tsx - setSourceManagerOpen((o) => !o)} -/> -``` - -#### [x] 3. Remove stale code -**File**: `apps/web/src/App.tsx` -**Changes**: -- The `onRollAllInitiative` and `onOpenSourceManager` props were already removed from `` in Phase 1 — verify no references remain -- Verify `sourceManagerOpen` state and the `` rendering block (lines 287-291) still work correctly — the SourceManager inline panel is still toggled by the same state, just from a different trigger location - -### Success Criteria: - -#### Automated Verification: -- [x] `pnpm check` passes - -#### Manual Verification: -- [ ] Top bar: only Prev, round badge + name, trash, Next — no D20 or Library buttons -- [ ] Bottom bar: input with Eye toggle, Add, conditional D20, overflow menu -- [ ] Roll All Initiative (D20 in bottom bar): visible when bestiary creatures lack initiative, hidden after rolling -- [ ] Overflow → Player Characters: opens player management modal -- [ ] Overflow → Manage Sources: toggles source manager panel (same as before, just different trigger) -- [ ] Overflow → Bulk Import: opens bulk import mode -- [ ] Browse mode (Eye toggle): search stat blocks without adding, selecting opens panel -- [ ] Clear encounter (top bar trash): still works with two-click confirmation -- [ ] All animations (bar transitions) unchanged -- [ ] Empty state: ActionBar centered with all functionality accessible - -**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase. - ---- - -## Testing Strategy - -### Unit Tests: -- No domain/application changes — existing tests should pass unchanged -- `pnpm check` covers typecheck + lint + existing test suite - -### Manual Testing Steps: -1. Start with empty encounter — verify ActionBar is centered with Eye toggle and overflow menu -2. Add a bestiary creature — verify D20 appears in bottom bar, top bar slides in with just 4 elements -3. Click D20 → initiative rolls → D20 disappears from bottom bar -4. Toggle Eye → input switches to browse mode → search and select → stat block opens → exits browse mode -5. Open overflow menu → click each item → verify correct modal/panel opens -6. Click trash in top bar → confirm → encounter clears, back to empty state -7. Add custom creature (no creatureId) → D20 should not appear (no bestiary creatures) -8. Add mix of custom + bestiary creatures → D20 visible → roll all → D20 hidden - -## Performance Considerations - -None — this is a pure UI reorganization with no new data fetching, state management changes, or rendering overhead. The `showRollAllInitiative` computation is a simple `.some()` over the combatant array, which is negligible. - -## References - -- Research: `docs/agents/research/2026-03-13-action-bars-and-buttons.md` -- Top bar: `apps/web/src/components/turn-navigation.tsx` -- Bottom bar: `apps/web/src/components/action-bar.tsx` -- App layout: `apps/web/src/App.tsx` -- Button: `apps/web/src/components/ui/button.tsx` -- ConfirmButton: `apps/web/src/components/ui/confirm-button.tsx` -- Roll all use case: `packages/application/src/roll-all-initiative-use-case.ts` -- Combatant type: `packages/domain/src/types.ts` diff --git a/package.json b/package.json index 44af0f4..8387887 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,11 @@ { "private": true, "packageManager": "pnpm@10.6.0", + "pnpm": { + "overrides": { + "undici": ">=7.24.0" + } + }, "devDependencies": { "@biomejs/biome": "2.0.0", "@vitest/coverage-v8": "^3.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0a45ee..08887f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + undici: '>=7.24.0' + importers: .: @@ -2011,8 +2014,8 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} - undici@7.22.0: - resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==} + undici@7.24.2: + resolution: {integrity: sha512-P9J1HWYV/ajFr8uCqk5QixwiRKmB1wOamgS0e+o2Z4A44Ej2+thFVRLG/eA7qprx88XXhnV5Bl8LHXTURpzB3Q==} engines: {node: '>=20.18.1'} universalify@2.0.1: @@ -3420,7 +3423,7 @@ snapshots: saxes: 6.0.0 symbol-tree: 3.2.4 tough-cookie: 6.0.0 - undici: 7.22.0 + undici: 7.24.2 w3c-xmlserializer: 5.0.0 webidl-conversions: 8.0.1 whatwg-mimetype: 5.0.0 @@ -3973,7 +3976,7 @@ snapshots: undici-types@7.18.2: {} - undici@7.22.0: {} + undici@7.24.2: {} universalify@2.0.1: {}