import { useState, useMemo, useCallback } from 'react' import { Search, Plus, ChevronDown, ChevronRight, X } from 'lucide-react' import { getDeviceRenderConfig, CATEGORY_LABELS, CATEGORY_ORDER } from '../nodes/deviceRegistry' import type { DeviceTypeResponse, DeviceTypeCreate } from '@/types' import { deviceTypesApi } from '@/api' interface DeviceToolbarProps { deviceTypes: DeviceTypeResponse[] onDeviceTypesChange: () => void } export function DeviceToolbar({ deviceTypes, onDeviceTypesChange }: DeviceToolbarProps) { const [search, setSearch] = useState('') const [collapsedCategories, setCollapsedCategories] = useState>(new Set()) const [showAddForm, setShowAddForm] = useState(false) const [newType, setNewType] = useState({ slug: '', label: '', category: 'network' }) const [addError, setAddError] = useState(null) const [addLoading, setAddLoading] = useState(false) const filteredByCategory = useMemo(() => { const lower = search.toLowerCase() const filtered = search ? deviceTypes.filter(dt => dt.label.toLowerCase().includes(lower) || dt.slug.toLowerCase().includes(lower)) : deviceTypes const grouped: Record = {} for (const dt of filtered) { if (!grouped[dt.category]) grouped[dt.category] = [] grouped[dt.category].push(dt) } return grouped }, [deviceTypes, search]) const toggleCategory = useCallback((cat: string) => { setCollapsedCategories(prev => { const next = new Set(prev) if (next.has(cat)) next.delete(cat) else next.add(cat) return next }) }, []) const handleDragStart = useCallback((e: React.DragEvent, deviceType: DeviceTypeResponse) => { e.dataTransfer.setData('application/reactflow-device', JSON.stringify({ slug: deviceType.slug, label: deviceType.label, category: deviceType.category, })) e.dataTransfer.effectAllowed = 'move' }, []) const handleAddType = useCallback(async () => { if (!newType.slug || !newType.label) { setAddError('Slug and label are required') return } setAddLoading(true) setAddError(null) try { await deviceTypesApi.create(newType) setNewType({ slug: '', label: '', category: 'network' }) setShowAddForm(false) onDeviceTypesChange() } catch (err: unknown) { const msg = err instanceof Error ? err.message : 'Failed to create device type' setAddError(msg) } finally { setAddLoading(false) } }, [newType, onDeviceTypesChange]) return (
setSearch(e.target.value)} className="w-full rounded-md border border-default bg-input pl-8 pr-2 py-1.5 text-xs text-primary placeholder:text-muted-foreground focus:border-accent focus:outline-none" />
{CATEGORY_ORDER.map(cat => { const items = filteredByCategory[cat] if (!items?.length) return null const collapsed = collapsedCategories.has(cat) return (
{!collapsed && (
{items.map(dt => { const { icon: Icon, color } = getDeviceRenderConfig(dt.slug, dt.category) return (
handleDragStart(e, dt)} className="flex cursor-grab items-center gap-2 rounded px-2 py-1.5 text-xs text-primary hover:bg-elevated active:cursor-grabbing" > {dt.label}
) })}
)}
) })}
{!showAddForm ? ( ) : (
New Type
setNewType(prev => ({ ...prev, slug: e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '') }))} className="rounded border border-default bg-input px-2 py-1 text-xs text-primary placeholder:text-muted-foreground focus:border-accent focus:outline-none" /> setNewType(prev => ({ ...prev, label: e.target.value }))} className="rounded border border-default bg-input px-2 py-1 text-xs text-primary placeholder:text-muted-foreground focus:border-accent focus:outline-none" /> {addError &&

{addError}

}
)}
) }