import { useEffect, useLayoutEffect, useRef, useState, type ReactNode, type RefObject } from "react" import { ChevronDownIcon, ChevronRightIcon, XIcon } from "lucide-react" import { Button, Card, CardAction, CardHeader, CardTitle, Tooltip, TooltipContent, TooltipTrigger } from "@hm-design/ui" import { makePanelDraggableAndCollapsible } from "../engine/panelUtils" type Props = { title: string icon: ReactNode /** Extra header content after the title (count badge…). */ meta?: ReactNode /** Extra header buttons before collapse / close. */ actions?: ReactNode open: boolean onClose: () => void /** Initial slot from the right edge of the anchor (0 = right-most). */ slot: number /** Area the window is first placed in (the canvas stage). */ anchorRef: RefObject /** Fixed starting height; omitted = fit content (capped by CSS). */ size?: "tall" | "auto" children: ReactNode } const GAP = 12 /** * A floating tool window over the canvas: HM Card inside a positioned frame. * * Drag (header), collapse (header double-click / button) and focus-to-front come from the ported * `makePanelDraggableAndCollapsible` (src/engine/panelUtils.ts, unchanged); resizing is CSS `resize: both` on the * frame, as in the sample. The frame keeps a static className because panelUtils toggles `collapsed` on it, and * React would overwrite a changing className; panelUtils also owns its inline left/top/z-index/height. Open/closed * is the `hidden` attribute, so neither of those is touched. */ export function FloatingPanel({ title, icon, meta, actions, open, onClose, slot, anchorRef, size = "tall", children }: Props) { const frameRef = useRef(null) const headerRef = useRef(null) const bodyRef = useRef(null) const control = useRef<{ toggleCollapse: () => void } | null>(null) const placed = useRef(false) const [collapsed, setCollapsed] = useState(false) useEffect(() => { const frame = frameRef.current // panelUtils has no teardown; attach once per element (StrictMode runs effects twice in development, // and two dblclick listeners would cancel each other out). if (!frame || frame.dataset.panelReady) return frame.dataset.panelReady = "1" control.current = makePanelDraggableAndCollapsible(frame, { handleEl: headerRef.current, bodyEl: bodyRef.current, onCollapseToggle: setCollapsed, }) }, []) // First open: place the window inside the canvas stage, stacked from the right edge. useLayoutEffect(() => { const frame = frameRef.current const anchor = anchorRef.current if (!open || placed.current || !frame || !anchor) return placed.current = true const r = anchor.getBoundingClientRect() const w = frame.offsetWidth const left = Math.max(r.left + GAP, r.right - GAP - (slot + 1) * w - slot * GAP) frame.style.left = `${Math.round(left)}px` frame.style.top = `${Math.round(r.top + GAP)}px` }, [open, slot, anchorRef]) return ( ) }