From 3cf7083ef8e4a40de0390e387d75287b2b9b09ab Mon Sep 17 00:00:00 2001 From: Tim Gage Date: Sat, 16 May 2026 17:22:28 +0100 Subject: [PATCH] Component details overlay --- .../components/designer/ComponentPalette.tsx | 58 ++++++++++++------- .../components/designer/DragAndDropFitter.tsx | 48 +++++++++++---- 2 files changed, 75 insertions(+), 31 deletions(-) diff --git a/frontend/src/components/designer/ComponentPalette.tsx b/frontend/src/components/designer/ComponentPalette.tsx index f33cf477..5c18a94f 100644 --- a/frontend/src/components/designer/ComponentPalette.tsx +++ b/frontend/src/components/designer/ComponentPalette.tsx @@ -112,6 +112,42 @@ export function ComponentPalette({ ); } +export function ComponentDetails({ + component, + showImage = true, +}: { + component: DesignerComponentEntry; + showImage?: boolean; +}) { + const imageUrl = getComponentImageUrl(component.id); + return ( + <> + {showImage && ( + imageUrl ? ( + + ) : ( +
+ {component.componentType.slice(0, 3).toUpperCase()} +
+ ) + )} +
+
{component.name}
+
Mass {component.mass} kt
+ {primaryStat(component) ? ( +
{primaryStat(component)}
+ ) : null} +
+ + ); +} + function PaletteItem({ component, active, @@ -125,7 +161,6 @@ function PaletteItem({ onSelectComponent?: (componentId: string) => void; readOnly: boolean; }) { - const imageUrl = getComponentImageUrl(component.id); const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: `palette-${component.id}`, data: { kind: "palette", componentId: component.id }, @@ -156,26 +191,7 @@ function PaletteItem({ : { touchAction: "none" } } > - {imageUrl ? ( - - ) : ( -
- {component.componentType.slice(0, 3).toUpperCase()} -
- )} -
-
{component.name}
-
Mass {component.mass} kt
- {primaryStat(component) ? ( -
{primaryStat(component)}
- ) : null} -
+ ); } diff --git a/frontend/src/components/designer/DragAndDropFitter.tsx b/frontend/src/components/designer/DragAndDropFitter.tsx index 40efd710..04e80ebe 100644 --- a/frontend/src/components/designer/DragAndDropFitter.tsx +++ b/frontend/src/components/designer/DragAndDropFitter.tsx @@ -8,7 +8,8 @@ import { useSensors, type DragEndEvent, } from "@dnd-kit/core"; -import { useMemo, useState, type ReactNode } from "react"; +import { useMemo, useRef, useState, type ReactNode } from "react"; +import { createPortal } from "react-dom"; import type { DesignerComponentEntry, HullDefinition, @@ -21,7 +22,7 @@ import { } from "../../lib/designerFit"; import { getComponentImageUrl } from "../../lib/componentImages"; import { computeDerivedStats } from "../../lib/designerStats"; -import { ComponentPalette } from "./ComponentPalette"; +import { ComponentDetails, ComponentPalette } from "./ComponentPalette"; import { HullLayout } from "./HullLayout"; import { StatsPanel } from "./StatsPanel"; @@ -114,14 +115,15 @@ export function DragAndDropFitter({ {actions} -
- -
+ {!readOnly && ( +
+ +
+ )} ); @@ -152,6 +154,20 @@ function DroppableSlot({ data: { kind: "slot", slotNumber: slot.slotNumber, slotCategories: slot.slotCategories }, disabled: readOnly, }); + const hoverTimerRef = useRef | null>(null); + const [hoverRect, setHoverRect] = useState(null); + + function handleMouseEnter(event: React.MouseEvent) { + if (!component) return; + const rect = event.currentTarget.getBoundingClientRect(); + hoverTimerRef.current = setTimeout(() => setHoverRect(rect), 300); + } + + function handleMouseLeave() { + if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current); + setHoverRect(null); + } + return (
+ {hoverRect && component && + createPortal( +
+ +
, + document.body, + )}
{componentImageUrl ? (