mirror of
https://github.com/hanzoai/bootnode.git
synced 2026-09-30 20:46:32 +00:00
Names the two halves for what they are. The repo already holds both (12407e9 merged bootnode/bootnode into this history); this makes the frontend directory say so, so the layout reads the way the owner describes the system: backend and frontend separate, one repo. git mv throughout — 187 renames, history preserved. Every path reference follows: hanzo.yml build context and dockerfile, compose.yml, compose.prod.yml, start.sh, LLM.md, infra/k8s/web-deployments.yaml, infra/cloud/deploy.sh, and the Dockerfile's own COPY paths. Image and container names (bootnode-web, the -web tag suffix) are deliberately NOT renamed here: those are a deploy contract the App CR and ingress bind to on port 3001, and breaking them is a separate, staged change. Console builds green after the move; the Go tree builds. Co-authored-by: Hanzo Dev <[email protected]>
98 lines
3.8 KiB
TypeScript
98 lines
3.8 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useRef } from "react"
|
|
import Link from "next/link"
|
|
import { usePathname } from "next/navigation"
|
|
import { useAuth } from "@/lib/auth"
|
|
import { useBrand } from "@/components/brand-provider"
|
|
import { DASHBOARD_NAVIGATION, currentHref } from "@/lib/dashboard-navigation"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
// The ONE console navigation surface, in lux.cloud's shape.
|
|
//
|
|
// Desktop: a 236px sticky rail under the header, sections grouped under 11px
|
|
// uppercase labels, rows at 8px radius that fill with `accent` when active.
|
|
//
|
|
// Below `lg` the rail becomes a single horizontally-scrolling strip of the
|
|
// same rows. lux.cloud's note on why: wrapping the sections instead produced a
|
|
// five-row wall of pills eating a quarter of a phone screen above the fold on
|
|
// every page. The strip scrolls inside itself — the page body never scrolls
|
|
// sideways — and the active row is scrolled into view so you can always see
|
|
// where you are.
|
|
|
|
export function ConsoleSidebar() {
|
|
const pathname = usePathname()
|
|
const { isAdmin } = useAuth()
|
|
const brand = useBrand()
|
|
const ref = useRef<HTMLElement>(null)
|
|
|
|
const groups = DASHBOARD_NAVIGATION.map((group) => ({
|
|
...group,
|
|
items: group.items.filter((item) => !item.admin || isAdmin),
|
|
})).filter((group) => group.items.length > 0)
|
|
|
|
const active = currentHref(
|
|
pathname,
|
|
groups.flatMap((g) => g.items.map((i) => i.href))
|
|
)
|
|
|
|
useEffect(() => {
|
|
ref.current?.querySelector('[data-active="true"]')?.scrollIntoView({ block: "nearest", inline: "nearest" })
|
|
}, [pathname])
|
|
|
|
const rowClass = (active: boolean) =>
|
|
cn(
|
|
"flex shrink-0 items-center justify-between gap-2 whitespace-nowrap rounded-md px-3 py-2 text-sm transition-colors max-lg:min-h-10 max-lg:scroll-mx-3",
|
|
active
|
|
? "bg-accent font-medium text-accent-foreground"
|
|
: "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
|
|
)
|
|
|
|
return (
|
|
<nav
|
|
ref={ref}
|
|
aria-label="Console sections"
|
|
className={cn(
|
|
// Desktop rail.
|
|
"sticky top-(--header-h) h-[calc(100dvh-var(--header-h))] w-(--sidebar-w) shrink-0 self-start overflow-y-auto border-r border-border px-3 pb-10 pt-5",
|
|
// Phone/tablet strip.
|
|
"no-scrollbar max-lg:static max-lg:h-auto max-lg:w-auto max-lg:flex-nowrap max-lg:gap-1 max-lg:overflow-x-auto max-lg:overflow-y-hidden max-lg:overscroll-x-contain max-lg:border-b max-lg:border-r-0 max-lg:px-3 max-lg:py-2 max-lg:[display:flex]"
|
|
)}
|
|
>
|
|
{groups.map((group) => (
|
|
// `display: contents` on the strip flattens the groups into the one
|
|
// scrolling row without duplicating the markup for two layouts.
|
|
<div key={group.label} className="mb-4 max-lg:mb-0 max-lg:[display:contents]">
|
|
<div className="px-3 py-1.5 text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground max-lg:hidden">
|
|
{group.label}
|
|
</div>
|
|
{group.items.map((item) => (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
data-active={item.href === active}
|
|
aria-current={item.href === active ? "page" : undefined}
|
|
className={rowClass(item.href === active)}
|
|
>
|
|
{item.name}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
))}
|
|
|
|
{/* Status is the brand's own page, off-site by definition. */}
|
|
<div className="mb-4 max-lg:mb-0 max-lg:[display:contents]">
|
|
<div className="px-3 py-1.5 text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground max-lg:hidden">
|
|
Platform
|
|
</div>
|
|
<a href={brand.statusUrl} target="_blank" rel="noreferrer" className={rowClass(false)}>
|
|
<span>Status</span>
|
|
<span aria-hidden="true" className="text-xs opacity-50">
|
|
↗
|
|
</span>
|
|
</a>
|
|
</div>
|
|
</nav>
|
|
)
|
|
}
|