Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions src/components/encounters/MenuBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,15 @@ interface MenuBarProps {
onShowHiddenChange: (show: boolean) => void
onViewLocations?: () => void
locationsViewActive?: boolean
locationsPoppedOut?: boolean
}

export default function MenuBar({
showHidden,
onShowHiddenChange,
onViewLocations,
locationsViewActive,
locationsPoppedOut,
}: MenuBarProps) {
const theme = useTheme()
const { encounter, updateEncounter } = useEncounter()
Expand Down Expand Up @@ -288,19 +290,23 @@ export default function MenuBar({
<IconButton
onClick={() => onViewLocations?.()}
sx={{
color: "white",
color: locationsPoppedOut ? "warning.main" : "white",
px: { xs: 0.5, sm: 1 },
backgroundColor: locationsViewActive
? "rgba(255, 255, 255, 0.2)"
: "transparent",
backgroundColor:
locationsViewActive || locationsPoppedOut
? "rgba(255, 255, 255, 0.2)"
: "transparent",
borderRadius: 1,
"&:hover": {
backgroundColor: locationsViewActive
? "rgba(255, 255, 255, 0.3)"
: "rgba(255, 255, 255, 0.1)",
backgroundColor:
locationsViewActive || locationsPoppedOut
? "rgba(255, 255, 255, 0.3)"
: "rgba(255, 255, 255, 0.1)",
},
}}
title="View Locations"
title={
locationsPoppedOut ? "Locations (popped out)" : "View Locations"
}
>
<FaMapMarkerAlt size={18} />
</IconButton>
Expand Down
48 changes: 46 additions & 2 deletions src/components/encounters/ShotCounter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,25 @@ import {
SpeedCheckPanel,
} from "@/components/encounters"
import { LocationsPanel } from "@/components/encounters/locations"
import PopOutWindow from "@/components/ui/PopOutWindow"
import { useEncounter } from "@/contexts"
import { useToast } from "@/contexts"
import { useLocalStorage } from "@/contexts/LocalStorageContext"
import { usePopOutWindow } from "@/hooks"
import { getAllVisibleShots } from "@/components/encounters/attacks/shotSorting"

export default function ShotCounter() {
const { encounter, selectedActorId, setSelectedActor } = useEncounter()
const { getLocally, saveLocally } = useLocalStorage()
const { toastError } = useToast()
const [showHidden, setShowHidden] = useState(true)
const [activePanel, setActivePanel] = useState<string | null>(null)

const fightName = encounter?.name || "Fight"
const { popOut, popIn, isPoppedOut, containerEl } = usePopOutWindow(
`Locations - ${fightName}`
)

// Refs for each panel
const attackPanelRef = useRef<HTMLDivElement>(null)
const healPanelRef = useRef<HTMLDivElement>(null)
Expand Down Expand Up @@ -109,8 +118,34 @@ export default function ShotCounter() {
return shot?.character || null
}, [selectedActorId, allVisibleShots])

const handlePopOutLocations = () => {
if (isPoppedOut) {
popIn()
} else {
const result = popOut()
if (result === null) {
toastError("Pop-up blocked. Please allow pop-ups for this site.")
return
}
// Close the inline panel if it's open
if (activePanel === "locations") {
setActivePanel(null)
}
}
}

// Handle action from EncounterActionBar
const handleAction = (action: string) => {
// If locations are popped out and user clicks locations, focus the window
if (action === "locations" && isPoppedOut) {
const result = popOut()
if (result === null) {
toastError("Pop-up blocked. Please allow pop-ups for this site.")
setActivePanel("locations")
}
return
}

// Toggle panel - if clicking same action, close it
if (activePanel === action) {
setActivePanel(null)
Expand Down Expand Up @@ -156,6 +191,7 @@ export default function ShotCounter() {
onShowHiddenChange={handleShowHiddenChange}
onViewLocations={() => handleAction("locations")}
locationsViewActive={activePanel === "locations"}
locationsPoppedOut={isPoppedOut}
/>

{/* Character selector below MenuBar */}
Expand Down Expand Up @@ -253,12 +289,20 @@ export default function ShotCounter() {
</Box>
)}

{activePanel === "locations" && (
{activePanel === "locations" && !isPoppedOut && (
<Box ref={locationsPanelRef}>
<LocationsPanel onClose={handlePanelClose} />
<LocationsPanel
onClose={handlePanelClose}
onPopOut={handlePopOutLocations}
/>
</Box>
)}

{/* Pop-out window for locations */}
<PopOutWindow isPoppedOut={isPoppedOut} containerEl={containerEl}>
<LocationsPanel onClose={popIn} poppedOut />
</PopOutWindow>

{/* Shot List */}
<List>
{visibleShots.map((shot, index) => (
Expand Down
37 changes: 32 additions & 5 deletions src/components/encounters/locations/LocationsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
Map as CanvasIcon,
Add as AddIcon,
Timeline as ConnectIcon,
OpenInNew as OpenInNewIcon,
} from "@mui/icons-material"
import { FaMapLocationDot } from "react-icons/fa6"
import {
Expand Down Expand Up @@ -70,6 +71,8 @@ const ZOOM_STEP = 0.1

interface LocationsPanelProps {
onClose: () => void
poppedOut?: boolean
onPopOut?: () => void
}

type ViewMode = "grid" | "canvas"
Expand Down Expand Up @@ -100,7 +103,11 @@ function rectsOverlap(a: Rect, b: Rect, padding: number = 0): boolean {
* Phase 2: Drag-and-drop support for moving characters between zones.
* Phase 3: Canvas mode with draggable/resizable zones.
*/
export default function LocationsPanel({ onClose }: LocationsPanelProps) {
export default function LocationsPanel({
onClose,
poppedOut,
onPopOut,
}: LocationsPanelProps) {
const { encounter, dispatchEncounter } = useEncounter()
const { client } = useClient()
const { toastSuccess, toastError } = useToast()
Expand Down Expand Up @@ -1255,12 +1262,22 @@ export default function LocationsPanel({ onClose }: LocationsPanelProps) {
return { x: padding, y: maxY + padding }
}

const panelSx = poppedOut
? {
position: "relative" as const,
height: "100vh",
overflow: "auto",
borderRadius: 0,
}
: { position: "relative" as const }

if (loading) {
return (
<BasePanel
title="Locations"
icon={<FaMapLocationDot />}
borderColor="info.main"
sx={panelSx}
>
<Box
sx={{
Expand All @@ -1282,6 +1299,7 @@ export default function LocationsPanel({ onClose }: LocationsPanelProps) {
title="Locations"
icon={<FaMapLocationDot />}
borderColor="error.main"
sx={panelSx}
>
<Alert severity="error">{error}</Alert>
</BasePanel>
Expand All @@ -1293,7 +1311,7 @@ export default function LocationsPanel({ onClose }: LocationsPanelProps) {
title="Locations"
icon={<FaMapLocationDot />}
borderColor="info.main"
sx={{ position: "relative" }}
sx={panelSx}
>
{/* Header controls */}
<Box
Expand Down Expand Up @@ -1354,9 +1372,18 @@ export default function LocationsPanel({ onClose }: LocationsPanelProps) {
</Tooltip>
</ToggleButton>
</ToggleButtonGroup>
<IconButton onClick={onClose} size="small">
<CloseIcon />
</IconButton>
{!poppedOut && onPopOut && (
<Tooltip title="Pop out to separate window">
<IconButton onClick={onPopOut} size="small">
<OpenInNewIcon fontSize="small" />
</IconButton>
</Tooltip>
)}
{!poppedOut && (
<IconButton onClick={onClose} size="small">
<CloseIcon />
</IconButton>
)}
</Box>

{displayData.locations.length === 0 ? (
Expand Down
41 changes: 41 additions & 0 deletions src/components/ui/PopOutWindow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"use client"

import { useMemo } from "react"
import { createPortal } from "react-dom"
import createCache from "@emotion/cache"
import { CacheProvider } from "@emotion/react"
import { ThemeProvider } from "@mui/material/styles"
import CssBaseline from "@mui/material/CssBaseline"
import theme from "@/theme"

interface PopOutWindowProps {
isPoppedOut: boolean
containerEl: HTMLElement | null
children: React.ReactNode
}

export default function PopOutWindow({
isPoppedOut,
containerEl,
children,
}: PopOutWindowProps) {
const emotionCache = useMemo(() => {
if (!containerEl) return null
return createCache({
key: "popout",
container: containerEl.ownerDocument.head,
})
}, [containerEl])

if (!isPoppedOut || !containerEl || !emotionCache) return null

return createPortal(
<CacheProvider value={emotionCache}>
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
</CacheProvider>,
containerEl
)
}
Loading
Loading