Files
k-frame/spa/src/components/app-shell.tsx
Gabriel Kaszewski fe59b68c37 theme config, layout preview, container alignment
Server: ThemeConfig entity + CRUD (GET/PUT /theme), SQLite persistence,
ThemeUpdate broadcast to ESP32 on save and initial connect.
Client: render engine uses theme colors, full-screen redraw on theme change.
SPA: theme page with color pickers + presets, layout preview with TS port
of layout engine, justify/align controls on containers.
DisplayHint refactored to struct (kind + h_align + v_align).
2026-06-19 03:26:18 +02:00

99 lines
2.9 KiB
TypeScript

import { Link, useNavigate, useRouterState } from "@tanstack/react-router"
import {
SidebarProvider,
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuItem,
SidebarMenuButton,
SidebarInset,
SidebarTrigger,
} from "@/components/ui/sidebar"
import { Separator } from "@/components/ui/separator"
import { Toaster } from "@/components/ui/sonner"
import { Button } from "@/components/ui/button"
import { clearToken } from "@/api/auth"
import {
LayoutDashboard,
Database,
Box,
Layers,
Palette,
Save,
BookOpen,
LogOut,
} from "lucide-react"
const NAV = [
{ to: "/", label: "Dashboard", icon: LayoutDashboard },
{ to: "/data-sources", label: "Data Sources", icon: Database },
{ to: "/widgets", label: "Widgets", icon: Box },
{ to: "/layout", label: "Layout", icon: Layers },
{ to: "/theme", label: "Theme", icon: Palette },
{ to: "/presets", label: "Presets", icon: Save },
{ to: "/guide", label: "Guide", icon: BookOpen },
] as const
export function AppShell({ children }: { children: React.ReactNode }) {
const { location } = useRouterState()
const navigate = useNavigate()
function logout() {
clearToken()
navigate({ to: "/login" })
}
return (
<SidebarProvider>
<Sidebar>
<SidebarHeader className="px-4 py-3">
<span className="text-lg font-bold tracking-tight">K-Frame</span>
</SidebarHeader>
<SidebarContent>
<SidebarMenu>
{NAV.map((item) => {
const active =
item.to === "/"
? location.pathname === "/"
: location.pathname.startsWith(item.to)
return (
<SidebarMenuItem key={item.to}>
<SidebarMenuButton asChild isActive={active}>
<Link to={item.to}>
<item.icon />
<span>{item.label}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
</SidebarMenu>
</SidebarContent>
<SidebarFooter className="p-2">
<Button variant="ghost" size="sm" className="w-full justify-start" onClick={logout}>
<LogOut className="mr-2 h-4 w-4" />
Sign Out
</Button>
</SidebarFooter>
</Sidebar>
<SidebarInset>
<header className="flex h-12 items-center gap-2 border-b px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<span className="text-muted-foreground text-sm">
{NAV.find((n) =>
n.to === "/"
? location.pathname === "/"
: location.pathname.startsWith(n.to),
)?.label ?? ""}
</span>
</header>
<main className="flex-1 p-6">{children}</main>
</SidebarInset>
<Toaster />
</SidebarProvider>
)
}