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
58 changes: 37 additions & 21 deletions frontend/src/components/designer/ComponentPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ? (
<img
src={imageUrl}
alt=""
aria-hidden="true"
className="h-16 w-16 flex-none object-contain [image-rendering:pixelated]"
draggable={false}
/>
) : (
<div className="flex h-16 w-16 flex-none items-center justify-center rounded border border-white/10 text-[0.65rem] text-muted-foreground">
{component.componentType.slice(0, 3).toUpperCase()}
</div>
)
)}
<div className="min-w-0">
<div className="truncate font-medium text-foreground">{component.name}</div>
<div className="text-muted-foreground">Mass {component.mass} kt</div>
{primaryStat(component) ? (
<div className="truncate text-muted-foreground">{primaryStat(component)}</div>
) : null}
</div>
</>
);
}

function PaletteItem({
component,
active,
Expand All @@ -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 },
Expand Down Expand Up @@ -156,26 +191,7 @@ function PaletteItem({
: { touchAction: "none" }
}
>
{imageUrl ? (
<img
src={imageUrl}
alt=""
aria-hidden="true"
className="h-16 w-16 flex-none object-contain [image-rendering:pixelated]"
draggable={false}
/>
) : (
<div className="flex h-16 w-16 flex-none items-center justify-center rounded border border-white/10 text-[0.65rem] text-muted-foreground">
{component.componentType.slice(0, 3).toUpperCase()}
</div>
)}
<div className="min-w-0">
<div className="truncate font-medium text-foreground">{component.name}</div>
<div className="text-muted-foreground">Mass {component.mass} kt</div>
{primaryStat(component) ? (
<div className="truncate text-muted-foreground">{primaryStat(component)}</div>
) : null}
</div>
<ComponentDetails component={component} />
</button>
);
}
Expand Down
48 changes: 38 additions & 10 deletions frontend/src/components/designer/DragAndDropFitter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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";

Expand Down Expand Up @@ -114,14 +115,15 @@ export function DragAndDropFitter({
<StatsPanel stats={stats} />
{actions}
</div>
<div className="min-h-0">
<ComponentPalette
components={components}
selectedComponentId={selectedComponentId}
onSelectComponent={setSelectedComponentId}
readOnly={readOnly}
/>
</div>
{!readOnly && (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Collapse read-only fitter to one column

When readOnly is true this change stops rendering the palette, but the parent grid still uses xl:grid-cols-[minmax(0,1fr)_24rem], so at extra-large breakpoints the right 24rem track becomes empty and the hull/details area is unnecessarily compressed. This regresses the design-inspection view (DragAndDropFitter in read-only mode) by wasting horizontal space; conditionally switching to a single-column grid (or keeping a right-column placeholder) would avoid the layout gap.

Useful? React with 👍 / 👎.

<div className="min-h-0">
<ComponentPalette
components={components}
selectedComponentId={selectedComponentId}
onSelectComponent={setSelectedComponentId}
/>
</div>
)}
</div>
</DndContext>
);
Expand Down Expand Up @@ -152,6 +154,20 @@ function DroppableSlot({
data: { kind: "slot", slotNumber: slot.slotNumber, slotCategories: slot.slotCategories },
disabled: readOnly,
});
const hoverTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [hoverRect, setHoverRect] = useState<DOMRect | null>(null);

function handleMouseEnter(event: React.MouseEvent<HTMLDivElement>) {
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 (
<div
ref={setNodeRef}
Expand All @@ -168,12 +184,24 @@ function DroppableSlot({
onAddSelected();
}
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={[
"relative h-full rounded p-1 outline-none",
isOver ? "bg-blue-500/15" : "",
rejected ? "animate-pulse bg-red-500/20" : "",
].join(" ")}
>
{hoverRect && component &&
createPortal(
<div
className="pointer-events-none fixed z-[200] rounded-md border border-[var(--color-panel-border)] bg-[var(--color-popover)] p-2 text-xs shadow-xl"
style={{ left: hoverRect.right + 8, top: hoverRect.top }}
>
<ComponentDetails component={component} showImage={false} />
</div>,
document.body,
)}
<div className="absolute inset-1 flex items-center justify-center">
{componentImageUrl ? (
<img
Expand Down
Loading