import type { Creature } from "@initiative/domain"; import { useCallback, useEffect, useRef, useState } from "react"; import { ActionBar } from "./components/action-bar"; import { CombatantRow } from "./components/combatant-row"; import { StatBlockPanel } from "./components/stat-block-panel"; import { TurnNavigation } from "./components/turn-navigation"; import { useBestiary } from "./hooks/use-bestiary"; import { useEncounter } from "./hooks/use-encounter"; export function App() { const { encounter, advanceTurn, retreatTurn, addCombatant, removeCombatant, editCombatant, setInitiative, setHp, adjustHp, setAc, toggleCondition, toggleConcentration, addFromBestiary, } = useEncounter(); const { search, getCreature, isLoaded } = useBestiary(); const [selectedCreature, setSelectedCreature] = useState( null, ); const [suggestions, setSuggestions] = useState([]); const handleAddFromBestiary = useCallback( (creature: Creature) => { addFromBestiary(creature); setSelectedCreature(creature); }, [addFromBestiary], ); const handleShowStatBlock = useCallback((creature: Creature) => { setSelectedCreature(creature); }, []); const handleCombatantStatBlock = useCallback( (creatureId: string) => { const creature = getCreature( creatureId as import("@initiative/domain").CreatureId, ); if (creature) setSelectedCreature(creature); }, [getCreature], ); const handleSearchChange = useCallback( (query: string) => { if (!isLoaded || query.length < 2) { setSuggestions([]); return; } setSuggestions(search(query)); }, [isLoaded, search], ); // Auto-scroll to the active combatant when the turn changes const activeRowRef = useRef(null); useEffect(() => { activeRowRef.current?.scrollIntoView({ block: "nearest", behavior: "smooth", }); }, [encounter.activeIndex]); // Auto-show stat block for the active combatant when turn changes, // but only when the viewport is wide enough to show it alongside the tracker useEffect(() => { if (!window.matchMedia("(min-width: 1024px)").matches) return; const active = encounter.combatants[encounter.activeIndex]; if (!active?.creatureId || !isLoaded) return; const creature = getCreature( active.creatureId as import("@initiative/domain").CreatureId, ); if (creature) setSelectedCreature(creature); }, [encounter.activeIndex, encounter.combatants, getCreature, isLoaded]); return (
{/* Turn Navigation — fixed at top */}
{/* Scrollable area — combatant list */}

Initiative Tracker

{encounter.combatants.length === 0 ? (

No combatants yet — add one to get started

) : ( encounter.combatants.map((c, i) => ( handleCombatantStatBlock(c.creatureId as string) : undefined } /> )) )}
{/* Action Bar — fixed at bottom */}
{/* Stat Block Panel */} setSelectedCreature(null)} />
); }