09a801487d
Weak/Normal/Elite toggle in PF2e stat block header applies standard adjustments (level, AC, HP, saves, Perception, attacks, damage) to individual combatants. Adjusted stats are highlighted blue (elite) or red (weak). Persisted via creatureAdjustment field on Combatant. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
119 lines
3.2 KiB
TypeScript
119 lines
3.2 KiB
TypeScript
import type { CombatantId, CreatureId } from "@initiative/domain";
|
|
import { useCallback, useEffect, useState } from "react";
|
|
|
|
type PanelView =
|
|
| { mode: "closed" }
|
|
| { mode: "creature"; creatureId: CreatureId; combatantId?: CombatantId }
|
|
| { mode: "bulk-import" }
|
|
| { mode: "source-manager" };
|
|
|
|
interface SidePanelState {
|
|
panelView: PanelView;
|
|
selectedCreatureId: CreatureId | null;
|
|
selectedCombatantId: CombatantId | null;
|
|
bulkImportMode: boolean;
|
|
sourceManagerMode: boolean;
|
|
isRightPanelCollapsed: boolean;
|
|
pinnedCreatureId: CreatureId | null;
|
|
isWideDesktop: boolean;
|
|
}
|
|
|
|
interface SidePanelActions {
|
|
showCreature: (creatureId: CreatureId, combatantId?: CombatantId) => void;
|
|
updateCreature: (creatureId: CreatureId, combatantId?: CombatantId) => void;
|
|
showBulkImport: () => void;
|
|
showSourceManager: () => void;
|
|
dismissPanel: () => void;
|
|
toggleCollapse: () => void;
|
|
togglePin: () => void;
|
|
unpin: () => void;
|
|
}
|
|
|
|
export function useSidePanelState(): SidePanelState & SidePanelActions {
|
|
const [panelView, setPanelView] = useState<PanelView>({ mode: "closed" });
|
|
const [isRightPanelCollapsed, setIsRightPanelCollapsed] = useState(false);
|
|
const [pinnedCreatureId, setPinnedCreatureId] = useState<CreatureId | null>(
|
|
null,
|
|
);
|
|
const [isWideDesktop, setIsWideDesktop] = useState(
|
|
() => globalThis.matchMedia("(min-width: 1280px)").matches,
|
|
);
|
|
|
|
useEffect(() => {
|
|
const mq = globalThis.matchMedia("(min-width: 1280px)");
|
|
const handler = (e: MediaQueryListEvent) => setIsWideDesktop(e.matches);
|
|
mq.addEventListener("change", handler);
|
|
return () => mq.removeEventListener("change", handler);
|
|
}, []);
|
|
|
|
const selectedCreatureId =
|
|
panelView.mode === "creature" ? panelView.creatureId : null;
|
|
|
|
const selectedCombatantId =
|
|
panelView.mode === "creature" ? (panelView.combatantId ?? null) : null;
|
|
|
|
const showCreature = useCallback(
|
|
(creatureId: CreatureId, combatantId?: CombatantId) => {
|
|
setPanelView({ mode: "creature", creatureId, combatantId });
|
|
setIsRightPanelCollapsed(false);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const updateCreature = useCallback(
|
|
(creatureId: CreatureId, combatantId?: CombatantId) => {
|
|
setPanelView({ mode: "creature", creatureId, combatantId });
|
|
},
|
|
[],
|
|
);
|
|
|
|
const showBulkImport = useCallback(() => {
|
|
setPanelView({ mode: "bulk-import" });
|
|
setIsRightPanelCollapsed(false);
|
|
}, []);
|
|
|
|
const showSourceManager = useCallback(() => {
|
|
setPanelView({ mode: "source-manager" });
|
|
setIsRightPanelCollapsed(false);
|
|
}, []);
|
|
|
|
const dismissPanel = useCallback(() => {
|
|
setPanelView({ mode: "closed" });
|
|
}, []);
|
|
|
|
const toggleCollapse = useCallback(() => {
|
|
setIsRightPanelCollapsed((f) => !f);
|
|
}, []);
|
|
|
|
const togglePin = useCallback(() => {
|
|
if (selectedCreatureId) {
|
|
setPinnedCreatureId((prev) =>
|
|
prev === selectedCreatureId ? null : selectedCreatureId,
|
|
);
|
|
}
|
|
}, [selectedCreatureId]);
|
|
|
|
const unpin = useCallback(() => {
|
|
setPinnedCreatureId(null);
|
|
}, []);
|
|
|
|
return {
|
|
panelView,
|
|
selectedCreatureId,
|
|
selectedCombatantId,
|
|
bulkImportMode: panelView.mode === "bulk-import",
|
|
sourceManagerMode: panelView.mode === "source-manager",
|
|
isRightPanelCollapsed,
|
|
pinnedCreatureId,
|
|
isWideDesktop,
|
|
showCreature,
|
|
updateCreature,
|
|
showBulkImport,
|
|
showSourceManager,
|
|
dismissPanel,
|
|
toggleCollapse,
|
|
togglePin,
|
|
unpin,
|
|
};
|
|
}
|