import type { CreatureId } from "@initiative/domain"; import { PanelRightClose, Pin, PinOff } from "lucide-react"; import type { ReactNode } from "react"; import { useEffect, useState } from "react"; import { useBestiaryContext } from "../contexts/bestiary-context.js"; import { useSidePanelContext } from "../contexts/side-panel-context.js"; import { useSwipeToDismiss } from "../hooks/use-swipe-to-dismiss.js"; import { cn } from "../lib/utils.js"; import { BulkImportPrompt } from "./bulk-import-prompt.js"; import { SourceFetchPrompt } from "./source-fetch-prompt.js"; import { SourceManager } from "./source-manager.js"; import { StatBlock } from "./stat-block.js"; import { Button } from "./ui/button.js"; interface StatBlockPanelProps { panelRole: "browse" | "pinned"; side: "left" | "right"; } function extractSourceCode(cId: CreatureId): string { const colonIndex = cId.indexOf(":"); if (colonIndex === -1) return ""; return cId.slice(0, colonIndex).toUpperCase(); } function CollapsedTab({ creatureName, side, onToggleCollapse, }: Readonly<{ creatureName: string; side: "left" | "right"; onToggleCollapse: () => void; }>) { return ( ); } function PanelHeader({ panelRole, showPinButton, onToggleCollapse, onPin, onUnpin, }: Readonly<{ panelRole: "browse" | "pinned"; showPinButton: boolean; onToggleCollapse: () => void; onPin: () => void; onUnpin: () => void; }>) { return (
{panelRole === "browse" && ( )}
{panelRole === "browse" && showPinButton && ( )} {panelRole === "pinned" && ( )}
); } function DesktopPanel({ isCollapsed, side, creatureName, panelRole, showPinButton, onToggleCollapse, onPin, onUnpin, children, }: Readonly<{ isCollapsed: boolean; side: "left" | "right"; creatureName: string; panelRole: "browse" | "pinned"; showPinButton: boolean; onToggleCollapse: () => void; onPin: () => void; onUnpin: () => void; children: ReactNode; }>) { const sideClasses = side === "left" ? "left-0 border-r" : "right-0 border-l"; const collapsedTranslate = side === "right" ? "translate-x-[calc(100%-40px)]" : "translate-x-[calc(-100%+40px)]"; return (
{isCollapsed ? ( ) : ( <>
{children}
)}
); } function MobileDrawer({ onDismiss, children, }: Readonly<{ onDismiss: () => void; children: ReactNode; }>) { const { offsetX, isSwiping, handlers } = useSwipeToDismiss(onDismiss); return (
{children}
); } function usePanelRole(panelRole: "browse" | "pinned") { const sidePanel = useSidePanelContext(); const { getCreature } = useBestiaryContext(); const creatureId = panelRole === "browse" ? sidePanel.selectedCreatureId : sidePanel.pinnedCreatureId; const creature = creatureId ? (getCreature(creatureId) ?? null) : null; const isBrowse = panelRole === "browse"; return { creatureId, creature, isCollapsed: isBrowse ? sidePanel.isRightPanelCollapsed : false, onToggleCollapse: isBrowse ? sidePanel.toggleCollapse : () => {}, onDismiss: isBrowse ? sidePanel.dismissPanel : () => {}, onPin: isBrowse ? sidePanel.togglePin : () => {}, onUnpin: panelRole === "pinned" ? sidePanel.unpin : () => {}, showPinButton: isBrowse && sidePanel.isWideDesktop && !!creature, bulkImportMode: isBrowse && sidePanel.bulkImportMode, sourceManagerMode: isBrowse && sidePanel.sourceManagerMode, }; } export function StatBlockPanel({ panelRole, side, }: Readonly) { const { isSourceCached } = useBestiaryContext(); const { creatureId, creature, isCollapsed, onToggleCollapse, onDismiss, onPin, onUnpin, showPinButton, bulkImportMode, sourceManagerMode, } = usePanelRole(panelRole); const [isDesktop, setIsDesktop] = useState( () => globalThis.matchMedia("(min-width: 1024px)").matches, ); const [needsFetch, setNeedsFetch] = useState(false); const [checkingCache, setCheckingCache] = useState(false); useEffect(() => { const mq = globalThis.matchMedia("(min-width: 1024px)"); const handler = (e: MediaQueryListEvent) => setIsDesktop(e.matches); mq.addEventListener("change", handler); return () => mq.removeEventListener("change", handler); }, []); useEffect(() => { if (!creatureId || creature) { setNeedsFetch(false); return; } const sourceCode = extractSourceCode(creatureId); if (!sourceCode) { setNeedsFetch(false); return; } setCheckingCache(true); void isSourceCached(sourceCode).then((cached) => { setNeedsFetch(!cached); setCheckingCache(false); }); }, [creatureId, creature, isSourceCached]); if (!creatureId && !bulkImportMode && !sourceManagerMode) return null; const sourceCode = creatureId ? extractSourceCode(creatureId) : ""; const handleSourceLoaded = () => { setNeedsFetch(false); }; const renderContent = () => { if (sourceManagerMode) { return ; } if (bulkImportMode) { return ; } if (checkingCache) { return (
Loading...
); } if (creature) { return ; } if (needsFetch && sourceCode) { return ( ); } return (
No stat block available
); }; let fallbackName = "Creature"; if (sourceManagerMode) fallbackName = "Sources"; else if (bulkImportMode) fallbackName = "Import All Sources"; const creatureName = creature?.name ?? fallbackName; if (isDesktop) { return ( {renderContent()} ); } if (panelRole === "pinned" || isCollapsed) return null; return {renderContent()}; }