feat: add frontend source (fix submodule), compact node design
- Remove nested .git from Vite scaffold - Horizontal node layout: icon left, label+IP right, reduced height
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import { useState } from 'react'
|
||||
import { Network, Plus, Save, ScanLine, ChevronLeft, ChevronRight, LayoutDashboard, Clock, EyeOff } from 'lucide-react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { useCanvasStore } from '@/stores/canvasStore'
|
||||
|
||||
type SidebarView = 'canvas' | 'pending' | 'hidden' | 'history'
|
||||
|
||||
const VIEWS = [
|
||||
{ id: 'canvas' as SidebarView, icon: LayoutDashboard, label: 'Canvas' },
|
||||
{ id: 'pending' as SidebarView, icon: ScanLine, label: 'Pending Devices' },
|
||||
{ id: 'hidden' as SidebarView, icon: EyeOff, label: 'Hidden Devices' },
|
||||
{ id: 'history' as SidebarView, icon: Clock, label: 'Scan History' },
|
||||
]
|
||||
|
||||
interface SidebarProps {
|
||||
onAddNode: () => void
|
||||
onScan: () => void
|
||||
onSave: () => void
|
||||
}
|
||||
|
||||
export function Sidebar({ onAddNode, onScan, onSave }: SidebarProps) {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const [activeView, setActiveView] = useState<SidebarView>('canvas')
|
||||
const { nodes, hasUnsavedChanges } = useCanvasStore()
|
||||
|
||||
const onlineCount = nodes.filter((n) => n.data.status === 'online').length
|
||||
const offlineCount = nodes.filter((n) => n.data.status === 'offline').length
|
||||
|
||||
return (
|
||||
<aside
|
||||
className="flex flex-col border-r border-border bg-[#161b22] transition-all duration-200 relative shrink-0"
|
||||
style={{ width: collapsed ? 48 : 220 }}
|
||||
>
|
||||
{/* Toggle */}
|
||||
<button
|
||||
onClick={() => setCollapsed((c) => !c)}
|
||||
className="absolute -right-3 top-6 z-10 flex items-center justify-center w-6 h-6 rounded-full border border-border bg-[#21262d] text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{collapsed ? <ChevronRight size={12} /> : <ChevronLeft size={12} />}
|
||||
</button>
|
||||
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-2 px-3 py-4 border-b border-border">
|
||||
<div className="flex items-center justify-center w-7 h-7 rounded-md bg-[#00d4ff]/10 text-[#00d4ff] shrink-0">
|
||||
<Network size={16} />
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<span className="font-semibold text-sm tracking-wide text-foreground">Homelable</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Views */}
|
||||
<nav className="flex flex-col gap-0.5 p-2 flex-1">
|
||||
{VIEWS.map(({ id, icon: Icon, label }) => (
|
||||
<SidebarItem
|
||||
key={id}
|
||||
icon={Icon}
|
||||
label={label}
|
||||
collapsed={collapsed}
|
||||
active={activeView === id}
|
||||
onClick={() => setActiveView(id)}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Stats */}
|
||||
{!collapsed && (
|
||||
<div className="px-3 py-2 border-t border-border text-xs text-muted-foreground space-y-0.5">
|
||||
<div className="flex justify-between">
|
||||
<span>Total</span>
|
||||
<span className="text-foreground font-mono">{nodes.length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-[#39d353]">Online</span>
|
||||
<span className="font-mono text-[#39d353]">{onlineCount}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-[#f85149]">Offline</span>
|
||||
<span className="font-mono text-[#f85149]">{offlineCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<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={ScanLine} label="Scan Network" collapsed={collapsed} onClick={onScan} />
|
||||
<SidebarItem
|
||||
icon={Save}
|
||||
label="Save Canvas"
|
||||
collapsed={collapsed}
|
||||
onClick={onSave}
|
||||
badge={hasUnsavedChanges}
|
||||
accent
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
interface SidebarItemProps {
|
||||
icon: React.ElementType
|
||||
label: string
|
||||
collapsed: boolean
|
||||
active?: boolean
|
||||
badge?: boolean
|
||||
accent?: boolean
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
function SidebarItem({ icon: Icon, label, collapsed, active, badge, accent, onClick }: SidebarItemProps) {
|
||||
const btn = (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`relative flex items-center gap-2 w-full px-2 py-1.5 rounded-md text-sm transition-colors ${
|
||||
active
|
||||
? 'bg-[#00d4ff]/10 text-[#00d4ff]'
|
||||
: accent
|
||||
? 'text-[#00d4ff] hover:bg-[#00d4ff]/10'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-[#21262d]'
|
||||
}`}
|
||||
>
|
||||
<Icon size={16} className="shrink-0" />
|
||||
{!collapsed && <span className="truncate">{label}</span>}
|
||||
{badge && (
|
||||
<span className="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-[#e3b341]" />
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
|
||||
if (collapsed) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>{btn}</TooltipTrigger>
|
||||
<TooltipContent side="right">{label}</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return btn
|
||||
}
|
||||
Reference in New Issue
Block a user