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 { useEffect, useRef } from 'react'
|
||||||
import {
|
import {
|
||||||
Copy, CopyPlus, Trash2, ClipboardPaste, BoxSelect, Maximize2, BringToFront, SendToBack,
|
Copy, CopyPlus, Trash2, ClipboardPaste, BoxSelect, Ungroup, Maximize2, BringToFront, SendToBack,
|
||||||
AlignStartVertical, AlignCenterHorizontal, AlignEndVertical,
|
AlignStartVertical, AlignCenterHorizontal, AlignEndVertical,
|
||||||
AlignStartHorizontal, AlignCenterVertical, AlignEndHorizontal,
|
AlignStartHorizontal, AlignCenterVertical, AlignEndHorizontal,
|
||||||
AlignHorizontalSpaceAround, AlignVerticalSpaceAround,
|
AlignHorizontalSpaceAround, AlignVerticalSpaceAround,
|
||||||
@@ -168,7 +168,7 @@ export function ContextMenu({
|
|||||||
)}
|
)}
|
||||||
{canUngroup && (
|
{canUngroup && (
|
||||||
<button onClick={() => { onUngroupSelection?.(); onClose() }} className="flex w-full items-center gap-2 px-3 py-2 text-xs text-primary hover:bg-elevated">
|
<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>
|
</button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ function ConnectionEdgeComponent(props: EdgeProps) {
|
|||||||
{props.label && (
|
{props.label && (
|
||||||
<EdgeLabelRenderer>
|
<EdgeLabelRenderer>
|
||||||
<div
|
<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={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ function DeviceNodeComponent({ id, data, selected, width, height }: NodeProps) {
|
|||||||
<NodeStatusIndicator status={status}>
|
<NodeStatusIndicator status={status}>
|
||||||
<NodeTooltip>
|
<NodeTooltip>
|
||||||
<NodeTooltipTrigger>
|
<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)} />
|
<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">
|
<BaseNodeHeader className="flex h-full flex-col items-center justify-center gap-2 px-2 py-3">
|
||||||
<div
|
<div
|
||||||
@@ -94,7 +94,7 @@ function DeviceNodeComponent({ id, data, selected, width, height }: NodeProps) {
|
|||||||
)}
|
)}
|
||||||
style={{ width: iconPlateSize, height: iconPlateSize }}
|
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">
|
<div className="relative z-10">
|
||||||
<Icon size={iconPx} style={{ color }} />
|
<Icon size={iconPx} style={{ color }} />
|
||||||
</div>
|
</div>
|
||||||
@@ -135,7 +135,7 @@ function DeviceNodeComponent({ id, data, selected, width, height }: NodeProps) {
|
|||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
style={{ fontSize: metaPx }}
|
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, ' ')}
|
{CATEGORY_LABELS[category] ?? nodeData.deviceType.replace(/-/g, ' ')}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const GroupNodeComponent = ({ data, selected, id }: NodeProps) => {
|
|||||||
boxSizing: 'border-box',
|
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 ? (
|
{editing ? (
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
@@ -62,12 +62,12 @@ const GroupNodeComponent = ({ data, selected, id }: NodeProps) => {
|
|||||||
if (e.key === 'Enter' || e.key === 'Escape') handleLabelCommit()
|
if (e.key === 'Enter' || e.key === 'Escape') handleLabelCommit()
|
||||||
e.stopPropagation()
|
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 }}
|
style={{ color }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span
|
<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 }}
|
style={{ color }}
|
||||||
onDoubleClick={() => setEditing(true)}
|
onDoubleClick={() => setEditing(true)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -151,22 +151,22 @@ export function DeviceToolbar({ deviceTypes, onDeviceTypesChange }: DeviceToolba
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
{[
|
{[
|
||||||
{ slug: 'subnet', label: 'Subnet' },
|
{ slug: 'subnet', label: 'Subnet', color: '#60a5fa' },
|
||||||
{ slug: 'vlan', label: 'VLAN' },
|
{ slug: 'vlan', label: 'VLAN', color: '#a78bfa' },
|
||||||
{ slug: 'site', label: 'Site' },
|
{ slug: 'site', label: 'Site', color: '#34d399' },
|
||||||
{ slug: 'dmz', label: 'DMZ' },
|
{ slug: 'dmz', label: 'DMZ', color: '#f87171' },
|
||||||
].map(item => (
|
].map(item => (
|
||||||
<div
|
<div
|
||||||
key={item.slug}
|
key={item.slug}
|
||||||
draggable
|
draggable
|
||||||
onDragStart={e => {
|
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'
|
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"
|
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" />
|
<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>
|
<span>{item.label}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
AlignStartVertical, AlignCenterHorizontal, AlignEndVertical,
|
AlignStartVertical, AlignCenterHorizontal, AlignEndVertical,
|
||||||
AlignStartHorizontal, AlignCenterVertical, AlignEndHorizontal,
|
AlignStartHorizontal, AlignCenterVertical, AlignEndHorizontal,
|
||||||
AlignHorizontalSpaceAround, AlignVerticalSpaceAround,
|
AlignHorizontalSpaceAround, AlignVerticalSpaceAround,
|
||||||
BoxSelect, Ungroup,
|
BoxSelect, Ungroup, MousePointer,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import type { DeviceProperties, DiagramEdge } from '@/types'
|
import type { DeviceProperties, DiagramEdge } from '@/types'
|
||||||
@@ -235,12 +235,15 @@ export function PropertiesPanel({
|
|||||||
|
|
||||||
if (!selectedNode && !selectedEdge) {
|
if (!selectedNode && !selectedEdge) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-[260px] flex-col items-center justify-center border-l border-default bg-sidebar px-4">
|
<div className="flex h-full w-[260px] flex-col items-center justify-center border-l border-default bg-sidebar px-6">
|
||||||
<p className="text-center text-xs text-muted-foreground">
|
<div className="mb-3 flex h-9 w-9 items-center justify-center rounded-lg border border-default bg-elevated text-muted-foreground">
|
||||||
Select a device or connection to edit its properties
|
<MousePointer size={15} />
|
||||||
|
</div>
|
||||||
|
<p className="text-center text-xs font-medium text-muted-foreground">
|
||||||
|
Select a device or connection
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-center text-[10px] text-muted-foreground/60">
|
<p className="mt-1 text-center text-[10px] text-muted-foreground/50 leading-relaxed">
|
||||||
Hover a device to preview its info
|
Properties appear here. Hover a device to see a quick summary.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export function BaseHandle({ className, children, ...props }: ComponentProps<typ
|
|||||||
<Handle
|
<Handle
|
||||||
{...props}
|
{...props}
|
||||||
className={cn(
|
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',
|
'opacity-0 group-hover:opacity-100 group-focus-within:opacity-100',
|
||||||
'[.rf-connect-mode_&]:opacity-100',
|
'[.rf-connect-mode_&]:opacity-100',
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ const STATUS_BORDER_COLORS: Record<NodeStatus, string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_GLOW: Record<NodeStatus, string> = {
|
const STATUS_GLOW: Record<NodeStatus, string> = {
|
||||||
online: 'shadow-[0_0_8px_rgba(52,211,153,0.3)]',
|
online: 'shadow-[0_0_6px_rgba(52,211,153,0.15)]',
|
||||||
offline: 'shadow-[0_0_8px_rgba(248,113,113,0.3)]',
|
offline: 'shadow-[0_0_6px_rgba(248,113,113,0.15)]',
|
||||||
degraded: 'shadow-[0_0_8px_rgba(250,204,21,0.3)]',
|
degraded: 'shadow-[0_0_6px_rgba(250,204,21,0.15)]',
|
||||||
unknown: '',
|
unknown: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -853,11 +853,11 @@ function DiagramEditorInner() {
|
|||||||
{nodes.length === 0 && !loading && (
|
{nodes.length === 0 && !loading && (
|
||||||
<CanvasEmptyPrompt onGenerate={handleAIGenerate} />
|
<CanvasEmptyPrompt onGenerate={handleAIGenerate} />
|
||||||
)}
|
)}
|
||||||
{/* Keyboard shortcut hint button — bottom-right corner */}
|
{/* Keyboard shortcut hint button — above the MiniMap */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowShortcuts(true)}
|
onClick={() => setShowShortcuts(true)}
|
||||||
title="Keyboard shortcuts (?)"
|
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>
|
</button>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
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 { cn } from '@/lib/utils'
|
||||||
import { networkDiagramsApi } from '@/api'
|
import { networkDiagramsApi } from '@/api'
|
||||||
import { toast } from '@/lib/toast'
|
import { toast } from '@/lib/toast'
|
||||||
@@ -444,8 +444,16 @@ export default function NetworkDiagramsPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mb-2 flex h-[120px] items-center justify-center rounded border border-default bg-elevated">
|
<div className="relative mb-2 flex h-[120px] items-center justify-center overflow-hidden rounded border border-default bg-[#0e1016]">
|
||||||
<Network size={32} className="text-muted-foreground/30" />
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{d.node_count > 0 && (
|
{d.node_count > 0 && (
|
||||||
@@ -482,20 +490,24 @@ export default function NetworkDiagramsPage() {
|
|||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={e => { e.stopPropagation(); navigate(`/network-diagrams/${d.id}`) }}
|
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
|
Open
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={e => { e.stopPropagation(); handleDuplicate(d.id) }}
|
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
|
Duplicate
|
||||||
</button>
|
</button>
|
||||||
|
<div className="my-1 border-t border-default" />
|
||||||
<button
|
<button
|
||||||
onClick={e => { e.stopPropagation(); setConfirmArchiveId(d.id) }}
|
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…
|
Archive…
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user