diff --git a/client/src/components/rack/ModuleBlock.tsx b/client/src/components/rack/ModuleBlock.tsx
index 9d3be26..ca5cc66 100644
--- a/client/src/components/rack/ModuleBlock.tsx
+++ b/client/src/components/rack/ModuleBlock.tsx
@@ -4,8 +4,7 @@ import { Trash2, GripVertical, GripHorizontal } from 'lucide-react';
import { toast } from 'sonner';
import type { Module } from '../../types';
import { cn } from '../../lib/utils';
-import { MODULE_TYPE_COLORS, MODULE_TYPE_LABELS, U_HEIGHT_PX } from '../../lib/constants';
-import { Badge } from '../ui/Badge';
+import { MODULE_TYPE_COLORS, U_HEIGHT_PX, PORTS_PER_ROW } from '../../lib/constants';
import { ConfirmDialog } from '../ui/ConfirmDialog';
import { ModuleEditPanel } from '../modals/ModuleEditPanel';
import { PortConfigModal } from '../modals/PortConfigModal';
@@ -49,6 +48,18 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
const height = displayUSize * U_HEIGHT_PX;
const hasPorts = module.ports.length > 0;
+ // Split ports into rows of PORTS_PER_ROW
+ const portRows: (typeof module.ports)[] = [];
+ for (let i = 0; i < module.ports.length; i += PORTS_PER_ROW) {
+ portRows.push(module.ports.slice(i, i + PORTS_PER_ROW));
+ }
+ // Only show as many rows as fit within the current height
+ // Each row needs ~14px (10px dot + 4px gap/padding)
+ const availableForPorts = height - 16; // subtract top padding + resize handle
+ const maxRows = Math.max(1, Math.floor(availableForPorts / 14));
+ const visibleRows = portRows.slice(0, maxRows);
+ const hiddenPortCount = module.ports.length - visibleRows.flat().length;
+
// Compute the maximum allowed uSize for this module (rack bounds + collision)
const maxResizeU = useCallback((): number => {
const rack = racks.find((r) => r.id === module.rackId);
@@ -120,7 +131,7 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
setHovered(true)}
onMouseLeave={() => setHovered(false)}
onClick={() => !isDragging && !isResizing.current && setEditOpen(true)}
@@ -136,77 +148,75 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
aria-label={`Edit ${module.name}`}
onKeyDown={(e) => e.key === 'Enter' && setEditOpen(true)}
>
- {/* Drag handle */}
+ {/* Drag handle — slim left strip */}
e.stopPropagation()}
aria-label={`Drag ${module.name}`}
>
-
+
- {/* Main content */}
-
- {module.name}
-
- {MODULE_TYPE_LABELS[module.type]}
-
-
-
- {module.ipAddress && (
-
{module.ipAddress}
- )}
-
- {/* U-size preview label during resize */}
- {previewUSize !== null && (
-
-
- {previewUSize}U
-
-
- )}
-
- {/* Port dots — only if module has ports and enough height */}
- {hasPorts && height >= 28 && previewUSize === null && (
+ {/* Port grid — primary face content */}
+ {hasPorts && previewUSize === null ? (
e.stopPropagation()}
>
- {module.ports.slice(0, 32).map((port) => {
- const hasVlan = port.vlans.length > 0;
- return (
-
+ ) : (
+ /* No ports or resizing — show nothing (color communicates type) */
+ previewUSize === null && (
+
no ports
+ )
+ )}
+
+ {/* Resize preview label */}
+ {previewUSize !== null && (
+
+
+ {previewUSize}U
+
+
)}
{/* Delete button — hover only */}
{hovered && previewUSize === null && (
{
e.stopPropagation();
setConfirmDeleteOpen(true);
}}
aria-label={`Delete ${module.name}`}
>
-
+
)}
@@ -215,9 +225,7 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
className={cn(
'absolute bottom-0 left-0 right-0 h-3 flex items-center justify-center z-20',
'cursor-ns-resize touch-none',
- hovered || previewUSize !== null
- ? 'opacity-100'
- : 'opacity-0 hover:opacity-100',
+ hovered || previewUSize !== null ? 'opacity-100' : 'opacity-0 hover:opacity-100',
'transition-opacity'
)}
onPointerDown={handleResizePointerDown}
@@ -227,7 +235,7 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
aria-label="Resize module"
title="Drag to resize"
>
-
+
diff --git a/client/src/components/rack/RackColumn.tsx b/client/src/components/rack/RackColumn.tsx
index b5007f6..58c3ccf 100644
--- a/client/src/components/rack/RackColumn.tsx
+++ b/client/src/components/rack/RackColumn.tsx
@@ -51,7 +51,7 @@ export function RackColumn({ rack, draggingModuleId }: RackColumnProps) {
return (
<>
-