Top bar stripped to turn navigation only (Prev, round badge, Clear, Next). Roll All Initiative, Manage Sources, and Bulk Import moved to a new overflow menu in the bottom bar. Player Characters also moved there. Browse stat blocks is now an Eye/EyeOff toggle inside the search input that switches between add mode and browse mode. Add button only appears when entering a custom creature name. Roll All Initiative button shows conditionally — only when bestiary creatures lack initiative values. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
10 KiB
date, git_commit, branch, topic, tags, status
| date | git_commit | branch | topic | tags | status | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| 2026-03-13T14:39:15.661886+00:00 | 75778884bd |
main | Action Bars Setup — Top Bar and Bottom Bar Buttons |
|
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:
- Top bar —
TurnNavigation(turn-navigation.tsx) — turn controls, round/combatant display, and encounter-wide actions. - 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 playsanimate-slide-down-in. - Populated → empty: ActionBar plays
animate-rise-to-center, TurnNavigation playsanimate-slide-up-out(withabsolutepositioning 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 stateonAdvanceTurn,onRetreatTurn— turn navigation callbacksonClearEncounter— destructive clear with confirmationonRollAllInitiative— rolls initiative for all combatantsonOpenSourceManager— 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→advanceTurnfromuseEncounter()onRetreatTurn→retreatTurnfromuseEncounter()onClearEncounter→clearEncounterfromuseEncounter()onRollAllInitiative→handleRollAllInitiative→ callsrollAllInitiativeUseCase(makeStore(), rollDice, getCreature)onOpenSourceManager→ togglessourceManagerOpenstate
Bottom Bar — ActionBar (action-bar.tsx)
Props interface (action-bar.tsx:20-36):
onAddCombatant— adds custom combatant with optional init/AC/maxHPonAddFromBestiary— adds creature from search resultbestiarySearch— search function returningSearchResult[]bestiaryLoaded— whether bestiary index is loadedonViewStatBlock— opens stat block panel for a creatureonBulkImport— triggers bulk source import modebulkImportDisabled— disables import button during loadinginputRef— external ref to the name inputplayerCharacters— list of player characters for quick-addonAddFromPlayerCharacter— adds a player character to encounteronManagePlayers— opens player management modalautoFocus— 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
Esckeyboard 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:
Minusbutton — decrements count (removes queue at 0)- Count badge — current queued count
Plusbutton — increments countCheckbutton — 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→addCombatantfromuseEncounter()onAddFromBestiary→handleAddFromBestiary→addFromBestiaryfromuseEncounter()bestiarySearch→searchfromuseBestiary()onViewStatBlock→handleViewStatBlock→ constructsCreatureIdand setsselectedCreatureIdonBulkImport→handleBulkImport→ setsbulkImportModeand clears selectiononAddFromPlayerCharacter→addFromPlayerCharacterfromuseEncounter()onManagePlayers→ opensmanagementOpenstate (showsPlayerManagementmodal)
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 colorhover: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—useActionBarAnimationhook for bar transitionsapps/web/src/App.tsx:243-344— Layout structure with both barsapps/web/src/components/ui/button.tsx— Shared Button componentapps/web/src/components/ui/confirm-button.tsx— Two-step confirmation buttonapps/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.