initial commit

This commit is contained in:
developer committed 2026-09-27 11:20:34 +00:00
1 parent 5b935301e5
commit 85453852d5
105 files changed
+24846

No files matched your search

+264
View File
@@ -0,0 +1,264 @@
import { useCallback, useEffect, useState } from 'react'
import type { ReactNode } from 'react'
import { NavLink, useLocation, useNavigate } from 'react-router-dom'
import type { LucideIcon } from 'lucide-react'
import {
Database,
Globe,
LayoutDashboard,
ListChecks,
LogOut,
Menu,
Network,
PanelLeftClose,
ScrollText,
Settings,
Users,
X,
} from 'lucide-react'
import { cn } from '../../lib/cn.ts'
import { logout } from '../../lib/api.ts'
import type { Health } from '../../lib/types.ts'
import { HealthPill } from './HealthPill.tsx'
import { ThemeToggle } from './ThemeToggle.tsx'
import { Tooltip } from '../ui/Tooltip.tsx'
interface NavItem {
to: string
label: string
icon: LucideIcon
end?: boolean
}
const NAV: NavItem[] = [
{ to: '/', label: 'Dashboard', icon: LayoutDashboard, end: true },
{ to: '/blocklists', label: 'Blocklists', icon: ListChecks },
{ to: '/domains', label: 'Domains', icon: Globe },
{ to: '/clients', label: 'Clients', icon: Users },
{ to: '/upstreams', label: 'Upstreams', icon: Network },
{ to: '/logs', label: 'Logs', icon: ScrollText },
{ to: '/cache', label: 'Cache', icon: Database },
{ to: '/settings', label: 'Settings', icon: Settings },
]
const MOBILE_QUERY = '(max-width: 1023px)'
export function AppShell({
children,
health,
nodeId,
}: {
children: ReactNode
health?: Health
nodeId?: string
}) {
const [collapsed, setCollapsed] = useState(false)
const [sheetOpen, setSheetOpen] = useState(false)
const [isMobile, setIsMobile] = useState(false)
const [signingOut, setSigningOut] = useState(false)
const location = useLocation()
const navigate = useNavigate()
useEffect(() => {
const query = window.matchMedia(MOBILE_QUERY)
const sync = (): void => {
setIsMobile(query.matches)
if (!query.matches) setSheetOpen(false)
}
sync()
query.addEventListener('change', sync)
return () => query.removeEventListener('change', sync)
}, [])
// Any navigation closes the mobile sheet.
useEffect(() => {
setSheetOpen(false)
}, [location.pathname])
useEffect(() => {
if (!sheetOpen) return
const onKey = (event: KeyboardEvent): void => {
if (event.key === 'Escape') setSheetOpen(false)
}
document.addEventListener('keydown', onKey)
const previous = document.body.style.overflow
document.body.style.overflow = 'hidden'
return () => {
document.removeEventListener('keydown', onKey)
document.body.style.overflow = previous
}
}, [sheetOpen])
const onSignOut = useCallback(() => {
setSigningOut(true)
logout()
.catch(() => {
/* the token is cleared either way */
})
.finally(() => {
setSigningOut(false)
navigate('/auth/login', { replace: true })
})
}, [navigate])
const rail = !isMobile && collapsed
return (
<div className="min-h-svh bg-canvas">
{/* Desktop rail toggle lives in the top bar; on mobile the bar carries
the hamburger that opens the sheet instead. */}
<header className="fixed inset-x-0 top-0 z-40 flex h-14 items-center gap-2 border-b border-line bg-surface-2 px-3">
{isMobile ? (
<button
type="button"
aria-label="Open navigation"
onClick={() => setSheetOpen(true)}
className="inline-flex size-9 shrink-0 items-center justify-center rounded-lg text-fg-muted transition-colors hover:bg-surface-3 hover:text-fg"
>
<Menu size={18} strokeWidth={2} aria-hidden="true" />
</button>
) : (
<button
type="button"
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
onClick={() => setCollapsed((value) => !value)}
className="inline-flex size-9 shrink-0 items-center justify-center rounded-lg text-fg-muted transition-colors hover:bg-surface-3 hover:text-fg"
>
<PanelLeftClose
size={18}
strokeWidth={2}
aria-hidden="true"
className={cn('transition-transform', collapsed && 'rotate-180')}
/>
</button>
)}
<div className="flex min-w-0 items-center gap-2">
<span className="grid size-7 shrink-0 place-items-center rounded-md bg-accent-soft text-accent">
<Globe size={15} strokeWidth={2.25} aria-hidden="true" />
</span>
<span className="truncate text-sm font-semibold tracking-tight text-fg">fancy-dns</span>
</div>
<div className="ml-auto flex items-center gap-2">
{nodeId !== undefined && nodeId !== '' && (
<span
title={`Node ${nodeId}`}
className="tabular hidden max-w-40 truncate rounded-md border border-line bg-surface px-2 py-1 font-mono text-xs text-fg-muted sm:inline-block"
>
{nodeId}
</span>
)}
{health !== undefined && <HealthPill health={health} />}
<ThemeToggle />
<button
type="button"
aria-label="Sign out"
title="Sign out"
disabled={signingOut}
onClick={onSignOut}
className="inline-flex size-9 shrink-0 items-center justify-center rounded-lg text-fg-muted transition-colors hover:bg-surface-3 hover:text-fg disabled:opacity-50"
>
<LogOut size={16} strokeWidth={2} aria-hidden="true" />
</button>
</div>
</header>
{/* Desktop sidebar. */}
<aside
className={cn(
'fixed top-14 bottom-0 left-0 z-30 hidden shrink-0 border-r border-line bg-surface transition-[width] duration-200 lg:flex lg:flex-col',
rail ? 'w-16' : 'w-56',
)}
>
<nav className="flex flex-1 flex-col gap-0.5 overflow-y-auto p-2">
{NAV.map((item) => (
<NavLink
key={item.to}
to={item.to}
end={item.end}
className={({ isActive }) =>
cn(
'flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium transition-colors',
isActive
? 'bg-accent-soft text-accent'
: 'text-fg-muted hover:bg-surface-3 hover:text-fg',
rail && 'justify-center px-0',
)
}
title={rail ? item.label : undefined}
>
<item.icon size={16} strokeWidth={2} aria-hidden="true" />
{!rail && <span className="truncate">{item.label}</span>}
</NavLink>
))}
</nav>
</aside>
{/* Mobile sheet. */}
{sheetOpen && (
<div className="fixed inset-0 z-50 lg:hidden">
<button
type="button"
aria-label="Close navigation"
onClick={() => setSheetOpen(false)}
className="absolute inset-0 bg-black/60"
/>
<aside
role="dialog"
aria-modal="true"
aria-label="Navigation"
className="absolute inset-y-0 left-0 flex w-64 flex-col border-r border-line bg-surface shadow-panel"
>
<div className="flex h-14 items-center justify-between border-b border-line px-3">
<span className="text-sm font-semibold text-fg">fancy-dns</span>
<button
type="button"
aria-label="Close navigation"
onClick={() => setSheetOpen(false)}
className="inline-flex size-8 items-center justify-center rounded-lg text-fg-muted transition-colors hover:bg-surface-3 hover:text-fg"
>
<X size={16} strokeWidth={2} aria-hidden="true" />
</button>
</div>
<nav className="flex flex-1 flex-col gap-0.5 overflow-y-auto p-2">
{NAV.map((item) => (
<NavLink
key={item.to}
to={item.to}
end={item.end}
onClick={() => setSheetOpen(false)}
className={({ isActive }) =>
cn(
'flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium transition-colors',
isActive
? 'bg-accent-soft text-accent'
: 'text-fg-muted hover:bg-surface-3 hover:text-fg',
)
}
>
<item.icon size={16} strokeWidth={2} aria-hidden="true" />
<span className="truncate">{item.label}</span>
</NavLink>
))}
</nav>
</aside>
</div>
)}
<main
className={cn(
'pt-14 transition-[padding] duration-200',
isMobile ? 'px-3 py-4' : rail ? 'pl-16' : 'pl-56',
)}
>
<div className="mx-auto flex w-full max-w-[1600px] flex-col gap-4 p-3 sm:p-4 lg:p-6">
{children}
</div>
</main>
</div>
)
}
/** Re-exported so pages can label collapsed rail items without importing ui. */
export { Tooltip }