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 { useCallback } from 'react'
|
||||||
import { Node, Edge } from '@xyflow/react'
|
import type { Node, Edge } from '@xyflow/react'
|
||||||
|
|
||||||
interface UseDiagramCommandsParams {
|
interface UseDiagramCommandsParams {
|
||||||
nodes: Node[]
|
nodes: Node[]
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { useCallback, useState, useEffect } from 'react'
|
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 { cn } from '@/lib/utils'
|
||||||
import type { DeviceProperties, DiagramEdge } from '@/types'
|
import type { DeviceProperties, DiagramEdge } from '@/types'
|
||||||
import type { Node, Edge } from '@xyflow/react'
|
import type { Node, Edge } from '@xyflow/react'
|
||||||
@@ -15,6 +20,17 @@ interface PropertiesPanelProps {
|
|||||||
onSendToBack: (nodeId: string) => void
|
onSendToBack: (nodeId: string) => void
|
||||||
onDeleteNode: (nodeId: string) => void
|
onDeleteNode: (nodeId: string) => void
|
||||||
onDeleteEdge: (edgeId: 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'
|
type NodeStatus = 'online' | 'offline' | 'degraded' | 'unknown'
|
||||||
@@ -78,6 +94,17 @@ export function PropertiesPanel({
|
|||||||
onSendToBack,
|
onSendToBack,
|
||||||
onDeleteNode,
|
onDeleteNode,
|
||||||
onDeleteEdge,
|
onDeleteEdge,
|
||||||
|
selectedNodeCount,
|
||||||
|
onAlignLeft,
|
||||||
|
onAlignRight,
|
||||||
|
onAlignCenterH,
|
||||||
|
onAlignTop,
|
||||||
|
onAlignBottom,
|
||||||
|
onAlignCenterV,
|
||||||
|
onDistributeH,
|
||||||
|
onDistributeV,
|
||||||
|
canAlign,
|
||||||
|
canDistribute,
|
||||||
}: PropertiesPanelProps) {
|
}: PropertiesPanelProps) {
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState(false)
|
const [deleteConfirm, setDeleteConfirm] = useState(false)
|
||||||
|
|
||||||
@@ -98,6 +125,64 @@ export function PropertiesPanel({
|
|||||||
onNodeUpdate(selectedNode.id, { label: value } as Partial<DeviceNodeData>)
|
onNodeUpdate(selectedNode.id, { label: value } as Partial<DeviceNodeData>)
|
||||||
}, [selectedNode, onNodeUpdate])
|
}, [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) {
|
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-4">
|
||||||
|
|||||||
@@ -709,6 +709,17 @@ function DiagramEditorInner() {
|
|||||||
onSendToBack={handleSendToBack}
|
onSendToBack={handleSendToBack}
|
||||||
onDeleteNode={handleDeleteNode}
|
onDeleteNode={handleDeleteNode}
|
||||||
onDeleteEdge={handleDeleteEdge}
|
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>
|
</div>
|
||||||
{contextMenu && (
|
{contextMenu && (
|
||||||
|
|||||||
Reference in New Issue
Block a user