Files
initiative/docs/agents/research/2026-03-13-action-bars-and-buttons.md
Lukas bd39808000
All checks were successful
CI / check (push) Successful in 48s
CI / build-image (push) Successful in 18s
Declutter action bars: overflow menu, browse toggle, conditional D20
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>
2026-03-13 16:31:25 +01:00

10 KiB
Raw Permalink Blame History

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
research
codebase
action-bar
turn-navigation
layout
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:

  1. Top barTurnNavigation (turn-navigation.tsx) — turn controls, round/combatant display, and encounter-wide actions.
  2. Bottom barActionBar (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: LeftCenterRight 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):

  • onAdvanceTurnadvanceTurn from useEncounter()
  • onRetreatTurnretreatTurn from useEncounter()
  • onClearEncounterclearEncounter from useEncounter()
  • onRollAllInitiativehandleRollAllInitiative → 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):

  • onAddCombatantaddCombatant from useEncounter()
  • onAddFromBestiaryhandleAddFromBestiaryaddFromBestiary from useEncounter()
  • bestiarySearchsearch from useBestiary()
  • onViewStatBlockhandleViewStatBlock → constructs CreatureId and sets selectedCreatureId
  • onBulkImporthandleBulkImport → sets bulkImportMode and clears selection
  • onAddFromPlayerCharacteraddFromPlayerCharacter 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-66useActionBarAnimation 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.