feat: rename Rectangle to Zone, add border width selector
- Rename "Rectangle" → "Zone" in sidebar, add modal and edit modal - Add border width selector (1–5px, default 2px) to the Zone modal - Border width persisted in custom_colors.border_width and applied in GroupRectNode
This commit is contained in:
@@ -182,6 +182,7 @@ export default function App() {
|
|||||||
custom_colors: {
|
custom_colors: {
|
||||||
border: data.border_color,
|
border: data.border_color,
|
||||||
border_style: data.border_style,
|
border_style: data.border_style,
|
||||||
|
border_width: data.border_width,
|
||||||
background: data.background_color,
|
background: data.background_color,
|
||||||
text_color: data.text_color,
|
text_color: data.text_color,
|
||||||
text_position: data.text_position,
|
text_position: data.text_position,
|
||||||
@@ -198,6 +199,7 @@ export default function App() {
|
|||||||
|
|
||||||
const handleUpdateGroupRect = useCallback((data: GroupRectFormData) => {
|
const handleUpdateGroupRect = useCallback((data: GroupRectFormData) => {
|
||||||
if (!editingGroupRectId) return
|
if (!editingGroupRectId) return
|
||||||
|
snapshotHistory()
|
||||||
const existing = nodes.find((n) => n.id === editingGroupRectId)
|
const existing = nodes.find((n) => n.id === editingGroupRectId)
|
||||||
updateNode(editingGroupRectId, {
|
updateNode(editingGroupRectId, {
|
||||||
label: data.label,
|
label: data.label,
|
||||||
@@ -205,6 +207,7 @@ export default function App() {
|
|||||||
...existing?.data.custom_colors,
|
...existing?.data.custom_colors,
|
||||||
border: data.border_color,
|
border: data.border_color,
|
||||||
border_style: data.border_style,
|
border_style: data.border_style,
|
||||||
|
border_width: data.border_width,
|
||||||
background: data.background_color,
|
background: data.background_color,
|
||||||
text_color: data.text_color,
|
text_color: data.text_color,
|
||||||
text_position: data.text_position,
|
text_position: data.text_position,
|
||||||
@@ -214,7 +217,7 @@ export default function App() {
|
|||||||
})
|
})
|
||||||
setNodeZIndex(editingGroupRectId, data.z_order - 10)
|
setNodeZIndex(editingGroupRectId, data.z_order - 10)
|
||||||
setEditingGroupRectId(null)
|
setEditingGroupRectId(null)
|
||||||
}, [editingGroupRectId, nodes, updateNode, setNodeZIndex, setEditingGroupRectId])
|
}, [editingGroupRectId, nodes, updateNode, setNodeZIndex, setEditingGroupRectId, snapshotHistory])
|
||||||
|
|
||||||
const handleDeleteGroupRect = useCallback(() => {
|
const handleDeleteGroupRect = useCallback(() => {
|
||||||
if (!editingGroupRectId) return
|
if (!editingGroupRectId) return
|
||||||
@@ -436,7 +439,7 @@ export default function App() {
|
|||||||
open={addGroupRectOpen}
|
open={addGroupRectOpen}
|
||||||
onClose={() => setAddGroupRectOpen(false)}
|
onClose={() => setAddGroupRectOpen(false)}
|
||||||
onSubmit={handleAddGroupRect}
|
onSubmit={handleAddGroupRect}
|
||||||
title="Add Rectangle"
|
title="Add Zone"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* key forces re-mount when editing a different rect */}
|
{/* key forces re-mount when editing a different rect */}
|
||||||
@@ -457,11 +460,12 @@ export default function App() {
|
|||||||
text_position: rc.text_position ?? 'top-left',
|
text_position: rc.text_position ?? 'top-left',
|
||||||
border_color: rc.border ?? '#00d4ff',
|
border_color: rc.border ?? '#00d4ff',
|
||||||
border_style: rc.border_style ?? 'solid',
|
border_style: rc.border_style ?? 'solid',
|
||||||
|
border_width: rc.border_width ?? 2,
|
||||||
background_color: rc.background ?? '#00d4ff0d',
|
background_color: rc.background ?? '#00d4ff0d',
|
||||||
z_order: rc.z_order ?? 1,
|
z_order: rc.z_order ?? 1,
|
||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
title="Edit Rectangle"
|
title="Edit Zone"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* key forces re-mount on open so useState captures current theme as original */}
|
{/* key forces re-mount on open so useState captures current theme as original */}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export function GroupRectNode({ id, data, selected }: NodeProps<Node<NodeData>>)
|
|||||||
const rc = data.custom_colors ?? {}
|
const rc = data.custom_colors ?? {}
|
||||||
const borderColor = rc.border ?? '#00d4ff'
|
const borderColor = rc.border ?? '#00d4ff'
|
||||||
const borderStyle = rc.border_style ?? 'solid'
|
const borderStyle = rc.border_style ?? 'solid'
|
||||||
|
const borderWidth = rc.border_width ?? 2
|
||||||
const backgroundColor = rc.background ?? 'rgba(0,212,255,0.05)'
|
const backgroundColor = rc.background ?? 'rgba(0,212,255,0.05)'
|
||||||
const textColor = rc.text_color ?? '#e6edf3'
|
const textColor = rc.text_color ?? '#e6edf3'
|
||||||
const fontFamily = FONT_FAMILIES[rc.font ?? 'inter'] ?? FONT_FAMILIES.inter
|
const fontFamily = FONT_FAMILIES[rc.font ?? 'inter'] ?? FONT_FAMILIES.inter
|
||||||
@@ -62,7 +63,7 @@ export function GroupRectNode({ id, data, selected }: NodeProps<Node<NodeData>>)
|
|||||||
justifyContent: posStyle.justifyContent,
|
justifyContent: posStyle.justifyContent,
|
||||||
padding: 12,
|
padding: 12,
|
||||||
background: backgroundColor,
|
background: backgroundColor,
|
||||||
border: `${selected ? 2 : 1}px ${selected ? 'solid' : borderStyle} ${selected ? '#00d4ff' : borderColor}`,
|
border: `${selected ? borderWidth + 1 : borderWidth}px ${selected ? 'solid' : borderStyle} ${selected ? '#00d4ff' : borderColor}`,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
color: textColor,
|
color: textColor,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface GroupRectFormData {
|
|||||||
text_position: TextPosition
|
text_position: TextPosition
|
||||||
border_color: string
|
border_color: string
|
||||||
border_style: BorderStyle
|
border_style: BorderStyle
|
||||||
|
border_width: number
|
||||||
background_color: string
|
background_color: string
|
||||||
z_order: number
|
z_order: number
|
||||||
}
|
}
|
||||||
@@ -27,6 +28,14 @@ const BORDER_STYLES: { value: BorderStyle; label: string; preview: string }[] =
|
|||||||
{ value: 'none', label: 'None', preview: ' ' },
|
{ value: 'none', label: 'None', preview: ' ' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const BORDER_WIDTHS: { value: number; label: string }[] = [
|
||||||
|
{ value: 1, label: '1px' },
|
||||||
|
{ value: 2, label: '2px' },
|
||||||
|
{ value: 3, label: '3px' },
|
||||||
|
{ value: 4, label: '4px' },
|
||||||
|
{ value: 5, label: '5px' },
|
||||||
|
]
|
||||||
|
|
||||||
const DEFAULT_FORM: GroupRectFormData = {
|
const DEFAULT_FORM: GroupRectFormData = {
|
||||||
label: '',
|
label: '',
|
||||||
font: 'inter',
|
font: 'inter',
|
||||||
@@ -34,6 +43,7 @@ const DEFAULT_FORM: GroupRectFormData = {
|
|||||||
text_position: 'top-left',
|
text_position: 'top-left',
|
||||||
border_color: '#00d4ff',
|
border_color: '#00d4ff',
|
||||||
border_style: 'solid',
|
border_style: 'solid',
|
||||||
|
border_width: 2,
|
||||||
background_color: '#00d4ff0d',
|
background_color: '#00d4ff0d',
|
||||||
z_order: 1,
|
z_order: 1,
|
||||||
}
|
}
|
||||||
@@ -65,7 +75,7 @@ interface GroupRectModalProps {
|
|||||||
title?: string
|
title?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupRectModal({ open, onClose, onSubmit, onDelete, initial, title = 'Add Rectangle' }: GroupRectModalProps) {
|
export function GroupRectModal({ open, onClose, onSubmit, onDelete, initial, title = 'Add Zone' }: GroupRectModalProps) {
|
||||||
const [form, setForm] = useState<GroupRectFormData>({ ...DEFAULT_FORM, ...initial })
|
const [form, setForm] = useState<GroupRectFormData>({ ...DEFAULT_FORM, ...initial })
|
||||||
|
|
||||||
const set = <K extends keyof GroupRectFormData>(key: K, value: GroupRectFormData[K]) =>
|
const set = <K extends keyof GroupRectFormData>(key: K, value: GroupRectFormData[K]) =>
|
||||||
@@ -196,6 +206,31 @@ export function GroupRectModal({ open, onClose, onSubmit, onDelete, initial, tit
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Border width */}
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Label className="text-xs text-muted-foreground">Border Width</Label>
|
||||||
|
<div className="grid grid-cols-5 gap-1">
|
||||||
|
{BORDER_WIDTHS.map(({ value, label }) => {
|
||||||
|
const isSelected = form.border_width === value
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => set('border_width', value)}
|
||||||
|
className="flex items-center justify-center h-8 rounded text-xs transition-colors"
|
||||||
|
style={{
|
||||||
|
background: isSelected ? '#00d4ff22' : '#21262d',
|
||||||
|
border: `1px solid ${isSelected ? '#00d4ff88' : '#30363d'}`,
|
||||||
|
color: isSelected ? '#00d4ff' : '#8b949e',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Z-order */}
|
{/* Z-order */}
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<Label className="text-xs text-muted-foreground">Z-Order (1 = furthest back)</Label>
|
<Label className="text-xs text-muted-foreground">Z-Order (1 = furthest back)</Label>
|
||||||
@@ -230,7 +265,7 @@ export function GroupRectModal({ open, onClose, onSubmit, onDelete, initial, tit
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" size="sm" className="bg-[#00d4ff] text-[#0d1117] hover:bg-[#00d4ff]/90">
|
<Button type="submit" size="sm" className="bg-[#00d4ff] text-[#0d1117] hover:bg-[#00d4ff]/90">
|
||||||
{title === 'Add Rectangle' ? 'Add' : 'Save'}
|
{title === 'Add Zone' ? 'Add' : 'Save'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,14 +13,15 @@ describe('GroupRectModal', () => {
|
|||||||
it('renders form fields when open', () => {
|
it('renders form fields when open', () => {
|
||||||
render(<GroupRectModal open onClose={vi.fn()} onSubmit={vi.fn()} />)
|
render(<GroupRectModal open onClose={vi.fn()} onSubmit={vi.fn()} />)
|
||||||
expect(screen.getByPlaceholderText('Zone name…')).toBeDefined()
|
expect(screen.getByPlaceholderText('Zone name…')).toBeDefined()
|
||||||
expect(screen.getByText('Add Rectangle')).toBeDefined()
|
expect(screen.getByText('Add Zone')).toBeDefined()
|
||||||
expect(screen.getByText('Text Position')).toBeDefined()
|
expect(screen.getByText('Text Position')).toBeDefined()
|
||||||
|
expect(screen.getByText('Border Width')).toBeDefined()
|
||||||
expect(screen.getByText('Z-Order (1 = furthest back)')).toBeDefined()
|
expect(screen.getByText('Z-Order (1 = furthest back)')).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders Edit Rectangle title when provided', () => {
|
it('renders Edit Zone title when provided', () => {
|
||||||
render(<GroupRectModal open onClose={vi.fn()} onSubmit={vi.fn()} title="Edit Rectangle" />)
|
render(<GroupRectModal open onClose={vi.fn()} onSubmit={vi.fn()} title="Edit Zone" />)
|
||||||
expect(screen.getByText('Edit Rectangle')).toBeDefined()
|
expect(screen.getByText('Edit Zone')).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('calls onSubmit with form data on submit', () => {
|
it('calls onSubmit with form data on submit', () => {
|
||||||
@@ -123,6 +124,46 @@ describe('GroupRectModal', () => {
|
|||||||
expect(submitted.border_style).toBe('dotted')
|
expect(submitted.border_style).toBe('dotted')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('renders Border Width section with 5 options', () => {
|
||||||
|
render(<GroupRectModal open onClose={vi.fn()} onSubmit={vi.fn()} />)
|
||||||
|
expect(screen.getByText('Border Width')).toBeDefined()
|
||||||
|
expect(screen.getByText('1px')).toBeDefined()
|
||||||
|
expect(screen.getByText('3px')).toBeDefined()
|
||||||
|
expect(screen.getByText('5px')).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('defaults border_width to 2', () => {
|
||||||
|
const onSubmit = vi.fn()
|
||||||
|
render(<GroupRectModal open onClose={vi.fn()} onSubmit={onSubmit} />)
|
||||||
|
fireEvent.click(screen.getByText('Add'))
|
||||||
|
const submitted = onSubmit.mock.calls[0][0] as GroupRectFormData
|
||||||
|
expect(submitted.border_width).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('selects border width on click', () => {
|
||||||
|
const onSubmit = vi.fn()
|
||||||
|
render(<GroupRectModal open onClose={vi.fn()} onSubmit={onSubmit} />)
|
||||||
|
fireEvent.click(screen.getByText('4px'))
|
||||||
|
fireEvent.click(screen.getByText('Add'))
|
||||||
|
const submitted = onSubmit.mock.calls[0][0] as GroupRectFormData
|
||||||
|
expect(submitted.border_width).toBe(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('pre-fills border_width from initial prop', () => {
|
||||||
|
const onSubmit = vi.fn()
|
||||||
|
render(
|
||||||
|
<GroupRectModal
|
||||||
|
open
|
||||||
|
onClose={vi.fn()}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
initial={{ border_width: 5 }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
fireEvent.click(screen.getByText('Add'))
|
||||||
|
const submitted = onSubmit.mock.calls[0][0] as GroupRectFormData
|
||||||
|
expect(submitted.border_width).toBe(5)
|
||||||
|
})
|
||||||
|
|
||||||
it('toggles border style — clicking selected style deselects back to solid', () => {
|
it('toggles border style — clicking selected style deselects back to solid', () => {
|
||||||
const onSubmit = vi.fn()
|
const onSubmit = vi.fn()
|
||||||
render(<GroupRectModal open onClose={vi.fn()} onSubmit={onSubmit} />)
|
render(<GroupRectModal open onClose={vi.fn()} onSubmit={onSubmit} />)
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export function Sidebar({ onAddNode, onAddGroupRect, onScan, onSave, onNodeAppro
|
|||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex flex-col gap-0.5 p-2 border-t border-border">
|
<div className="flex flex-col gap-0.5 p-2 border-t border-border">
|
||||||
<SidebarItem icon={Plus} label="Add Node" collapsed={collapsed} onClick={onAddNode} />
|
<SidebarItem icon={Plus} label="Add Node" collapsed={collapsed} onClick={onAddNode} />
|
||||||
<SidebarItem icon={Square} label="Add Rectangle" collapsed={collapsed} onClick={onAddGroupRect} />
|
<SidebarItem icon={Square} label="Add Zone" collapsed={collapsed} onClick={onAddGroupRect} />
|
||||||
{!STANDALONE && <SidebarItem icon={ScanLine} label="Scan Network" collapsed={collapsed} onClick={handleScan} />}
|
{!STANDALONE && <SidebarItem icon={ScanLine} label="Scan Network" collapsed={collapsed} onClick={handleScan} />}
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon={hideIp ? EyeOff : Eye}
|
icon={hideIp ? EyeOff : Eye}
|
||||||
@@ -303,7 +303,7 @@ function HiddenDevicesPanel() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useState(() => { load() })
|
useEffect(() => { load() }, [load])
|
||||||
|
|
||||||
const handleIgnore = async (id: string) => {
|
const handleIgnore = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user