import type { PlayerCharacter } from "@initiative/domain"; import { Check, Eye, EyeOff, Import, Library, Minus, Plus, Settings, Users, } from "lucide-react"; import React, { type RefObject, useCallback, useState } from "react"; import type { SearchResult } from "../contexts/bestiary-context.js"; import { useBulkImportContext } from "../contexts/bulk-import-context.js"; import { useEncounterContext } from "../contexts/encounter-context.js"; import { useInitiativeRollsContext } from "../contexts/initiative-rolls-context.js"; import { creatureKey, type QueuedCreature, type SuggestionActions, useActionBarState, } from "../hooks/use-action-bar-state.js"; import { useLongPress } from "../hooks/use-long-press.js"; import { cn } from "../lib/utils.js"; import { D20Icon } from "./d20-icon.js"; import { PLAYER_COLOR_HEX, PLAYER_ICON_MAP } from "./player-icon-map.js"; import { RollModeMenu } from "./roll-mode-menu.js"; import { Button } from "./ui/button.js"; import { Input } from "./ui/input.js"; import { OverflowMenu, type OverflowMenuItem } from "./ui/overflow-menu.js"; interface ActionBarProps { inputRef?: RefObject; autoFocus?: boolean; onManagePlayers?: () => void; onOpenSettings?: () => void; } interface AddModeSuggestionsProps { nameInput: string; suggestions: SearchResult[]; pcMatches: PlayerCharacter[]; suggestionIndex: number; queued: QueuedCreature | null; actions: SuggestionActions; } function AddModeSuggestions({ nameInput, suggestions, pcMatches, suggestionIndex, queued, actions, }: Readonly) { return (
{pcMatches.length > 0 && ( <>
Players
    {pcMatches.map((pc) => { const PcIcon = pc.icon ? PLAYER_ICON_MAP[pc.icon] : undefined; const pcColor = pc.color ? PLAYER_COLOR_HEX[pc.color] : undefined; return (
  • ); })}
)} {suggestions.length > 0 && (
    {suggestions.map((result, i) => { const key = creatureKey(result); const isQueued = queued !== null && creatureKey(queued.result) === key; return (
  • {queued.count} ) : ( result.sourceDisplayName )}
  • ); })}
)}
); } interface BrowseSuggestionsProps { suggestions: SearchResult[]; suggestionIndex: number; onSelect: (result: SearchResult) => void; onHover: (index: number) => void; } function BrowseSuggestions({ suggestions, suggestionIndex, onSelect, onHover, }: Readonly) { if (suggestions.length === 0) return null; return (
    {suggestions.map((result, i) => (
  • ))}
); } interface CustomStatFieldsProps { customInit: string; customAc: string; customMaxHp: string; onInitChange: (v: string) => void; onAcChange: (v: string) => void; onMaxHpChange: (v: string) => void; } function CustomStatFields({ customInit, customAc, customMaxHp, onInitChange, onAcChange, onMaxHpChange, }: Readonly) { return (
onInitChange(e.target.value)} placeholder="Init" className="w-16 text-center" /> onAcChange(e.target.value)} placeholder="AC" className="w-16 text-center" /> onMaxHpChange(e.target.value)} placeholder="MaxHP" className="w-18 text-center" />
); } function RollAllButton() { const { hasCreatureCombatants, canRollAllInitiative } = useEncounterContext(); const { handleRollAllInitiative } = useInitiativeRollsContext(); const [menuPos, setMenuPos] = useState<{ x: number; y: number; } | null>(null); const openMenu = useCallback((x: number, y: number) => { setMenuPos({ x, y }); }, []); const longPress = useLongPress( useCallback( (e: React.TouchEvent) => { const touch = e.touches[0]; if (touch) openMenu(touch.clientX, touch.clientY); }, [openMenu], ), ); if (!hasCreatureCombatants) return null; return ( <> {!!menuPos && ( handleRollAllInitiative(mode)} onClose={() => setMenuPos(null)} /> )} ); } function buildOverflowItems(opts: { onManagePlayers?: () => void; onOpenSourceManager?: () => void; bestiaryLoaded: boolean; onBulkImport?: () => void; bulkImportDisabled?: boolean; onOpenSettings?: () => void; }): OverflowMenuItem[] { const items: OverflowMenuItem[] = []; if (opts.onManagePlayers) { items.push({ icon: , label: "Player Characters", onClick: opts.onManagePlayers, }); } if (opts.onOpenSourceManager) { items.push({ icon: , label: "Manage Sources", onClick: opts.onOpenSourceManager, }); } if (opts.bestiaryLoaded && opts.onBulkImport) { items.push({ icon: , label: "Import All Sources", onClick: opts.onBulkImport, disabled: opts.bulkImportDisabled, }); } if (opts.onOpenSettings) { items.push({ icon: , label: "Settings", onClick: opts.onOpenSettings, }); } return items; } export function ActionBar({ inputRef, autoFocus, onManagePlayers, onOpenSettings, }: Readonly) { const { nameInput, suggestions, pcMatches, suggestionIndex, queued, customInit, customAc, customMaxHp, browseMode, bestiaryLoaded, hasSuggestions, showBulkImport, showSourceManager, suggestionActions, handleNameChange, handleKeyDown, handleBrowseKeyDown, handleAdd, handleBrowseSelect, toggleBrowseMode, setCustomInit, setCustomAc, setCustomMaxHp, } = useActionBarState(); const { state: bulkImportState } = useBulkImportContext(); const overflowItems = buildOverflowItems({ onManagePlayers, onOpenSourceManager: showSourceManager, bestiaryLoaded, onBulkImport: showBulkImport, bulkImportDisabled: bulkImportState.status === "loading", onOpenSettings, }); return (
handleNameChange(e.target.value)} onKeyDown={browseMode ? handleBrowseKeyDown : handleKeyDown} placeholder={ browseMode ? "Search stat blocks..." : "+ Add combatants" } className="pr-8" autoFocus={autoFocus} /> {!!bestiaryLoaded && ( )} {!!browseMode && ( )} {!browseMode && hasSuggestions && ( )}
{!browseMode && nameInput.length >= 2 && !hasSuggestions && ( )} {!browseMode && nameInput.length >= 2 && !hasSuggestions && ( )} {overflowItems.length > 0 && }
); }