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.1 KiB
TypeScript

import { cn } from "@/lib/utils"
// The three things a surface can be when it is not showing data: loading,
// empty, or broken. One implementation each.
//
// Before this file the app hand-rolled `<div className="animate-spin
// rounded-full h-8 w-8 border-b-2 border-primary" />` in nine places, at four
// different sizes, with no shared idea of what "empty" looks like. lux.cloud
// has exactly one `.empty` block — centred, muted, 40px of air, with the
// recovery action directly under the sentence — and that is what these are.
/** The one spinner. Sized in `em`, so it matches whatever text it sits in. */
export function Spinner({ className }: { className?: string }) {
return (
<span
role="status"
aria-label="Loading"
className={cn(
"inline-block size-[1.15em] animate-spin rounded-full border-2 border-current border-t-transparent align-[-0.15em]",
className
)}
/>
)
}
/**
* A surface that is loading. `full` centres it in the viewport for a whole
* route; without it the block sits inside whatever panel owns it.
*/
export function Loading({ label, full = false }: { label?: string; full?: boolean }) {
return (
<div
className={cn(
"flex flex-col items-center justify-center gap-3 text-muted-foreground",
full ? "min-h-screen px-6" : "px-5 py-10"
)}
>
<Spinner className="size-6 text-foreground" />
{label && <p className="text-sm">{label}</p>}
</div>
)
}
/**
* A surface with nothing in it — because there is genuinely nothing, not
* because a request failed. Say which, and offer the action that would put
* something here.
*/
export function EmptyState({
title,
description,
action,
className,
}: {
title: string
description?: string
action?: React.ReactNode
className?: string
}) {
return (
<div className={cn("px-5 py-10 text-center", className)}>
<p className="text-sm font-medium text-foreground">{title}</p>
{description && <p className="mx-auto mt-1.5 max-w-md text-sm text-muted-foreground">{description}</p>}
{action && <div className="mt-4 flex flex-wrap justify-center gap-2">{action}</div>}
</div>
)
}
/**
* A surface that failed. It states what broke and never renders zeros in place
* of unknown data — a "0 nodes" that is really "the API refused" is a lie the
* reader cannot detect.
*/
export function ErrorState({
title = "Something went wrong",
description,
action,
className,
}: {
title?: string
description?: string
action?: React.ReactNode
className?: string
}) {
return (
<div className={cn("px-5 py-10 text-center", className)}>
<p className="text-sm font-medium text-destructive">{title}</p>
{description && <p className="mx-auto mt-1.5 max-w-md text-sm text-muted-foreground">{description}</p>}
{action && <div className="mt-4 flex flex-wrap justify-center gap-2">{action}</div>}
</div>
)
}
/** Placeholder for content whose SHAPE is known but whose values are not. */
export function Skeleton({ className }: { className?: string }) {
return <div aria-hidden="true" className={cn("animate-pulse rounded-md bg-foreground/[0.07]", className)} />
}