import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
} from "@/components/ui/dropdown-menu";
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Kbd } from "@/components/ui/kbd";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
} from "@/components/ui/select";
} from "@/components/ui/sidebar";
import { Switch } from "@/components/ui/switch";
} from "@/components/ui/table";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { useMemo, useRef, useState } from "react";
// Tiny striped flag (3 bands) used in location cards and the summary.
function Flag({ flag }: { flag: FlagData }) {
const [a, b, c] = flag.colors;
? `linear-gradient(to bottom, ${a} 33.3%, ${b} 33.3% 66.6%, ${c} 66.6%)`
: `linear-gradient(to right, ${a} 33.3%, ${b} 33.3% 66.6%, ${c} 66.6%)`;
className="inline-block h-3.5 w-5 shrink-0 rounded-[2px]"
style={{ background: bands }}
// Round distro mark: a coloured disc (Ubuntu gets its circle of friends).
<svg width={22} height={22} viewBox="0 0 24 24" aria-hidden>
<circle cx="12" cy="12" r="11" fill={color} />
<circle cx="5.6" cy="12" r="2" fill="#fff" />
<circle cx="15.2" cy="6.5" r="2" fill="#fff" />
<circle cx="15.2" cy="17.5" r="2" fill="#fff" />
// One numbered configuration step: Kbd index + title, optional description, then the controls.
children: React.ReactNode;
<Card className="gap-0 px-[26px] pt-[22px] pb-[26px]">
<div className="flex items-center gap-3">
<Kbd className="h-6 min-w-7 text-[11.5px]">{index}</Kbd>
<h2 className="text-lg font-semibold tracking-[-0.2px]">{title}</h2>
<p className="mt-3 text-sm text-muted-foreground">{description}</p>
<div className="mt-[18px]">{children}</div>
// Tailwind scans for whole class names, so each tint is spelled out.
1: "border-[color-mix(in_srgb,var(--chart-1)_22%,transparent)] bg-[color-mix(in_srgb,var(--chart-1)_12%,var(--card))] text-chart-1",
2: "border-[color-mix(in_srgb,var(--chart-2)_22%,transparent)] bg-[color-mix(in_srgb,var(--chart-2)_12%,var(--card))] text-chart-2",
3: "border-[color-mix(in_srgb,var(--chart-3)_22%,transparent)] bg-[color-mix(in_srgb,var(--chart-3)_12%,var(--card))] text-chart-3",
<div className={`flex-1 rounded-xl border px-3 py-2.5 ${tints[n]}`}>
<div className="flex items-center gap-1.5 text-[12.5px]">
<span className="text-foreground-2">{label}</span>
<div className="mt-1.5 font-mono text-[17px] text-foreground">
function Row({ label, value }: { label: string; value: string }) {
<div className="flex items-center justify-between text-[13.5px]">
<span className="text-muted-foreground">{label}</span>
<span className="font-medium">{value}</span>
// Sticky raised summary: the only raised surface besides the CTA and the billing segmented control.
<Card elevation="raised" className="gap-0 p-0">
<div className="relative overflow-hidden border-b border-border-2 bg-[linear-gradient(160deg,var(--brand-soft),var(--card)_75%)] px-5 pt-5 pb-[18px]">
<span className="absolute -right-[26px] -bottom-[30px] size-[110px] rounded-full bg-[color-mix(in_srgb,var(--chart-2)_35%,transparent)]" />
<span className="absolute right-[30px] -bottom-10 h-[90px] w-[70px] rounded-[40px] bg-[color-mix(in_srgb,var(--chart-1)_30%,transparent)]" />
<div className="relative flex items-center justify-between">
<span className="inline-flex size-11 items-center justify-center rounded-xl border border-border bg-popover">
<OsLogo color={image.color} ubuntu={image.id === "ubuntu"} />
<Badge variant="brand" shape="pill">
<div className="relative mt-4 text-[22px] font-semibold tracking-[-0.4px]">
<div className="relative mt-1 font-mono text-xs text-muted-foreground">
<div className="relative mt-3.5 flex items-center gap-2 text-sm font-medium">
<Flag flag={location.flag} />
<div className="flex flex-col gap-3.5 px-5 pt-[18px] pb-5">
<div className="flex gap-2">
icon={<CpuIcon className="size-3.5" />}
value={String(plan.vcpu)}
icon={<MemoryIcon className="size-3.5" />}
icon={<ServerIcon className="size-3.5" />}
<Row label="Image" value={`${image.name} ${version}`} />
<Row label="Billing term" value={termLabel} />
<Row label="Location" value={`${location.city}, ${location.country}`} />
<div className="rounded-xl border border-[color-mix(in_srgb,var(--brand)_25%,var(--popover))] bg-brand-soft px-4 py-3.5">
<div className="text-[13px] text-foreground-2">Purchase total</div>
<div className="mt-1 font-mono text-[30px] tracking-[-0.5px]">
<div className="mt-0.5 text-xs text-muted-foreground">
<div className="flex items-center justify-between rounded-xl bg-success-soft px-3.5 py-2.5 text-[13.5px]">
<span className="flex items-center gap-2">
<CreditCardIcon className="size-[15px]" />
<span className="font-mono font-medium text-success-text">
{formatUsd(availableCredit)}
<p className="rounded-xl border border-border px-3.5 py-3 text-xs leading-normal text-muted-foreground">
Review your configuration before purchasing. Credit is used only when
<Button elevation="raised" size="lg" className="w-full">
<Button variant="ghost" className="-mt-1.5 w-full">
// The sidebar subtree carries its own `dark` class: it stays navy in light mode (scoped theming).
{ label: "Platform", items: navPlatform },
{ label: "Workspace", items: navWorkspace },
<Sidebar collapsible="offcanvas">
<div className="dark flex h-full w-full flex-col bg-[#0b2a6f] text-sidebar-foreground">
<SidebarHeader className="flex-row items-center justify-between px-4 pt-5 pb-3">
className="flex items-center gap-2 font-brand text-xl font-semibold tracking-[-0.4px] text-white"
<SidebarTrigger className="text-muted-foreground hover:bg-[color-mix(in_srgb,white_10%,#0b2a6f)] hover:text-foreground" />
<SidebarContent className="px-2">
<SidebarGroup key={group.label}>
<SidebarGroupLabel className="text-[11.5px] tracking-[0.6px] uppercase">
{group.items.map((item) => {
const Icon = navIcons[item.icon];
const active = "active" in item && item.active;
const badge = "badge" in item ? item.badge : undefined;
<SidebarMenuItem key={item.label}>
className="h-9 text-muted-foreground hover:bg-[color-mix(in_srgb,white_5%,#0b2a6f)] data-[active]:bg-[color-mix(in_srgb,white_9%,#0b2a6f)] data-[active]:text-foreground data-[active]:shadow-[inset_0_0_0_1px_color-mix(in_srgb,var(--brand)_55%,#0b2a6f)]"
<span>{item.label}</span>
<SidebarMenuBadge className="rounded-md border border-border bg-secondary text-secondary-foreground">
<SidebarFooter className="px-2 pb-4">
className="h-9 text-muted-foreground hover:bg-[color-mix(in_srgb,white_5%,#0b2a6f)]"
render={<a href="#/api" />}
<span className="flex-1">API documentation</span>
<ArrowUpRightIcon className="size-3.5!" />
function WorkspaceSwitcher() {
const [current, setCurrent] = useState(workspaces[0]);
render={<Button variant="ghost" className="gap-2.5 px-2 font-medium" />}
<LogoMark className="size-[18px]" />
<CaretUpDownIcon className="text-muted-foreground" />
<DropdownMenuContent align="start" className="min-w-48">
<DropdownMenuLabel>Workspaces</DropdownMenuLabel>
<DropdownMenuItem key={w} onClick={() => setCurrent(w)}>
// The sidebar owns the toggle while it is open on desktop; the header takes over when it is closed or on mobile.
function HeaderTrigger() {
const { isMobile, state } = useSidebar();
return isMobile || state === "collapsed" ? <SidebarTrigger /> : null;
function AppShell({ children }: { children: React.ReactNode }) {
className="h-svh min-h-0 overflow-hidden"
style={{ "--sidebar-width": "15rem" } as React.CSSProperties}
<SidebarInset className="min-h-0 min-w-0 overflow-hidden bg-background">
<header className="flex h-16 shrink-0 items-center justify-between gap-3 border-b border-border bg-card px-4 md:px-7">
<div className="flex min-w-0 items-center gap-1">
<span className="px-1 text-muted-foreground max-sm:hidden">/</span>
<span className="text-muted-foreground max-sm:hidden">BeatVPS</span>
<div className="flex items-center gap-3 sm:gap-[18px]">
<span className="flex items-center gap-[7px] text-[13px] text-muted-foreground max-md:hidden">
<span className="size-1.5 rounded-full bg-success" />
<div className="flex items-center gap-2 text-muted-foreground">
<SunIcon className="size-4" />
<Switch aria-label="Dark mode" />
<MoonIcon className="size-4" />
<Button variant="ghost" size="sm" className="max-sm:hidden">
<div className="flex items-center gap-1.5">
<Avatar className="size-8">
<AvatarFallback className="text-xs">AR</AvatarFallback>
<CaretDownIcon className="size-3.5 text-muted-foreground max-sm:hidden" />
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
const summaryRef = useRef<HTMLDivElement>(null);
const [step, setStep] = useState("configure");
const [name, setName] = useState("");
const [region, setRegion] = useState<Region>("Europe");
const [locationId, setLocationId] = useState("fra");
const [category, setCategory] = useState<PlanCategory>("general");
const [planId, setPlanId] = useState("BEAT2C2G-S");
const [view, setView] = useState("list");
const [imageId, setImageId] = useState("ubuntu");
const [versions, setVersions] = useState<Record<string, string>>(() =>
Object.fromEntries(images.map((i) => [i.id, i.versions[0].value])),
const [months, setMonths] = useState("1");
const [autoRenew, setAutoRenew] = useState(true);
const location = locations.find((l) => l.id === locationId) ?? locations[0];
.find((p) => p.id === planId) ?? plans.general[0];
const image = images.find((i) => i.id === imageId) ?? images[0];
billingTerms.find((t) => String(t.months) === months) ?? billingTerms[0];
() => plan.price * term.months * (1 - term.discount / 100),
const regionLocations = locations.filter((l) => l.region === region);
planCategories.find((c) => c.id === category)?.label ?? "";
const pickRegion = (next: Region) => {
const first = locations.find((l) => l.region === next);
if (first) setLocationId(first.id);
const pickCategory = (next: PlanCategory) => {
setPlanId(plans[next][0].id);
<div className="mx-auto w-full max-w-[1328px] px-4 pt-8 pb-12 md:px-9">
<h1 className="text-[32px] font-semibold tracking-[-0.8px]">
<p className="mt-2 text-[15px] text-muted-foreground">
A place for your next project. Choose a location, configuration and
<Card className="mt-[26px] p-1.5">
summaryRef.current?.scrollIntoView({ behavior: "smooth" });
<TabsList variant="pills" className="flex-wrap">
<TabsTrigger value="configure" className="gap-2">
<span className="font-mono text-[11.5px]">01</span> Configure
<TabsTrigger value="review" className="gap-2">
<span className="font-mono text-[11.5px]">02</span> Review &
<div className="mt-6 flex flex-col items-start gap-6 lg:flex-row">
<div className="flex w-full min-w-0 flex-1 flex-col gap-5">
<StepCard index="01" title="Name your server">
<Label htmlFor="server-name">Server name</Label>
onChange={(e) => setName(e.target.value)}
placeholder="my-first-server"
A name to identify this server in your workspace.
title="Choose a location"
description="Place your server close to your users. Each location has its own configurations and images."
<Tabs value={region} onValueChange={(v) => pickRegion(v as Region)}>
<TabsList variant="line" className="mb-[18px] flex-wrap">
<TabsTrigger key={r} value={r}>
onValueChange={(v) => setLocationId(v as string)}
className="grid-cols-[repeat(auto-fill,minmax(170px,180px))] gap-3"
{regionLocations.map((l) => (
<FieldLabel key={l.id} htmlFor={`loc-${l.id}`}>
className="items-center gap-2.5 px-3.5 py-3"
<span className="flex-1 text-sm font-medium whitespace-nowrap">
<CheckCircleIcon className="size-4 text-brand" />
<span className="font-mono text-[11px] whitespace-nowrap text-muted-foreground">
<StepCard index="03" title="Choose your configuration">
<div className="-mt-1 flex items-center justify-between gap-3">
<span className="text-[13.5px] text-muted-foreground">
{configurationCount} configurations · monthly prices
onValueChange={(v) => v[0] && setView(v[0] as string)}
<ToggleGroupItem value="cards">Cards</ToggleGroupItem>
<ToggleGroupItem value="list">List</ToggleGroupItem>
onValueChange={(v) => pickCategory(v as PlanCategory)}
<TabsList variant="line" className="flex-wrap">
{planCategories.map((c) => (
<TabsTrigger key={c.id} value={c.id}>
onValueChange={(v) => setPlanId(v as string)}
<TableRow className="hover:bg-transparent">
<TableHead className="w-9" />
<TableHead>Plan</TableHead>
<TableHead>vCPU</TableHead>
<TableHead>RAM</TableHead>
<TableHead>SSD</TableHead>
<TableHead>Transfer</TableHead>
<TableHead numeric>Price / mo</TableHead>
{plans[category].map((p) => {
const selected = p.id === planId;
data-state={selected ? "selected" : undefined}
className="cursor-pointer [&>td]:py-3.5"
onClick={() => setPlanId(p.id)}
<RadioGroupItem value={p.id} aria-label={p.id} />
<TableCell className="font-mono text-[12.5px]">
<TableCell className="font-mono">{p.vcpu}</TableCell>
<TableCell className="font-mono">
<TableCell className="font-mono">
<TableCell className="font-mono text-muted-foreground">
className={selected ? "font-semibold" : ""}
<div className="grid gap-3 sm:grid-cols-2">
{plans[category].map((p) => (
<FieldLabel key={p.id} htmlFor={`plan-${p.id}`}>
<Field orientation="horizontal">
<FieldTitle className="font-mono text-[12.5px]">
{p.vcpu} vCPU · {p.ram} GB RAM · {p.ssd} GB SSD
<span className="font-mono text-sm">
{formatUsd(p.price)} / mo
<RadioGroupItem id={`plan-${p.id}`} value={p.id} />
description="The operating system installed on first boot."
onValueChange={(v) => setImageId(v as string)}
className="grid-cols-2 gap-3 md:grid-cols-4"
<FieldLabel key={img.id} htmlFor={`img-${img.id}`}>
<Field className="gap-3">
<div className="flex items-center gap-2.5">
<OsLogo color={img.color} ubuntu={img.id === "ubuntu"} />
<span className="flex-1 text-sm font-medium">
<RadioGroupItem id={`img-${img.id}`} value={img.id} />
aria-label={`${img.name} version`}
<SelectContent alignItemWithTrigger={false}>
{img.versions.map((v) => (
<SelectItem key={v.value} value={v.value}>
description="Pay for the full term up front. Longer terms are cheaper."
<div className="flex flex-wrap items-center justify-between gap-4">
onValueChange={(v) => v[0] && setMonths(v[0] as string)}
{billingTerms.map((t) => (
className="ml-1 h-[18px]"
<Label className="gap-2.5 text-[13.5px]">
<Switch checked={autoRenew} onCheckedChange={setAutoRenew} />
className="w-full scroll-mt-6 lg:sticky lg:top-6 lg:w-[330px] lg:shrink-0"
version={versions[image.id]}
export default function LayerbeatCreateExample() {