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).
99 lines
2.9 KiB
TypeScript
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>
|
|
)
|
|
}
|