--- date: "2026-03-13T14:39:15.661886+00:00" git_commit: 75778884bd1be7d135b2f5ea9b8a8e77a0149f7b branch: main topic: "Action Bars Setup — Top Bar and Bottom Bar Buttons" tags: [research, codebase, action-bar, turn-navigation, layout, buttons] status: complete --- # Research: Action Bars Setup — Top Bar and Bottom Bar Buttons ## Research Question How are the top and bottom action bars set up, what buttons do they contain, and how are their actions wired? ## Summary The application has two primary bar components that frame the encounter tracker UI: 1. **Top bar** — `TurnNavigation` (`turn-navigation.tsx`) — turn controls, round/combatant display, and encounter-wide actions. 2. **Bottom bar** — `ActionBar` (`action-bar.tsx`) — combatant input, bestiary search, stat block browsing, bulk import, and player character management. Both bars share the same visual container styling (`flex items-center gap-3 rounded-md border border-border bg-card px-4 py-3`). They are laid out in `App.tsx` within a flex column, with a scrollable combatant list between them. When the encounter is empty, only the ActionBar is shown (centered in the viewport); the TurnNavigation appears with an animation when the first combatant is added. ## Detailed Findings ### Layout Structure (`App.tsx:243-344`) The bars live inside a `max-w-2xl` centered column: ``` ┌──────────────────────────────────┐ │ TurnNavigation (pt-8, shrink-0) │ ← top bar, conditionally shown ├──────────────────────────────────┤ │ SourceManager (optional inline) │ ← toggled by Library button in top bar ├──────────────────────────────────┤ │ Combatant list (flex-1, │ ← scrollable │ overflow-y-auto) │ ├──────────────────────────────────┤ │ ActionBar (pb-8, shrink-0) │ ← bottom bar └──────────────────────────────────┘ ``` **Empty state**: When `encounter.combatants.length === 0`, the top bar is hidden and the ActionBar is vertically centered in a `flex items-center justify-center` wrapper with `pb-[15%]` offset. It receives `autoFocus` in this state. **Animation** (`useActionBarAnimation`, `App.tsx:30-66`): Manages transitions between empty and populated states: - Empty → populated: ActionBar plays `animate-settle-to-bottom`, TurnNavigation plays `animate-slide-down-in`. - Populated → empty: ActionBar plays `animate-rise-to-center`, TurnNavigation plays `animate-slide-up-out` (with `absolute` positioning during exit). The `showTopBar` flag is `true` when either combatants exist or the top bar exit animation is still running. ### Top Bar — TurnNavigation (`turn-navigation.tsx`) **Props interface** (`turn-navigation.tsx:7-14`): - `encounter: Encounter` — full encounter state - `onAdvanceTurn`, `onRetreatTurn` — turn navigation callbacks - `onClearEncounter` — destructive clear with confirmation - `onRollAllInitiative` — rolls initiative for all combatants - `onOpenSourceManager` — toggles source manager panel **Layout**: Left–Center–Right structure: ``` [ ◀ Prev ] | [ R1 Active Combatant Name ] | [ 🎲 📚 🗑 ] [ Next ▶ ] ``` **Buttons (left to right)**: | # | Icon | Component | Variant | Action | Disabled when | |---|------|-----------|---------|--------|---------------| | 1 | `StepBack` | `Button` | default | `onRetreatTurn` | No combatants OR at round 1 index 0 | | 2 | `D20Icon` | `Button` | ghost | `onRollAllInitiative` | Never | | 3 | `Library` | `Button` | ghost | `onOpenSourceManager` | Never | | 4 | `Trash2` | `ConfirmButton` | — | `onClearEncounter` | No combatants | | 5 | `StepForward` | `Button` | default | `onAdvanceTurn` | No combatants | **Center section** (`turn-navigation.tsx:40-49`): Displays a round badge (`R{n}` in a `rounded-full bg-muted` span) and the active combatant's name (truncated). Falls back to "No combatants" in muted text. **Button grouping**: Buttons 2-4 are grouped in a `gap-0` div (tight spacing), while button 5 (Next) is separated by the outer `gap-3`. **Wiring in App.tsx** (`App.tsx:251-258`): - `onAdvanceTurn` → `advanceTurn` from `useEncounter()` - `onRetreatTurn` → `retreatTurn` from `useEncounter()` - `onClearEncounter` → `clearEncounter` from `useEncounter()` - `onRollAllInitiative` → `handleRollAllInitiative` → calls `rollAllInitiativeUseCase(makeStore(), rollDice, getCreature)` - `onOpenSourceManager` → toggles `sourceManagerOpen` state ### Bottom Bar — ActionBar (`action-bar.tsx`) **Props interface** (`action-bar.tsx:20-36`): - `onAddCombatant` — adds custom combatant with optional init/AC/maxHP - `onAddFromBestiary` — adds creature from search result - `bestiarySearch` — search function returning `SearchResult[]` - `bestiaryLoaded` — whether bestiary index is loaded - `onViewStatBlock` — opens stat block panel for a creature - `onBulkImport` — triggers bulk source import mode - `bulkImportDisabled` — disables import button during loading - `inputRef` — external ref to the name input - `playerCharacters` — list of player characters for quick-add - `onAddFromPlayerCharacter` — adds a player character to encounter - `onManagePlayers` — opens player management modal - `autoFocus` — auto-focuses input (used in empty state) **Layout**: Form with input, contextual fields, submit button, and action icons: ``` [ + Add combatants... ] [ Init ] [ AC ] [ MaxHP ] [ Add ] [ 👥 👁 📥 ] ``` The Init/AC/MaxHP fields only appear when the input has 2+ characters and no bestiary suggestions are showing. **Buttons (left to right)**: | # | Icon | Component | Variant | Action | Condition | |---|------|-----------|---------|--------|-----------| | 1 | — | `Button` | sm | Form submit → `handleAdd` | Always shown | | 2 | `Users` | `Button` | ghost | `onManagePlayers` | Only if `onManagePlayers` provided | | 3 | `Eye` | `Button` | ghost | Toggle stat block viewer dropdown | Only if `bestiaryLoaded && onViewStatBlock` | | 4 | `Import` | `Button` | ghost | `onBulkImport` | Only if `bestiaryLoaded && onBulkImport` | **Button grouping**: Buttons 2-4 are grouped in a `gap-0` div, mirroring the top bar's icon button grouping. **Suggestion dropdown** (`action-bar.tsx:267-410`): Opens above the input when 2+ chars are typed and results exist. Contains: - A "Add as custom" escape row at the top (with `Esc` keyboard hint) - **Players section**: Lists matching player characters with colored icons; clicking adds them directly via `onAddFromPlayerCharacter` - **Bestiary section**: Lists search results; clicking queues a creature. Queued creatures show: - `Minus` button — decrements count (removes queue at 0) - Count badge — current queued count - `Plus` button — increments count - `Check` button — confirms and adds all queued copies **Stat block viewer dropdown** (`action-bar.tsx:470-513`): A separate search dropdown anchored to the Eye button. Has its own input, search results, and keyboard navigation. Selecting a result calls `onViewStatBlock`. **Keyboard handling** (`action-bar.tsx:168-186`): - Arrow Up/Down — navigate suggestion list - Enter — queue selected suggestion or confirm queued batch - Escape — clear suggestions and queue **Wiring in App.tsx** (`App.tsx:269-282` and `328-340`): - `onAddCombatant` → `addCombatant` from `useEncounter()` - `onAddFromBestiary` → `handleAddFromBestiary` → `addFromBestiary` from `useEncounter()` - `bestiarySearch` → `search` from `useBestiary()` - `onViewStatBlock` → `handleViewStatBlock` → constructs `CreatureId` and sets `selectedCreatureId` - `onBulkImport` → `handleBulkImport` → sets `bulkImportMode` and clears selection - `onAddFromPlayerCharacter` → `addFromPlayerCharacter` from `useEncounter()` - `onManagePlayers` → opens `managementOpen` state (shows `PlayerManagement` modal) ### Shared UI Primitives **`Button`** (`ui/button.tsx`): CVA-based component with variants (`default`, `outline`, `ghost`) and sizes (`default`, `sm`, `icon`). Both bars use `size="icon"` with `variant="ghost"` for their icon button clusters, and `size="icon"` with default variant for the primary navigation buttons (Prev/Next in top bar). **`ConfirmButton`** (`ui/confirm-button.tsx`): Two-click destructive action button. First click shows a red pulsing confirmation state with a Check icon; second click fires `onConfirm`. Auto-reverts after 5 seconds. Supports Escape and click-outside cancellation. Used for Clear Encounter in the top bar. ### Hover Color Convention Both bars use consistent hover color classes on their ghost icon buttons: - `hover:text-hover-action` — used on the D20 (roll initiative) button, suggesting an action/accent color - `hover:text-hover-neutral` — used on Library, Users, Eye, Import buttons, suggesting a neutral/informational color ## Code References - `apps/web/src/components/turn-navigation.tsx` — Top bar component (93 lines) - `apps/web/src/components/action-bar.tsx` — Bottom bar component (533 lines) - `apps/web/src/App.tsx:30-66` — `useActionBarAnimation` hook for bar transitions - `apps/web/src/App.tsx:243-344` — Layout structure with both bars - `apps/web/src/components/ui/button.tsx` — Shared Button component - `apps/web/src/components/ui/confirm-button.tsx` — Two-step confirmation button - `apps/web/src/components/d20-icon.tsx` — Custom D20 dice SVG icon ## Architecture Documentation The bars follow the app's adapter-layer convention: they are pure presentational React components that receive all behavior via callback props. No business logic lives in either bar — they delegate to handlers defined in `App.tsx`, which in turn call use-case functions from the application layer or manipulate local UI state. Both bars are rendered twice in `App.tsx` (once in the empty-state branch, once in the populated branch) rather than being conditionally repositioned, which simplifies the animation logic. The `ActionBar` is the more complex of the two, managing multiple pieces of local state (input value, suggestions, queued creatures, custom fields, stat block viewer) while `TurnNavigation` is fully stateless — all its data comes from the `encounter` prop.