Files
zeekayandHanzo Dev 7d293212cb refactor: web/ is now console/ — bootnode/bootnode is the backend, bootnode/console the frontend
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]>
2026-07-26 17:47:51 -07:00

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>
)
}