"use client"; import { PrototypeStateCanvasView } from "@prototype/components/prototypes/prototype-state-canvas-overlay"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@prototype/components/ui/tooltip"; import { usePrototypeReview } from "@prototype/lib/prototypes/prototype-review-context"; import { usePersistedLocalString } from "@prototype/lib/prototypes/use-persisted-local-state"; import { usePrototypeToolTheme } from "@prototype/lib/prototypes/use-prototype-tool-theme"; import { getPrototypePreviewStage, getPrototypeToolOverlayRoot, } from "@prototype/lib/tool-portal"; import { cn } from "@prototype/lib/utils"; import { Maximize2, Plus } from "lucide-react"; import { usePathname } from "next/navigation"; import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, type ButtonHTMLAttributes, type CSSProperties, type PointerEvent as ReactPointerEvent, type RefObject, } from "react"; import { createPortal } from "react-dom"; import styles from "./prototype-mini-state-map.module.scss"; const FLY_DURATION_MS = 320; const FLY_EASE = "cubic-bezier(0.215, 0.61, 0.355, 1)"; /** Below portaled review menus (prototype-floating-pill.module.scss z-index: 1051). */ const MINI_STATE_MAP_Z_INDEX = 1; const MINI_MAP_WIDTH_PX = 280; const MINI_MAP_HEIGHT_PX = 200; const MINI_MAP_STAGE_INSET_PX = 16; const MINI_MAP_BOTTOM_PX = 48; const MINI_MAP_CORNER_STORAGE_KEY = "prototype-review:mini-state-map-corner"; type MiniMapCorner = "top-left" | "top-right" | "bottom-left" | "bottom-right"; const MINI_MAP_CORNERS: MiniMapCorner[] = [ "top-left", "top-right", "bottom-left", "bottom-right", ]; function isMiniMapCorner(value: string): value is MiniMapCorner { return (MINI_MAP_CORNERS as string[]).includes(value); } type FlyDirection = "minimize" | "restore"; type FlyState = { direction: FlyDirection; }; export type PrototypeMiniStateMapHandle = { minimize: () => void; }; type PrototypeMiniStateMapProps = { minimized: boolean; onExpand: () => void; onMinimize: () => void; onRestoreComplete?: () => void; restoring?: boolean; minimizeTargetRef: RefObject; hoverPanelProps?: { onPointerEnter: (event: ReactPointerEvent) => void; onPointerLeave: () => void; }; }; function useOnStateMapPage(stateMapPagePath: string | null): boolean { const pathname = usePathname(); if (stateMapPagePath != null) { return pathname.startsWith(stateMapPagePath); } return /\/prototypes\/[^/]+\/states\/?$/.test(pathname); } function prefersReducedMotion(): boolean { return ( typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches ); } function getMiniMapCornerRect( corner: MiniMapCorner, tabElement: HTMLElement | null, ): DOMRect | null { const stage = getPrototypePreviewStage(); if (!stage) return null; const stageRect = stage.getBoundingClientRect(); const width = Math.min( MINI_MAP_WIDTH_PX, Math.max(0, stageRect.width - MINI_MAP_STAGE_INSET_PX * 2), ); const height = MINI_MAP_HEIGHT_PX; const inset = MINI_MAP_STAGE_INSET_PX; let left = stageRect.left + inset; let top = stageRect.top + inset; switch (corner) { case "top-left": break; case "top-right": left = stageRect.right - inset - width; break; case "bottom-left": top = stageRect.bottom - MINI_MAP_BOTTOM_PX - height; if (tabElement) { const tabRect = tabElement.getBoundingClientRect(); left = tabRect.left + tabRect.width / 2 - width / 2; const minLeft = stageRect.left + inset; const maxLeft = stageRect.right - inset - width; left = Math.min(Math.max(left, minLeft), maxLeft); } break; case "bottom-right": left = stageRect.right - inset - width; top = stageRect.bottom - MINI_MAP_BOTTOM_PX - height; break; } return new DOMRect(left, top, width, height); } function getAnchorTransform(anchor: DOMRect, target: DOMRect): string { const scaleX = target.width / anchor.width; const scaleY = target.height / anchor.height; const translateX = target.left + target.width / 2 - (anchor.left + anchor.width / 2); const translateY = target.top + target.height / 2 - (anchor.top + anchor.height / 2); return `translate(${translateX}px, ${translateY}px) scale(${scaleX}, ${scaleY})`; } function startFlyAnimation(setFlyActive: (value: boolean) => void) { setFlyActive(false); requestAnimationFrame(() => { requestAnimationFrame(() => { setFlyActive(true); }); }); } type MiniStateMapActionButtonProps = ButtonHTMLAttributes & { tooltip: string; actionClassName: string; tooltipClassName: string; }; const MiniStateMapActionButton = forwardRef< HTMLButtonElement, MiniStateMapActionButtonProps >(function MiniStateMapActionButton( { tooltip, actionClassName, tooltipClassName, className, ...props }, ref, ) { return (