polish(network): visual refinements across node, edge, and panel components

- DeviceNode: flat bg-card (no surface gradient), darker icon plate inset,
  correct text-muted token for category label
- GroupNode: label pill gets bg-card/90 background so it reads against canvas
- ConnectionEdge: label now has border + bg-card so it doesn't float invisible
- BaseHandle: tightened to 12px with accent-toned border
- NodeStatusIndicator: glow reduced to 0.15 opacity (design system compliant)
- ContextMenu: Ungroup now uses Ungroup icon instead of BoxSelect
- DeviceToolbar: group type icons coloured with semantic palette
- PropertiesPanel: empty state gets icon tile + cleaner copy hierarchy
- DiagramEditor: shortcut ? button repositioned above MiniMap, accent hover
- NetworkDiagrams list: card thumbnail placeholder uses dot-grid pattern,
  card menu gets icons and divider before destructive action

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit was merged in pull request #139.
This commit is contained in:
chihlasm
2026-04-14 05:35:25 +00:00
parent 015df1fe5f
commit b433b232dc
10 changed files with 48 additions and 33 deletions

View File

@@ -1,6 +1,6 @@
import { useEffect, useRef } from 'react'
import {
Copy, CopyPlus, Trash2, ClipboardPaste, BoxSelect, Maximize2, BringToFront, SendToBack,
Copy, CopyPlus, Trash2, ClipboardPaste, BoxSelect, Ungroup, Maximize2, BringToFront, SendToBack,
AlignStartVertical, AlignCenterHorizontal, AlignEndVertical,
AlignStartHorizontal, AlignCenterVertical, AlignEndHorizontal,
AlignHorizontalSpaceAround, AlignVerticalSpaceAround,
@@ -168,7 +168,7 @@ export function ContextMenu({
)}
{canUngroup && (
<button onClick={() => { onUngroupSelection?.(); onClose() }} className="flex w-full items-center gap-2 px-3 py-2 text-xs text-primary hover:bg-elevated">
<BoxSelect size={13} /> <span>Ungroup</span>
<Ungroup size={13} /> <span>Ungroup</span>
</button>
)}
</>

View File

@@ -54,7 +54,7 @@ function ConnectionEdgeComponent(props: EdgeProps) {
{props.label && (
<EdgeLabelRenderer>
<div
className="nodrag nopan rounded bg-page px-1.5 py-0.5 text-[10px] text-muted-foreground"
className="nodrag nopan rounded border border-default bg-card px-1.5 py-0.5 text-[10px] text-muted-foreground shadow-sm"
style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,

View File

@@ -84,7 +84,7 @@ function DeviceNodeComponent({ id, data, selected, width, height }: NodeProps) {
<NodeStatusIndicator status={status}>
<NodeTooltip>
<NodeTooltipTrigger>
<BaseNode className="group h-full w-full bg-gradient-to-b from-card via-card to-page/70">
<BaseNode className="group h-full w-full bg-card">
<div className={cn('pointer-events-none absolute inset-x-0 top-0 h-10 bg-gradient-to-b opacity-80', surfaceClass)} />
<BaseNodeHeader className="flex h-full flex-col items-center justify-center gap-2 px-2 py-3">
<div
@@ -94,7 +94,7 @@ function DeviceNodeComponent({ id, data, selected, width, height }: NodeProps) {
)}
style={{ width: iconPlateSize, height: iconPlateSize }}
>
<div className="absolute inset-[4px] rounded-[10px] border border-white/6 bg-page/55" />
<div className="absolute inset-[4px] rounded-[10px] border border-white/[0.06] bg-sidebar/50" />
<div className="relative z-10">
<Icon size={iconPx} style={{ color }} />
</div>
@@ -135,7 +135,7 @@ function DeviceNodeComponent({ id, data, selected, width, height }: NodeProps) {
)}
<span
style={{ fontSize: metaPx }}
className="text-[9px] uppercase tracking-[0.16em] text-muted-foreground/80"
className="text-[9px] uppercase tracking-[0.16em] text-muted"
>
{CATEGORY_LABELS[category] ?? nodeData.deviceType.replace(/-/g, ' ')}
</span>

View File

@@ -51,7 +51,7 @@ const GroupNodeComponent = ({ data, selected, id }: NodeProps) => {
boxSizing: 'border-box',
}}
>
<div className="absolute top-0 left-0 -translate-y-full pb-0.5 pl-1">
<div className="absolute top-0 left-2 -translate-y-full pb-0.5">
{editing ? (
<input
ref={inputRef}
@@ -62,12 +62,12 @@ const GroupNodeComponent = ({ data, selected, id }: NodeProps) => {
if (e.key === 'Enter' || e.key === 'Escape') handleLabelCommit()
e.stopPropagation()
}}
className="text-[11px] font-medium bg-transparent border-none outline-none text-primary min-w-[40px] max-w-[200px]"
className="rounded-sm px-1.5 py-0.5 text-[11px] font-semibold bg-card/90 border-none outline-none min-w-[40px] max-w-[200px]"
style={{ color }}
/>
) : (
<span
className="text-[11px] font-medium cursor-text select-none"
className="inline-block rounded-sm bg-card/90 px-1.5 py-0.5 text-[11px] font-semibold cursor-text select-none tracking-wide"
style={{ color }}
onDoubleClick={() => setEditing(true)}
>

View File

@@ -151,22 +151,22 @@ export function DeviceToolbar({ deviceTypes, onDeviceTypesChange }: DeviceToolba
</div>
<div className="flex flex-col gap-0.5">
{[
{ slug: 'subnet', label: 'Subnet' },
{ slug: 'vlan', label: 'VLAN' },
{ slug: 'site', label: 'Site' },
{ slug: 'dmz', label: 'DMZ' },
{ slug: 'subnet', label: 'Subnet', color: '#60a5fa' },
{ slug: 'vlan', label: 'VLAN', color: '#a78bfa' },
{ slug: 'site', label: 'Site', color: '#34d399' },
{ slug: 'dmz', label: 'DMZ', color: '#f87171' },
].map(item => (
<div
key={item.slug}
draggable
onDragStart={e => {
e.dataTransfer.setData('application/reactflow-group', JSON.stringify(item))
e.dataTransfer.setData('application/reactflow-group', JSON.stringify({ slug: item.slug, label: item.label }))
e.dataTransfer.effectAllowed = 'move'
}}
className="flex cursor-grab items-center gap-2 rounded px-2 py-1.5 text-xs text-primary hover:bg-elevated active:cursor-grabbing active:scale-[0.98] transition-transform"
>
<GripVertical size={12} className="shrink-0 text-muted-foreground/50" />
<LayoutGrid size={14} className="text-muted-foreground" />
<LayoutGrid size={14} style={{ color: item.color }} />
<span>{item.label}</span>
</div>
))}

View File

@@ -4,7 +4,7 @@ import {
AlignStartVertical, AlignCenterHorizontal, AlignEndVertical,
AlignStartHorizontal, AlignCenterVertical, AlignEndHorizontal,
AlignHorizontalSpaceAround, AlignVerticalSpaceAround,
BoxSelect, Ungroup,
BoxSelect, Ungroup, MousePointer,
} from 'lucide-react'
import { cn } from '@/lib/utils'
import type { DeviceProperties, DiagramEdge } from '@/types'
@@ -235,12 +235,15 @@ export function PropertiesPanel({
if (!selectedNode && !selectedEdge) {
return (
<div className="flex h-full w-[260px] flex-col items-center justify-center border-l border-default bg-sidebar px-4">
<p className="text-center text-xs text-muted-foreground">
Select a device or connection to edit its properties
<div className="flex h-full w-[260px] flex-col items-center justify-center border-l border-default bg-sidebar px-6">
<div className="mb-3 flex h-9 w-9 items-center justify-center rounded-lg border border-default bg-elevated text-muted-foreground">
<MousePointer size={15} />
</div>
<p className="text-center text-xs font-medium text-muted-foreground">
Select a device or connection
</p>
<p className="mt-1 text-center text-[10px] text-muted-foreground/60">
Hover a device to preview its info
<p className="mt-1 text-center text-[10px] text-muted-foreground/50 leading-relaxed">
Properties appear here. Hover a device to see a quick summary.
</p>
</div>
)

View File

@@ -9,7 +9,7 @@ export function BaseHandle({ className, children, ...props }: ComponentProps<typ
<Handle
{...props}
className={cn(
'h-[14px] w-[14px] rounded-full border border-default bg-elevated transition-opacity',
'h-3 w-3 rounded-full border border-accent/60 bg-card transition-opacity',
'opacity-0 group-hover:opacity-100 group-focus-within:opacity-100',
'[.rf-connect-mode_&]:opacity-100',
className,

View File

@@ -11,9 +11,9 @@ const STATUS_BORDER_COLORS: Record<NodeStatus, string> = {
}
const STATUS_GLOW: Record<NodeStatus, string> = {
online: 'shadow-[0_0_8px_rgba(52,211,153,0.3)]',
offline: 'shadow-[0_0_8px_rgba(248,113,113,0.3)]',
degraded: 'shadow-[0_0_8px_rgba(250,204,21,0.3)]',
online: 'shadow-[0_0_6px_rgba(52,211,153,0.15)]',
offline: 'shadow-[0_0_6px_rgba(248,113,113,0.15)]',
degraded: 'shadow-[0_0_6px_rgba(250,204,21,0.15)]',
unknown: '',
}

View File

@@ -853,11 +853,11 @@ function DiagramEditorInner() {
{nodes.length === 0 && !loading && (
<CanvasEmptyPrompt onGenerate={handleAIGenerate} />
)}
{/* Keyboard shortcut hint button — bottom-right corner */}
{/* Keyboard shortcut hint button — above the MiniMap */}
<button
onClick={() => setShowShortcuts(true)}
title="Keyboard shortcuts (?)"
className="absolute bottom-4 right-4 z-10 flex h-7 w-7 items-center justify-center rounded-full border border-default bg-card text-[11px] font-semibold text-muted-foreground hover:border-hover hover:text-primary"
className="absolute bottom-[175px] right-3 z-10 flex h-6 w-6 items-center justify-center rounded-full border border-default bg-card text-[11px] font-semibold text-muted-foreground hover:border-accent hover:text-accent transition-colors"
>
?
</button>

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import { Plus, Search, Network, MoreHorizontal, Upload, ChevronDown, FileJson, FileOutput } from 'lucide-react'
import { Plus, Search, Network, MoreHorizontal, Upload, ChevronDown, FileJson, FileOutput, ExternalLink, Copy, Archive } from 'lucide-react'
import { cn } from '@/lib/utils'
import { networkDiagramsApi } from '@/api'
import { toast } from '@/lib/toast'
@@ -444,8 +444,16 @@ export default function NetworkDiagramsPage() {
/>
</div>
) : (
<div className="mb-2 flex h-[120px] items-center justify-center rounded border border-default bg-elevated">
<Network size={32} className="text-muted-foreground/30" />
<div className="relative mb-2 flex h-[120px] items-center justify-center overflow-hidden rounded border border-default bg-[#0e1016]">
<svg className="absolute inset-0 h-full w-full opacity-30" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id={`dots-${d.id}`} x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.8" fill="#4f5666" />
</pattern>
</defs>
<rect width="100%" height="100%" fill={`url(#dots-${d.id})`} />
</svg>
<Network size={24} className="relative text-muted-foreground/20" />
</div>
)}
{d.node_count > 0 && (
@@ -482,20 +490,24 @@ export default function NetworkDiagramsPage() {
<>
<button
onClick={e => { e.stopPropagation(); navigate(`/network-diagrams/${d.id}`) }}
className="w-full px-3 py-1.5 text-left text-xs text-primary hover:bg-elevated"
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs text-primary hover:bg-elevated"
>
<ExternalLink size={12} className="text-muted-foreground" />
Open
</button>
<button
onClick={e => { e.stopPropagation(); handleDuplicate(d.id) }}
className="w-full px-3 py-1.5 text-left text-xs text-primary hover:bg-elevated"
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs text-primary hover:bg-elevated"
>
<Copy size={12} className="text-muted-foreground" />
Duplicate
</button>
<div className="my-1 border-t border-default" />
<button
onClick={e => { e.stopPropagation(); setConfirmArchiveId(d.id) }}
className="w-full px-3 py-1.5 text-left text-xs text-red-400 hover:bg-elevated"
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs text-red-400 hover:bg-elevated"
>
<Archive size={12} />
Archive
</button>
</>