feat(network): add alignment toolbar to PropertiesPanel for multi-select
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react'
|
||||
import { Node, Edge } from '@xyflow/react'
|
||||
import type { Node, Edge } from '@xyflow/react'
|
||||
|
||||
interface UseDiagramCommandsParams {
|
||||
nodes: Node[]
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useCallback, useState, useEffect } from 'react'
|
||||
import { Trash2, Minus, Spline, GitBranch, BringToFront, SendToBack } from 'lucide-react'
|
||||
import {
|
||||
Trash2, Minus, Spline, GitBranch, BringToFront, SendToBack,
|
||||
AlignStartVertical, AlignCenterHorizontal, AlignEndVertical,
|
||||
AlignStartHorizontal, AlignCenterVertical, AlignEndHorizontal,
|
||||
AlignHorizontalSpaceAround, AlignVerticalSpaceAround,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { DeviceProperties, DiagramEdge } from '@/types'
|
||||
import type { Node, Edge } from '@xyflow/react'
|
||||
@@ -15,6 +20,17 @@ interface PropertiesPanelProps {
|
||||
onSendToBack: (nodeId: string) => void
|
||||
onDeleteNode: (nodeId: string) => void
|
||||
onDeleteEdge: (edgeId: string) => void
|
||||
selectedNodeCount: number
|
||||
onAlignLeft: () => void
|
||||
onAlignRight: () => void
|
||||
onAlignCenterH: () => void
|
||||
onAlignTop: () => void
|
||||
onAlignBottom: () => void
|
||||
onAlignCenterV: () => void
|
||||
onDistributeH: () => void
|
||||
onDistributeV: () => void
|
||||
canAlign: boolean
|
||||
canDistribute: boolean
|
||||
}
|
||||
|
||||
type NodeStatus = 'online' | 'offline' | 'degraded' | 'unknown'
|
||||
@@ -78,6 +94,17 @@ export function PropertiesPanel({
|
||||
onSendToBack,
|
||||
onDeleteNode,
|
||||
onDeleteEdge,
|
||||
selectedNodeCount,
|
||||
onAlignLeft,
|
||||
onAlignRight,
|
||||
onAlignCenterH,
|
||||
onAlignTop,
|
||||
onAlignBottom,
|
||||
onAlignCenterV,
|
||||
onDistributeH,
|
||||
onDistributeV,
|
||||
canAlign,
|
||||
canDistribute,
|
||||
}: PropertiesPanelProps) {
|
||||
const [deleteConfirm, setDeleteConfirm] = useState(false)
|
||||
|
||||
@@ -98,6 +125,64 @@ export function PropertiesPanel({
|
||||
onNodeUpdate(selectedNode.id, { label: value } as Partial<DeviceNodeData>)
|
||||
}, [selectedNode, onNodeUpdate])
|
||||
|
||||
if (!selectedNode && !selectedEdge && selectedNodeCount >= 2) {
|
||||
return (
|
||||
<div className="w-[260px] border-l border-default bg-sidebar flex flex-col">
|
||||
<div className="px-4 py-3 border-b border-default">
|
||||
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
{selectedNodeCount} nodes selected
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 flex flex-col gap-4">
|
||||
{canAlign && (
|
||||
<div>
|
||||
<div className="text-[10px] font-medium text-muted uppercase tracking-wider mb-2">Align</div>
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{([
|
||||
{ label: 'Left', icon: AlignStartVertical, action: onAlignLeft },
|
||||
{ label: 'Center', icon: AlignCenterHorizontal, action: onAlignCenterH },
|
||||
{ label: 'Right', icon: AlignEndVertical, action: onAlignRight },
|
||||
{ label: 'Top', icon: AlignStartHorizontal, action: onAlignTop },
|
||||
{ label: 'Middle', icon: AlignCenterVertical, action: onAlignCenterV },
|
||||
{ label: 'Bottom', icon: AlignEndHorizontal, action: onAlignBottom },
|
||||
] as const).map(({ label, icon: Icon, action }) => (
|
||||
<button
|
||||
key={label}
|
||||
onClick={action}
|
||||
title={`Align ${label}`}
|
||||
className="flex flex-col items-center gap-1 p-2 rounded bg-elevated hover:bg-card border border-default hover:border-hover transition-colors text-muted-foreground hover:text-primary"
|
||||
>
|
||||
<Icon size={14} />
|
||||
<span className="text-[9px]">{label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{canDistribute && (
|
||||
<div>
|
||||
<div className="text-[10px] font-medium text-muted uppercase tracking-wider mb-2">Distribute</div>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
<button
|
||||
onClick={onDistributeH}
|
||||
className="flex items-center gap-1.5 px-2 py-1.5 rounded bg-elevated hover:bg-card border border-default hover:border-hover transition-colors text-muted-foreground hover:text-primary text-xs"
|
||||
>
|
||||
<AlignHorizontalSpaceAround size={13} /> Horizontal
|
||||
</button>
|
||||
<button
|
||||
onClick={onDistributeV}
|
||||
className="flex items-center gap-1.5 px-2 py-1.5 rounded bg-elevated hover:bg-card border border-default hover:border-hover transition-colors text-muted-foreground hover:text-primary text-xs"
|
||||
>
|
||||
<AlignVerticalSpaceAround size={13} /> Vertical
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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">
|
||||
|
||||
@@ -709,6 +709,17 @@ function DiagramEditorInner() {
|
||||
onSendToBack={handleSendToBack}
|
||||
onDeleteNode={handleDeleteNode}
|
||||
onDeleteEdge={handleDeleteEdge}
|
||||
selectedNodeCount={nodes.filter(n => n.selected).length}
|
||||
onAlignLeft={diagramCommands.alignLeft}
|
||||
onAlignRight={diagramCommands.alignRight}
|
||||
onAlignCenterH={diagramCommands.alignCenterH}
|
||||
onAlignTop={diagramCommands.alignTop}
|
||||
onAlignBottom={diagramCommands.alignBottom}
|
||||
onAlignCenterV={diagramCommands.alignCenterV}
|
||||
onDistributeH={diagramCommands.distributeHorizontally}
|
||||
onDistributeV={diagramCommands.distributeVertically}
|
||||
canAlign={diagramCommands.canAlign}
|
||||
canDistribute={diagramCommands.canDistribute}
|
||||
/>
|
||||
</div>
|
||||
{contextMenu && (
|
||||
|
||||
Reference in New Issue
Block a user