Atlas: store admin
A dense, quiet six-page admin for an online store: a dashboard with a live date range, sortable orders with bulk actions, an order page with a refund flow, products, customers and settings.
// Ballmac UI: Atlas template fonts. https://ui.ballmac.com/templates/template-atlas
import { Geist_Mono, Manrope } from "next/font/google"
export const atlasSans = Manrope({ variable: "--atlas-sans", subsets: ["latin"], display: "swap", preload: false })
export const atlasMono = Geist_Mono({ variable: "--atlas-mono", subsets: ["latin"], display: "swap", preload: false })// Ballmac UI: Atlas sample data. https://ui.ballmac.com/templates/template-atlas
export type AtlasPayment = "paid" | "pending" | "refunded"
export type AtlasFulfillment = "unfulfilled" | "shipped" | "delivered"
export type AtlasChannel = "Online store" | "Instagram" | "Wholesale" | "Pop-up"
export type AtlasOrder = {
id: string
/** ISO date-time, shown in UTC. */
date: string
customerId: string
channel: AtlasChannel
payment: AtlasPayment
fulfillment: AtlasFulfillment
items: { productId: string; qty: number }[]
}
export type AtlasProduct = { id: string; name: string; category: string; price: number; stock: number; status: "active" | "draft"; hue: number; sold: number }
export type AtlasCustomer = { id: string; name: string; email: string; city: string; segment: "VIP" | "New" | "At risk" | "Regular"; tone: number }
export const products: AtlasProduct[] = [
{ id: "p-01", name: "Field notebook, set of 3", category: "Paper", price: 24, stock: 142, status: "active", hue: 85, sold: 812 },
{ id: "p-02", name: "Brass pen, matte", category: "Writing", price: 58, stock: 7, status: "active", hue: 70, sold: 244 },
{ id: "p-03", name: "Waxed canvas tote", category: "Bags", price: 96, stock: 38, status: "active", hue: 150, sold: 301 },
{ id: "p-04", name: "Linen desk mat", category: "Desk", price: 68, stock: 55, status: "active", hue: 40, sold: 187 },
{ id: "p-05", name: "Graphite pencils, 12", category: "Writing", price: 16, stock: 260, status: "active", hue: 260, sold: 1204 },
{ id: "p-06", name: "Ceramic ink well", category: "Desk", price: 42, stock: 0, status: "active", hue: 200, sold: 98 },
{ id: "p-07", name: "Leather pen roll", category: "Bags", price: 74, stock: 21, status: "active", hue: 30, sold: 156 },
{ id: "p-08", name: "Stitched journal, A5", category: "Paper", price: 32, stock: 9, status: "active", hue: 120, sold: 530 },
{ id: "p-09", name: "Wooden letter tray", category: "Desk", price: 54, stock: 33, status: "draft", hue: 55, sold: 0 },
{ id: "p-10", name: "Wax seal kit", category: "Paper", price: 28, stock: 64, status: "active", hue: 15, sold: 219 },
{ id: "p-11", name: "Fountain pen, steel", category: "Writing", price: 120, stock: 12, status: "active", hue: 230, sold: 143 },
{ id: "p-12", name: "Travel ink, three colours", category: "Writing", price: 36, stock: 5, status: "active", hue: 300, sold: 267 },
]
export const customers: AtlasCustomer[] = [
{ id: "c-01", name: "Amara Okafor", email: "amara@fernhill.co", city: "Lagos", segment: "VIP", tone: 1 },
{ id: "c-02", name: "Lucas Meyer", email: "lucas.meyer@mail.de", city: "Berlin", segment: "Regular", tone: 2 },
{ id: "c-03", name: "Sofia Alvarez", email: "sofia@alvarez.studio", city: "Mexico City", segment: "VIP", tone: 3 },
{ id: "c-04", name: "Hana Kobayashi", email: "hana.k@tokyo.jp", city: "Tokyo", segment: "New", tone: 4 },
{ id: "c-05", name: "Daniel Reyes", email: "dreyes@harbor.io", city: "Manila", segment: "At risk", tone: 5 },
{ id: "c-06", name: "Priya Raman", email: "priya@raman.in", city: "Chennai", segment: "Regular", tone: 1 },
{ id: "c-07", name: "Marcus Lindqvist", email: "marcus@fjord.se", city: "Stockholm", segment: "VIP", tone: 2 },
{ id: "c-08", name: "Elena Rossi", email: "elena@rossi.it", city: "Milan", segment: "Regular", tone: 3 },
{ id: "c-09", name: "Tomás Beltrán", email: "tomas@beltran.ar", city: "Buenos Aires", segment: "New", tone: 4 },
{ id: "c-10", name: "Grace Mutasa", email: "grace@mutasa.zw", city: "Harare", segment: "At risk", tone: 5 },
{ id: "c-11", name: "Noah Fischer", email: "noah.fischer@web.ch", city: "Zürich", segment: "Regular", tone: 1 },
{ id: "c-12", name: "Yuki Tanaka", email: "yuki@tanaka.jp", city: "Osaka", segment: "VIP", tone: 2 },
]
const channels: AtlasChannel[] = ["Online store", "Online store", "Instagram", "Online store", "Wholesale", "Pop-up", "Online store", "Instagram"]
const payments: AtlasPayment[] = ["paid", "paid", "paid", "pending", "paid", "paid", "refunded", "paid", "paid", "pending"]
const fulfilments: AtlasFulfillment[] = ["delivered", "shipped", "unfulfilled", "delivered", "unfulfilled", "shipped", "delivered", "unfulfilled", "delivered", "shipped"]
/** 36 orders over three weeks, newest first. Built from fixed arithmetic so the server and browser agree. */
export const orders: AtlasOrder[] = Array.from({ length: 36 }, (_, i) => {
const n = 10480 - i
const day = 29 - Math.floor(i / 2)
const hour = i % 2 === 0 ? 14 + (i % 5) : 8 + (i % 4)
const minute = (i * 13) % 60
const lines = 1 + (i % 3)
const age = i < 6 ? 0 : i < 14 ? 1 : 2
const payment = payments[i % payments.length]
const fulfillment: AtlasFulfillment = payment === "pending" ? "unfulfilled" : age === 0 ? (i % 2 ? "unfulfilled" : "shipped") : fulfilments[i % fulfilments.length]
return {
id: `ORD-${n}`,
date: `2026-09-${String(day).padStart(2, "0")}T${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}:00Z`,
customerId: customers[(i * 5 + 1) % customers.length].id,
channel: channels[i % channels.length],
payment,
fulfillment,
items: Array.from({ length: lines }, (_, k) => ({ productId: products[(i * 3 + k * 4) % products.length].id, qty: 1 + ((i + k) % 3) })),
}
})
export const getCustomer = (id: string) => customers.find((c) => c.id === id) ?? customers[0]
export const getProduct = (id: string) => products.find((p) => p.id === id) ?? products[0]
export const orderTotal = (o: AtlasOrder) => o.items.reduce((n, l) => n + getProduct(l.productId).price * l.qty, 0)
export const orderShipping = (o: AtlasOrder) => (orderTotal(o) >= 100 ? 0 : 8)
export const orderGrand = (o: AtlasOrder) => orderTotal(o) + orderShipping(o)
export const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 })
export const moneyExact = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2 })
const dateFmt = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC" })
const timeFmt = new Intl.DateTimeFormat("en-US", { hour: "numeric", minute: "2-digit", timeZone: "UTC" })
export const formatDate = (iso: string) => dateFmt.format(new Date(iso))
export const formatTime = (iso: string) => timeFmt.format(new Date(iso))
export const initials = (name: string) => name.split(" ").map((w) => w[0]).join("").slice(0, 2)
/** Thirty daily revenue figures, the last one being today. */
export const revenueSeries = Array.from({ length: 30 }, (_, i) => {
const base = 3200 + i * 46 + Math.round(Math.sin(i * 0.9) * 520) + (i % 7 === 5 ? 700 : 0)
const previous = 2900 + i * 30 + Math.round(Math.cos(i * 0.8) * 430)
return { day: `Sep ${i + 1}`, revenue: base, previous }
})// Ballmac UI: Atlas template shell. https://ui.ballmac.com/templates/template-atlas
"use client"
import * as React from "react"
import { Bell, ChevronsUpDown, CircleHelp, LayoutDashboard, Menu, Package, Receipt, Search, Settings, Users } from "lucide-react"
import { Badge } from "@/components/ballmac/badge"
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandShortcut } from "@/components/ballmac/command"
import { Sheet, SheetContent, SheetDescription, SheetTitle } from "@/components/ballmac/sheet"
import { atlasMono, atlasSans } from "@/components/ballmac/templates/atlas/atlas-fonts"
import { cn } from "@/lib/utils"
type AtlasPage = "dashboard" | "orders" | "products" | "customers" | "settings"
/** Where each page lives. `order` is the order detail page. */
type AtlasHrefs = Record<AtlasPage | "order", string>
const defaultHrefs: AtlasHrefs = {
dashboard: "/atlas",
orders: "/atlas/orders",
order: "/atlas/orders/ORD-10479",
products: "/atlas/products",
customers: "/atlas/customers",
settings: "/atlas/settings",
}
/** Atlas's palette: warm stone neutrals with one teal. Dense, quiet and easy to read all day. */
const atlasCss = `
.atlas-theme,body:not(:has([data-gallery])):has(.atlas-theme){--background:oklch(0.985 0.004 90);--foreground:oklch(0.2 0.012 80);--card:oklch(1 0 0);--card-foreground:oklch(0.2 0.012 80);--popover:oklch(1 0 0);--popover-foreground:oklch(0.2 0.012 80);--primary:oklch(0.26 0.012 80);--primary-foreground:oklch(0.985 0.004 90);--secondary:oklch(0.955 0.006 90);--secondary-foreground:oklch(0.2 0.012 80);--muted:oklch(0.955 0.006 90);--muted-foreground:oklch(0.47 0.015 85);--accent:oklch(0.94 0.008 90);--accent-foreground:oklch(0.2 0.012 80);--border:oklch(0.2 0.012 80 / 11%);--input:oklch(0.2 0.012 80 / 16%);--ring:oklch(0.5 0.1 190);--surface:oklch(0.968 0.006 90);--destructive:oklch(0.55 0.21 27);--chart-1:oklch(0.5 0.1 190);--chart-2:oklch(0.5 0.13 150);--chart-3:oklch(0.62 0.14 70);--chart-4:oklch(0.5 0.14 320);--chart-5:oklch(0.52 0.12 250);--radius:0.625rem}
.dark .atlas-theme,.dark body:not(:has([data-gallery])):has(.atlas-theme){--background:oklch(0.165 0.008 80);--foreground:oklch(0.95 0.006 90);--card:oklch(0.2 0.009 80);--card-foreground:oklch(0.95 0.006 90);--popover:oklch(0.22 0.01 80);--popover-foreground:oklch(0.95 0.006 90);--primary:oklch(0.95 0.006 90);--primary-foreground:oklch(0.2 0.012 80);--secondary:oklch(0.25 0.01 80);--secondary-foreground:oklch(0.95 0.006 90);--muted:oklch(0.24 0.01 80);--muted-foreground:oklch(0.72 0.015 85);--accent:oklch(0.27 0.012 80);--accent-foreground:oklch(0.95 0.006 90);--border:oklch(1 0 0 / 10%);--input:oklch(1 0 0 / 14%);--ring:oklch(0.74 0.11 190);--surface:oklch(0.185 0.009 80);--destructive:oklch(0.7 0.19 25);--chart-1:oklch(0.78 0.11 190);--chart-2:oklch(0.78 0.14 150);--chart-3:oklch(0.82 0.14 80);--chart-4:oklch(0.76 0.13 320);--chart-5:oklch(0.76 0.11 250)}
`
/** Dialogs, sheets and menus render in a portal on <body>, so the font is set there too while an Atlas page is mounted. */
const atlasBodyCss = `body:not(:has([data-gallery])):has(.atlas-theme){font-family:var(--atlas-sans),ui-sans-serif,system-ui,sans-serif}`
const nav: { key: AtlasPage; label: string; icon: typeof Receipt; badge?: string }[] = [
{ key: "dashboard", label: "Dashboard", icon: LayoutDashboard },
{ key: "orders", label: "Orders", icon: Receipt, badge: "8" },
{ key: "products", label: "Products", icon: Package },
{ key: "customers", label: "Customers", icon: Users },
{ key: "settings", label: "Settings", icon: Settings },
]
function AtlasMark({ className }: { className?: string }) {
return (
<svg viewBox="0 0 28 28" aria-hidden="true" className={cn("size-7", className)} fill="none">
<rect width="28" height="28" rx="8" fill="var(--chart-1)" />
<path d="M8 19 14 8l6 11M10.5 15h7" stroke="var(--card)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
}
function SidebarNav({ page, hrefs, onNavigate }: { page: AtlasPage; hrefs: AtlasHrefs; onNavigate?: () => void }) {
return (
<div className="flex h-full flex-col">
<div className="flex h-14 items-center gap-2.5 px-4">
<AtlasMark />
<div className="leading-tight">
<p className="text-sm font-bold tracking-tight">Atlas</p>
<p className="text-muted-foreground text-[11px]">Fieldnote Goods</p>
</div>
</div>
<nav aria-label="Main" className="flex-1 space-y-0.5 px-2.5 py-3">
{nav.map((n) => (
<a
key={n.key}
href={hrefs[n.key]}
onClick={onNavigate}
aria-current={page === n.key ? "page" : undefined}
className="text-muted-foreground hover:text-foreground hover:bg-accent aria-[current=page]:bg-accent aria-[current=page]:text-foreground focus-visible:ring-ring/50 flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium outline-none transition-colors focus-visible:ring-[3px]"
>
<n.icon className="size-4" aria-hidden="true" />
{n.label}
{n.badge && <span className="bg-chart-1 text-primary-foreground ms-auto rounded-full px-1.5 text-[11px] font-semibold tabular-nums">{n.badge}</span>}
</a>
))}
</nav>
<div className="border-t p-2.5">
<button type="button" className="hover:bg-accent focus-visible:ring-ring/50 flex w-full items-center gap-2.5 rounded-lg p-2 text-start outline-none focus-visible:ring-[3px]">
<span className="bg-chart-4/25 flex size-8 items-center justify-center rounded-full text-xs font-bold" aria-hidden="true">MK</span>
<span className="min-w-0 flex-1 leading-tight">
<span className="block truncate text-sm font-semibold">Mina Kovac</span>
<span className="text-muted-foreground block truncate text-xs">Owner</span>
</span>
<ChevronsUpDown className="text-muted-foreground size-4" aria-hidden="true" />
</button>
</div>
</div>
)
}
type AtlasShellProps = Omit<React.ComponentProps<"div">, "title"> & {
/** The page being shown, so its nav link is marked current. */
page: AtlasPage
/** Page title shown in the top bar. */
title: string
/** Override where pages live (used by previews). */
hrefs?: Partial<AtlasHrefs>
/** Controls on the right of the top bar. */
actions?: React.ReactNode
}
/** Atlas's frame: sidebar, top bar with a command menu (⌘K), and a drawer on phones. Wrap every Atlas page in it. */
function AtlasShell({ page, title, hrefs: overrides, actions, className, style, children, ...props }: AtlasShellProps) {
const hrefs = { ...defaultHrefs, ...overrides }
const [drawer, setDrawer] = React.useState(false)
const [palette, setPalette] = React.useState(false)
// Portaled UI (command menu, sheet) lives outside this element; give <body> the font variables too.
React.useEffect(() => {
const classes = [atlasSans.variable, atlasMono.variable].filter(Boolean)
document.body.classList.add(...classes)
return () => document.body.classList.remove(...classes)
}, [])
return (
<div
data-slot="atlas"
className={cn("atlas-theme bg-background text-foreground relative min-h-dvh lg:grid lg:grid-cols-[15rem_minmax(0,1fr)]", atlasSans.variable, atlasMono.variable, className)}
style={{ fontFamily: "var(--atlas-sans), ui-sans-serif, system-ui, sans-serif", ...style }}
{...props}
>
<style>{atlasCss + atlasBodyCss}</style>
<aside className="bg-surface sticky top-0 hidden h-dvh border-e lg:block">
<SidebarNav page={page} hrefs={hrefs} />
</aside>
<Sheet open={drawer} onOpenChange={setDrawer}>
<SheetContent side="start" className="w-72 p-0" closeLabel="Close navigation">
<SheetTitle className="sr-only">Navigation</SheetTitle>
<SheetDescription className="sr-only">Pages in the Fieldnote Goods store</SheetDescription>
<SidebarNav page={page} hrefs={hrefs} onNavigate={() => setDrawer(false)} />
</SheetContent>
</Sheet>
<div className="min-w-0">
<header className="bg-background/85 sticky top-0 z-30 flex h-14 items-center gap-3 border-b px-4 backdrop-blur-lg sm:px-6">
<button type="button" aria-label="Open navigation" onClick={() => setDrawer(true)} className="hover:bg-accent focus-visible:ring-ring/50 -ms-1 inline-flex size-9 items-center justify-center rounded-lg outline-none focus-visible:ring-[3px] lg:hidden">
<Menu className="size-5" aria-hidden="true" />
</button>
<h1 className="min-w-0 truncate text-[15px] font-bold tracking-tight">{title}</h1>
<div className="ms-auto flex items-center gap-2">
<button
type="button"
onClick={() => setPalette(true)}
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 bg-card flex h-9 items-center gap-2 rounded-lg border px-2.5 text-sm outline-none transition-colors focus-visible:ring-[3px] sm:w-56"
>
<Search className="size-4" aria-hidden="true" />
<span className="hidden flex-1 text-start sm:block">Search</span>
<kbd className="bg-muted hidden rounded px-1.5 py-0.5 text-[11px] sm:block" style={{ fontFamily: "var(--atlas-mono)" }}>⌘K</kbd>
<span className="sr-only sm:hidden">Search the store</span>
</button>
{actions}
<button type="button" aria-label="Notifications, 3 unread" className="hover:bg-accent focus-visible:ring-ring/50 relative inline-flex size-9 items-center justify-center rounded-lg outline-none focus-visible:ring-[3px]">
<Bell className="size-[18px]" aria-hidden="true" />
<span className="bg-chart-3 absolute top-2 end-2.5 size-1.5 rounded-full" aria-hidden="true" />
</button>
</div>
</header>
{children}
</div>
<CommandDialog open={palette} onOpenChange={setPalette} title="Search the store">
<CommandInput placeholder="Search pages, orders, products…" />
<CommandList>
<CommandEmpty>No results.</CommandEmpty>
<CommandGroup heading="Go to">
{nav.map((n) => (
<CommandItem key={n.key} value={n.label} onSelect={() => { setPalette(false); window.location.assign(hrefs[n.key]) }}>
<n.icon aria-hidden="true" /> {n.label}
</CommandItem>
))}
</CommandGroup>
<CommandGroup heading="Recent orders">
{["ORD-10479", "ORD-10479", "ORD-10476"].map((id) => (
<CommandItem key={id} value={id} onSelect={() => { setPalette(false); window.location.assign(hrefs.order) }}>
<Receipt aria-hidden="true" /> {id}
<CommandShortcut>Order</CommandShortcut>
</CommandItem>
))}
</CommandGroup>
<CommandGroup heading="Help">
<CommandItem value="Help center"><CircleHelp aria-hidden="true" /> Help center</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
</div>
)
}
/** The heading row of a page: title text, a description and actions. */
function AtlasPageHeader({ title, description, children }: { title: string; description?: string; children?: React.ReactNode }) {
return (
<div className="flex flex-wrap items-end justify-between gap-4">
<div className="min-w-0">
<h2 className="text-2xl font-extrabold tracking-[-0.03em]">{title}</h2>
{description && <p className="text-muted-foreground mt-1 text-sm text-pretty">{description}</p>}
</div>
{children && <div className="flex flex-wrap items-center gap-2">{children}</div>}
</div>
)
}
const buttonBase = "focus-visible:ring-ring/50 inline-flex h-9 shrink-0 items-center justify-center gap-2 rounded-lg px-3.5 text-sm font-semibold outline-none transition-colors focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4"
/** Class names for Atlas buttons, so links and buttons match. */
const atlasButton = {
primary: cn(buttonBase, "bg-primary text-primary-foreground hover:opacity-90"),
outline: cn(buttonBase, "bg-card hover:bg-accent border"),
ghost: cn(buttonBase, "hover:bg-accent"),
danger: cn(buttonBase, "bg-card text-destructive border-destructive/40 hover:bg-destructive/10 border"),
}
const paymentTone = { paid: "success", pending: "warning", refunded: "neutral" } as const
const fulfilmentTone = { unfulfilled: "warning", shipped: "neutral", delivered: "success" } as const
const label = (s: string) => s[0].toUpperCase() + s.slice(1)
/** Payment status as a badge. */
function PaymentBadge({ value }: { value: keyof typeof paymentTone }) {
return <Badge status={paymentTone[value]} variant="outline">{label(value)}</Badge>
}
/** Fulfilment status as a badge. */
function FulfilmentBadge({ value }: { value: keyof typeof fulfilmentTone }) {
return <Badge status={fulfilmentTone[value]} variant="outline">{label(value)}</Badge>
}
/** A small painted tile standing in for a product photo. */
function ProductTile({ hue, className }: { hue: number; className?: string }) {
return (
<span
aria-hidden="true"
className={cn("relative block overflow-hidden rounded-lg", className)}
style={{ background: `linear-gradient(135deg, oklch(0.9 0.06 ${hue}), oklch(0.78 0.1 ${hue + 30}))` }}
>
<span className="absolute inset-x-[22%] bottom-0 h-[58%] rounded-t-md" style={{ background: `oklch(0.55 0.1 ${hue} / 0.55)` }} />
</span>
)
}
export { AtlasMark, AtlasPageHeader, AtlasShell, FulfilmentBadge, PaymentBadge, ProductTile, atlasButton, defaultHrefs as atlasDefaultHrefs, type AtlasHrefs, type AtlasPage, type AtlasShellProps }// Ballmac UI: Atlas dashboard page. https://ui.ballmac.com/templates/template-atlas
"use client"
import * as React from "react"
import { AlertTriangle, ArrowDownRight, ArrowRight, ArrowUpRight, Download, PackageX, Truck } from "lucide-react"
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"
import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, type ChartConfig } from "@/components/ballmac/chart"
import { SegmentedControl, SegmentedControlItem } from "@/components/ballmac/segmented-control"
import { Sparkline } from "@/components/ballmac/sparkline"
import { formatDate, getCustomer, initials, money, moneyExact, orderGrand, orders, products, revenueSeries } from "@/components/ballmac/templates/atlas/atlas-data"
import { AtlasPageHeader, AtlasShell, FulfilmentBadge, PaymentBadge, ProductTile, atlasButton, type AtlasHrefs } from "@/components/ballmac/templates/atlas/atlas-theme"
import { cn } from "@/lib/utils"
type Range = "7" | "30"
const config = { revenue: { label: "This period", color: "var(--chart-1)" }, previous: { label: "Previous period", color: "var(--chart-3)" } } satisfies ChartConfig
const sum = (xs: number[]) => xs.reduce((a, b) => a + b, 0)
const pct = (a: number, b: number) => ((a - b) / b) * 100
type AtlasDashboardProps = React.ComponentProps<"div"> & { hrefs?: Partial<AtlasHrefs> }
/** The Atlas dashboard: a date range that recomputes the KPIs and chart, work that needs attention, top products and recent orders. */
function AtlasDashboard({ hrefs, ...props }: AtlasDashboardProps) {
const [range, setRange] = React.useState<Range>("30")
const n = Number(range)
const data = revenueSeries.slice(-n)
const revenue = sum(data.map((d) => d.revenue))
const previous = sum(data.map((d) => d.previous))
const orderCount = Math.round(revenue / 71)
const prevOrders = Math.round(previous / 69)
const conversion = 2.1 + n * 0.004
const aov = revenue / orderCount
const kpis = [
{ label: "Revenue", value: money.format(revenue), delta: pct(revenue, previous), trend: data.map((d) => d.revenue) },
{ label: "Orders", value: orderCount.toLocaleString("en-US"), delta: pct(orderCount, prevOrders), trend: data.map((d) => Math.round(d.revenue / 70)) },
{ label: "Conversion", value: `${conversion.toFixed(1)}%`, delta: 4.2, trend: data.map((d, i) => 2 + Math.sin(i * 0.5) * 0.3 + i * 0.01) },
{ label: "Average order", value: moneyExact.format(aov), delta: -1.8, trend: data.map((d, i) => 70 + Math.cos(i * 0.7) * 6 - i * 0.1) },
]
const unfulfilled = orders.filter((o) => o.fulfillment === "unfulfilled" && o.payment !== "pending").length
const pending = orders.filter((o) => o.payment === "pending").length
const low = products.filter((p) => p.stock <= 10 && p.status === "active")
const top = [...products].filter((p) => p.status === "active").sort((a, b) => b.sold * b.price - a.sold * a.price).slice(0, 5)
const topMax = top[0].sold * top[0].price
const recent = orders.slice(0, 6)
const h = { orders: hrefs?.orders ?? "/atlas/orders", order: hrefs?.order ?? "/atlas/orders/ORD-10479", products: hrefs?.products ?? "/atlas/products" }
return (
<AtlasShell
page="dashboard"
title="Dashboard"
hrefs={hrefs}
actions={<a href={h.orders} className={cn(atlasButton.primary, "hidden sm:inline-flex")}>View orders</a>}
{...props}
>
<main className="mx-auto max-w-7xl space-y-6 px-4 py-6 sm:px-6">
<AtlasPageHeader title="Good morning, Mina" description="Here is how Fieldnote Goods is doing.">
<SegmentedControl aria-label="Date range" value={range} onValueChange={(v) => setRange(v as Range)} size="sm">
<SegmentedControlItem value="7">7 days</SegmentedControlItem>
<SegmentedControlItem value="30">30 days</SegmentedControlItem>
</SegmentedControl>
<button type="button" className={atlasButton.outline}><Download aria-hidden="true" /> Export</button>
</AtlasPageHeader>
<section aria-label="Key figures" className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
{kpis.map((k) => {
const up = k.delta >= 0
const Arrow = up ? ArrowUpRight : ArrowDownRight
return (
<div key={k.label} className="bg-card rounded-xl border p-4">
<p className="text-muted-foreground text-sm font-medium">{k.label}</p>
<div className="mt-2 flex items-end justify-between gap-3">
<p className="text-[1.65rem] leading-none font-extrabold tracking-[-0.03em] tabular-nums">{k.value}</p>
<Sparkline values={k.trend} label={`${k.label} trend over ${n} days`} height={32} showEnd={false} className="text-chart-1 w-20 shrink-0" />
</div>
<p className="mt-3 flex items-center gap-1 text-xs">
<Arrow className={cn("size-3.5", up ? "text-chart-2" : "text-destructive")} aria-hidden="true" />
<span className="font-semibold tabular-nums">{Math.abs(k.delta).toFixed(1)}%</span>
<span className="text-muted-foreground">{up ? "up" : "down"} from the previous {n} days</span>
</p>
</div>
)
})}
</section>
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_21rem]">
<section aria-labelledby="atlas-rev" className="bg-card min-w-0 rounded-xl border p-4 sm:p-5">
<div className="flex items-baseline justify-between gap-3">
<h3 id="atlas-rev" className="text-sm font-bold">Revenue</h3>
<p className="text-muted-foreground text-xs">Daily, last {n} days</p>
</div>
<ChartContainer
config={config}
label={`Daily revenue for the last ${n} days compared with the previous period`}
summary={`Revenue totalled ${money.format(revenue)}, ${Math.abs(pct(revenue, previous)).toFixed(1)} percent ${revenue >= previous ? "above" : "below"} the previous period.`}
className="mt-4 aspect-[16/7] w-full"
>
<AreaChart data={data} margin={{ left: 0, right: 8, top: 8 }} accessibilityLayer>
<defs>
<linearGradient id="atlas-fill" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--color-revenue)" stopOpacity={0.28} />
<stop offset="95%" stopColor="var(--color-revenue)" stopOpacity={0.02} />
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis dataKey="day" tickLine={false} axisLine={false} tickMargin={8} interval={n === 7 ? 0 : 4} />
<YAxis tickLine={false} axisLine={false} width={44} tickFormatter={(v: number) => `$${(v / 1000).toFixed(1)}k`} />
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="line" formatter={(v) => money.format(Number(v))} />} />
<Area dataKey="previous" type="monotone" fill="none" stroke="var(--color-previous)" strokeWidth={1.5} strokeDasharray="4 4" dot={false} isAnimationActive={false} />
<Area dataKey="revenue" type="monotone" fill="url(#atlas-fill)" stroke="var(--color-revenue)" strokeWidth={2} dot={false} isAnimationActive={false} />
<ChartLegend content={<ChartLegendContent />} />
</AreaChart>
</ChartContainer>
</section>
<section aria-labelledby="atlas-attn" className="bg-card rounded-xl border">
<h3 id="atlas-attn" className="px-4 pt-4 text-sm font-bold sm:px-5">Needs attention</h3>
<ul className="mt-2 divide-y">
{[
{ icon: Truck, text: `${unfulfilled} orders to fulfil`, hint: "Oldest from 2 days ago", href: h.orders, tone: "bg-chart-3/20" },
{ icon: AlertTriangle, text: `${pending} payments pending`, hint: "Bank transfers, 1 to 3 days", href: h.orders, tone: "bg-chart-5/20" },
{ icon: PackageX, text: `${low.length} products low on stock`, hint: low.slice(0, 2).map((p) => p.name.split(",")[0]).join(", ") + " and more", href: h.products, tone: "bg-destructive/15" },
].map((a) => (
<li key={a.text}>
<a href={a.href} className="hover:bg-accent focus-visible:ring-ring/50 flex items-center gap-3 px-4 py-3 outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-inset sm:px-5">
<span className={cn("flex size-9 shrink-0 items-center justify-center rounded-lg", a.tone)}><a.icon className="size-4" aria-hidden="true" /></span>
<span className="min-w-0 flex-1"><span className="block text-sm font-semibold">{a.text}</span><span className="text-muted-foreground block truncate text-xs">{a.hint}</span></span>
<ArrowRight className="text-muted-foreground size-4 rtl:rotate-180" aria-hidden="true" />
</a>
</li>
))}
</ul>
<div className="border-t p-4 sm:p-5">
<h3 className="text-sm font-bold">Top products</h3>
<ul className="mt-3 space-y-3">
{top.map((p) => (
<li key={p.id} className="flex items-center gap-3">
<ProductTile hue={p.hue} className="size-9 shrink-0" />
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{p.name}</p>
<div className="bg-muted mt-1.5 h-1.5 rounded-full"><div className="bg-chart-1 h-full rounded-full" style={{ width: `${(p.sold * p.price / topMax) * 100}%` }} /></div>
</div>
<p className="text-sm font-semibold tabular-nums">{money.format(p.sold * p.price)}</p>
</li>
))}
</ul>
</div>
</section>
</div>
<section aria-labelledby="atlas-recent" className="bg-card overflow-hidden rounded-xl border">
<div className="flex items-center justify-between px-4 py-3.5 sm:px-5">
<h3 id="atlas-recent" className="text-sm font-bold">Recent orders</h3>
<a href={h.orders} className="text-chart-1 focus-visible:ring-ring/50 rounded text-sm font-semibold underline-offset-4 outline-none hover:underline focus-visible:ring-[3px]">View all</a>
</div>
<div tabIndex={0} role="region" aria-label="Recent orders" className="focus-visible:ring-ring/50 overflow-x-auto outline-none focus-visible:ring-[3px] focus-visible:ring-inset">
<table className="w-full min-w-[40rem] text-start text-sm">
<caption className="sr-only">Most recent orders</caption>
<thead>
<tr className="text-muted-foreground bg-surface border-y text-xs">
{["Order", "Date", "Customer", "Payment", "Fulfilment", "Total"].map((c, i) => <th key={c} scope="col" className={cn("px-4 py-2.5 font-semibold sm:px-5", i === 5 && "text-end")}>{c}</th>)}
</tr>
</thead>
<tbody className="divide-y">
{recent.map((o) => {
const c = getCustomer(o.customerId)
return (
<tr key={o.id} className="hover:bg-accent/50">
<td className="px-4 py-3 sm:px-5"><a href={h.order} className="text-chart-1 font-semibold underline-offset-4 hover:underline" style={{ fontFamily: "var(--atlas-mono)" }}>{o.id}</a></td>
<td className="text-muted-foreground px-4 py-3 whitespace-nowrap sm:px-5">{formatDate(o.date)}</td>
<td className="px-4 py-3 sm:px-5"><span className="flex items-center gap-2.5"><span className={cn("flex size-7 items-center justify-center rounded-full text-[11px] font-bold", ["bg-chart-1/20", "bg-chart-2/20", "bg-chart-3/25", "bg-chart-4/20", "bg-chart-5/20"][c.tone - 1])} aria-hidden="true">{initials(c.name)}</span>{c.name}</span></td>
<td className="px-4 py-3 sm:px-5"><PaymentBadge value={o.payment} /></td>
<td className="px-4 py-3 sm:px-5"><FulfilmentBadge value={o.fulfillment} /></td>
<td className="px-4 py-3 text-end font-semibold tabular-nums sm:px-5">{moneyExact.format(orderGrand(o))}</td>
</tr>
)
})}
</tbody>
</table>
</div>
</section>
</main>
</AtlasShell>
)
}
export { AtlasDashboard, type AtlasDashboardProps }// Ballmac UI: Atlas orders page. https://ui.ballmac.com/templates/template-atlas
"use client"
import * as React from "react"
import { ArrowDown, ArrowUp, ArrowUpDown, ChevronLeft, ChevronRight, Download, Printer, Search, Truck, X } from "lucide-react"
import { Checkbox } from "@/components/ballmac/checkbox"
import { formatDate, formatTime, getCustomer, initials, moneyExact, orderGrand, orders as seed, type AtlasOrder } from "@/components/ballmac/templates/atlas/atlas-data"
import { AtlasPageHeader, AtlasShell, FulfilmentBadge, PaymentBadge, atlasButton, type AtlasHrefs } from "@/components/ballmac/templates/atlas/atlas-theme"
import { cn } from "@/lib/utils"
type Tab = "all" | "unfulfilled" | "unpaid" | "shipped" | "delivered"
type SortKey = "date" | "customer" | "channel" | "total"
const tabs: { id: Tab; label: string; test: (o: AtlasOrder) => boolean }[] = [
{ id: "all", label: "All", test: () => true },
{ id: "unfulfilled", label: "Unfulfilled", test: (o) => o.fulfillment === "unfulfilled" },
{ id: "unpaid", label: "Unpaid", test: (o) => o.payment === "pending" },
{ id: "shipped", label: "Shipped", test: (o) => o.fulfillment === "shipped" },
{ id: "delivered", label: "Delivered", test: (o) => o.fulfillment === "delivered" },
]
const PAGE = 10
const tones = ["bg-chart-1/20", "bg-chart-2/20", "bg-chart-3/25", "bg-chart-4/20", "bg-chart-5/20"]
type AtlasOrdersProps = React.ComponentProps<"div"> & { hrefs?: Partial<AtlasHrefs> }
/** The Atlas orders page: status tabs with counts, search, sortable columns, row selection with bulk actions, and pagination. */
function AtlasOrders({ hrefs, ...props }: AtlasOrdersProps) {
const [rows, setRows] = React.useState<AtlasOrder[]>(seed)
const [tab, setTab] = React.useState<Tab>("all")
const [query, setQuery] = React.useState("")
const [sort, setSort] = React.useState<{ key: SortKey; dir: "asc" | "desc" }>({ key: "date", dir: "desc" })
const [page, setPage] = React.useState(0)
const [selected, setSelected] = React.useState<Set<string>>(new Set())
const orderHref = hrefs?.order ?? "/atlas/orders/ORD-10479"
const q = query.trim().toLowerCase()
const filtered = rows
.filter(tabs.find((t) => t.id === tab)!.test)
.filter((o) => !q || [o.id, getCustomer(o.customerId).name, o.channel].join(" ").toLowerCase().includes(q))
const sorted = [...filtered].sort((a, b) => {
const dir = sort.dir === "asc" ? 1 : -1
const val = (o: AtlasOrder) => (sort.key === "date" ? o.date : sort.key === "customer" ? getCustomer(o.customerId).name : sort.key === "channel" ? o.channel : orderGrand(o))
const x = val(a)
const y = val(b)
return (x < y ? -1 : x > y ? 1 : 0) * dir
})
const pages = Math.max(1, Math.ceil(sorted.length / PAGE))
const safePage = Math.min(page, pages - 1)
const view = sorted.slice(safePage * PAGE, safePage * PAGE + PAGE)
const allOnPage = view.length > 0 && view.every((o) => selected.has(o.id))
const someOnPage = view.some((o) => selected.has(o.id))
function toggleSort(key: SortKey) {
setSort((s) => (s.key === key ? { key, dir: s.dir === "asc" ? "desc" : "asc" } : { key, dir: key === "total" || key === "date" ? "desc" : "asc" }))
setPage(0)
}
function toggleAll(on: boolean) {
setSelected((s) => {
const next = new Set(s)
for (const o of view) on ? next.add(o.id) : next.delete(o.id)
return next
})
}
function toggleOne(id: string, on: boolean) {
setSelected((s) => {
const next = new Set(s)
on ? next.add(id) : next.delete(id)
return next
})
}
function markShipped() {
setRows((all) => all.map((o) => (selected.has(o.id) && o.fulfillment === "unfulfilled" && o.payment !== "pending" ? { ...o, fulfillment: "shipped" } : o)))
setSelected(new Set())
}
const head = (k: SortKey, children: React.ReactNode, right?: boolean) => (
<th key={k} scope="col" aria-sort={sort.key === k ? (sort.dir === "asc" ? "ascending" : "descending") : "none"} className={cn("px-3 py-2.5 font-semibold", right && "text-end")}>
<button type="button" onClick={() => toggleSort(k)} className={cn("hover:text-foreground focus-visible:ring-ring/50 -mx-1.5 inline-flex items-center gap-1 rounded px-1.5 py-0.5 outline-none focus-visible:ring-[3px]", sort.key === k && "text-foreground")}>
{children}
{sort.key === k ? (sort.dir === "asc" ? <ArrowUp className="size-3" aria-hidden="true" /> : <ArrowDown className="size-3" aria-hidden="true" />) : <ArrowUpDown className="size-3 opacity-50" aria-hidden="true" />}
</button>
</th>
)
return (
<AtlasShell page="orders" title="Orders" hrefs={hrefs} actions={<button type="button" className={cn(atlasButton.outline, "hidden sm:inline-flex")}><Download aria-hidden="true" /> Export</button>} {...props}>
<main className="mx-auto max-w-7xl space-y-5 px-4 py-6 sm:px-6">
<AtlasPageHeader title="Orders" description="Everything customers have bought, across every channel." />
<div className="bg-card rounded-xl border">
<div className="flex flex-col gap-3 border-b p-3 lg:flex-row lg:items-center lg:justify-between">
<div role="group" aria-label="Filter by status" className="flex gap-1 overflow-x-auto [scrollbar-width:none]">
{tabs.map((t) => {
const count = rows.filter(t.test).length
const on = tab === t.id
return (
<button
key={t.id}
type="button"
aria-pressed={on}
onClick={() => { setTab(t.id); setPage(0); setSelected(new Set()) }}
className={cn("focus-visible:ring-ring/50 flex shrink-0 items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-semibold outline-none transition-colors focus-visible:ring-[3px]", on ? "bg-accent text-foreground" : "text-muted-foreground hover:text-foreground")}
>
{t.label} <span className="text-muted-foreground text-xs font-medium tabular-nums">{count}</span>
</button>
)
})}
</div>
<div className="relative lg:w-72">
<Search className="text-muted-foreground pointer-events-none absolute top-1/2 start-3 size-4 -translate-y-1/2" aria-hidden="true" />
<input
type="search"
aria-label="Search orders"
value={query}
onChange={(e) => { setQuery(e.target.value); setPage(0) }}
placeholder="Search order, customer or channel"
className="bg-background focus-visible:ring-ring/50 placeholder:text-muted-foreground h-9 w-full rounded-lg border pe-3 ps-9 text-sm outline-none focus-visible:ring-[3px]"
/>
</div>
</div>
{selected.size > 0 && (
<div role="region" aria-label="Bulk actions" className="bg-accent flex flex-wrap items-center gap-2 border-b px-3 py-2">
<p className="me-auto text-sm font-semibold" aria-live="polite">{selected.size} selected</p>
<button type="button" onClick={markShipped} className={atlasButton.outline}><Truck aria-hidden="true" /> Mark as shipped</button>
<button type="button" className={atlasButton.outline}><Printer aria-hidden="true" /> Print labels</button>
<button type="button" onClick={() => setSelected(new Set())} aria-label="Clear selection" className={cn(atlasButton.ghost, "px-2.5")}><X aria-hidden="true" /></button>
</div>
)}
<div tabIndex={0} role="region" aria-label="Orders table" className="focus-visible:ring-ring/50 overflow-x-auto outline-none focus-visible:ring-[3px] focus-visible:ring-inset">
<table className="w-full min-w-[52rem] text-start text-sm">
<caption className="sr-only">Orders, {sorted.length} shown</caption>
<thead>
<tr className="text-muted-foreground bg-surface border-b text-xs">
<th scope="col" className="w-10 px-3 py-2.5"><Checkbox aria-label="Select all orders on this page" checked={allOnPage ? true : someOnPage ? "indeterminate" : false} onCheckedChange={(v) => toggleAll(v === true)} /></th>
<th scope="col" className="px-3 py-2.5 font-semibold">Order</th>
{head("date", "Date")}
{head("customer", "Customer")}
{head("channel", "Channel")}
<th scope="col" className="px-3 py-2.5 font-semibold">Payment</th>
<th scope="col" className="px-3 py-2.5 font-semibold">Fulfilment</th>
{head("total", "Total", true)}
</tr>
</thead>
<tbody className="divide-y">
{view.map((o) => {
const c = getCustomer(o.customerId)
const on = selected.has(o.id)
return (
<tr key={o.id} data-state={on ? "selected" : undefined} className="hover:bg-accent/40 data-[state=selected]:bg-accent/60">
<td className="px-3 py-3"><Checkbox aria-label={`Select ${o.id}`} checked={on} onCheckedChange={(v) => toggleOne(o.id, v === true)} /></td>
<td className="px-3 py-3"><a href={orderHref} className="text-chart-1 font-semibold underline-offset-4 hover:underline" style={{ fontFamily: "var(--atlas-mono)" }}>{o.id}</a></td>
<td className="text-muted-foreground px-3 py-3 whitespace-nowrap">{formatDate(o.date)}<span className="ms-1.5 text-xs">{formatTime(o.date)}</span></td>
<td className="px-3 py-3"><span className="flex items-center gap-2.5 whitespace-nowrap"><span className={cn("flex size-7 shrink-0 items-center justify-center rounded-full text-[11px] font-bold", tones[c.tone - 1])} aria-hidden="true">{initials(c.name)}</span>{c.name}</span></td>
<td className="text-muted-foreground px-3 py-3 whitespace-nowrap">{o.channel}</td>
<td className="px-3 py-3"><PaymentBadge value={o.payment} /></td>
<td className="px-3 py-3"><FulfilmentBadge value={o.fulfillment} /></td>
<td className="px-3 py-3 text-end font-semibold tabular-nums">{moneyExact.format(orderGrand(o))}</td>
</tr>
)
})}
{view.length === 0 && (
<tr><td colSpan={8} className="text-muted-foreground px-3 py-16 text-center">No orders match. Try another status or clear the search.</td></tr>
)}
</tbody>
</table>
</div>
<div className="flex items-center justify-between gap-3 border-t px-3 py-2.5 text-sm">
<p className="text-muted-foreground" role="status">
{sorted.length === 0 ? "No orders" : `Showing ${safePage * PAGE + 1}–${safePage * PAGE + view.length} of ${sorted.length}`}
</p>
<div className="flex items-center gap-1">
<button type="button" aria-label="Previous page" disabled={safePage === 0} onClick={() => setPage(safePage - 1)} className={cn(atlasButton.outline, "px-2.5")}><ChevronLeft aria-hidden="true" className="rtl:rotate-180"/></button>
<span className="text-muted-foreground px-2 text-xs tabular-nums">Page {safePage + 1} of {pages}</span>
<button type="button" aria-label="Next page" disabled={safePage >= pages - 1} onClick={() => setPage(safePage + 1)} className={cn(atlasButton.outline, "px-2.5")}><ChevronRight aria-hidden="true" className="rtl:rotate-180"/></button>
</div>
</div>
</div>
</main>
</AtlasShell>
)
}
export { AtlasOrders, type AtlasOrdersProps }// Ballmac UI: Atlas order detail page. https://ui.ballmac.com/templates/template-atlas
"use client"
import * as React from "react"
import { Check, ChevronRight, CreditCard, Mail, MapPin, MoreHorizontal, Printer, RotateCcw, Truck } from "lucide-react"
import { Checkbox } from "@/components/ballmac/checkbox"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ballmac/dialog"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ballmac/dropdown-menu"
import { formatDate, formatTime, getCustomer, getProduct, initials, moneyExact, orderShipping, orderTotal, orders } from "@/components/ballmac/templates/atlas/atlas-data"
import { AtlasShell, FulfilmentBadge, PaymentBadge, ProductTile, atlasButton, type AtlasHrefs } from "@/components/ballmac/templates/atlas/atlas-theme"
import { cn } from "@/lib/utils"
type Event = { id: string; title: string; detail?: string; at: string }
type AtlasOrderProps = React.ComponentProps<"div"> & {
/** Which sample order to show. */
orderId?: string
hrefs?: Partial<AtlasHrefs>
}
/** The Atlas order page: items and totals, a live timeline, fulfilment and a refund flow with validation, plus customer and notes. */
function AtlasOrder({ orderId = "ORD-10479", hrefs, ...props }: AtlasOrderProps) {
const base = orders.find((o) => o.id === orderId) ?? orders[1]
const customer = getCustomer(base.customerId)
const [fulfillment, setFulfillment] = React.useState(base.fulfillment)
const [payment, setPayment] = React.useState(base.payment)
const [refunded, setRefunded] = React.useState(0)
const [events, setEvents] = React.useState<Event[]>([
{ id: "e3", title: "Payment captured", detail: `${moneyExact.format(orderTotal(base) + orderShipping(base))} on Visa ending 4242`, at: base.date },
{ id: "e2", title: "Order confirmation sent", detail: `Emailed to ${customer.email}`, at: base.date },
{ id: "e1", title: "Order placed", detail: `via ${base.channel}`, at: base.date },
])
const [note, setNote] = React.useState("Gift wrap please. Leave with the neighbour if nobody is home.")
const [savedNote, setSavedNote] = React.useState(note)
const [refundOpen, setRefundOpen] = React.useState(false)
const [picked, setPicked] = React.useState<Record<string, boolean>>({})
const [reason, setReason] = React.useState("")
const [error, setError] = React.useState("")
const [announce, setAnnounce] = React.useState("")
const subtotal = orderTotal(base)
const shipping = orderShipping(base)
const total = subtotal + shipping
const refundAmount = base.items.reduce((n, l) => n + (picked[l.productId] ? getProduct(l.productId).price * l.qty : 0), 0)
const stamp = "2026-09-29T14:20:00Z"
function addEvent(title: string, detail?: string) {
setEvents((all) => [{ id: `e${all.length + 1}-${title}`, title, detail, at: stamp }, ...all])
}
function ship() {
setFulfillment("shipped")
addEvent("Marked as shipped", "Tracking sent to the customer")
setAnnounce(`${base.id} marked as shipped`)
}
function refund() {
if (refundAmount === 0) return setError("Choose at least one item to refund.")
if (!reason) return setError("Pick a reason so your team can follow up.")
setRefunded(refundAmount)
if (refundAmount === subtotal) setPayment("refunded")
addEvent(`Refunded ${moneyExact.format(refundAmount)}`, `Reason: ${reason}`)
setAnnounce(`Refunded ${moneyExact.format(refundAmount)}`)
setRefundOpen(false)
setPicked({})
setReason("")
setError("")
}
return (
<AtlasShell page="orders" title="Orders" hrefs={hrefs} {...props}>
<main className="mx-auto max-w-6xl space-y-5 px-4 py-6 sm:px-6">
<nav aria-label="Breadcrumb" className="text-muted-foreground flex items-center gap-1 text-sm">
<a href={hrefs?.orders ?? "/atlas/orders"} className="hover:text-foreground focus-visible:ring-ring/50 rounded outline-none focus-visible:ring-[3px]">Orders</a>
<ChevronRight className="size-3.5 rtl:rotate-180" aria-hidden="true" />
<span className="text-foreground font-medium" aria-current="page">{base.id}</span>
</nav>
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<div className="flex flex-wrap items-center gap-2.5">
<h2 className="text-2xl font-extrabold tracking-[-0.03em]" style={{ fontFamily: "var(--atlas-mono)" }}>{base.id}</h2>
<PaymentBadge value={payment} />
<FulfilmentBadge value={fulfillment} />
</div>
<p className="text-muted-foreground mt-1.5 text-sm">{formatDate(base.date)} at {formatTime(base.date)} · {base.channel}</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<button type="button" className={atlasButton.outline} onClick={() => setRefundOpen(true)} disabled={payment === "refunded"}><RotateCcw aria-hidden="true" /> Refund</button>
{fulfillment === "unfulfilled" && payment !== "pending" ? (
<button type="button" className={atlasButton.primary} onClick={ship}><Truck aria-hidden="true" /> Mark as shipped</button>
) : (
<button type="button" className={atlasButton.outline}><Printer aria-hidden="true" /> Print packing slip</button>
)}
<DropdownMenu>
<DropdownMenuTrigger className={cn(atlasButton.outline, "px-2.5")} aria-label="More actions"><MoreHorizontal aria-hidden="true" /></DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Duplicate order</DropdownMenuItem>
<DropdownMenuItem>Download invoice</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem destructive>Cancel order</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<p role="status" className="sr-only">{announce}</p>
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_20rem]">
<div className="min-w-0 space-y-4">
<section aria-labelledby="ao-items" className="bg-card rounded-xl border">
<h3 id="ao-items" className="px-5 pt-4 text-sm font-bold">Items</h3>
<ul className="divide-y px-5">
{base.items.map((l) => {
const p = getProduct(l.productId)
return (
<li key={l.productId} className="flex items-center gap-4 py-4">
<ProductTile hue={p.hue} className="size-14 shrink-0" />
<div className="min-w-0 flex-1">
<p className="truncate font-semibold">{p.name}</p>
<p className="text-muted-foreground text-sm">{p.category} · {moneyExact.format(p.price)} each</p>
</div>
<p className="text-muted-foreground text-sm tabular-nums">× {l.qty}</p>
<p className="w-20 text-end font-semibold tabular-nums">{moneyExact.format(p.price * l.qty)}</p>
</li>
)
})}
</ul>
<dl className="bg-surface space-y-2 rounded-b-xl border-t px-5 py-4 text-sm">
<div className="flex justify-between"><dt className="text-muted-foreground">Subtotal</dt><dd className="tabular-nums">{moneyExact.format(subtotal)}</dd></div>
<div className="flex justify-between"><dt className="text-muted-foreground">Shipping</dt><dd className="tabular-nums">{shipping === 0 ? "Free" : moneyExact.format(shipping)}</dd></div>
{refunded > 0 && <div className="flex justify-between"><dt className="text-muted-foreground">Refunded</dt><dd className="text-destructive tabular-nums">−{moneyExact.format(refunded)}</dd></div>}
<div className="flex justify-between border-t pt-3 text-base font-bold"><dt>Total</dt><dd className="tabular-nums">{moneyExact.format(total - refunded)}</dd></div>
</dl>
</section>
<section aria-labelledby="ao-timeline" className="bg-card rounded-xl border p-5">
<h3 id="ao-timeline" className="text-sm font-bold">Timeline</h3>
<ol className="mt-4">
{events.map((e, i) => (
<li key={e.id} className="relative flex gap-3.5 pb-5 last:pb-0">
{i < events.length - 1 && <span className="bg-border absolute top-5 bottom-0 start-[7px] w-px" aria-hidden="true" />}
<span className={cn("relative mt-1 size-3.5 shrink-0 rounded-full border-2", i === 0 ? "border-chart-1 bg-chart-1" : "bg-card border-muted-foreground/50")} aria-hidden="true" />
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold">{e.title}</p>
{e.detail && <p className="text-muted-foreground text-sm text-pretty">{e.detail}</p>}
</div>
<p className="text-muted-foreground shrink-0 text-xs tabular-nums">{formatDate(e.at)}, {formatTime(e.at)}</p>
</li>
))}
</ol>
</section>
</div>
<aside className="space-y-4">
<section aria-labelledby="ao-customer" className="bg-card rounded-xl border p-5">
<h3 id="ao-customer" className="text-sm font-bold">Customer</h3>
<div className="mt-4 flex items-center gap-3">
<span className="bg-chart-1/20 flex size-11 items-center justify-center rounded-full text-sm font-bold" aria-hidden="true">{initials(customer.name)}</span>
<div className="min-w-0"><p className="truncate font-semibold">{customer.name}</p><p className="text-muted-foreground text-sm">{customer.segment === "VIP" ? "VIP customer" : "Customer since 2025"}</p></div>
</div>
<a href={`mailto:${customer.email}`} className="text-chart-1 mt-4 flex items-center gap-2 text-sm underline-offset-4 hover:underline"><Mail className="size-4" aria-hidden="true" />{customer.email}</a>
<p className="text-muted-foreground mt-3 flex items-start gap-2 text-sm"><MapPin className="mt-0.5 size-4 shrink-0" aria-hidden="true" />{customer.city}<br />Shipping and billing address match</p>
<p className="text-muted-foreground mt-3 flex items-center gap-2 text-sm"><CreditCard className="size-4" aria-hidden="true" />Visa ending 4242</p>
</section>
<section aria-labelledby="ao-notes" className="bg-card rounded-xl border p-5">
<h3 id="ao-notes" className="text-sm font-bold">Notes</h3>
<label htmlFor="ao-note" className="sr-only">Order notes</label>
<textarea id="ao-note" rows={4} value={note} onChange={(e) => setNote(e.target.value)} className="bg-background focus-visible:ring-ring/50 mt-3 w-full resize-none rounded-lg border p-3 text-sm outline-none focus-visible:ring-[3px]" />
<div className="mt-3 flex items-center justify-between">
<p className="text-muted-foreground flex items-center gap-1.5 text-xs" role="status">{note === savedNote ? <><Check className="size-3.5" aria-hidden="true" />Saved</> : "Unsaved changes"}</p>
<button type="button" disabled={note === savedNote} onClick={() => { setSavedNote(note); addEvent("Note updated") }} className={atlasButton.outline}>Save note</button>
</div>
</section>
</aside>
</div>
</main>
<Dialog open={refundOpen} onOpenChange={(o) => { setRefundOpen(o); if (!o) setError("") }}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Refund {base.id}</DialogTitle>
<DialogDescription>Choose what to send back to the customer. The money returns to their Visa in 5 to 10 days.</DialogDescription>
</DialogHeader>
<fieldset className="space-y-2">
<legend className="sr-only">Items to refund</legend>
{base.items.map((l) => {
const p = getProduct(l.productId)
return (
<label key={l.productId} className="hover:bg-accent/50 flex cursor-pointer items-center gap-3 rounded-lg border p-3 text-sm">
<Checkbox checked={!!picked[l.productId]} onCheckedChange={(v) => setPicked((s) => ({ ...s, [l.productId]: v === true }))} />
<span className="min-w-0 flex-1 truncate font-medium">{p.name} <span className="text-muted-foreground font-normal">× {l.qty}</span></span>
<span className="tabular-nums">{moneyExact.format(p.price * l.qty)}</span>
</label>
)
})}
</fieldset>
<div>
<label htmlFor="ao-reason" className="text-sm font-medium">Reason</label>
<select id="ao-reason" value={reason} onChange={(e) => setReason(e.target.value)} className="bg-background focus-visible:ring-ring/50 mt-1.5 h-10 w-full rounded-lg border px-3 text-sm outline-none focus-visible:ring-[3px]">
<option value="">Choose a reason</option>
<option>Damaged in transit</option>
<option>Changed their mind</option>
<option>Wrong item sent</option>
</select>
</div>
{error && <p role="alert" className="text-destructive text-sm">{error}</p>}
<DialogFooter>
<button type="button" className={atlasButton.outline} onClick={() => setRefundOpen(false)}>Cancel</button>
<button type="button" className={atlasButton.primary} onClick={refund}>Refund {refundAmount > 0 ? moneyExact.format(refundAmount) : ""}</button>
</DialogFooter>
</DialogContent>
</Dialog>
</AtlasShell>
)
}
export { AtlasOrder, type AtlasOrderProps }// Ballmac UI: Atlas products page. https://ui.ballmac.com/templates/template-atlas
"use client"
import * as React from "react"
import { LayoutGrid, List, Minus, Plus, Search } from "lucide-react"
import { Badge } from "@/components/ballmac/badge"
import { SegmentedControl, SegmentedControlItem } from "@/components/ballmac/segmented-control"
import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from "@/components/ballmac/sheet"
import { moneyExact, products as seed, type AtlasProduct } from "@/components/ballmac/templates/atlas/atlas-data"
import { AtlasPageHeader, AtlasShell, ProductTile, atlasButton, type AtlasHrefs } from "@/components/ballmac/templates/atlas/atlas-theme"
import { cn } from "@/lib/utils"
type View = "grid" | "list"
function Stock({ p }: { p: AtlasProduct }) {
if (p.stock === 0) return <Badge status="error" variant="outline">Out of stock</Badge>
if (p.stock <= 10) return <Badge status="warning" variant="outline">{p.stock} left</Badge>
return <span className="text-muted-foreground text-sm tabular-nums">{p.stock} in stock</span>
}
const categories = ["Paper", "Writing", "Bags", "Desk"]
const fieldClass = "bg-background focus-visible:ring-ring/50 mt-1.5 h-10 w-full rounded-lg border px-3 text-sm outline-none focus-visible:ring-[3px] aria-[invalid=true]:border-destructive"
type AtlasProductsProps = React.ComponentProps<"div"> & { hrefs?: Partial<AtlasHrefs> }
/** The Atlas products page: search, category and low-stock filters, grid or list, quick stock edits and an add-product drawer with validation. */
function AtlasProducts({ hrefs, ...props }: AtlasProductsProps) {
const [items, setItems] = React.useState<AtlasProduct[]>(seed)
const [query, setQuery] = React.useState("")
const [category, setCategory] = React.useState("all")
const [lowOnly, setLowOnly] = React.useState(false)
const [view, setView] = React.useState<View>("grid")
const [open, setOpen] = React.useState(false)
const [errors, setErrors] = React.useState<{ name?: string; price?: string }>({})
const q = query.trim().toLowerCase()
const shown = items.filter((p) => (category === "all" || p.category === category) && (!lowOnly || p.stock <= 10) && (!q || p.name.toLowerCase().includes(q)))
const lowCount = items.filter((p) => p.stock <= 10).length
function adjust(id: string, by: number) {
setItems((all) => all.map((p) => (p.id === id ? { ...p, stock: Math.max(0, p.stock + by) } : p)))
}
function add(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const f = new FormData(e.currentTarget)
const name = String(f.get("name") ?? "").trim()
const price = Number(f.get("price"))
const next: typeof errors = {}
if (!name) next.name = "Give the product a name."
if (!(price > 0)) next.price = "Enter a price above $0."
setErrors(next)
if (next.name || next.price) {
;(e.currentTarget.elements.namedItem(next.name ? "name" : "price") as HTMLElement | null)?.focus()
return
}
setItems((all) => [{ id: `p-new-${all.length}`, name, category: String(f.get("category")), price, stock: Number(f.get("stock")) || 0, status: f.get("status") === "draft" ? "draft" : "active", hue: 180 + all.length * 17, sold: 0 }, ...all])
setOpen(false)
}
return (
<AtlasShell page="products" title="Products" hrefs={hrefs} actions={<button type="button" onClick={() => setOpen(true)} className={cn(atlasButton.primary, "hidden sm:inline-flex")}><Plus aria-hidden="true" /> Add product</button>} {...props}>
<main className="mx-auto max-w-7xl space-y-5 px-4 py-6 sm:px-6">
<AtlasPageHeader title="Products" description={`${items.length} products · ${lowCount} need restocking`}>
<button type="button" onClick={() => setOpen(true)} className={cn(atlasButton.primary, "sm:hidden")}><Plus aria-hidden="true" /> Add product</button>
</AtlasPageHeader>
<div className="flex flex-wrap items-center gap-2">
<div className="relative min-w-52 flex-1 sm:max-w-xs">
<Search className="text-muted-foreground pointer-events-none absolute top-1/2 start-3 size-4 -translate-y-1/2" aria-hidden="true" />
<input type="search" aria-label="Search products" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search products" className="bg-card focus-visible:ring-ring/50 placeholder:text-muted-foreground h-9 w-full rounded-lg border pe-3 ps-9 text-sm outline-none focus-visible:ring-[3px]" />
</div>
<select aria-label="Category" value={category} onChange={(e) => setCategory(e.target.value)} className="bg-card focus-visible:ring-ring/50 h-9 rounded-lg border px-3 text-sm outline-none focus-visible:ring-[3px]">
<option value="all">All categories</option>
{categories.map((c) => <option key={c}>{c}</option>)}
</select>
<button type="button" aria-pressed={lowOnly} onClick={() => setLowOnly((v) => !v)} className={cn("focus-visible:ring-ring/50 h-9 rounded-lg border px-3 text-sm font-semibold outline-none transition-colors focus-visible:ring-[3px]", lowOnly ? "bg-accent border-foreground/30" : "bg-card hover:bg-accent")}>
Low stock <span className="text-muted-foreground ms-1 tabular-nums">{lowCount}</span>
</button>
<SegmentedControl aria-label="Layout" value={view} onValueChange={(v) => setView(v as View)} size="sm" className="ms-auto">
<SegmentedControlItem value="grid" aria-label="Grid view"><LayoutGrid /></SegmentedControlItem>
<SegmentedControlItem value="list" aria-label="List view"><List /></SegmentedControlItem>
</SegmentedControl>
</div>
<p className="sr-only" role="status">{shown.length} products shown</p>
{shown.length === 0 ? (
<div className="bg-card text-muted-foreground rounded-xl border px-6 py-20 text-center text-sm">No products match these filters.</div>
) : view === "grid" ? (
<ul className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{shown.map((p) => (
<li key={p.id} className="bg-card group rounded-xl border p-3 transition-shadow hover:shadow-md">
<ProductTile hue={p.hue} className="aspect-[4/3] w-full" />
<div className="mt-3 px-1">
<div className="flex items-start justify-between gap-2">
<h3 className="text-sm leading-snug font-semibold text-pretty">{p.name}</h3>
{p.status === "draft" && <Badge variant="secondary">Draft</Badge>}
</div>
<p className="text-muted-foreground mt-0.5 text-xs">{p.category}</p>
<div className="mt-3 flex items-center justify-between"><p className="font-bold tabular-nums">{moneyExact.format(p.price)}</p><Stock p={p} /></div>
</div>
</li>
))}
</ul>
) : (
<div className="bg-card overflow-hidden rounded-xl border">
<div tabIndex={0} role="region" aria-label="Products table" className="focus-visible:ring-ring/50 overflow-x-auto outline-none focus-visible:ring-[3px] focus-visible:ring-inset">
<table className="w-full min-w-[40rem] text-start text-sm">
<caption className="sr-only">Products</caption>
<thead><tr className="text-muted-foreground bg-surface border-b text-xs">{["Product", "Category", "Status", "Price", "Stock"].map((c, i) => <th key={c} scope="col" className={cn("px-4 py-2.5 font-semibold", i === 3 && "text-end", i === 4 && "text-end")}>{c}</th>)}</tr></thead>
<tbody className="divide-y">
{shown.map((p) => (
<tr key={p.id} className="hover:bg-accent/40">
<td className="px-4 py-2.5"><span className="flex items-center gap-3"><ProductTile hue={p.hue} className="size-9 shrink-0" /><span className="font-semibold">{p.name}</span></span></td>
<td className="text-muted-foreground px-4 py-2.5">{p.category}</td>
<td className="px-4 py-2.5"><Badge status={p.status === "active" ? "success" : "neutral"} variant="outline">{p.status === "active" ? "Active" : "Draft"}</Badge></td>
<td className="px-4 py-2.5 text-end font-semibold tabular-nums">{moneyExact.format(p.price)}</td>
<td className="px-4 py-2.5">
<div className="flex items-center justify-end gap-1.5">
<button type="button" aria-label={`Remove one ${p.name}`} onClick={() => adjust(p.id, -1)} className="hover:bg-accent focus-visible:ring-ring/50 inline-flex size-7 items-center justify-center rounded-md border outline-none focus-visible:ring-[3px]"><Minus className="size-3.5" aria-hidden="true" /></button>
<span className="w-10 text-center font-semibold tabular-nums" aria-live="polite">{p.stock}</span>
<button type="button" aria-label={`Add one ${p.name}`} onClick={() => adjust(p.id, 1)} className="hover:bg-accent focus-visible:ring-ring/50 inline-flex size-7 items-center justify-center rounded-md border outline-none focus-visible:ring-[3px]"><Plus className="size-3.5" aria-hidden="true" /></button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</main>
<Sheet open={open} onOpenChange={(o) => { setOpen(o); if (!o) setErrors({}) }}>
<SheetContent side="end" className="w-full sm:max-w-md" closeLabel="Close">
<form onSubmit={add} noValidate className="flex h-full flex-col">
<SheetHeader>
<SheetTitle>Add product</SheetTitle>
<SheetDescription>New products start as active and appear in your store right away.</SheetDescription>
</SheetHeader>
<div className="flex-1 space-y-4 overflow-y-auto px-4 pb-4">
<div>
<label htmlFor="ap-name" className="text-sm font-medium">Name</label>
<input id="ap-name" name="name" aria-invalid={!!errors.name} aria-describedby={errors.name ? "ap-name-err" : undefined} className={fieldClass} />
{errors.name && <p id="ap-name-err" className="text-destructive mt-1.5 text-sm">{errors.name}</p>}
</div>
<div>
<label htmlFor="ap-cat" className="text-sm font-medium">Category</label>
<select id="ap-cat" name="category" className={fieldClass}>{categories.map((c) => <option key={c}>{c}</option>)}</select>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label htmlFor="ap-price" className="text-sm font-medium">Price (USD)</label>
<input id="ap-price" name="price" type="number" step="0.01" min="0" inputMode="decimal" aria-invalid={!!errors.price} aria-describedby={errors.price ? "ap-price-err" : undefined} className={fieldClass} />
{errors.price && <p id="ap-price-err" className="text-destructive mt-1.5 text-sm">{errors.price}</p>}
</div>
<div>
<label htmlFor="ap-stock" className="text-sm font-medium">Stock</label>
<input id="ap-stock" name="stock" type="number" min="0" defaultValue="0" className={fieldClass} />
</div>
</div>
<div>
<label htmlFor="ap-status" className="text-sm font-medium">Status</label>
<select id="ap-status" name="status" className={fieldClass}><option value="active">Active</option><option value="draft">Draft</option></select>
</div>
</div>
<SheetFooter>
<button type="button" className={atlasButton.outline} onClick={() => setOpen(false)}>Cancel</button>
<button type="submit" className={atlasButton.primary}>Add product</button>
</SheetFooter>
</form>
</SheetContent>
</Sheet>
</AtlasShell>
)
}
export { AtlasProducts, type AtlasProductsProps }// Ballmac UI: Atlas customers page. https://ui.ballmac.com/templates/template-atlas
"use client"
import * as React from "react"
import { Mail, Search } from "lucide-react"
import { Badge } from "@/components/ballmac/badge"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ballmac/sheet"
import { customers, formatDate, initials, money, moneyExact, orderGrand, orders, type AtlasCustomer } from "@/components/ballmac/templates/atlas/atlas-data"
import { AtlasPageHeader, AtlasShell, FulfilmentBadge, atlasButton, type AtlasHrefs } from "@/components/ballmac/templates/atlas/atlas-theme"
import { cn } from "@/lib/utils"
type Segment = "All" | AtlasCustomer["segment"]
const segments: Segment[] = ["All", "VIP", "Regular", "New", "At risk"]
const tones = ["bg-chart-1/20", "bg-chart-2/20", "bg-chart-3/25", "bg-chart-4/20", "bg-chart-5/20"]
const segmentStatus = { VIP: "success", Regular: "neutral", New: "neutral", "At risk": "warning" } as const
function stats(c: AtlasCustomer) {
const mine = orders.filter((o) => o.customerId === c.id)
const spent = mine.reduce((n, o) => n + (o.payment === "refunded" ? 0 : orderGrand(o)), 0)
return { mine, spent, last: mine[0]?.date }
}
type AtlasCustomersProps = React.ComponentProps<"div"> & { hrefs?: Partial<AtlasHrefs> }
/** The Atlas customers page: segment filters, search, lifetime value bars and a profile drawer with recent orders. */
function AtlasCustomers({ hrefs, ...props }: AtlasCustomersProps) {
const [segment, setSegment] = React.useState<Segment>("All")
const [query, setQuery] = React.useState("")
const [openId, setOpenId] = React.useState<string | null>(null)
const q = query.trim().toLowerCase()
const rows = customers
.map((c) => ({ c, ...stats(c) }))
.filter((r) => (segment === "All" || r.c.segment === segment) && (!q || [r.c.name, r.c.email, r.c.city].join(" ").toLowerCase().includes(q)))
.sort((a, b) => b.spent - a.spent)
const max = Math.max(...customers.map((c) => stats(c).spent))
const open = customers.find((c) => c.id === openId)
const openStats = open ? stats(open) : null
return (
<AtlasShell page="customers" title="Customers" hrefs={hrefs} {...props}>
<main className="mx-auto max-w-7xl space-y-5 px-4 py-6 sm:px-6">
<AtlasPageHeader title="Customers" description={`${customers.length} people have bought from Fieldnote Goods.`} />
<div className="bg-card rounded-xl border">
<div className="flex flex-col gap-3 border-b p-3 lg:flex-row lg:items-center lg:justify-between">
<div role="group" aria-label="Filter by segment" className="flex gap-1 overflow-x-auto [scrollbar-width:none]">
{segments.map((s) => {
const on = segment === s
const count = s === "All" ? customers.length : customers.filter((c) => c.segment === s).length
return (
<button key={s} type="button" aria-pressed={on} onClick={() => setSegment(s)} className={cn("focus-visible:ring-ring/50 flex shrink-0 items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-semibold outline-none transition-colors focus-visible:ring-[3px]", on ? "bg-accent text-foreground" : "text-muted-foreground hover:text-foreground")}>
{s} <span className="text-muted-foreground text-xs font-medium tabular-nums">{count}</span>
</button>
)
})}
</div>
<div className="relative lg:w-72">
<Search className="text-muted-foreground pointer-events-none absolute top-1/2 start-3 size-4 -translate-y-1/2" aria-hidden="true" />
<input type="search" aria-label="Search customers" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search name, email or city" className="bg-background focus-visible:ring-ring/50 placeholder:text-muted-foreground h-9 w-full rounded-lg border pe-3 ps-9 text-sm outline-none focus-visible:ring-[3px]" />
</div>
</div>
<div tabIndex={0} role="region" aria-label="Customers table" className="focus-visible:ring-ring/50 overflow-x-auto outline-none focus-visible:ring-[3px] focus-visible:ring-inset">
<table className="w-full min-w-[44rem] text-start text-sm">
<caption className="sr-only">Customers by lifetime spend</caption>
<thead><tr className="text-muted-foreground bg-surface border-b text-xs">{["Customer", "Segment", "Orders", "Last order", "Lifetime spend"].map((c) => <th key={c} scope="col" className="px-4 py-2.5 font-semibold">{c}</th>)}</tr></thead>
<tbody className="divide-y">
{rows.map(({ c, mine, spent, last }) => (
<tr key={c.id} className="hover:bg-accent/40">
<td className="px-4 py-3">
<button type="button" onClick={() => setOpenId(c.id)} className="focus-visible:ring-ring/50 -m-1 flex items-center gap-3 rounded-lg p-1 text-start outline-none focus-visible:ring-[3px]">
<span className={cn("flex size-9 shrink-0 items-center justify-center rounded-full text-xs font-bold", tones[c.tone - 1])} aria-hidden="true">{initials(c.name)}</span>
<span><span className="block font-semibold">{c.name}</span><span className="text-muted-foreground block text-xs">{c.email} · {c.city}</span></span>
</button>
</td>
<td className="px-4 py-3"><Badge status={segmentStatus[c.segment]} variant="outline">{c.segment}</Badge></td>
<td className="px-4 py-3 tabular-nums">{mine.length}</td>
<td className="text-muted-foreground px-4 py-3 whitespace-nowrap">{last ? formatDate(last) : "—"}</td>
<td className="px-4 py-3">
<div className="flex items-center gap-3">
<div className="bg-muted hidden h-1.5 w-24 rounded-full sm:block" aria-hidden="true"><div className="bg-chart-1 h-full rounded-full" style={{ width: `${(spent / max) * 100}%` }} /></div>
<span className="w-20 text-end font-semibold tabular-nums">{money.format(spent)}</span>
</div>
</td>
</tr>
))}
{rows.length === 0 && <tr><td colSpan={5} className="text-muted-foreground px-4 py-16 text-center">No customers match.</td></tr>}
</tbody>
</table>
</div>
<p className="text-muted-foreground border-t px-4 py-2.5 text-sm" role="status">{rows.length} of {customers.length} customers</p>
</div>
</main>
<Sheet open={!!open} onOpenChange={(o) => !o && setOpenId(null)}>
<SheetContent side="end" className="w-full sm:max-w-md" closeLabel="Close profile">
{open && openStats && (
<>
<SheetHeader>
<div className="flex items-center gap-3">
<span className={cn("flex size-12 items-center justify-center rounded-full text-sm font-bold", tones[open.tone - 1])} aria-hidden="true">{initials(open.name)}</span>
<div><SheetTitle>{open.name}</SheetTitle><SheetDescription>{open.city} · {open.segment}</SheetDescription></div>
</div>
</SheetHeader>
<div className="space-y-5 overflow-y-auto px-4 pb-6">
<dl className="grid grid-cols-3 gap-2 text-center">
{[["Spent", money.format(openStats.spent)], ["Orders", String(openStats.mine.length)], ["Average", openStats.mine.length ? moneyExact.format(openStats.spent / openStats.mine.length) : "—"]].map(([k, v]) => (
<div key={k} className="bg-surface rounded-lg border p-3"><dt className="text-muted-foreground text-xs">{k}</dt><dd className="mt-1 font-bold tabular-nums">{v}</dd></div>
))}
</dl>
<a href={`mailto:${open.email}`} className={cn(atlasButton.outline, "w-full")}><Mail aria-hidden="true" /> Email {open.name.split(" ")[0]}</a>
<section aria-labelledby="ac-recent">
<h3 id="ac-recent" className="text-sm font-bold">Recent orders</h3>
<ul className="mt-2 divide-y rounded-lg border">
{openStats.mine.slice(0, 4).map((o) => (
<li key={o.id} className="flex items-center justify-between gap-3 px-3 py-2.5 text-sm">
<span><span className="font-semibold" style={{ fontFamily: "var(--atlas-mono)" }}>{o.id}</span><span className="text-muted-foreground ms-2 text-xs">{formatDate(o.date)}</span></span>
<span className="flex items-center gap-2"><FulfilmentBadge value={o.fulfillment} /><span className="tabular-nums">{moneyExact.format(orderGrand(o))}</span></span>
</li>
))}
</ul>
</section>
</div>
</>
)}
</SheetContent>
</Sheet>
</AtlasShell>
)
}
export { AtlasCustomers, type AtlasCustomersProps }// Ballmac UI: Atlas settings page. https://ui.ballmac.com/templates/template-atlas
"use client"
import * as React from "react"
import { Check } from "lucide-react"
import { Switch } from "@/components/ballmac/switch"
import { AtlasPageHeader, AtlasShell, atlasButton, type AtlasHrefs } from "@/components/ballmac/templates/atlas/atlas-theme"
import { cn } from "@/lib/utils"
const field = "bg-background focus-visible:ring-ring/50 mt-1.5 h-10 w-full rounded-lg border px-3 text-sm outline-none focus-visible:ring-[3px]"
type State = {
name: string
email: string
currency: string
rates: Record<string, boolean>
alerts: Record<string, boolean>
}
const initial: State = {
name: "Fieldnote Goods",
email: "hello@fieldnote.example",
currency: "USD",
rates: { standard: true, express: true, pickup: false },
alerts: { orders: true, stock: true, reviews: false, digest: true },
}
const rateRows = [
["standard", "Standard shipping", "3 to 5 business days · $8, free over $100"],
["express", "Express shipping", "1 to 2 business days · $19"],
["pickup", "Local pickup", "Collect from the Portland studio · Free"],
]
const alertRows = [
["orders", "New orders", "Email me as soon as an order is placed."],
["stock", "Low stock", "Tell me when a product drops to 10 or fewer."],
["reviews", "Customer reviews", "Tell me about new reviews that need a reply."],
["digest", "Weekly digest", "A Monday summary of sales, orders and top products."],
]
const sections = [["profile", "Store profile"], ["shipping", "Shipping"], ["alerts", "Notifications"], ["danger", "Danger zone"]]
type AtlasSettingsProps = React.ComponentProps<"div"> & { hrefs?: Partial<AtlasHrefs> }
/** The Atlas settings page: store profile, shipping rates and notification switches, with a save bar that appears only when something changed. */
function AtlasSettings({ hrefs, ...props }: AtlasSettingsProps) {
const [saved, setSaved] = React.useState<State>(initial)
const [draft, setDraft] = React.useState<State>(initial)
const [status, setStatus] = React.useState<"idle" | "saved">("idle")
const dirty = JSON.stringify(saved) !== JSON.stringify(draft)
function set<K extends keyof State>(key: K, value: State[K]) {
setStatus("idle")
setDraft((d) => ({ ...d, [key]: value }))
}
return (
<AtlasShell page="settings" title="Settings" hrefs={hrefs} {...props}>
<main className="mx-auto grid max-w-5xl gap-8 px-4 py-6 pb-28 sm:px-6 lg:grid-cols-[11rem_minmax(0,1fr)]">
<nav aria-label="Settings sections" className="hidden lg:block">
<ul className="sticky top-20 space-y-0.5">
{sections.map(([id, label]) => <li key={id}><a href={`#${id}`} className="text-muted-foreground hover:text-foreground hover:bg-accent focus-visible:ring-ring/50 block rounded-lg px-3 py-2 text-sm font-medium outline-none focus-visible:ring-[3px]">{label}</a></li>)}
</ul>
</nav>
<div className="min-w-0 space-y-6">
<AtlasPageHeader title="Settings" description="Manage how your store looks, ships and talks to you." />
<section id="profile" aria-labelledby="as-profile" className="bg-card scroll-mt-20 rounded-xl border p-5">
<h3 id="as-profile" className="font-bold">Store profile</h3>
<div className="mt-4 grid gap-4 sm:grid-cols-2">
<div><label htmlFor="as-name" className="text-sm font-medium">Store name</label><input id="as-name" value={draft.name} onChange={(e) => set("name", e.target.value)} className={field} /></div>
<div><label htmlFor="as-email" className="text-sm font-medium">Contact email</label><input id="as-email" type="email" value={draft.email} onChange={(e) => set("email", e.target.value)} className={field} /></div>
<div><label htmlFor="as-cur" className="text-sm font-medium">Currency</label>
<select id="as-cur" value={draft.currency} onChange={(e) => set("currency", e.target.value)} className={field}><option>USD</option><option>EUR</option><option>GBP</option><option>CAD</option></select>
</div>
</div>
</section>
<section id="shipping" aria-labelledby="as-ship" className="bg-card scroll-mt-20 rounded-xl border">
<h3 id="as-ship" className="p-5 pb-3 font-bold">Shipping rates</h3>
<ul className="divide-y border-t">
{rateRows.map(([id, title, text]) => (
<li key={id} className="flex items-center justify-between gap-4 px-5 py-4">
<div><p id={`as-rate-${id}`} className="text-sm font-semibold">{title}</p><p className="text-muted-foreground text-sm">{text}</p></div>
<Switch aria-labelledby={`as-rate-${id}`} checked={draft.rates[id]} onCheckedChange={(v) => set("rates", { ...draft.rates, [id]: v })} />
</li>
))}
</ul>
</section>
<section id="alerts" aria-labelledby="as-alerts" className="bg-card scroll-mt-20 rounded-xl border">
<h3 id="as-alerts" className="p-5 pb-3 font-bold">Notifications</h3>
<ul className="divide-y border-t">
{alertRows.map(([id, title, text]) => (
<li key={id} className="flex items-center justify-between gap-4 px-5 py-4">
<div><p id={`as-alert-${id}`} className="text-sm font-semibold">{title}</p><p className="text-muted-foreground text-sm">{text}</p></div>
<Switch aria-labelledby={`as-alert-${id}`} checked={draft.alerts[id]} onCheckedChange={(v) => set("alerts", { ...draft.alerts, [id]: v })} />
</li>
))}
</ul>
</section>
<section id="danger" aria-labelledby="as-danger" className="border-destructive/40 bg-card scroll-mt-20 rounded-xl border p-5">
<h3 id="as-danger" className="font-bold">Danger zone</h3>
<p className="text-muted-foreground mt-1 text-sm text-pretty">Closing the store hides it from customers. Orders and customer data are kept for 90 days.</p>
<button type="button" className={cn(atlasButton.danger, "mt-4")}>Close store</button>
</section>
</div>
</main>
{(dirty || status === "saved") && (
<div role="region" aria-label="Save changes" className="bg-popover fixed inset-x-3 bottom-4 z-40 mx-auto flex max-w-xl items-center justify-between gap-3 rounded-2xl border p-3 ps-5 shadow-[0_20px_50px_-12px_rgb(0_0_0/0.35)]">
<p className="text-sm font-semibold" role="status">{dirty ? "You have unsaved changes" : <span className="flex items-center gap-1.5"><Check className="text-chart-2 size-4" aria-hidden="true" />Changes saved</span>}</p>
{dirty && (
<div className="flex gap-2">
<button type="button" className={atlasButton.outline} onClick={() => setDraft(saved)}>Discard</button>
<button type="button" className={atlasButton.primary} onClick={() => { setSaved(draft); setStatus("saved") }}>Save changes</button>
</div>
)}
</div>
)}
</AtlasShell>
)
}
export { AtlasSettings, type AtlasSettingsProps }// Ballmac UI: Atlas template route. https://ui.ballmac.com/templates/template-atlas
import type { Metadata } from "next"
import { AtlasDashboard } from "@/components/ballmac/templates/atlas/atlas-dashboard"
export const metadata: Metadata = {
title: "Atlas dashboard · Atlas",
description: "Sales, orders and stock for Fieldnote Goods.",
}
export default function Page() {
return <AtlasDashboard />
}// Ballmac UI: Atlas template route. https://ui.ballmac.com/templates/template-atlas
import type { Metadata } from "next"
import { AtlasOrders } from "@/components/ballmac/templates/atlas/atlas-orders"
export const metadata: Metadata = {
title: "Orders · Atlas",
description: "Search, sort and fulfil every order.",
}
export default function Page() {
return <AtlasOrders />
}// Ballmac UI: Atlas template route. https://ui.ballmac.com/templates/template-atlas
import type { Metadata } from "next"
import { AtlasOrder } from "@/components/ballmac/templates/atlas/atlas-order"
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
const { id } = await params
return { title: `Order ${id} · Atlas`, description: "Items, timeline, fulfilment and refunds for one order." }
}
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
return <AtlasOrder orderId={id} />
}// Ballmac UI: Atlas template route. https://ui.ballmac.com/templates/template-atlas
import type { Metadata } from "next"
import { AtlasProducts } from "@/components/ballmac/templates/atlas/atlas-products"
export const metadata: Metadata = {
title: "Products · Atlas",
description: "Catalog, stock and pricing.",
}
export default function Page() {
return <AtlasProducts />
}// Ballmac UI: Atlas template route. https://ui.ballmac.com/templates/template-atlas
import type { Metadata } from "next"
import { AtlasCustomers } from "@/components/ballmac/templates/atlas/atlas-customers"
export const metadata: Metadata = {
title: "Customers · Atlas",
description: "Segments, lifetime spend and recent orders.",
}
export default function Page() {
return <AtlasCustomers />
}// Ballmac UI: Atlas template route. https://ui.ballmac.com/templates/template-atlas
import type { Metadata } from "next"
import { AtlasSettings } from "@/components/ballmac/templates/atlas/atlas-settings"
export const metadata: Metadata = {
title: "Settings · Atlas",
description: "Store profile, shipping and notifications.",
}
export default function Page() {
return <AtlasSettings />
}Installation
$ pnpm dlx shadcn@latest add @ballmac/template-atlasInstall the dependencies.
$ pnpm add lucide-react recharts@^3Add the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/badge @ballmac/checkbox @ballmac/chart @ballmac/command @ballmac/dialog @ballmac/dropdown-menu @ballmac/segmented-control @ballmac/sheet @ballmac/sparkline @ballmac/switchCopy the source into your project.
components/ballmac/templates/atlas/atlas-fonts.ts// Ballmac UI: Atlas template fonts. https://ui.ballmac.com/templates/template-atlas import { Geist_Mono, Manrope } from "next/font/google" export const atlasSans = Manrope({ variable: "--atlas-sans", subsets: ["latin"], display: "swap", preload: false }) export const atlasMono = Geist_Mono({ variable: "--atlas-mono", subsets: ["latin"], display: "swap", preload: false })components/ballmac/templates/atlas/atlas-data.ts// Ballmac UI: Atlas sample data. https://ui.ballmac.com/templates/template-atlas export type AtlasPayment = "paid" | "pending" | "refunded" export type AtlasFulfillment = "unfulfilled" | "shipped" | "delivered" export type AtlasChannel = "Online store" | "Instagram" | "Wholesale" | "Pop-up" export type AtlasOrder = { id: string /** ISO date-time, shown in UTC. */ date: string customerId: string channel: AtlasChannel payment: AtlasPayment fulfillment: AtlasFulfillment items: { productId: string; qty: number }[] } export type AtlasProduct = { id: string; name: string; category: string; price: number; stock: number; status: "active" | "draft"; hue: number; sold: number } export type AtlasCustomer = { id: string; name: string; email: string; city: string; segment: "VIP" | "New" | "At risk" | "Regular"; tone: number } export const products: AtlasProduct[] = [ { id: "p-01", name: "Field notebook, set of 3", category: "Paper", price: 24, stock: 142, status: "active", hue: 85, sold: 812 }, { id: "p-02", name: "Brass pen, matte", category: "Writing", price: 58, stock: 7, status: "active", hue: 70, sold: 244 }, { id: "p-03", name: "Waxed canvas tote", category: "Bags", price: 96, stock: 38, status: "active", hue: 150, sold: 301 }, { id: "p-04", name: "Linen desk mat", category: "Desk", price: 68, stock: 55, status: "active", hue: 40, sold: 187 }, { id: "p-05", name: "Graphite pencils, 12", category: "Writing", price: 16, stock: 260, status: "active", hue: 260, sold: 1204 }, { id: "p-06", name: "Ceramic ink well", category: "Desk", price: 42, stock: 0, status: "active", hue: 200, sold: 98 }, { id: "p-07", name: "Leather pen roll", category: "Bags", price: 74, stock: 21, status: "active", hue: 30, sold: 156 }, { id: "p-08", name: "Stitched journal, A5", category: "Paper", price: 32, stock: 9, status: "active", hue: 120, sold: 530 }, { id: "p-09", name: "Wooden letter tray", category: "Desk", price: 54, stock: 33, status: "draft", hue: 55, sold: 0 }, { id: "p-10", name: "Wax seal kit", category: "Paper", price: 28, stock: 64, status: "active", hue: 15, sold: 219 }, { id: "p-11", name: "Fountain pen, steel", category: "Writing", price: 120, stock: 12, status: "active", hue: 230, sold: 143 }, { id: "p-12", name: "Travel ink, three colours", category: "Writing", price: 36, stock: 5, status: "active", hue: 300, sold: 267 }, ] export const customers: AtlasCustomer[] = [ { id: "c-01", name: "Amara Okafor", email: "amara@fernhill.co", city: "Lagos", segment: "VIP", tone: 1 }, { id: "c-02", name: "Lucas Meyer", email: "lucas.meyer@mail.de", city: "Berlin", segment: "Regular", tone: 2 }, { id: "c-03", name: "Sofia Alvarez", email: "sofia@alvarez.studio", city: "Mexico City", segment: "VIP", tone: 3 }, { id: "c-04", name: "Hana Kobayashi", email: "hana.k@tokyo.jp", city: "Tokyo", segment: "New", tone: 4 }, { id: "c-05", name: "Daniel Reyes", email: "dreyes@harbor.io", city: "Manila", segment: "At risk", tone: 5 }, { id: "c-06", name: "Priya Raman", email: "priya@raman.in", city: "Chennai", segment: "Regular", tone: 1 }, { id: "c-07", name: "Marcus Lindqvist", email: "marcus@fjord.se", city: "Stockholm", segment: "VIP", tone: 2 }, { id: "c-08", name: "Elena Rossi", email: "elena@rossi.it", city: "Milan", segment: "Regular", tone: 3 }, { id: "c-09", name: "Tomás Beltrán", email: "tomas@beltran.ar", city: "Buenos Aires", segment: "New", tone: 4 }, { id: "c-10", name: "Grace Mutasa", email: "grace@mutasa.zw", city: "Harare", segment: "At risk", tone: 5 }, { id: "c-11", name: "Noah Fischer", email: "noah.fischer@web.ch", city: "Zürich", segment: "Regular", tone: 1 }, { id: "c-12", name: "Yuki Tanaka", email: "yuki@tanaka.jp", city: "Osaka", segment: "VIP", tone: 2 }, ] const channels: AtlasChannel[] = ["Online store", "Online store", "Instagram", "Online store", "Wholesale", "Pop-up", "Online store", "Instagram"] const payments: AtlasPayment[] = ["paid", "paid", "paid", "pending", "paid", "paid", "refunded", "paid", "paid", "pending"] const fulfilments: AtlasFulfillment[] = ["delivered", "shipped", "unfulfilled", "delivered", "unfulfilled", "shipped", "delivered", "unfulfilled", "delivered", "shipped"] /** 36 orders over three weeks, newest first. Built from fixed arithmetic so the server and browser agree. */ export const orders: AtlasOrder[] = Array.from({ length: 36 }, (_, i) => { const n = 10480 - i const day = 29 - Math.floor(i / 2) const hour = i % 2 === 0 ? 14 + (i % 5) : 8 + (i % 4) const minute = (i * 13) % 60 const lines = 1 + (i % 3) const age = i < 6 ? 0 : i < 14 ? 1 : 2 const payment = payments[i % payments.length] const fulfillment: AtlasFulfillment = payment === "pending" ? "unfulfilled" : age === 0 ? (i % 2 ? "unfulfilled" : "shipped") : fulfilments[i % fulfilments.length] return { id: `ORD-${n}`, date: `2026-09-${String(day).padStart(2, "0")}T${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}:00Z`, customerId: customers[(i * 5 + 1) % customers.length].id, channel: channels[i % channels.length], payment, fulfillment, items: Array.from({ length: lines }, (_, k) => ({ productId: products[(i * 3 + k * 4) % products.length].id, qty: 1 + ((i + k) % 3) })), } }) export const getCustomer = (id: string) => customers.find((c) => c.id === id) ?? customers[0] export const getProduct = (id: string) => products.find((p) => p.id === id) ?? products[0] export const orderTotal = (o: AtlasOrder) => o.items.reduce((n, l) => n + getProduct(l.productId).price * l.qty, 0) export const orderShipping = (o: AtlasOrder) => (orderTotal(o) >= 100 ? 0 : 8) export const orderGrand = (o: AtlasOrder) => orderTotal(o) + orderShipping(o) export const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 }) export const moneyExact = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2 }) const dateFmt = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC" }) const timeFmt = new Intl.DateTimeFormat("en-US", { hour: "numeric", minute: "2-digit", timeZone: "UTC" }) export const formatDate = (iso: string) => dateFmt.format(new Date(iso)) export const formatTime = (iso: string) => timeFmt.format(new Date(iso)) export const initials = (name: string) => name.split(" ").map((w) => w[0]).join("").slice(0, 2) /** Thirty daily revenue figures, the last one being today. */ export const revenueSeries = Array.from({ length: 30 }, (_, i) => { const base = 3200 + i * 46 + Math.round(Math.sin(i * 0.9) * 520) + (i % 7 === 5 ? 700 : 0) const previous = 2900 + i * 30 + Math.round(Math.cos(i * 0.8) * 430) return { day: `Sep ${i + 1}`, revenue: base, previous } })components/ballmac/templates/atlas/atlas-theme.tsx// Ballmac UI: Atlas template shell. https://ui.ballmac.com/templates/template-atlas "use client" import * as React from "react" import { Bell, ChevronsUpDown, CircleHelp, LayoutDashboard, Menu, Package, Receipt, Search, Settings, Users } from "lucide-react" import { Badge } from "@/components/ballmac/badge" import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandShortcut } from "@/components/ballmac/command" import { Sheet, SheetContent, SheetDescription, SheetTitle } from "@/components/ballmac/sheet" import { atlasMono, atlasSans } from "@/components/ballmac/templates/atlas/atlas-fonts" import { cn } from "@/lib/utils" type AtlasPage = "dashboard" | "orders" | "products" | "customers" | "settings" /** Where each page lives. `order` is the order detail page. */ type AtlasHrefs = Record<AtlasPage | "order", string> const defaultHrefs: AtlasHrefs = { dashboard: "/atlas", orders: "/atlas/orders", order: "/atlas/orders/ORD-10479", products: "/atlas/products", customers: "/atlas/customers", settings: "/atlas/settings", } /** Atlas's palette: warm stone neutrals with one teal. Dense, quiet and easy to read all day. */ const atlasCss = ` .atlas-theme,body:not(:has([data-gallery])):has(.atlas-theme){--background:oklch(0.985 0.004 90);--foreground:oklch(0.2 0.012 80);--card:oklch(1 0 0);--card-foreground:oklch(0.2 0.012 80);--popover:oklch(1 0 0);--popover-foreground:oklch(0.2 0.012 80);--primary:oklch(0.26 0.012 80);--primary-foreground:oklch(0.985 0.004 90);--secondary:oklch(0.955 0.006 90);--secondary-foreground:oklch(0.2 0.012 80);--muted:oklch(0.955 0.006 90);--muted-foreground:oklch(0.47 0.015 85);--accent:oklch(0.94 0.008 90);--accent-foreground:oklch(0.2 0.012 80);--border:oklch(0.2 0.012 80 / 11%);--input:oklch(0.2 0.012 80 / 16%);--ring:oklch(0.5 0.1 190);--surface:oklch(0.968 0.006 90);--destructive:oklch(0.55 0.21 27);--chart-1:oklch(0.5 0.1 190);--chart-2:oklch(0.5 0.13 150);--chart-3:oklch(0.62 0.14 70);--chart-4:oklch(0.5 0.14 320);--chart-5:oklch(0.52 0.12 250);--radius:0.625rem} .dark .atlas-theme,.dark body:not(:has([data-gallery])):has(.atlas-theme){--background:oklch(0.165 0.008 80);--foreground:oklch(0.95 0.006 90);--card:oklch(0.2 0.009 80);--card-foreground:oklch(0.95 0.006 90);--popover:oklch(0.22 0.01 80);--popover-foreground:oklch(0.95 0.006 90);--primary:oklch(0.95 0.006 90);--primary-foreground:oklch(0.2 0.012 80);--secondary:oklch(0.25 0.01 80);--secondary-foreground:oklch(0.95 0.006 90);--muted:oklch(0.24 0.01 80);--muted-foreground:oklch(0.72 0.015 85);--accent:oklch(0.27 0.012 80);--accent-foreground:oklch(0.95 0.006 90);--border:oklch(1 0 0 / 10%);--input:oklch(1 0 0 / 14%);--ring:oklch(0.74 0.11 190);--surface:oklch(0.185 0.009 80);--destructive:oklch(0.7 0.19 25);--chart-1:oklch(0.78 0.11 190);--chart-2:oklch(0.78 0.14 150);--chart-3:oklch(0.82 0.14 80);--chart-4:oklch(0.76 0.13 320);--chart-5:oklch(0.76 0.11 250)} ` /** Dialogs, sheets and menus render in a portal on <body>, so the font is set there too while an Atlas page is mounted. */ const atlasBodyCss = `body:not(:has([data-gallery])):has(.atlas-theme){font-family:var(--atlas-sans),ui-sans-serif,system-ui,sans-serif}` const nav: { key: AtlasPage; label: string; icon: typeof Receipt; badge?: string }[] = [ { key: "dashboard", label: "Dashboard", icon: LayoutDashboard }, { key: "orders", label: "Orders", icon: Receipt, badge: "8" }, { key: "products", label: "Products", icon: Package }, { key: "customers", label: "Customers", icon: Users }, { key: "settings", label: "Settings", icon: Settings }, ] function AtlasMark({ className }: { className?: string }) { return ( <svg viewBox="0 0 28 28" aria-hidden="true" className={cn("size-7", className)} fill="none"> <rect width="28" height="28" rx="8" fill="var(--chart-1)" /> <path d="M8 19 14 8l6 11M10.5 15h7" stroke="var(--card)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" /> </svg> ) } function SidebarNav({ page, hrefs, onNavigate }: { page: AtlasPage; hrefs: AtlasHrefs; onNavigate?: () => void }) { return ( <div className="flex h-full flex-col"> <div className="flex h-14 items-center gap-2.5 px-4"> <AtlasMark /> <div className="leading-tight"> <p className="text-sm font-bold tracking-tight">Atlas</p> <p className="text-muted-foreground text-[11px]">Fieldnote Goods</p> </div> </div> <nav aria-label="Main" className="flex-1 space-y-0.5 px-2.5 py-3"> {nav.map((n) => ( <a key={n.key} href={hrefs[n.key]} onClick={onNavigate} aria-current={page === n.key ? "page" : undefined} className="text-muted-foreground hover:text-foreground hover:bg-accent aria-[current=page]:bg-accent aria-[current=page]:text-foreground focus-visible:ring-ring/50 flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium outline-none transition-colors focus-visible:ring-[3px]" > <n.icon className="size-4" aria-hidden="true" /> {n.label} {n.badge && <span className="bg-chart-1 text-primary-foreground ms-auto rounded-full px-1.5 text-[11px] font-semibold tabular-nums">{n.badge}</span>} </a> ))} </nav> <div className="border-t p-2.5"> <button type="button" className="hover:bg-accent focus-visible:ring-ring/50 flex w-full items-center gap-2.5 rounded-lg p-2 text-start outline-none focus-visible:ring-[3px]"> <span className="bg-chart-4/25 flex size-8 items-center justify-center rounded-full text-xs font-bold" aria-hidden="true">MK</span> <span className="min-w-0 flex-1 leading-tight"> <span className="block truncate text-sm font-semibold">Mina Kovac</span> <span className="text-muted-foreground block truncate text-xs">Owner</span> </span> <ChevronsUpDown className="text-muted-foreground size-4" aria-hidden="true" /> </button> </div> </div> ) } type AtlasShellProps = Omit<React.ComponentProps<"div">, "title"> & { /** The page being shown, so its nav link is marked current. */ page: AtlasPage /** Page title shown in the top bar. */ title: string /** Override where pages live (used by previews). */ hrefs?: Partial<AtlasHrefs> /** Controls on the right of the top bar. */ actions?: React.ReactNode } /** Atlas's frame: sidebar, top bar with a command menu (⌘K), and a drawer on phones. Wrap every Atlas page in it. */ function AtlasShell({ page, title, hrefs: overrides, actions, className, style, children, ...props }: AtlasShellProps) { const hrefs = { ...defaultHrefs, ...overrides } const [drawer, setDrawer] = React.useState(false) const [palette, setPalette] = React.useState(false) // Portaled UI (command menu, sheet) lives outside this element; give <body> the font variables too. React.useEffect(() => { const classes = [atlasSans.variable, atlasMono.variable].filter(Boolean) document.body.classList.add(...classes) return () => document.body.classList.remove(...classes) }, []) return ( <div data-slot="atlas" className={cn("atlas-theme bg-background text-foreground relative min-h-dvh lg:grid lg:grid-cols-[15rem_minmax(0,1fr)]", atlasSans.variable, atlasMono.variable, className)} style={{ fontFamily: "var(--atlas-sans), ui-sans-serif, system-ui, sans-serif", ...style }} {...props} > <style>{atlasCss + atlasBodyCss}</style> <aside className="bg-surface sticky top-0 hidden h-dvh border-e lg:block"> <SidebarNav page={page} hrefs={hrefs} /> </aside> <Sheet open={drawer} onOpenChange={setDrawer}> <SheetContent side="start" className="w-72 p-0" closeLabel="Close navigation"> <SheetTitle className="sr-only">Navigation</SheetTitle> <SheetDescription className="sr-only">Pages in the Fieldnote Goods store</SheetDescription> <SidebarNav page={page} hrefs={hrefs} onNavigate={() => setDrawer(false)} /> </SheetContent> </Sheet> <div className="min-w-0"> <header className="bg-background/85 sticky top-0 z-30 flex h-14 items-center gap-3 border-b px-4 backdrop-blur-lg sm:px-6"> <button type="button" aria-label="Open navigation" onClick={() => setDrawer(true)} className="hover:bg-accent focus-visible:ring-ring/50 -ms-1 inline-flex size-9 items-center justify-center rounded-lg outline-none focus-visible:ring-[3px] lg:hidden"> <Menu className="size-5" aria-hidden="true" /> </button> <h1 className="min-w-0 truncate text-[15px] font-bold tracking-tight">{title}</h1> <div className="ms-auto flex items-center gap-2"> <button type="button" onClick={() => setPalette(true)} className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 bg-card flex h-9 items-center gap-2 rounded-lg border px-2.5 text-sm outline-none transition-colors focus-visible:ring-[3px] sm:w-56" > <Search className="size-4" aria-hidden="true" /> <span className="hidden flex-1 text-start sm:block">Search</span> <kbd className="bg-muted hidden rounded px-1.5 py-0.5 text-[11px] sm:block" style={{ fontFamily: "var(--atlas-mono)" }}>⌘K</kbd> <span className="sr-only sm:hidden">Search the store</span> </button> {actions} <button type="button" aria-label="Notifications, 3 unread" className="hover:bg-accent focus-visible:ring-ring/50 relative inline-flex size-9 items-center justify-center rounded-lg outline-none focus-visible:ring-[3px]"> <Bell className="size-[18px]" aria-hidden="true" /> <span className="bg-chart-3 absolute top-2 end-2.5 size-1.5 rounded-full" aria-hidden="true" /> </button> </div> </header> {children} </div> <CommandDialog open={palette} onOpenChange={setPalette} title="Search the store"> <CommandInput placeholder="Search pages, orders, products…" /> <CommandList> <CommandEmpty>No results.</CommandEmpty> <CommandGroup heading="Go to"> {nav.map((n) => ( <CommandItem key={n.key} value={n.label} onSelect={() => { setPalette(false); window.location.assign(hrefs[n.key]) }}> <n.icon aria-hidden="true" /> {n.label} </CommandItem> ))} </CommandGroup> <CommandGroup heading="Recent orders"> {["ORD-10479", "ORD-10479", "ORD-10476"].map((id) => ( <CommandItem key={id} value={id} onSelect={() => { setPalette(false); window.location.assign(hrefs.order) }}> <Receipt aria-hidden="true" /> {id} <CommandShortcut>Order</CommandShortcut> </CommandItem> ))} </CommandGroup> <CommandGroup heading="Help"> <CommandItem value="Help center"><CircleHelp aria-hidden="true" /> Help center</CommandItem> </CommandGroup> </CommandList> </CommandDialog> </div> ) } /** The heading row of a page: title text, a description and actions. */ function AtlasPageHeader({ title, description, children }: { title: string; description?: string; children?: React.ReactNode }) { return ( <div className="flex flex-wrap items-end justify-between gap-4"> <div className="min-w-0"> <h2 className="text-2xl font-extrabold tracking-[-0.03em]">{title}</h2> {description && <p className="text-muted-foreground mt-1 text-sm text-pretty">{description}</p>} </div> {children && <div className="flex flex-wrap items-center gap-2">{children}</div>} </div> ) } const buttonBase = "focus-visible:ring-ring/50 inline-flex h-9 shrink-0 items-center justify-center gap-2 rounded-lg px-3.5 text-sm font-semibold outline-none transition-colors focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4" /** Class names for Atlas buttons, so links and buttons match. */ const atlasButton = { primary: cn(buttonBase, "bg-primary text-primary-foreground hover:opacity-90"), outline: cn(buttonBase, "bg-card hover:bg-accent border"), ghost: cn(buttonBase, "hover:bg-accent"), danger: cn(buttonBase, "bg-card text-destructive border-destructive/40 hover:bg-destructive/10 border"), } const paymentTone = { paid: "success", pending: "warning", refunded: "neutral" } as const const fulfilmentTone = { unfulfilled: "warning", shipped: "neutral", delivered: "success" } as const const label = (s: string) => s[0].toUpperCase() + s.slice(1) /** Payment status as a badge. */ function PaymentBadge({ value }: { value: keyof typeof paymentTone }) { return <Badge status={paymentTone[value]} variant="outline">{label(value)}</Badge> } /** Fulfilment status as a badge. */ function FulfilmentBadge({ value }: { value: keyof typeof fulfilmentTone }) { return <Badge status={fulfilmentTone[value]} variant="outline">{label(value)}</Badge> } /** A small painted tile standing in for a product photo. */ function ProductTile({ hue, className }: { hue: number; className?: string }) { return ( <span aria-hidden="true" className={cn("relative block overflow-hidden rounded-lg", className)} style={{ background: `linear-gradient(135deg, oklch(0.9 0.06 ${hue}), oklch(0.78 0.1 ${hue + 30}))` }} > <span className="absolute inset-x-[22%] bottom-0 h-[58%] rounded-t-md" style={{ background: `oklch(0.55 0.1 ${hue} / 0.55)` }} /> </span> ) } export { AtlasMark, AtlasPageHeader, AtlasShell, FulfilmentBadge, PaymentBadge, ProductTile, atlasButton, defaultHrefs as atlasDefaultHrefs, type AtlasHrefs, type AtlasPage, type AtlasShellProps }components/ballmac/templates/atlas/atlas-dashboard.tsx// Ballmac UI: Atlas dashboard page. https://ui.ballmac.com/templates/template-atlas "use client" import * as React from "react" import { AlertTriangle, ArrowDownRight, ArrowRight, ArrowUpRight, Download, PackageX, Truck } from "lucide-react" import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts" import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, type ChartConfig } from "@/components/ballmac/chart" import { SegmentedControl, SegmentedControlItem } from "@/components/ballmac/segmented-control" import { Sparkline } from "@/components/ballmac/sparkline" import { formatDate, getCustomer, initials, money, moneyExact, orderGrand, orders, products, revenueSeries } from "@/components/ballmac/templates/atlas/atlas-data" import { AtlasPageHeader, AtlasShell, FulfilmentBadge, PaymentBadge, ProductTile, atlasButton, type AtlasHrefs } from "@/components/ballmac/templates/atlas/atlas-theme" import { cn } from "@/lib/utils" type Range = "7" | "30" const config = { revenue: { label: "This period", color: "var(--chart-1)" }, previous: { label: "Previous period", color: "var(--chart-3)" } } satisfies ChartConfig const sum = (xs: number[]) => xs.reduce((a, b) => a + b, 0) const pct = (a: number, b: number) => ((a - b) / b) * 100 type AtlasDashboardProps = React.ComponentProps<"div"> & { hrefs?: Partial<AtlasHrefs> } /** The Atlas dashboard: a date range that recomputes the KPIs and chart, work that needs attention, top products and recent orders. */ function AtlasDashboard({ hrefs, ...props }: AtlasDashboardProps) { const [range, setRange] = React.useState<Range>("30") const n = Number(range) const data = revenueSeries.slice(-n) const revenue = sum(data.map((d) => d.revenue)) const previous = sum(data.map((d) => d.previous)) const orderCount = Math.round(revenue / 71) const prevOrders = Math.round(previous / 69) const conversion = 2.1 + n * 0.004 const aov = revenue / orderCount const kpis = [ { label: "Revenue", value: money.format(revenue), delta: pct(revenue, previous), trend: data.map((d) => d.revenue) }, { label: "Orders", value: orderCount.toLocaleString("en-US"), delta: pct(orderCount, prevOrders), trend: data.map((d) => Math.round(d.revenue / 70)) }, { label: "Conversion", value: `${conversion.toFixed(1)}%`, delta: 4.2, trend: data.map((d, i) => 2 + Math.sin(i * 0.5) * 0.3 + i * 0.01) }, { label: "Average order", value: moneyExact.format(aov), delta: -1.8, trend: data.map((d, i) => 70 + Math.cos(i * 0.7) * 6 - i * 0.1) }, ] const unfulfilled = orders.filter((o) => o.fulfillment === "unfulfilled" && o.payment !== "pending").length const pending = orders.filter((o) => o.payment === "pending").length const low = products.filter((p) => p.stock <= 10 && p.status === "active") const top = [...products].filter((p) => p.status === "active").sort((a, b) => b.sold * b.price - a.sold * a.price).slice(0, 5) const topMax = top[0].sold * top[0].price const recent = orders.slice(0, 6) const h = { orders: hrefs?.orders ?? "/atlas/orders", order: hrefs?.order ?? "/atlas/orders/ORD-10479", products: hrefs?.products ?? "/atlas/products" } return ( <AtlasShell page="dashboard" title="Dashboard" hrefs={hrefs} actions={<a href={h.orders} className={cn(atlasButton.primary, "hidden sm:inline-flex")}>View orders</a>} {...props} > <main className="mx-auto max-w-7xl space-y-6 px-4 py-6 sm:px-6"> <AtlasPageHeader title="Good morning, Mina" description="Here is how Fieldnote Goods is doing."> <SegmentedControl aria-label="Date range" value={range} onValueChange={(v) => setRange(v as Range)} size="sm"> <SegmentedControlItem value="7">7 days</SegmentedControlItem> <SegmentedControlItem value="30">30 days</SegmentedControlItem> </SegmentedControl> <button type="button" className={atlasButton.outline}><Download aria-hidden="true" /> Export</button> </AtlasPageHeader> <section aria-label="Key figures" className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4"> {kpis.map((k) => { const up = k.delta >= 0 const Arrow = up ? ArrowUpRight : ArrowDownRight return ( <div key={k.label} className="bg-card rounded-xl border p-4"> <p className="text-muted-foreground text-sm font-medium">{k.label}</p> <div className="mt-2 flex items-end justify-between gap-3"> <p className="text-[1.65rem] leading-none font-extrabold tracking-[-0.03em] tabular-nums">{k.value}</p> <Sparkline values={k.trend} label={`${k.label} trend over ${n} days`} height={32} showEnd={false} className="text-chart-1 w-20 shrink-0" /> </div> <p className="mt-3 flex items-center gap-1 text-xs"> <Arrow className={cn("size-3.5", up ? "text-chart-2" : "text-destructive")} aria-hidden="true" /> <span className="font-semibold tabular-nums">{Math.abs(k.delta).toFixed(1)}%</span> <span className="text-muted-foreground">{up ? "up" : "down"} from the previous {n} days</span> </p> </div> ) })} </section> <div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_21rem]"> <section aria-labelledby="atlas-rev" className="bg-card min-w-0 rounded-xl border p-4 sm:p-5"> <div className="flex items-baseline justify-between gap-3"> <h3 id="atlas-rev" className="text-sm font-bold">Revenue</h3> <p className="text-muted-foreground text-xs">Daily, last {n} days</p> </div> <ChartContainer config={config} label={`Daily revenue for the last ${n} days compared with the previous period`} summary={`Revenue totalled ${money.format(revenue)}, ${Math.abs(pct(revenue, previous)).toFixed(1)} percent ${revenue >= previous ? "above" : "below"} the previous period.`} className="mt-4 aspect-[16/7] w-full" > <AreaChart data={data} margin={{ left: 0, right: 8, top: 8 }} accessibilityLayer> <defs> <linearGradient id="atlas-fill" x1="0" y1="0" x2="0" y2="1"> <stop offset="5%" stopColor="var(--color-revenue)" stopOpacity={0.28} /> <stop offset="95%" stopColor="var(--color-revenue)" stopOpacity={0.02} /> </linearGradient> </defs> <CartesianGrid vertical={false} /> <XAxis dataKey="day" tickLine={false} axisLine={false} tickMargin={8} interval={n === 7 ? 0 : 4} /> <YAxis tickLine={false} axisLine={false} width={44} tickFormatter={(v: number) => `$${(v / 1000).toFixed(1)}k`} /> <ChartTooltip cursor={false} content={<ChartTooltipContent indicator="line" formatter={(v) => money.format(Number(v))} />} /> <Area dataKey="previous" type="monotone" fill="none" stroke="var(--color-previous)" strokeWidth={1.5} strokeDasharray="4 4" dot={false} isAnimationActive={false} /> <Area dataKey="revenue" type="monotone" fill="url(#atlas-fill)" stroke="var(--color-revenue)" strokeWidth={2} dot={false} isAnimationActive={false} /> <ChartLegend content={<ChartLegendContent />} /> </AreaChart> </ChartContainer> </section> <section aria-labelledby="atlas-attn" className="bg-card rounded-xl border"> <h3 id="atlas-attn" className="px-4 pt-4 text-sm font-bold sm:px-5">Needs attention</h3> <ul className="mt-2 divide-y"> {[ { icon: Truck, text: `${unfulfilled} orders to fulfil`, hint: "Oldest from 2 days ago", href: h.orders, tone: "bg-chart-3/20" }, { icon: AlertTriangle, text: `${pending} payments pending`, hint: "Bank transfers, 1 to 3 days", href: h.orders, tone: "bg-chart-5/20" }, { icon: PackageX, text: `${low.length} products low on stock`, hint: low.slice(0, 2).map((p) => p.name.split(",")[0]).join(", ") + " and more", href: h.products, tone: "bg-destructive/15" }, ].map((a) => ( <li key={a.text}> <a href={a.href} className="hover:bg-accent focus-visible:ring-ring/50 flex items-center gap-3 px-4 py-3 outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-inset sm:px-5"> <span className={cn("flex size-9 shrink-0 items-center justify-center rounded-lg", a.tone)}><a.icon className="size-4" aria-hidden="true" /></span> <span className="min-w-0 flex-1"><span className="block text-sm font-semibold">{a.text}</span><span className="text-muted-foreground block truncate text-xs">{a.hint}</span></span> <ArrowRight className="text-muted-foreground size-4 rtl:rotate-180" aria-hidden="true" /> </a> </li> ))} </ul> <div className="border-t p-4 sm:p-5"> <h3 className="text-sm font-bold">Top products</h3> <ul className="mt-3 space-y-3"> {top.map((p) => ( <li key={p.id} className="flex items-center gap-3"> <ProductTile hue={p.hue} className="size-9 shrink-0" /> <div className="min-w-0 flex-1"> <p className="truncate text-sm font-medium">{p.name}</p> <div className="bg-muted mt-1.5 h-1.5 rounded-full"><div className="bg-chart-1 h-full rounded-full" style={{ width: `${(p.sold * p.price / topMax) * 100}%` }} /></div> </div> <p className="text-sm font-semibold tabular-nums">{money.format(p.sold * p.price)}</p> </li> ))} </ul> </div> </section> </div> <section aria-labelledby="atlas-recent" className="bg-card overflow-hidden rounded-xl border"> <div className="flex items-center justify-between px-4 py-3.5 sm:px-5"> <h3 id="atlas-recent" className="text-sm font-bold">Recent orders</h3> <a href={h.orders} className="text-chart-1 focus-visible:ring-ring/50 rounded text-sm font-semibold underline-offset-4 outline-none hover:underline focus-visible:ring-[3px]">View all</a> </div> <div tabIndex={0} role="region" aria-label="Recent orders" className="focus-visible:ring-ring/50 overflow-x-auto outline-none focus-visible:ring-[3px] focus-visible:ring-inset"> <table className="w-full min-w-[40rem] text-start text-sm"> <caption className="sr-only">Most recent orders</caption> <thead> <tr className="text-muted-foreground bg-surface border-y text-xs"> {["Order", "Date", "Customer", "Payment", "Fulfilment", "Total"].map((c, i) => <th key={c} scope="col" className={cn("px-4 py-2.5 font-semibold sm:px-5", i === 5 && "text-end")}>{c}</th>)} </tr> </thead> <tbody className="divide-y"> {recent.map((o) => { const c = getCustomer(o.customerId) return ( <tr key={o.id} className="hover:bg-accent/50"> <td className="px-4 py-3 sm:px-5"><a href={h.order} className="text-chart-1 font-semibold underline-offset-4 hover:underline" style={{ fontFamily: "var(--atlas-mono)" }}>{o.id}</a></td> <td className="text-muted-foreground px-4 py-3 whitespace-nowrap sm:px-5">{formatDate(o.date)}</td> <td className="px-4 py-3 sm:px-5"><span className="flex items-center gap-2.5"><span className={cn("flex size-7 items-center justify-center rounded-full text-[11px] font-bold", ["bg-chart-1/20", "bg-chart-2/20", "bg-chart-3/25", "bg-chart-4/20", "bg-chart-5/20"][c.tone - 1])} aria-hidden="true">{initials(c.name)}</span>{c.name}</span></td> <td className="px-4 py-3 sm:px-5"><PaymentBadge value={o.payment} /></td> <td className="px-4 py-3 sm:px-5"><FulfilmentBadge value={o.fulfillment} /></td> <td className="px-4 py-3 text-end font-semibold tabular-nums sm:px-5">{moneyExact.format(orderGrand(o))}</td> </tr> ) })} </tbody> </table> </div> </section> </main> </AtlasShell> ) } export { AtlasDashboard, type AtlasDashboardProps }components/ballmac/templates/atlas/atlas-orders.tsx// Ballmac UI: Atlas orders page. https://ui.ballmac.com/templates/template-atlas "use client" import * as React from "react" import { ArrowDown, ArrowUp, ArrowUpDown, ChevronLeft, ChevronRight, Download, Printer, Search, Truck, X } from "lucide-react" import { Checkbox } from "@/components/ballmac/checkbox" import { formatDate, formatTime, getCustomer, initials, moneyExact, orderGrand, orders as seed, type AtlasOrder } from "@/components/ballmac/templates/atlas/atlas-data" import { AtlasPageHeader, AtlasShell, FulfilmentBadge, PaymentBadge, atlasButton, type AtlasHrefs } from "@/components/ballmac/templates/atlas/atlas-theme" import { cn } from "@/lib/utils" type Tab = "all" | "unfulfilled" | "unpaid" | "shipped" | "delivered" type SortKey = "date" | "customer" | "channel" | "total" const tabs: { id: Tab; label: string; test: (o: AtlasOrder) => boolean }[] = [ { id: "all", label: "All", test: () => true }, { id: "unfulfilled", label: "Unfulfilled", test: (o) => o.fulfillment === "unfulfilled" }, { id: "unpaid", label: "Unpaid", test: (o) => o.payment === "pending" }, { id: "shipped", label: "Shipped", test: (o) => o.fulfillment === "shipped" }, { id: "delivered", label: "Delivered", test: (o) => o.fulfillment === "delivered" }, ] const PAGE = 10 const tones = ["bg-chart-1/20", "bg-chart-2/20", "bg-chart-3/25", "bg-chart-4/20", "bg-chart-5/20"] type AtlasOrdersProps = React.ComponentProps<"div"> & { hrefs?: Partial<AtlasHrefs> } /** The Atlas orders page: status tabs with counts, search, sortable columns, row selection with bulk actions, and pagination. */ function AtlasOrders({ hrefs, ...props }: AtlasOrdersProps) { const [rows, setRows] = React.useState<AtlasOrder[]>(seed) const [tab, setTab] = React.useState<Tab>("all") const [query, setQuery] = React.useState("") const [sort, setSort] = React.useState<{ key: SortKey; dir: "asc" | "desc" }>({ key: "date", dir: "desc" }) const [page, setPage] = React.useState(0) const [selected, setSelected] = React.useState<Set<string>>(new Set()) const orderHref = hrefs?.order ?? "/atlas/orders/ORD-10479" const q = query.trim().toLowerCase() const filtered = rows .filter(tabs.find((t) => t.id === tab)!.test) .filter((o) => !q || [o.id, getCustomer(o.customerId).name, o.channel].join(" ").toLowerCase().includes(q)) const sorted = [...filtered].sort((a, b) => { const dir = sort.dir === "asc" ? 1 : -1 const val = (o: AtlasOrder) => (sort.key === "date" ? o.date : sort.key === "customer" ? getCustomer(o.customerId).name : sort.key === "channel" ? o.channel : orderGrand(o)) const x = val(a) const y = val(b) return (x < y ? -1 : x > y ? 1 : 0) * dir }) const pages = Math.max(1, Math.ceil(sorted.length / PAGE)) const safePage = Math.min(page, pages - 1) const view = sorted.slice(safePage * PAGE, safePage * PAGE + PAGE) const allOnPage = view.length > 0 && view.every((o) => selected.has(o.id)) const someOnPage = view.some((o) => selected.has(o.id)) function toggleSort(key: SortKey) { setSort((s) => (s.key === key ? { key, dir: s.dir === "asc" ? "desc" : "asc" } : { key, dir: key === "total" || key === "date" ? "desc" : "asc" })) setPage(0) } function toggleAll(on: boolean) { setSelected((s) => { const next = new Set(s) for (const o of view) on ? next.add(o.id) : next.delete(o.id) return next }) } function toggleOne(id: string, on: boolean) { setSelected((s) => { const next = new Set(s) on ? next.add(id) : next.delete(id) return next }) } function markShipped() { setRows((all) => all.map((o) => (selected.has(o.id) && o.fulfillment === "unfulfilled" && o.payment !== "pending" ? { ...o, fulfillment: "shipped" } : o))) setSelected(new Set()) } const head = (k: SortKey, children: React.ReactNode, right?: boolean) => ( <th key={k} scope="col" aria-sort={sort.key === k ? (sort.dir === "asc" ? "ascending" : "descending") : "none"} className={cn("px-3 py-2.5 font-semibold", right && "text-end")}> <button type="button" onClick={() => toggleSort(k)} className={cn("hover:text-foreground focus-visible:ring-ring/50 -mx-1.5 inline-flex items-center gap-1 rounded px-1.5 py-0.5 outline-none focus-visible:ring-[3px]", sort.key === k && "text-foreground")}> {children} {sort.key === k ? (sort.dir === "asc" ? <ArrowUp className="size-3" aria-hidden="true" /> : <ArrowDown className="size-3" aria-hidden="true" />) : <ArrowUpDown className="size-3 opacity-50" aria-hidden="true" />} </button> </th> ) return ( <AtlasShell page="orders" title="Orders" hrefs={hrefs} actions={<button type="button" className={cn(atlasButton.outline, "hidden sm:inline-flex")}><Download aria-hidden="true" /> Export</button>} {...props}> <main className="mx-auto max-w-7xl space-y-5 px-4 py-6 sm:px-6"> <AtlasPageHeader title="Orders" description="Everything customers have bought, across every channel." /> <div className="bg-card rounded-xl border"> <div className="flex flex-col gap-3 border-b p-3 lg:flex-row lg:items-center lg:justify-between"> <div role="group" aria-label="Filter by status" className="flex gap-1 overflow-x-auto [scrollbar-width:none]"> {tabs.map((t) => { const count = rows.filter(t.test).length const on = tab === t.id return ( <button key={t.id} type="button" aria-pressed={on} onClick={() => { setTab(t.id); setPage(0); setSelected(new Set()) }} className={cn("focus-visible:ring-ring/50 flex shrink-0 items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-semibold outline-none transition-colors focus-visible:ring-[3px]", on ? "bg-accent text-foreground" : "text-muted-foreground hover:text-foreground")} > {t.label} <span className="text-muted-foreground text-xs font-medium tabular-nums">{count}</span> </button> ) })} </div> <div className="relative lg:w-72"> <Search className="text-muted-foreground pointer-events-none absolute top-1/2 start-3 size-4 -translate-y-1/2" aria-hidden="true" /> <input type="search" aria-label="Search orders" value={query} onChange={(e) => { setQuery(e.target.value); setPage(0) }} placeholder="Search order, customer or channel" className="bg-background focus-visible:ring-ring/50 placeholder:text-muted-foreground h-9 w-full rounded-lg border pe-3 ps-9 text-sm outline-none focus-visible:ring-[3px]" /> </div> </div> {selected.size > 0 && ( <div role="region" aria-label="Bulk actions" className="bg-accent flex flex-wrap items-center gap-2 border-b px-3 py-2"> <p className="me-auto text-sm font-semibold" aria-live="polite">{selected.size} selected</p> <button type="button" onClick={markShipped} className={atlasButton.outline}><Truck aria-hidden="true" /> Mark as shipped</button> <button type="button" className={atlasButton.outline}><Printer aria-hidden="true" /> Print labels</button> <button type="button" onClick={() => setSelected(new Set())} aria-label="Clear selection" className={cn(atlasButton.ghost, "px-2.5")}><X aria-hidden="true" /></button> </div> )} <div tabIndex={0} role="region" aria-label="Orders table" className="focus-visible:ring-ring/50 overflow-x-auto outline-none focus-visible:ring-[3px] focus-visible:ring-inset"> <table className="w-full min-w-[52rem] text-start text-sm"> <caption className="sr-only">Orders, {sorted.length} shown</caption> <thead> <tr className="text-muted-foreground bg-surface border-b text-xs"> <th scope="col" className="w-10 px-3 py-2.5"><Checkbox aria-label="Select all orders on this page" checked={allOnPage ? true : someOnPage ? "indeterminate" : false} onCheckedChange={(v) => toggleAll(v === true)} /></th> <th scope="col" className="px-3 py-2.5 font-semibold">Order</th> {head("date", "Date")} {head("customer", "Customer")} {head("channel", "Channel")} <th scope="col" className="px-3 py-2.5 font-semibold">Payment</th> <th scope="col" className="px-3 py-2.5 font-semibold">Fulfilment</th> {head("total", "Total", true)} </tr> </thead> <tbody className="divide-y"> {view.map((o) => { const c = getCustomer(o.customerId) const on = selected.has(o.id) return ( <tr key={o.id} data-state={on ? "selected" : undefined} className="hover:bg-accent/40 data-[state=selected]:bg-accent/60"> <td className="px-3 py-3"><Checkbox aria-label={`Select ${o.id}`} checked={on} onCheckedChange={(v) => toggleOne(o.id, v === true)} /></td> <td className="px-3 py-3"><a href={orderHref} className="text-chart-1 font-semibold underline-offset-4 hover:underline" style={{ fontFamily: "var(--atlas-mono)" }}>{o.id}</a></td> <td className="text-muted-foreground px-3 py-3 whitespace-nowrap">{formatDate(o.date)}<span className="ms-1.5 text-xs">{formatTime(o.date)}</span></td> <td className="px-3 py-3"><span className="flex items-center gap-2.5 whitespace-nowrap"><span className={cn("flex size-7 shrink-0 items-center justify-center rounded-full text-[11px] font-bold", tones[c.tone - 1])} aria-hidden="true">{initials(c.name)}</span>{c.name}</span></td> <td className="text-muted-foreground px-3 py-3 whitespace-nowrap">{o.channel}</td> <td className="px-3 py-3"><PaymentBadge value={o.payment} /></td> <td className="px-3 py-3"><FulfilmentBadge value={o.fulfillment} /></td> <td className="px-3 py-3 text-end font-semibold tabular-nums">{moneyExact.format(orderGrand(o))}</td> </tr> ) })} {view.length === 0 && ( <tr><td colSpan={8} className="text-muted-foreground px-3 py-16 text-center">No orders match. Try another status or clear the search.</td></tr> )} </tbody> </table> </div> <div className="flex items-center justify-between gap-3 border-t px-3 py-2.5 text-sm"> <p className="text-muted-foreground" role="status"> {sorted.length === 0 ? "No orders" : `Showing ${safePage * PAGE + 1}–${safePage * PAGE + view.length} of ${sorted.length}`} </p> <div className="flex items-center gap-1"> <button type="button" aria-label="Previous page" disabled={safePage === 0} onClick={() => setPage(safePage - 1)} className={cn(atlasButton.outline, "px-2.5")}><ChevronLeft aria-hidden="true" className="rtl:rotate-180"/></button> <span className="text-muted-foreground px-2 text-xs tabular-nums">Page {safePage + 1} of {pages}</span> <button type="button" aria-label="Next page" disabled={safePage >= pages - 1} onClick={() => setPage(safePage + 1)} className={cn(atlasButton.outline, "px-2.5")}><ChevronRight aria-hidden="true" className="rtl:rotate-180"/></button> </div> </div> </div> </main> </AtlasShell> ) } export { AtlasOrders, type AtlasOrdersProps }components/ballmac/templates/atlas/atlas-order.tsx// Ballmac UI: Atlas order detail page. https://ui.ballmac.com/templates/template-atlas "use client" import * as React from "react" import { Check, ChevronRight, CreditCard, Mail, MapPin, MoreHorizontal, Printer, RotateCcw, Truck } from "lucide-react" import { Checkbox } from "@/components/ballmac/checkbox" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ballmac/dialog" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ballmac/dropdown-menu" import { formatDate, formatTime, getCustomer, getProduct, initials, moneyExact, orderShipping, orderTotal, orders } from "@/components/ballmac/templates/atlas/atlas-data" import { AtlasShell, FulfilmentBadge, PaymentBadge, ProductTile, atlasButton, type AtlasHrefs } from "@/components/ballmac/templates/atlas/atlas-theme" import { cn } from "@/lib/utils" type Event = { id: string; title: string; detail?: string; at: string } type AtlasOrderProps = React.ComponentProps<"div"> & { /** Which sample order to show. */ orderId?: string hrefs?: Partial<AtlasHrefs> } /** The Atlas order page: items and totals, a live timeline, fulfilment and a refund flow with validation, plus customer and notes. */ function AtlasOrder({ orderId = "ORD-10479", hrefs, ...props }: AtlasOrderProps) { const base = orders.find((o) => o.id === orderId) ?? orders[1] const customer = getCustomer(base.customerId) const [fulfillment, setFulfillment] = React.useState(base.fulfillment) const [payment, setPayment] = React.useState(base.payment) const [refunded, setRefunded] = React.useState(0) const [events, setEvents] = React.useState<Event[]>([ { id: "e3", title: "Payment captured", detail: `${moneyExact.format(orderTotal(base) + orderShipping(base))} on Visa ending 4242`, at: base.date }, { id: "e2", title: "Order confirmation sent", detail: `Emailed to ${customer.email}`, at: base.date }, { id: "e1", title: "Order placed", detail: `via ${base.channel}`, at: base.date }, ]) const [note, setNote] = React.useState("Gift wrap please. Leave with the neighbour if nobody is home.") const [savedNote, setSavedNote] = React.useState(note) const [refundOpen, setRefundOpen] = React.useState(false) const [picked, setPicked] = React.useState<Record<string, boolean>>({}) const [reason, setReason] = React.useState("") const [error, setError] = React.useState("") const [announce, setAnnounce] = React.useState("") const subtotal = orderTotal(base) const shipping = orderShipping(base) const total = subtotal + shipping const refundAmount = base.items.reduce((n, l) => n + (picked[l.productId] ? getProduct(l.productId).price * l.qty : 0), 0) const stamp = "2026-09-29T14:20:00Z" function addEvent(title: string, detail?: string) { setEvents((all) => [{ id: `e${all.length + 1}-${title}`, title, detail, at: stamp }, ...all]) } function ship() { setFulfillment("shipped") addEvent("Marked as shipped", "Tracking sent to the customer") setAnnounce(`${base.id} marked as shipped`) } function refund() { if (refundAmount === 0) return setError("Choose at least one item to refund.") if (!reason) return setError("Pick a reason so your team can follow up.") setRefunded(refundAmount) if (refundAmount === subtotal) setPayment("refunded") addEvent(`Refunded ${moneyExact.format(refundAmount)}`, `Reason: ${reason}`) setAnnounce(`Refunded ${moneyExact.format(refundAmount)}`) setRefundOpen(false) setPicked({}) setReason("") setError("") } return ( <AtlasShell page="orders" title="Orders" hrefs={hrefs} {...props}> <main className="mx-auto max-w-6xl space-y-5 px-4 py-6 sm:px-6"> <nav aria-label="Breadcrumb" className="text-muted-foreground flex items-center gap-1 text-sm"> <a href={hrefs?.orders ?? "/atlas/orders"} className="hover:text-foreground focus-visible:ring-ring/50 rounded outline-none focus-visible:ring-[3px]">Orders</a> <ChevronRight className="size-3.5 rtl:rotate-180" aria-hidden="true" /> <span className="text-foreground font-medium" aria-current="page">{base.id}</span> </nav> <div className="flex flex-wrap items-start justify-between gap-4"> <div> <div className="flex flex-wrap items-center gap-2.5"> <h2 className="text-2xl font-extrabold tracking-[-0.03em]" style={{ fontFamily: "var(--atlas-mono)" }}>{base.id}</h2> <PaymentBadge value={payment} /> <FulfilmentBadge value={fulfillment} /> </div> <p className="text-muted-foreground mt-1.5 text-sm">{formatDate(base.date)} at {formatTime(base.date)} · {base.channel}</p> </div> <div className="flex flex-wrap items-center gap-2"> <button type="button" className={atlasButton.outline} onClick={() => setRefundOpen(true)} disabled={payment === "refunded"}><RotateCcw aria-hidden="true" /> Refund</button> {fulfillment === "unfulfilled" && payment !== "pending" ? ( <button type="button" className={atlasButton.primary} onClick={ship}><Truck aria-hidden="true" /> Mark as shipped</button> ) : ( <button type="button" className={atlasButton.outline}><Printer aria-hidden="true" /> Print packing slip</button> )} <DropdownMenu> <DropdownMenuTrigger className={cn(atlasButton.outline, "px-2.5")} aria-label="More actions"><MoreHorizontal aria-hidden="true" /></DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuItem>Duplicate order</DropdownMenuItem> <DropdownMenuItem>Download invoice</DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem destructive>Cancel order</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </div> </div> <p role="status" className="sr-only">{announce}</p> <div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_20rem]"> <div className="min-w-0 space-y-4"> <section aria-labelledby="ao-items" className="bg-card rounded-xl border"> <h3 id="ao-items" className="px-5 pt-4 text-sm font-bold">Items</h3> <ul className="divide-y px-5"> {base.items.map((l) => { const p = getProduct(l.productId) return ( <li key={l.productId} className="flex items-center gap-4 py-4"> <ProductTile hue={p.hue} className="size-14 shrink-0" /> <div className="min-w-0 flex-1"> <p className="truncate font-semibold">{p.name}</p> <p className="text-muted-foreground text-sm">{p.category} · {moneyExact.format(p.price)} each</p> </div> <p className="text-muted-foreground text-sm tabular-nums">× {l.qty}</p> <p className="w-20 text-end font-semibold tabular-nums">{moneyExact.format(p.price * l.qty)}</p> </li> ) })} </ul> <dl className="bg-surface space-y-2 rounded-b-xl border-t px-5 py-4 text-sm"> <div className="flex justify-between"><dt className="text-muted-foreground">Subtotal</dt><dd className="tabular-nums">{moneyExact.format(subtotal)}</dd></div> <div className="flex justify-between"><dt className="text-muted-foreground">Shipping</dt><dd className="tabular-nums">{shipping === 0 ? "Free" : moneyExact.format(shipping)}</dd></div> {refunded > 0 && <div className="flex justify-between"><dt className="text-muted-foreground">Refunded</dt><dd className="text-destructive tabular-nums">−{moneyExact.format(refunded)}</dd></div>} <div className="flex justify-between border-t pt-3 text-base font-bold"><dt>Total</dt><dd className="tabular-nums">{moneyExact.format(total - refunded)}</dd></div> </dl> </section> <section aria-labelledby="ao-timeline" className="bg-card rounded-xl border p-5"> <h3 id="ao-timeline" className="text-sm font-bold">Timeline</h3> <ol className="mt-4"> {events.map((e, i) => ( <li key={e.id} className="relative flex gap-3.5 pb-5 last:pb-0"> {i < events.length - 1 && <span className="bg-border absolute top-5 bottom-0 start-[7px] w-px" aria-hidden="true" />} <span className={cn("relative mt-1 size-3.5 shrink-0 rounded-full border-2", i === 0 ? "border-chart-1 bg-chart-1" : "bg-card border-muted-foreground/50")} aria-hidden="true" /> <div className="min-w-0 flex-1"> <p className="text-sm font-semibold">{e.title}</p> {e.detail && <p className="text-muted-foreground text-sm text-pretty">{e.detail}</p>} </div> <p className="text-muted-foreground shrink-0 text-xs tabular-nums">{formatDate(e.at)}, {formatTime(e.at)}</p> </li> ))} </ol> </section> </div> <aside className="space-y-4"> <section aria-labelledby="ao-customer" className="bg-card rounded-xl border p-5"> <h3 id="ao-customer" className="text-sm font-bold">Customer</h3> <div className="mt-4 flex items-center gap-3"> <span className="bg-chart-1/20 flex size-11 items-center justify-center rounded-full text-sm font-bold" aria-hidden="true">{initials(customer.name)}</span> <div className="min-w-0"><p className="truncate font-semibold">{customer.name}</p><p className="text-muted-foreground text-sm">{customer.segment === "VIP" ? "VIP customer" : "Customer since 2025"}</p></div> </div> <a href={`mailto:${customer.email}`} className="text-chart-1 mt-4 flex items-center gap-2 text-sm underline-offset-4 hover:underline"><Mail className="size-4" aria-hidden="true" />{customer.email}</a> <p className="text-muted-foreground mt-3 flex items-start gap-2 text-sm"><MapPin className="mt-0.5 size-4 shrink-0" aria-hidden="true" />{customer.city}<br />Shipping and billing address match</p> <p className="text-muted-foreground mt-3 flex items-center gap-2 text-sm"><CreditCard className="size-4" aria-hidden="true" />Visa ending 4242</p> </section> <section aria-labelledby="ao-notes" className="bg-card rounded-xl border p-5"> <h3 id="ao-notes" className="text-sm font-bold">Notes</h3> <label htmlFor="ao-note" className="sr-only">Order notes</label> <textarea id="ao-note" rows={4} value={note} onChange={(e) => setNote(e.target.value)} className="bg-background focus-visible:ring-ring/50 mt-3 w-full resize-none rounded-lg border p-3 text-sm outline-none focus-visible:ring-[3px]" /> <div className="mt-3 flex items-center justify-between"> <p className="text-muted-foreground flex items-center gap-1.5 text-xs" role="status">{note === savedNote ? <><Check className="size-3.5" aria-hidden="true" />Saved</> : "Unsaved changes"}</p> <button type="button" disabled={note === savedNote} onClick={() => { setSavedNote(note); addEvent("Note updated") }} className={atlasButton.outline}>Save note</button> </div> </section> </aside> </div> </main> <Dialog open={refundOpen} onOpenChange={(o) => { setRefundOpen(o); if (!o) setError("") }}> <DialogContent className="max-w-md"> <DialogHeader> <DialogTitle>Refund {base.id}</DialogTitle> <DialogDescription>Choose what to send back to the customer. The money returns to their Visa in 5 to 10 days.</DialogDescription> </DialogHeader> <fieldset className="space-y-2"> <legend className="sr-only">Items to refund</legend> {base.items.map((l) => { const p = getProduct(l.productId) return ( <label key={l.productId} className="hover:bg-accent/50 flex cursor-pointer items-center gap-3 rounded-lg border p-3 text-sm"> <Checkbox checked={!!picked[l.productId]} onCheckedChange={(v) => setPicked((s) => ({ ...s, [l.productId]: v === true }))} /> <span className="min-w-0 flex-1 truncate font-medium">{p.name} <span className="text-muted-foreground font-normal">× {l.qty}</span></span> <span className="tabular-nums">{moneyExact.format(p.price * l.qty)}</span> </label> ) })} </fieldset> <div> <label htmlFor="ao-reason" className="text-sm font-medium">Reason</label> <select id="ao-reason" value={reason} onChange={(e) => setReason(e.target.value)} className="bg-background focus-visible:ring-ring/50 mt-1.5 h-10 w-full rounded-lg border px-3 text-sm outline-none focus-visible:ring-[3px]"> <option value="">Choose a reason</option> <option>Damaged in transit</option> <option>Changed their mind</option> <option>Wrong item sent</option> </select> </div> {error && <p role="alert" className="text-destructive text-sm">{error}</p>} <DialogFooter> <button type="button" className={atlasButton.outline} onClick={() => setRefundOpen(false)}>Cancel</button> <button type="button" className={atlasButton.primary} onClick={refund}>Refund {refundAmount > 0 ? moneyExact.format(refundAmount) : ""}</button> </DialogFooter> </DialogContent> </Dialog> </AtlasShell> ) } export { AtlasOrder, type AtlasOrderProps }components/ballmac/templates/atlas/atlas-products.tsx// Ballmac UI: Atlas products page. https://ui.ballmac.com/templates/template-atlas "use client" import * as React from "react" import { LayoutGrid, List, Minus, Plus, Search } from "lucide-react" import { Badge } from "@/components/ballmac/badge" import { SegmentedControl, SegmentedControlItem } from "@/components/ballmac/segmented-control" import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from "@/components/ballmac/sheet" import { moneyExact, products as seed, type AtlasProduct } from "@/components/ballmac/templates/atlas/atlas-data" import { AtlasPageHeader, AtlasShell, ProductTile, atlasButton, type AtlasHrefs } from "@/components/ballmac/templates/atlas/atlas-theme" import { cn } from "@/lib/utils" type View = "grid" | "list" function Stock({ p }: { p: AtlasProduct }) { if (p.stock === 0) return <Badge status="error" variant="outline">Out of stock</Badge> if (p.stock <= 10) return <Badge status="warning" variant="outline">{p.stock} left</Badge> return <span className="text-muted-foreground text-sm tabular-nums">{p.stock} in stock</span> } const categories = ["Paper", "Writing", "Bags", "Desk"] const fieldClass = "bg-background focus-visible:ring-ring/50 mt-1.5 h-10 w-full rounded-lg border px-3 text-sm outline-none focus-visible:ring-[3px] aria-[invalid=true]:border-destructive" type AtlasProductsProps = React.ComponentProps<"div"> & { hrefs?: Partial<AtlasHrefs> } /** The Atlas products page: search, category and low-stock filters, grid or list, quick stock edits and an add-product drawer with validation. */ function AtlasProducts({ hrefs, ...props }: AtlasProductsProps) { const [items, setItems] = React.useState<AtlasProduct[]>(seed) const [query, setQuery] = React.useState("") const [category, setCategory] = React.useState("all") const [lowOnly, setLowOnly] = React.useState(false) const [view, setView] = React.useState<View>("grid") const [open, setOpen] = React.useState(false) const [errors, setErrors] = React.useState<{ name?: string; price?: string }>({}) const q = query.trim().toLowerCase() const shown = items.filter((p) => (category === "all" || p.category === category) && (!lowOnly || p.stock <= 10) && (!q || p.name.toLowerCase().includes(q))) const lowCount = items.filter((p) => p.stock <= 10).length function adjust(id: string, by: number) { setItems((all) => all.map((p) => (p.id === id ? { ...p, stock: Math.max(0, p.stock + by) } : p))) } function add(e: React.FormEvent<HTMLFormElement>) { e.preventDefault() const f = new FormData(e.currentTarget) const name = String(f.get("name") ?? "").trim() const price = Number(f.get("price")) const next: typeof errors = {} if (!name) next.name = "Give the product a name." if (!(price > 0)) next.price = "Enter a price above $0." setErrors(next) if (next.name || next.price) { ;(e.currentTarget.elements.namedItem(next.name ? "name" : "price") as HTMLElement | null)?.focus() return } setItems((all) => [{ id: `p-new-${all.length}`, name, category: String(f.get("category")), price, stock: Number(f.get("stock")) || 0, status: f.get("status") === "draft" ? "draft" : "active", hue: 180 + all.length * 17, sold: 0 }, ...all]) setOpen(false) } return ( <AtlasShell page="products" title="Products" hrefs={hrefs} actions={<button type="button" onClick={() => setOpen(true)} className={cn(atlasButton.primary, "hidden sm:inline-flex")}><Plus aria-hidden="true" /> Add product</button>} {...props}> <main className="mx-auto max-w-7xl space-y-5 px-4 py-6 sm:px-6"> <AtlasPageHeader title="Products" description={`${items.length} products · ${lowCount} need restocking`}> <button type="button" onClick={() => setOpen(true)} className={cn(atlasButton.primary, "sm:hidden")}><Plus aria-hidden="true" /> Add product</button> </AtlasPageHeader> <div className="flex flex-wrap items-center gap-2"> <div className="relative min-w-52 flex-1 sm:max-w-xs"> <Search className="text-muted-foreground pointer-events-none absolute top-1/2 start-3 size-4 -translate-y-1/2" aria-hidden="true" /> <input type="search" aria-label="Search products" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search products" className="bg-card focus-visible:ring-ring/50 placeholder:text-muted-foreground h-9 w-full rounded-lg border pe-3 ps-9 text-sm outline-none focus-visible:ring-[3px]" /> </div> <select aria-label="Category" value={category} onChange={(e) => setCategory(e.target.value)} className="bg-card focus-visible:ring-ring/50 h-9 rounded-lg border px-3 text-sm outline-none focus-visible:ring-[3px]"> <option value="all">All categories</option> {categories.map((c) => <option key={c}>{c}</option>)} </select> <button type="button" aria-pressed={lowOnly} onClick={() => setLowOnly((v) => !v)} className={cn("focus-visible:ring-ring/50 h-9 rounded-lg border px-3 text-sm font-semibold outline-none transition-colors focus-visible:ring-[3px]", lowOnly ? "bg-accent border-foreground/30" : "bg-card hover:bg-accent")}> Low stock <span className="text-muted-foreground ms-1 tabular-nums">{lowCount}</span> </button> <SegmentedControl aria-label="Layout" value={view} onValueChange={(v) => setView(v as View)} size="sm" className="ms-auto"> <SegmentedControlItem value="grid" aria-label="Grid view"><LayoutGrid /></SegmentedControlItem> <SegmentedControlItem value="list" aria-label="List view"><List /></SegmentedControlItem> </SegmentedControl> </div> <p className="sr-only" role="status">{shown.length} products shown</p> {shown.length === 0 ? ( <div className="bg-card text-muted-foreground rounded-xl border px-6 py-20 text-center text-sm">No products match these filters.</div> ) : view === "grid" ? ( <ul className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> {shown.map((p) => ( <li key={p.id} className="bg-card group rounded-xl border p-3 transition-shadow hover:shadow-md"> <ProductTile hue={p.hue} className="aspect-[4/3] w-full" /> <div className="mt-3 px-1"> <div className="flex items-start justify-between gap-2"> <h3 className="text-sm leading-snug font-semibold text-pretty">{p.name}</h3> {p.status === "draft" && <Badge variant="secondary">Draft</Badge>} </div> <p className="text-muted-foreground mt-0.5 text-xs">{p.category}</p> <div className="mt-3 flex items-center justify-between"><p className="font-bold tabular-nums">{moneyExact.format(p.price)}</p><Stock p={p} /></div> </div> </li> ))} </ul> ) : ( <div className="bg-card overflow-hidden rounded-xl border"> <div tabIndex={0} role="region" aria-label="Products table" className="focus-visible:ring-ring/50 overflow-x-auto outline-none focus-visible:ring-[3px] focus-visible:ring-inset"> <table className="w-full min-w-[40rem] text-start text-sm"> <caption className="sr-only">Products</caption> <thead><tr className="text-muted-foreground bg-surface border-b text-xs">{["Product", "Category", "Status", "Price", "Stock"].map((c, i) => <th key={c} scope="col" className={cn("px-4 py-2.5 font-semibold", i === 3 && "text-end", i === 4 && "text-end")}>{c}</th>)}</tr></thead> <tbody className="divide-y"> {shown.map((p) => ( <tr key={p.id} className="hover:bg-accent/40"> <td className="px-4 py-2.5"><span className="flex items-center gap-3"><ProductTile hue={p.hue} className="size-9 shrink-0" /><span className="font-semibold">{p.name}</span></span></td> <td className="text-muted-foreground px-4 py-2.5">{p.category}</td> <td className="px-4 py-2.5"><Badge status={p.status === "active" ? "success" : "neutral"} variant="outline">{p.status === "active" ? "Active" : "Draft"}</Badge></td> <td className="px-4 py-2.5 text-end font-semibold tabular-nums">{moneyExact.format(p.price)}</td> <td className="px-4 py-2.5"> <div className="flex items-center justify-end gap-1.5"> <button type="button" aria-label={`Remove one ${p.name}`} onClick={() => adjust(p.id, -1)} className="hover:bg-accent focus-visible:ring-ring/50 inline-flex size-7 items-center justify-center rounded-md border outline-none focus-visible:ring-[3px]"><Minus className="size-3.5" aria-hidden="true" /></button> <span className="w-10 text-center font-semibold tabular-nums" aria-live="polite">{p.stock}</span> <button type="button" aria-label={`Add one ${p.name}`} onClick={() => adjust(p.id, 1)} className="hover:bg-accent focus-visible:ring-ring/50 inline-flex size-7 items-center justify-center rounded-md border outline-none focus-visible:ring-[3px]"><Plus className="size-3.5" aria-hidden="true" /></button> </div> </td> </tr> ))} </tbody> </table> </div> </div> )} </main> <Sheet open={open} onOpenChange={(o) => { setOpen(o); if (!o) setErrors({}) }}> <SheetContent side="end" className="w-full sm:max-w-md" closeLabel="Close"> <form onSubmit={add} noValidate className="flex h-full flex-col"> <SheetHeader> <SheetTitle>Add product</SheetTitle> <SheetDescription>New products start as active and appear in your store right away.</SheetDescription> </SheetHeader> <div className="flex-1 space-y-4 overflow-y-auto px-4 pb-4"> <div> <label htmlFor="ap-name" className="text-sm font-medium">Name</label> <input id="ap-name" name="name" aria-invalid={!!errors.name} aria-describedby={errors.name ? "ap-name-err" : undefined} className={fieldClass} /> {errors.name && <p id="ap-name-err" className="text-destructive mt-1.5 text-sm">{errors.name}</p>} </div> <div> <label htmlFor="ap-cat" className="text-sm font-medium">Category</label> <select id="ap-cat" name="category" className={fieldClass}>{categories.map((c) => <option key={c}>{c}</option>)}</select> </div> <div className="grid grid-cols-2 gap-3"> <div> <label htmlFor="ap-price" className="text-sm font-medium">Price (USD)</label> <input id="ap-price" name="price" type="number" step="0.01" min="0" inputMode="decimal" aria-invalid={!!errors.price} aria-describedby={errors.price ? "ap-price-err" : undefined} className={fieldClass} /> {errors.price && <p id="ap-price-err" className="text-destructive mt-1.5 text-sm">{errors.price}</p>} </div> <div> <label htmlFor="ap-stock" className="text-sm font-medium">Stock</label> <input id="ap-stock" name="stock" type="number" min="0" defaultValue="0" className={fieldClass} /> </div> </div> <div> <label htmlFor="ap-status" className="text-sm font-medium">Status</label> <select id="ap-status" name="status" className={fieldClass}><option value="active">Active</option><option value="draft">Draft</option></select> </div> </div> <SheetFooter> <button type="button" className={atlasButton.outline} onClick={() => setOpen(false)}>Cancel</button> <button type="submit" className={atlasButton.primary}>Add product</button> </SheetFooter> </form> </SheetContent> </Sheet> </AtlasShell> ) } export { AtlasProducts, type AtlasProductsProps }components/ballmac/templates/atlas/atlas-customers.tsx// Ballmac UI: Atlas customers page. https://ui.ballmac.com/templates/template-atlas "use client" import * as React from "react" import { Mail, Search } from "lucide-react" import { Badge } from "@/components/ballmac/badge" import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ballmac/sheet" import { customers, formatDate, initials, money, moneyExact, orderGrand, orders, type AtlasCustomer } from "@/components/ballmac/templates/atlas/atlas-data" import { AtlasPageHeader, AtlasShell, FulfilmentBadge, atlasButton, type AtlasHrefs } from "@/components/ballmac/templates/atlas/atlas-theme" import { cn } from "@/lib/utils" type Segment = "All" | AtlasCustomer["segment"] const segments: Segment[] = ["All", "VIP", "Regular", "New", "At risk"] const tones = ["bg-chart-1/20", "bg-chart-2/20", "bg-chart-3/25", "bg-chart-4/20", "bg-chart-5/20"] const segmentStatus = { VIP: "success", Regular: "neutral", New: "neutral", "At risk": "warning" } as const function stats(c: AtlasCustomer) { const mine = orders.filter((o) => o.customerId === c.id) const spent = mine.reduce((n, o) => n + (o.payment === "refunded" ? 0 : orderGrand(o)), 0) return { mine, spent, last: mine[0]?.date } } type AtlasCustomersProps = React.ComponentProps<"div"> & { hrefs?: Partial<AtlasHrefs> } /** The Atlas customers page: segment filters, search, lifetime value bars and a profile drawer with recent orders. */ function AtlasCustomers({ hrefs, ...props }: AtlasCustomersProps) { const [segment, setSegment] = React.useState<Segment>("All") const [query, setQuery] = React.useState("") const [openId, setOpenId] = React.useState<string | null>(null) const q = query.trim().toLowerCase() const rows = customers .map((c) => ({ c, ...stats(c) })) .filter((r) => (segment === "All" || r.c.segment === segment) && (!q || [r.c.name, r.c.email, r.c.city].join(" ").toLowerCase().includes(q))) .sort((a, b) => b.spent - a.spent) const max = Math.max(...customers.map((c) => stats(c).spent)) const open = customers.find((c) => c.id === openId) const openStats = open ? stats(open) : null return ( <AtlasShell page="customers" title="Customers" hrefs={hrefs} {...props}> <main className="mx-auto max-w-7xl space-y-5 px-4 py-6 sm:px-6"> <AtlasPageHeader title="Customers" description={`${customers.length} people have bought from Fieldnote Goods.`} /> <div className="bg-card rounded-xl border"> <div className="flex flex-col gap-3 border-b p-3 lg:flex-row lg:items-center lg:justify-between"> <div role="group" aria-label="Filter by segment" className="flex gap-1 overflow-x-auto [scrollbar-width:none]"> {segments.map((s) => { const on = segment === s const count = s === "All" ? customers.length : customers.filter((c) => c.segment === s).length return ( <button key={s} type="button" aria-pressed={on} onClick={() => setSegment(s)} className={cn("focus-visible:ring-ring/50 flex shrink-0 items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-semibold outline-none transition-colors focus-visible:ring-[3px]", on ? "bg-accent text-foreground" : "text-muted-foreground hover:text-foreground")}> {s} <span className="text-muted-foreground text-xs font-medium tabular-nums">{count}</span> </button> ) })} </div> <div className="relative lg:w-72"> <Search className="text-muted-foreground pointer-events-none absolute top-1/2 start-3 size-4 -translate-y-1/2" aria-hidden="true" /> <input type="search" aria-label="Search customers" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search name, email or city" className="bg-background focus-visible:ring-ring/50 placeholder:text-muted-foreground h-9 w-full rounded-lg border pe-3 ps-9 text-sm outline-none focus-visible:ring-[3px]" /> </div> </div> <div tabIndex={0} role="region" aria-label="Customers table" className="focus-visible:ring-ring/50 overflow-x-auto outline-none focus-visible:ring-[3px] focus-visible:ring-inset"> <table className="w-full min-w-[44rem] text-start text-sm"> <caption className="sr-only">Customers by lifetime spend</caption> <thead><tr className="text-muted-foreground bg-surface border-b text-xs">{["Customer", "Segment", "Orders", "Last order", "Lifetime spend"].map((c) => <th key={c} scope="col" className="px-4 py-2.5 font-semibold">{c}</th>)}</tr></thead> <tbody className="divide-y"> {rows.map(({ c, mine, spent, last }) => ( <tr key={c.id} className="hover:bg-accent/40"> <td className="px-4 py-3"> <button type="button" onClick={() => setOpenId(c.id)} className="focus-visible:ring-ring/50 -m-1 flex items-center gap-3 rounded-lg p-1 text-start outline-none focus-visible:ring-[3px]"> <span className={cn("flex size-9 shrink-0 items-center justify-center rounded-full text-xs font-bold", tones[c.tone - 1])} aria-hidden="true">{initials(c.name)}</span> <span><span className="block font-semibold">{c.name}</span><span className="text-muted-foreground block text-xs">{c.email} · {c.city}</span></span> </button> </td> <td className="px-4 py-3"><Badge status={segmentStatus[c.segment]} variant="outline">{c.segment}</Badge></td> <td className="px-4 py-3 tabular-nums">{mine.length}</td> <td className="text-muted-foreground px-4 py-3 whitespace-nowrap">{last ? formatDate(last) : "—"}</td> <td className="px-4 py-3"> <div className="flex items-center gap-3"> <div className="bg-muted hidden h-1.5 w-24 rounded-full sm:block" aria-hidden="true"><div className="bg-chart-1 h-full rounded-full" style={{ width: `${(spent / max) * 100}%` }} /></div> <span className="w-20 text-end font-semibold tabular-nums">{money.format(spent)}</span> </div> </td> </tr> ))} {rows.length === 0 && <tr><td colSpan={5} className="text-muted-foreground px-4 py-16 text-center">No customers match.</td></tr>} </tbody> </table> </div> <p className="text-muted-foreground border-t px-4 py-2.5 text-sm" role="status">{rows.length} of {customers.length} customers</p> </div> </main> <Sheet open={!!open} onOpenChange={(o) => !o && setOpenId(null)}> <SheetContent side="end" className="w-full sm:max-w-md" closeLabel="Close profile"> {open && openStats && ( <> <SheetHeader> <div className="flex items-center gap-3"> <span className={cn("flex size-12 items-center justify-center rounded-full text-sm font-bold", tones[open.tone - 1])} aria-hidden="true">{initials(open.name)}</span> <div><SheetTitle>{open.name}</SheetTitle><SheetDescription>{open.city} · {open.segment}</SheetDescription></div> </div> </SheetHeader> <div className="space-y-5 overflow-y-auto px-4 pb-6"> <dl className="grid grid-cols-3 gap-2 text-center"> {[["Spent", money.format(openStats.spent)], ["Orders", String(openStats.mine.length)], ["Average", openStats.mine.length ? moneyExact.format(openStats.spent / openStats.mine.length) : "—"]].map(([k, v]) => ( <div key={k} className="bg-surface rounded-lg border p-3"><dt className="text-muted-foreground text-xs">{k}</dt><dd className="mt-1 font-bold tabular-nums">{v}</dd></div> ))} </dl> <a href={`mailto:${open.email}`} className={cn(atlasButton.outline, "w-full")}><Mail aria-hidden="true" /> Email {open.name.split(" ")[0]}</a> <section aria-labelledby="ac-recent"> <h3 id="ac-recent" className="text-sm font-bold">Recent orders</h3> <ul className="mt-2 divide-y rounded-lg border"> {openStats.mine.slice(0, 4).map((o) => ( <li key={o.id} className="flex items-center justify-between gap-3 px-3 py-2.5 text-sm"> <span><span className="font-semibold" style={{ fontFamily: "var(--atlas-mono)" }}>{o.id}</span><span className="text-muted-foreground ms-2 text-xs">{formatDate(o.date)}</span></span> <span className="flex items-center gap-2"><FulfilmentBadge value={o.fulfillment} /><span className="tabular-nums">{moneyExact.format(orderGrand(o))}</span></span> </li> ))} </ul> </section> </div> </> )} </SheetContent> </Sheet> </AtlasShell> ) } export { AtlasCustomers, type AtlasCustomersProps }components/ballmac/templates/atlas/atlas-settings.tsx// Ballmac UI: Atlas settings page. https://ui.ballmac.com/templates/template-atlas "use client" import * as React from "react" import { Check } from "lucide-react" import { Switch } from "@/components/ballmac/switch" import { AtlasPageHeader, AtlasShell, atlasButton, type AtlasHrefs } from "@/components/ballmac/templates/atlas/atlas-theme" import { cn } from "@/lib/utils" const field = "bg-background focus-visible:ring-ring/50 mt-1.5 h-10 w-full rounded-lg border px-3 text-sm outline-none focus-visible:ring-[3px]" type State = { name: string email: string currency: string rates: Record<string, boolean> alerts: Record<string, boolean> } const initial: State = { name: "Fieldnote Goods", email: "hello@fieldnote.example", currency: "USD", rates: { standard: true, express: true, pickup: false }, alerts: { orders: true, stock: true, reviews: false, digest: true }, } const rateRows = [ ["standard", "Standard shipping", "3 to 5 business days · $8, free over $100"], ["express", "Express shipping", "1 to 2 business days · $19"], ["pickup", "Local pickup", "Collect from the Portland studio · Free"], ] const alertRows = [ ["orders", "New orders", "Email me as soon as an order is placed."], ["stock", "Low stock", "Tell me when a product drops to 10 or fewer."], ["reviews", "Customer reviews", "Tell me about new reviews that need a reply."], ["digest", "Weekly digest", "A Monday summary of sales, orders and top products."], ] const sections = [["profile", "Store profile"], ["shipping", "Shipping"], ["alerts", "Notifications"], ["danger", "Danger zone"]] type AtlasSettingsProps = React.ComponentProps<"div"> & { hrefs?: Partial<AtlasHrefs> } /** The Atlas settings page: store profile, shipping rates and notification switches, with a save bar that appears only when something changed. */ function AtlasSettings({ hrefs, ...props }: AtlasSettingsProps) { const [saved, setSaved] = React.useState<State>(initial) const [draft, setDraft] = React.useState<State>(initial) const [status, setStatus] = React.useState<"idle" | "saved">("idle") const dirty = JSON.stringify(saved) !== JSON.stringify(draft) function set<K extends keyof State>(key: K, value: State[K]) { setStatus("idle") setDraft((d) => ({ ...d, [key]: value })) } return ( <AtlasShell page="settings" title="Settings" hrefs={hrefs} {...props}> <main className="mx-auto grid max-w-5xl gap-8 px-4 py-6 pb-28 sm:px-6 lg:grid-cols-[11rem_minmax(0,1fr)]"> <nav aria-label="Settings sections" className="hidden lg:block"> <ul className="sticky top-20 space-y-0.5"> {sections.map(([id, label]) => <li key={id}><a href={`#${id}`} className="text-muted-foreground hover:text-foreground hover:bg-accent focus-visible:ring-ring/50 block rounded-lg px-3 py-2 text-sm font-medium outline-none focus-visible:ring-[3px]">{label}</a></li>)} </ul> </nav> <div className="min-w-0 space-y-6"> <AtlasPageHeader title="Settings" description="Manage how your store looks, ships and talks to you." /> <section id="profile" aria-labelledby="as-profile" className="bg-card scroll-mt-20 rounded-xl border p-5"> <h3 id="as-profile" className="font-bold">Store profile</h3> <div className="mt-4 grid gap-4 sm:grid-cols-2"> <div><label htmlFor="as-name" className="text-sm font-medium">Store name</label><input id="as-name" value={draft.name} onChange={(e) => set("name", e.target.value)} className={field} /></div> <div><label htmlFor="as-email" className="text-sm font-medium">Contact email</label><input id="as-email" type="email" value={draft.email} onChange={(e) => set("email", e.target.value)} className={field} /></div> <div><label htmlFor="as-cur" className="text-sm font-medium">Currency</label> <select id="as-cur" value={draft.currency} onChange={(e) => set("currency", e.target.value)} className={field}><option>USD</option><option>EUR</option><option>GBP</option><option>CAD</option></select> </div> </div> </section> <section id="shipping" aria-labelledby="as-ship" className="bg-card scroll-mt-20 rounded-xl border"> <h3 id="as-ship" className="p-5 pb-3 font-bold">Shipping rates</h3> <ul className="divide-y border-t"> {rateRows.map(([id, title, text]) => ( <li key={id} className="flex items-center justify-between gap-4 px-5 py-4"> <div><p id={`as-rate-${id}`} className="text-sm font-semibold">{title}</p><p className="text-muted-foreground text-sm">{text}</p></div> <Switch aria-labelledby={`as-rate-${id}`} checked={draft.rates[id]} onCheckedChange={(v) => set("rates", { ...draft.rates, [id]: v })} /> </li> ))} </ul> </section> <section id="alerts" aria-labelledby="as-alerts" className="bg-card scroll-mt-20 rounded-xl border"> <h3 id="as-alerts" className="p-5 pb-3 font-bold">Notifications</h3> <ul className="divide-y border-t"> {alertRows.map(([id, title, text]) => ( <li key={id} className="flex items-center justify-between gap-4 px-5 py-4"> <div><p id={`as-alert-${id}`} className="text-sm font-semibold">{title}</p><p className="text-muted-foreground text-sm">{text}</p></div> <Switch aria-labelledby={`as-alert-${id}`} checked={draft.alerts[id]} onCheckedChange={(v) => set("alerts", { ...draft.alerts, [id]: v })} /> </li> ))} </ul> </section> <section id="danger" aria-labelledby="as-danger" className="border-destructive/40 bg-card scroll-mt-20 rounded-xl border p-5"> <h3 id="as-danger" className="font-bold">Danger zone</h3> <p className="text-muted-foreground mt-1 text-sm text-pretty">Closing the store hides it from customers. Orders and customer data are kept for 90 days.</p> <button type="button" className={cn(atlasButton.danger, "mt-4")}>Close store</button> </section> </div> </main> {(dirty || status === "saved") && ( <div role="region" aria-label="Save changes" className="bg-popover fixed inset-x-3 bottom-4 z-40 mx-auto flex max-w-xl items-center justify-between gap-3 rounded-2xl border p-3 ps-5 shadow-[0_20px_50px_-12px_rgb(0_0_0/0.35)]"> <p className="text-sm font-semibold" role="status">{dirty ? "You have unsaved changes" : <span className="flex items-center gap-1.5"><Check className="text-chart-2 size-4" aria-hidden="true" />Changes saved</span>}</p> {dirty && ( <div className="flex gap-2"> <button type="button" className={atlasButton.outline} onClick={() => setDraft(saved)}>Discard</button> <button type="button" className={atlasButton.primary} onClick={() => { setSaved(draft); setStatus("saved") }}>Save changes</button> </div> )} </div> )} </AtlasShell> ) } export { AtlasSettings, type AtlasSettingsProps }app/atlas/page.tsx// Ballmac UI: Atlas template route. https://ui.ballmac.com/templates/template-atlas import type { Metadata } from "next" import { AtlasDashboard } from "@/components/ballmac/templates/atlas/atlas-dashboard" export const metadata: Metadata = { title: "Atlas dashboard · Atlas", description: "Sales, orders and stock for Fieldnote Goods.", } export default function Page() { return <AtlasDashboard /> }app/atlas/orders/page.tsx// Ballmac UI: Atlas template route. https://ui.ballmac.com/templates/template-atlas import type { Metadata } from "next" import { AtlasOrders } from "@/components/ballmac/templates/atlas/atlas-orders" export const metadata: Metadata = { title: "Orders · Atlas", description: "Search, sort and fulfil every order.", } export default function Page() { return <AtlasOrders /> }app/atlas/orders/[id]/page.tsx// Ballmac UI: Atlas template route. https://ui.ballmac.com/templates/template-atlas import type { Metadata } from "next" import { AtlasOrder } from "@/components/ballmac/templates/atlas/atlas-order" export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> { const { id } = await params return { title: `Order ${id} · Atlas`, description: "Items, timeline, fulfilment and refunds for one order." } } export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params return <AtlasOrder orderId={id} /> }app/atlas/products/page.tsx// Ballmac UI: Atlas template route. https://ui.ballmac.com/templates/template-atlas import type { Metadata } from "next" import { AtlasProducts } from "@/components/ballmac/templates/atlas/atlas-products" export const metadata: Metadata = { title: "Products · Atlas", description: "Catalog, stock and pricing.", } export default function Page() { return <AtlasProducts /> }app/atlas/customers/page.tsx// Ballmac UI: Atlas template route. https://ui.ballmac.com/templates/template-atlas import type { Metadata } from "next" import { AtlasCustomers } from "@/components/ballmac/templates/atlas/atlas-customers" export const metadata: Metadata = { title: "Customers · Atlas", description: "Segments, lifetime spend and recent orders.", } export default function Page() { return <AtlasCustomers /> }app/atlas/settings/page.tsx// Ballmac UI: Atlas template route. https://ui.ballmac.com/templates/template-atlas import type { Metadata } from "next" import { AtlasSettings } from "@/components/ballmac/templates/atlas/atlas-settings" export const metadata: Metadata = { title: "Settings · Atlas", description: "Store profile, shipping and notifications.", } export default function Page() { return <AtlasSettings /> }Update the import paths to match your project setup.
The page is added as a route in your app, with every block it uses.
What's inside
- Pages
- Dashboard/atlas
- Orders/atlas/orders
- Order/atlas/orders/ORD-10479
- Products/atlas/products
- Customers/atlas/customers
- Settings/atlas/settings
- Fonts
- Manrope, Geist Mono (Google Fonts, loaded with next/font)
- Files
- 15 files · 10 Ballmac blocks and components
Built with
Dependencies
Use with AI
Installs a six-page store admin with working client-side filtering, sorting, selection, drawers and forms over sample data. Replace atlas-data.ts with your API, and edit atlasCss for the palette. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Atlas: store admin (@ballmac/template-atlas) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Back offices for shops, marketplaces and internal tools
- A starting point for dense admin tables with bulk actions
Not for
- Public marketing sites (use a marketing template)
Registry JSON: https://ui.ballmac.com/r/template-atlas.json
Free to use in personal and commercial projects.