import { createEffect, createSignal, onCleanup } from "solid-js"; import type { ProjectMenuTarget } from "../data/shell.data"; type ProjectContextMenuState = { target: ProjectMenuTarget; x: number; y: number; }; const readRootPixelToken = (name: string, fallback: number): number => { if (typeof window === "undefined") { return fallback; } const value = window.getComputedStyle(document.documentElement).getPropertyValue(name).trim(); const parsed = Number.parseFloat(value); if (!Number.isFinite(parsed)) { return fallback; } if (value.endsWith("px")) { return parsed; } return parsed * 16; }; const clampMenuPosition = (value: number, min: number, max: number): number => { if (max <= min) { return min; } return Math.min(Math.max(value, min), max); }; export const createProjectContextMenuController = () => { const [menuState, setMenuState] = createSignal(null); let menuRef: HTMLDivElement | undefined; const closeMenu = (): void => { setMenuState(null); }; const repositionMenu = (): void => { if (typeof window === "undefined" || !menuRef) { return; } const current = menuState(); if (!current) { return; } const viewportPadding = readRootPixelToken("--space-4", 16); const rect = menuRef.getBoundingClientRect(); const nextX = clampMenuPosition(current.x, viewportPadding, window.innerWidth - rect.width - viewportPadding); const nextY = clampMenuPosition(current.y, viewportPadding, window.innerHeight - rect.height - viewportPadding); if (nextX === current.x && nextY === current.y) { return; } setMenuState({ ...current, x: nextX, y: nextY }); }; const openMenu = (event: MouseEvent, target: ProjectMenuTarget): void => { event.preventDefault(); setMenuState({ target, x: event.clientX, y: event.clientY }); }; createEffect(() => { if (!menuState() || typeof window === "undefined") { return; } const frame = window.requestAnimationFrame(() => { repositionMenu(); }); const handlePointerDown = (event: PointerEvent): void => { if (!menuRef?.contains(event.target as Node)) { closeMenu(); } }; const handleKeyDown = (event: KeyboardEvent): void => { if (event.key === "Escape") { closeMenu(); } }; const handleViewportChange = (): void => { closeMenu(); }; document.addEventListener("pointerdown", handlePointerDown); window.addEventListener("resize", handleViewportChange); window.addEventListener("scroll", handleViewportChange, true); window.addEventListener("keydown", handleKeyDown); onCleanup(() => { window.cancelAnimationFrame(frame); document.removeEventListener("pointerdown", handlePointerDown); window.removeEventListener("resize", handleViewportChange); window.removeEventListener("scroll", handleViewportChange, true); window.removeEventListener("keydown", handleKeyDown); }); }); return { menuState, openMenu, closeMenu, setMenuRef: (element: HTMLDivElement): void => { menuRef = element; }, }; };