--- 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`