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:
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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)`,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: '',
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user