Northwind: B2B SaaS
A warm, editorial five-page site for a B2B product: serif headlines on cream paper, an interactive product tour with numbered pins, case studies, pricing, an about page and a demo request form.
// Ballmac UI: Northwind template fonts. https://ui.ballmac.com/templates/template-northwind
import { Figtree, Fraunces } from "next/font/google"
export const northwindSerif = Fraunces({ variable: "--northwind-serif", subsets: ["latin"], axes: ["opsz", "SOFT"], display: "swap", preload: false })
export const northwindSans = Figtree({ variable: "--northwind-sans", subsets: ["latin"], display: "swap", preload: false })// Ballmac UI: Northwind template shell. https://ui.ballmac.com/templates/template-northwind
"use client"
import * as React from "react"
import { Menu, X } from "lucide-react"
import { northwindSans, northwindSerif } from "@/components/ballmac/templates/northwind/northwind-fonts"
import { cn } from "@/lib/utils"
type NorthwindPage = "home" | "pricing" | "customers" | "about" | "contact"
type NorthwindHrefs = Record<NorthwindPage, string>
const defaultHrefs: NorthwindHrefs = { home: "/northwind", pricing: "/northwind/pricing", customers: "/northwind/customers", about: "/northwind/about", contact: "/northwind/contact" }
/** Northwind's palette: cream paper, forest ink and one terracotta. Dark mode turns to deep green. */
const northwindCss = `
.northwind-theme{--background:oklch(0.975 0.014 88);--foreground:oklch(0.24 0.035 155);--card:oklch(0.99 0.01 88);--card-foreground:oklch(0.24 0.035 155);--popover:oklch(0.99 0.01 88);--popover-foreground:oklch(0.24 0.035 155);--primary:oklch(0.36 0.075 158);--primary-foreground:oklch(0.975 0.014 88);--secondary:oklch(0.94 0.02 88);--secondary-foreground:oklch(0.24 0.035 155);--muted:oklch(0.94 0.02 88);--muted-foreground:oklch(0.48 0.03 150);--accent:oklch(0.925 0.025 95);--accent-foreground:oklch(0.24 0.035 155);--border:oklch(0.24 0.035 155 / 13%);--input:oklch(0.24 0.035 155 / 18%);--ring:oklch(0.5 0.1 158);--surface:oklch(0.955 0.018 88);--destructive:oklch(0.55 0.2 28);--chart-1:oklch(0.45 0.09 158);--chart-2:oklch(0.5 0.15 45);--chart-3:oklch(0.76 0.13 85);--chart-4:oklch(0.5 0.12 330);--chart-5:oklch(0.52 0.08 240);--radius:0.5rem}
.dark .northwind-theme{--background:oklch(0.19 0.025 158);--foreground:oklch(0.95 0.015 88);--card:oklch(0.23 0.03 158);--card-foreground:oklch(0.95 0.015 88);--popover:oklch(0.25 0.03 158);--popover-foreground:oklch(0.95 0.015 88);--primary:oklch(0.84 0.09 150);--primary-foreground:oklch(0.2 0.03 158);--secondary:oklch(0.27 0.032 158);--secondary-foreground:oklch(0.95 0.015 88);--muted:oklch(0.26 0.03 158);--muted-foreground:oklch(0.74 0.03 130);--accent:oklch(0.3 0.035 158);--accent-foreground:oklch(0.95 0.015 88);--border:oklch(0.95 0.015 88 / 14%);--input:oklch(0.95 0.015 88 / 18%);--ring:oklch(0.74 0.12 150);--surface:oklch(0.21 0.027 158);--destructive:oklch(0.7 0.18 28);--chart-1:oklch(0.78 0.11 150);--chart-2:oklch(0.76 0.13 50);--chart-3:oklch(0.84 0.12 90);--chart-4:oklch(0.74 0.12 335);--chart-5:oklch(0.76 0.09 240)}
`
const links: { key: NorthwindPage; label: string }[] = [
{ key: "home", label: "Product" },
{ key: "pricing", label: "Pricing" },
{ key: "customers", label: "Customers" },
{ key: "about", label: "About" },
]
function NorthwindMark({ className }: { className?: string }) {
return (
<svg viewBox="0 0 32 32" aria-hidden="true" className={cn("size-7", className)} fill="none">
<circle cx="16" cy="16" r="14" fill="var(--primary)" />
<path d="M16 6 20.5 16 16 26 11.5 16Z" fill="var(--primary-foreground)" />
<path d="M16 6v20" stroke="var(--primary)" strokeWidth="1.2" />
</svg>
)
}
/** A serif heading. Wrap one word in `<em>` for the italic accent. */
function NorthwindHeading({ as: Tag = "h2", className, ...props }: React.ComponentProps<"h2"> & { as?: "h1" | "h2" | "h3" }) {
return <Tag className={cn("[font-family:var(--northwind-serif),ui-serif,Georgia,serif] font-medium tracking-[-0.025em] text-balance [&_em]:text-chart-2 [&_em]:font-normal", className)} {...props} />
}
type NorthwindShellProps = React.ComponentProps<"div"> & {
/** The page being shown, so its nav link is marked current. */
page: NorthwindPage
/** Override where pages live (used by previews). */
hrefs?: Partial<NorthwindHrefs>
}
/** Northwind's frame: cream paper, a serif wordmark header and an editorial footer. Wrap every Northwind page in it. */
function NorthwindShell({ page, hrefs: overrides, className, style, children, ...props }: NorthwindShellProps) {
const hrefs = { ...defaultHrefs, ...overrides }
const [open, setOpen] = React.useState(false)
return (
<div
data-slot="northwind"
className={cn("northwind-theme bg-background text-foreground relative min-h-dvh overflow-x-clip", northwindSerif.variable, northwindSans.variable, className)}
style={{ fontFamily: "var(--northwind-sans), ui-sans-serif, system-ui, sans-serif", ...style }}
{...props}
>
<style>{northwindCss}</style>
<header className="bg-background/90 sticky top-0 z-40 border-b backdrop-blur-lg">
<div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-4 sm:px-6">
<a href={hrefs.home} className="focus-visible:ring-ring/50 flex items-center gap-2.5 rounded-md outline-none focus-visible:ring-[3px]">
<NorthwindMark />
<span className="text-xl font-medium tracking-tight [font-family:var(--northwind-serif),ui-serif,Georgia,serif]">Northwind</span>
</a>
<nav aria-label="Main" className="hidden items-center gap-8 md:flex">
{links.map((l) => (
<a key={l.key} href={hrefs[l.key]} aria-current={page === l.key ? "page" : undefined} className="text-muted-foreground hover:text-foreground aria-[current=page]:text-foreground aria-[current=page]:underline decoration-chart-2 focus-visible:ring-ring/50 rounded text-[15px] underline-offset-[10px] outline-none transition-colors focus-visible:ring-[3px] aria-[current=page]:decoration-2">
{l.label}
</a>
))}
</nav>
<div className="flex items-center gap-2">
<a href={hrefs.contact} aria-current={page === "contact" ? "page" : undefined} className="text-muted-foreground hover:text-foreground hidden px-2 text-[15px] sm:block">Contact</a>
<a href={hrefs.contact} className="bg-primary text-primary-foreground focus-visible:ring-ring/50 inline-flex h-10 items-center rounded-full px-5 text-sm font-medium outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]">Book a demo</a>
<button type="button" aria-label={open ? "Close menu" : "Open menu"} aria-expanded={open} aria-controls="northwind-mobile-menu" onClick={() => setOpen((v) => !v)} className="text-muted-foreground focus-visible:ring-ring/50 inline-flex size-10 items-center justify-center rounded-md outline-none focus-visible:ring-[3px] md:hidden">
{open ? <X className="size-5" aria-hidden="true" /> : <Menu className="size-5" aria-hidden="true" />}
</button>
</div>
</div>
{open && (
<nav id="northwind-mobile-menu" aria-label="Mobile" className="border-t px-4 py-2 md:hidden">
{[...links, { key: "contact" as const, label: "Contact" }].map((l) => <a key={l.key} href={hrefs[l.key]} className="hover:bg-accent block rounded-md px-3 py-3 text-base">{l.label}</a>)}
</nav>
)}
</header>
{children}
<footer className="bg-primary text-primary-foreground">
<div className="mx-auto max-w-6xl px-4 py-16 sm:px-6">
<div className="grid gap-12 lg:grid-cols-[1.5fr_repeat(3,1fr)]">
<div>
<p className="text-3xl leading-tight font-medium tracking-tight [font-family:var(--northwind-serif),ui-serif,Georgia,serif]">Know where every dollar goes.</p>
<a href={hrefs.contact} className="bg-primary-foreground text-primary focus-visible:ring-ring mt-6 inline-flex h-11 items-center rounded-full px-6 text-sm font-medium outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]">Book a demo</a>
</div>
{[
{ title: "Product", items: [["Cards", "home"], ["Approvals", "home"], ["Reporting", "home"], ["Pricing", "pricing"]] },
{ title: "Company", items: [["About", "about"], ["Customers", "customers"], ["Careers", "about"], ["Contact", "contact"]] },
{ title: "Resources", items: [["Guides", "home"], ["Security", "home"], ["Status", "home"], ["Press", "about"]] },
].map((c) => (
<div key={c.title}>
<h2 className="text-sm font-semibold opacity-80">{c.title}</h2>
<ul className="mt-4 space-y-3">
{c.items.map(([label, key]) => <li key={label}><a href={hrefs[key as NorthwindPage]} className="text-sm opacity-90 hover:underline">{label}</a></li>)}
</ul>
</div>
))}
</div>
<p className="mt-14 border-t border-current/20 pt-6 text-xs opacity-80">© 2026 Northwind Financial, Inc. Banking services provided by partner banks, Members FDIC.</p>
</div>
</footer>
</div>
)
}
export { NorthwindHeading, NorthwindMark, NorthwindShell, defaultHrefs as northwindDefaultHrefs, type NorthwindHrefs, type NorthwindPage, type NorthwindShellProps }// Ballmac UI: Northwind product tour. https://ui.ballmac.com/templates/template-northwind
"use client"
import * as React from "react"
import { BarChart3, Check, CreditCard, Home, ListChecks, Settings, Wallet } from "lucide-react"
import { RadioGroup as RadioGroupPrimitive } from "radix-ui"
import { NorthwindHeading } from "@/components/ballmac/templates/northwind/northwind-theme"
import { cn } from "@/lib/utils"
const steps = [
{ id: "cards", title: "Issue a card in seconds", text: "Virtual or physical, with a limit, a merchant lock and an owner. Employees never wait on finance to buy what they need." },
{ id: "approvals", title: "Approve from anywhere", text: "Requests reach the right person by policy, with the receipt and context attached. One tap from a phone is enough." },
{ id: "budgets", title: "Budgets that warn early", text: "Every team sees what is left as they spend, and finance is told at 80 percent, not after the overrun." },
{ id: "close", title: "Close the month in days", text: "Receipts are matched, coded and exported to your ledger as spend happens, so close day stops being a project." },
] as const
type StepId = (typeof steps)[number]["id"]
const cardRows = [
["Design tools", "•••• 4821", "$2,400", "Maya R."],
["Team offsite", "•••• 1190", "$8,000", "Dev P."],
["Cloud hosting", "•••• 7745", "$14,500", "Engineering"],
]
const queue = [
["Figma annual", "$1,440", "Maya R."],
["Flight to Lisbon", "$612", "Sam O."],
["Conference pass", "$890", "Dev P."],
]
const budgets = [
["Marketing", 82, "bg-chart-2"],
["Engineering", 64, "bg-chart-1"],
["Operations", 38, "bg-chart-5"],
]
function Region({ id, active, children, className }: { id: StepId; active: StepId; children: React.ReactNode; className?: string }) {
const on = id === active
return (
<div
data-region={id}
data-active={on}
className={cn("bg-card relative rounded-xl border p-3.5 transition-[box-shadow,opacity,border-color] duration-500 motion-reduce:transition-none", on ? "border-chart-2 shadow-[0_0_0_3px_color-mix(in_oklab,var(--chart-2)_22%,transparent)]" : "border-dashed", className)}
>
{children}
</div>
)
}
/** A numbered pin on the mock. It repeats the step list for pointer users, so it is hidden from keyboard and screen readers. */
function Pin({ n, id, active, onSelect }: { n: number; id: StepId; active: StepId; onSelect: (id: StepId) => void }) {
const on = id === active
return (
<button
type="button"
tabIndex={-1}
aria-hidden="true"
data-pin={id}
onClick={() => onSelect(id)}
className={cn("absolute -top-3 -start-3 z-10 flex size-7 items-center justify-center rounded-full border-2 text-xs font-semibold shadow-sm outline-none transition-colors", on ? "bg-chart-2 border-card text-white" : "bg-card text-foreground hover:bg-accent")}
>
{n}
</button>
)
}
/** An interactive product tour: numbered pins on a mock dashboard, tied to a list of steps. */
function NorthwindTour({ className }: { className?: string }) {
const [active, setActive] = React.useState<StepId>("cards")
const current = steps.find((s) => s.id === active)!
return (
<div data-slot="northwind-tour" className={cn("grid items-center gap-10 lg:grid-cols-[0.8fr_1.3fr] lg:gap-14", className)}>
<div>
<RadioGroupPrimitive.Root value={active} onValueChange={(v) => setActive(v as StepId)} aria-label="Product tour steps" className="space-y-1.5">
{steps.map((s, i) => {
const on = s.id === active
return (
<RadioGroupPrimitive.Item
key={s.id}
value={s.id}
className={cn("focus-visible:ring-ring/50 group block w-full rounded-xl border border-transparent p-4 text-start outline-none transition-colors focus-visible:ring-[3px]", on ? "bg-card border-border shadow-sm" : "hover:bg-accent/60")}
>
<span className="flex items-center gap-3">
<span className={cn("flex size-7 shrink-0 items-center justify-center rounded-full text-xs font-semibold transition-colors", on ? "bg-chart-2 text-white" : "bg-secondary")}>{i + 1}</span>
<NorthwindHeading as="h3" className="text-xl">{s.title}</NorthwindHeading>
</span>
{on && <span className="text-muted-foreground mt-2 block ps-10 text-[15px] leading-relaxed text-pretty">{s.text}</span>}
</RadioGroupPrimitive.Item>
)
})}
</RadioGroupPrimitive.Root>
</div>
<div className="bg-surface relative rounded-2xl border p-2.5 shadow-[0_30px_70px_-40px_oklch(0.3_0.05_155/0.5)] sm:p-3">
<p className="sr-only" aria-live="polite">Step {steps.indexOf(current) + 1} of {steps.length}: {current.title}. {current.text}</p>
<div aria-hidden="true" className="@container grid min-w-0 grid-cols-[2.75rem_1fr] gap-2.5 sm:grid-cols-[9rem_1fr]">
<div className="bg-card hidden rounded-xl border p-2.5 sm:block">
<p className="flex items-center gap-2 text-sm font-semibold"><span className="bg-primary size-5 rounded-full" />Acme Co</p>
<ul className="mt-4 space-y-1 text-[13px]">
{[[Home, "Home"], [CreditCard, "Cards"], [ListChecks, "Approvals"], [Wallet, "Budgets"], [BarChart3, "Reports"], [Settings, "Settings"]].map(([Icon, label], i) => {
const I = Icon as typeof Home
return <li key={label as string} className={cn("flex items-center gap-2 rounded-md px-2 py-1.5", i === 1 ? "bg-accent font-medium" : "text-muted-foreground")}><I className="size-3.5" />{label as string}</li>
})}
</ul>
</div>
<div className="bg-card flex flex-col items-center gap-3 rounded-xl border py-3 sm:hidden">
{[Home, CreditCard, ListChecks, Wallet].map((I, i) => <I key={i} className={cn("size-4", i === 1 ? "text-foreground" : "text-muted-foreground")} />)}
</div>
<div className="grid min-w-0 gap-2.5 @lg:grid-cols-2">
<Region id="cards" active={active} className="@lg:col-span-2">
<Pin n={1} id="cards" active={active} onSelect={setActive} />
<p className="text-xs font-semibold">Cards</p>
<ul className="mt-2 divide-y text-xs">
{cardRows.map((r) => (
<li key={r[1]} className="grid grid-cols-[1fr_auto] items-center gap-2 py-1.5 @md:grid-cols-[1.2fr_1fr_auto_auto]">
<span className="font-medium">{r[0]}</span>
<span className="text-muted-foreground hidden @md:block">{r[1]}</span>
<span className="text-muted-foreground hidden @md:block">{r[3]}</span>
<span className="tabular-nums">{r[2]}</span>
</li>
))}
</ul>
</Region>
<Region id="approvals" active={active}>
<Pin n={2} id="approvals" active={active} onSelect={setActive} />
<p className="text-xs font-semibold">Needs approval <span className="bg-chart-2/20 ms-1 rounded-full px-1.5">3</span></p>
<ul className="mt-2 space-y-1.5 text-xs">
{queue.map((q) => (
<li key={q[0]} className="bg-secondary/70 flex items-center justify-between gap-2 rounded-lg px-2.5 py-1.5">
<span className="min-w-0 truncate"><span className="font-medium">{q[0]}</span> <span className="text-muted-foreground">{q[2]}</span></span>
<span className="flex shrink-0 items-center gap-1.5 tabular-nums">{q[1]}<Check className="text-chart-1 size-3.5" /></span>
</li>
))}
</ul>
</Region>
<Region id="budgets" active={active}>
<Pin n={3} id="budgets" active={active} onSelect={setActive} />
<p className="text-xs font-semibold">Budgets this quarter</p>
<ul className="mt-2 space-y-2.5 text-xs">
{budgets.map(([name, pct, tone]) => (
<li key={name as string}>
<div className="flex justify-between"><span>{name}</span><span className={cn("tabular-nums", (pct as number) > 80 && "text-chart-2 font-semibold")}>{pct}%</span></div>
<div className="bg-secondary mt-1 h-1.5 rounded-full"><div className={cn("h-full rounded-full", tone as string)} style={{ width: `${pct}%` }} /></div>
</li>
))}
</ul>
</Region>
<Region id="close" active={active} className="@lg:col-span-2">
<Pin n={4} id="close" active={active} onSelect={setActive} />
<div className="flex items-center justify-between text-xs">
<p className="font-semibold">October close</p>
<p className="text-muted-foreground">Day 2 of 3</p>
</div>
<div className="bg-secondary mt-2.5 h-2 rounded-full"><div className="bg-chart-1 h-full w-[88%] rounded-full" /></div>
<p className="text-muted-foreground mt-2 text-xs">412 of 468 receipts matched · 56 waiting on employees</p>
</Region>
</div>
</div>
</div>
</div>
)
}
export { NorthwindTour }// Ballmac UI: Northwind home page. https://ui.ballmac.com/templates/template-northwind
"use client"
import * as React from "react"
import { ArrowRight, Check, Quote } from "lucide-react"
import { BlurFade } from "@/components/ballmac/blur-fade"
import { NumberTicker } from "@/components/ballmac/number-ticker"
import { NorthwindHeading, NorthwindShell, type NorthwindHrefs } from "@/components/ballmac/templates/northwind/northwind-theme"
import { NorthwindTour } from "@/components/ballmac/templates/northwind/northwind-tour"
import { cn } from "@/lib/utils"
const serif = "[font-family:var(--northwind-serif),ui-serif,Georgia,serif]"
const logos = ["Harlow & Pine", "Fernhill", "Oakline", "Brightwell", "Tidewater", "Marlow Co"]
const before = ["Receipts in a shared inbox", "Spreadsheets for every team’s budget", "Cards approved by whoever answers Slack", "Nine days to close the month"]
const after = ["Receipts captured and matched at purchase", "Live budgets every team can see", "Policy routes each request to the right person", "Three days to close the month"]
function Mini({ children, label }: { children: React.ReactNode; label: string }) {
return (
<div role="img" aria-label={label} className="bg-surface rounded-2xl border p-5 sm:p-7">
<div aria-hidden="true">{children}</div>
</div>
)
}
type NorthwindHomeProps = React.ComponentProps<"div"> & { hrefs?: Partial<NorthwindHrefs> }
/** The Northwind home page: an editorial hero, an interactive product tour, proof, three product stories and a quote. */
function NorthwindHome({ hrefs, ...props }: NorthwindHomeProps) {
const contact = hrefs?.contact ?? "/northwind/contact"
return (
<NorthwindShell page="home" hrefs={hrefs} {...props}>
<main>
<section className="mx-auto max-w-6xl px-4 pt-16 pb-12 sm:px-6 sm:pt-24">
<BlurFade>
<p className="text-chart-2 text-sm font-semibold tracking-wide uppercase">Spend management for finance teams</p>
<NorthwindHeading as="h1" className="mt-5 max-w-4xl text-5xl leading-[1.02] sm:text-7xl lg:text-[5.5rem]">
Spend with <em>clarity</em>, close with calm.
</NorthwindHeading>
<div className="mt-8 flex flex-col gap-8 lg:flex-row lg:items-end lg:justify-between">
<p className="text-muted-foreground max-w-xl text-lg text-pretty sm:text-xl">
Northwind gives every team the cards, approvals and budgets they need, and gives finance a ledger that is already right on the first of the month.
</p>
<div className="flex flex-col gap-3 sm:flex-row">
<a href={contact} className="bg-primary text-primary-foreground focus-visible:ring-ring/50 inline-flex h-13 items-center justify-center gap-2 rounded-full px-7 text-base font-medium outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]">
Book a demo <ArrowRight className="size-4 rtl:rotate-180" aria-hidden="true" />
</a>
<a href="#tour" className="hover:bg-accent focus-visible:ring-ring/50 inline-flex h-13 items-center justify-center rounded-full border px-7 text-base font-medium outline-none transition-colors focus-visible:ring-[3px]">Take the tour</a>
</div>
</div>
</BlurFade>
</section>
<section id="tour" aria-label="Product tour" className="mx-auto max-w-6xl scroll-mt-24 px-4 pb-20 sm:px-6">
<NorthwindTour />
</section>
<section aria-label="Customers" className="border-y">
<div className="mx-auto max-w-6xl px-4 py-10 sm:px-6">
<p className="text-muted-foreground text-center text-sm">Trusted by 1,400 finance teams</p>
<ul className="mt-6 grid grid-cols-2 gap-x-6 gap-y-5 text-center sm:grid-cols-3 lg:grid-cols-6">
{logos.map((l) => <li key={l} className={cn("text-muted-foreground text-xl font-medium tracking-tight", serif)}>{l}</li>)}
</ul>
</div>
</section>
<section aria-label="Results" className="mx-auto max-w-6xl px-4 py-20 sm:px-6">
<dl className="grid gap-10 sm:grid-cols-3">
{[
{ v: 62, s: "%", l: "Faster month-end close", d: 0 },
{ v: 4.2, s: "B", p: "$", l: "Spend managed each year", d: 1 },
{ v: 97, s: "%", l: "Receipts matched automatically", d: 0 },
].map((x) => (
<div key={x.l} className="border-t-2 border-current pt-5">
<dd className={cn("text-6xl font-medium tracking-[-0.04em] tabular-nums sm:text-7xl", serif)}>{x.p}<NumberTicker value={x.v} format={{ minimumFractionDigits: x.d, maximumFractionDigits: x.d }} />{x.s}</dd>
<dt className="text-muted-foreground mt-3">{x.l}</dt>
</div>
))}
</dl>
</section>
{/* Three stories */}
<section className="mx-auto max-w-6xl space-y-24 px-4 pb-24 sm:px-6">
<div className="grid items-center gap-10 lg:grid-cols-2 lg:gap-16">
<div>
<p className="text-chart-2 text-sm font-semibold tracking-wide uppercase">Cards</p>
<NorthwindHeading className="mt-3 text-4xl sm:text-5xl">Give every team a card that <em>knows</em> the rules.</NorthwindHeading>
<p className="text-muted-foreground mt-5 text-lg text-pretty">Limits, merchants and approval policies travel with the card, so the right purchase just works and the wrong one never leaves the building.</p>
<ul className="mt-6 space-y-3">{["Virtual cards for every subscription", "Merchant and category locks", "Instant freeze, from any device"].map((x) => <li key={x} className="flex items-start gap-3"><Check className="text-chart-1 mt-1 size-4 shrink-0" aria-hidden="true" />{x}</li>)}</ul>
</div>
<Mini label="Three virtual cards with limits">
<div className="space-y-3">
{[["Design tools", "$2,400", "bg-chart-1"], ["Cloud hosting", "$14,500", "bg-chart-2"], ["Team offsite", "$8,000", "bg-chart-5"]].map(([n, a, t]) => (
<div key={n} className="bg-card flex items-center gap-4 rounded-xl border p-4">
<span className={cn("h-10 w-14 rounded-md", t)} />
<div className="flex-1"><p className="font-medium">{n}</p><p className="text-muted-foreground text-sm">Monthly limit</p></div>
<p className="font-medium tabular-nums">{a}</p>
</div>
))}
</div>
</Mini>
</div>
<div className="grid items-center gap-10 lg:grid-cols-2 lg:gap-16">
<Mini label="Before and after comparison">
<div className="grid gap-4 sm:grid-cols-2">
<div><p className="text-muted-foreground text-sm font-semibold">Before</p><ul className="mt-3 space-y-3 text-sm">{before.map((x) => <li key={x} className="text-muted-foreground flex gap-2"><span aria-hidden="true">×</span>{x}</li>)}</ul></div>
<div><p className="text-chart-1 text-sm font-semibold">With Northwind</p><ul className="mt-3 space-y-3 text-sm">{after.map((x) => <li key={x} className="flex gap-2"><Check className="text-chart-1 mt-0.5 size-4 shrink-0" />{x}</li>)}</ul></div>
</div>
</Mini>
<div>
<p className="text-chart-2 text-sm font-semibold tracking-wide uppercase">Close</p>
<NorthwindHeading className="mt-3 text-4xl sm:text-5xl">The month closes itself, <em>nearly</em>.</NorthwindHeading>
<p className="text-muted-foreground mt-5 text-lg text-pretty">Every transaction is matched to a receipt, coded to your chart of accounts and synced to your ledger as it happens. What is left on close day is review, not data entry.</p>
</div>
</div>
<div className="grid items-center gap-10 lg:grid-cols-2 lg:gap-16">
<div>
<p className="text-chart-2 text-sm font-semibold tracking-wide uppercase">Reporting</p>
<NorthwindHeading className="mt-3 text-4xl sm:text-5xl">Answers for the board, <em>before</em> they ask.</NorthwindHeading>
<p className="text-muted-foreground mt-5 text-lg text-pretty">Spend by team, vendor and project, always current. Build a view once and share it with a link.</p>
</div>
<Mini label="Bar chart of spend by team over six months">
<div className="flex h-44 items-end gap-3">
{[48, 56, 52, 70, 64, 82].map((h, i) => <div key={i} className="flex flex-1 flex-col justify-end gap-1"><div className="bg-chart-1 rounded-t-md" style={{ height: `${h * 0.55}%` }} /><div className="bg-chart-2 rounded-t-md" style={{ height: `${h * 0.3}%` }} /></div>)}
</div>
<div className="text-muted-foreground mt-3 flex justify-between text-xs"><span>May</span><span>Oct</span></div>
</Mini>
</div>
</section>
<section className="bg-primary text-primary-foreground">
<figure className="mx-auto max-w-4xl px-4 py-24 text-center sm:px-6">
<Quote className="mx-auto size-8 opacity-50 rtl:-scale-x-100" aria-hidden="true" />
<blockquote className={cn("mt-6 text-3xl leading-snug tracking-[-0.02em] text-balance sm:text-5xl", serif)}>
“We went from nine days to three to close the books, and nobody on my team has asked me for a receipt since March.”
</blockquote>
<figcaption className="mt-8 text-sm opacity-80"><span className="font-semibold">Elena Marchetti</span> · Controller, Harlow & Pine</figcaption>
</figure>
</section>
<section className="mx-auto max-w-6xl px-4 py-24 text-center sm:px-6">
<NorthwindHeading className="mx-auto max-w-3xl text-4xl sm:text-6xl">See it with <em>your</em> numbers.</NorthwindHeading>
<p className="text-muted-foreground mx-auto mt-5 max-w-lg text-lg text-pretty">A 30-minute walkthrough, built around how your team spends today.</p>
<a href={contact} className="bg-primary text-primary-foreground focus-visible:ring-ring/50 mt-8 inline-flex h-13 items-center gap-2 rounded-full px-7 text-base font-medium outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]">Book a demo <ArrowRight className="size-4 rtl:rotate-180" aria-hidden="true" /></a>
</section>
</main>
</NorthwindShell>
)
}
export { NorthwindHome, type NorthwindHomeProps }// Ballmac UI: Northwind pricing page. https://ui.ballmac.com/templates/template-northwind
"use client"
import * as React from "react"
import { Check } from "lucide-react"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ballmac/accordion"
import { SegmentedControl, SegmentedControlItem } from "@/components/ballmac/segmented-control"
import { NorthwindHeading, NorthwindShell, type NorthwindHrefs } from "@/components/ballmac/templates/northwind/northwind-theme"
import { cn } from "@/lib/utils"
type Billing = "annual" | "monthly"
const plans = [
{ id: "starter", name: "Starter", blurb: "For teams of up to 25 that are moving off spreadsheets.", price: 8, unit: "per person / month", features: ["Virtual and physical cards", "Receipt capture and matching", "Basic approval rules", "Sync to QuickBooks or Xero"] },
{ id: "growth", name: "Growth", blurb: "For finance teams that want budgets and policy.", price: 14, unit: "per person / month", featured: true, features: ["Everything in Starter", "Team budgets with alerts", "Multi-step approval policies", "Bill pay and reimbursements", "NetSuite and Sage Intacct sync"] },
{ id: "enterprise", name: "Enterprise", blurb: "For multiple entities, countries and auditors.", price: null, unit: "annual contract", features: ["Everything in Growth", "Multi-entity and multi-currency", "SSO, SCIM and audit exports", "Dedicated implementation lead", "99.95% uptime SLA"] },
] as const
const faqs = [
{ q: "Do I pay for every employee?", a: "You pay for people who hold a card or approve spend. Employees who only submit reimbursements are free, so rolling out to the whole company doesn’t change your bill." },
{ q: "Is there a card fee?", a: "No. Cards are free, and we earn a small share of interchange from the card network. That is why we can keep seat prices low." },
{ q: "How long does setup take?", a: "Most teams are issuing cards in a day. Accounting sync and policy setup usually take a week, with a guide at your side." },
{ q: "What if we outgrow a plan?", a: "Change plans any time. We prorate to the day, and your cards, receipts and history carry over untouched." },
]
type NorthwindPricingProps = React.ComponentProps<"div"> & { hrefs?: Partial<NorthwindHrefs> }
/** Northwind pricing: three plans with an annual saving, an included-everywhere list and questions. */
function NorthwindPricing({ hrefs, ...props }: NorthwindPricingProps) {
const [billing, setBilling] = React.useState<Billing>("annual")
const contact = hrefs?.contact ?? "/northwind/contact"
return (
<NorthwindShell page="pricing" hrefs={hrefs} {...props}>
<main>
<section className="mx-auto max-w-6xl px-4 pt-16 sm:px-6 sm:pt-24">
<div className="mx-auto max-w-3xl text-center">
<NorthwindHeading as="h1" className="text-5xl leading-[1.04] sm:text-7xl">Pricing that <em>scales</em> with your team, not your spend.</NorthwindHeading>
<p className="text-muted-foreground mt-6 text-lg text-pretty">No percentage of volume, no card fees, no surprise add-ons.</p>
<SegmentedControl aria-label="Billing period" value={billing} onValueChange={(v) => setBilling(v as Billing)} className="mt-9">
<SegmentedControlItem value="annual">Annual · save 20%</SegmentedControlItem>
<SegmentedControlItem value="monthly">Monthly</SegmentedControlItem>
</SegmentedControl>
</div>
<div className="mt-14 grid gap-5 lg:grid-cols-3">
{plans.map((p) => {
const price = p.price === null ? null : billing === "annual" ? Math.round(p.price * 0.8) : p.price
const featured = "featured" in p && p.featured
return (
<article key={p.id} className={cn("flex flex-col rounded-3xl border p-8", featured ? "bg-primary text-primary-foreground border-transparent" : "bg-card")}>
<h2 className="text-[26px] font-medium tracking-tight [font-family:var(--northwind-serif),ui-serif,Georgia,serif]">{p.name}</h2>
<p className={cn("mt-2 min-h-12 text-[15px] text-pretty", featured ? "opacity-80" : "text-muted-foreground")}>{p.blurb}</p>
<p className="mt-6 flex items-baseline gap-2">
{price === null ? <span className="text-5xl font-medium tracking-tight [font-family:var(--northwind-serif),ui-serif,Georgia,serif]">Let’s talk</span> : (
<>
<span className="text-6xl font-medium tracking-[-0.04em] tabular-nums [font-family:var(--northwind-serif),ui-serif,Georgia,serif]">${price}</span>
<span className={cn("text-sm", featured ? "opacity-70" : "text-muted-foreground")}>{p.unit}</span>
</>
)}
</p>
<a href={contact} className={cn("focus-visible:ring-ring/50 mt-7 inline-flex h-12 items-center justify-center rounded-full text-[15px] font-medium outline-none transition-opacity focus-visible:ring-[3px]", featured ? "bg-primary-foreground text-primary hover:opacity-90" : "bg-primary text-primary-foreground hover:opacity-90")}>
{price === null ? "Contact sales" : "Book a demo"}
</a>
<ul className="mt-8 space-y-3.5 border-t border-current/15 pt-7 text-[15px]">
{p.features.map((f) => <li key={f} className="flex items-start gap-3"><Check className={cn("mt-1 size-4 shrink-0", featured ? "" : "text-chart-1")} aria-hidden="true" />{f}</li>)}
</ul>
</article>
)
})}
</div>
</section>
<section aria-labelledby="nw-included" className="mx-auto max-w-6xl px-4 py-24 sm:px-6">
<NorthwindHeading id="nw-included" className="max-w-xl text-3xl sm:text-5xl">Included with every plan.</NorthwindHeading>
<ul className="mt-10 grid gap-x-10 gap-y-6 sm:grid-cols-2 lg:grid-cols-3">
{["Unlimited virtual cards", "Real-time spend alerts", "Mobile app for iOS and Android", "Receipt capture by email and text", "CSV and API exports", "Bank-level encryption and 2FA", "US-based support, weekdays", "Free onboarding session"].map((x) => (
<li key={x} className="flex items-start gap-3 border-t pt-4"><Check className="text-chart-1 mt-1 size-4 shrink-0" aria-hidden="true" />{x}</li>
))}
</ul>
</section>
<section className="bg-surface border-y" aria-labelledby="nw-faq">
<div className="mx-auto grid max-w-6xl gap-10 px-4 py-24 sm:px-6 lg:grid-cols-[1fr_1.4fr]">
<NorthwindHeading id="nw-faq" className="text-3xl sm:text-5xl">Questions we hear <em>often</em>.</NorthwindHeading>
<Accordion type="single" collapsible>
{faqs.map((f) => (
<AccordionItem key={f.q} value={f.q}>
<AccordionTrigger className="text-lg">{f.q}</AccordionTrigger>
<AccordionContent className="text-muted-foreground text-[15px] text-pretty">{f.a}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</section>
</main>
</NorthwindShell>
)
}
export { NorthwindPricing, type NorthwindPricingProps }// Ballmac UI: Northwind customers page. https://ui.ballmac.com/templates/template-northwind
import * as React from "react"
import { ArrowUpRight } from "lucide-react"
import { NorthwindHeading, NorthwindShell, type NorthwindHrefs } from "@/components/ballmac/templates/northwind/northwind-theme"
import { cn } from "@/lib/utils"
const serif = "[font-family:var(--northwind-serif),ui-serif,Georgia,serif]"
const stories = [
{ name: "Fernhill", sector: "Architecture", stat: "70%", note: "less time chasing receipts", quote: "Our partners finally see project costs while the project is still running.", tone: "bg-chart-1/15" },
{ name: "Oakline", sector: "Logistics", stat: "$310k", note: "saved on duplicate vendors", quote: "We found three vendors we paid under different names within a week.", tone: "bg-chart-2/15" },
{ name: "Tidewater", sector: "Hospitality", stat: "11 days → 4", note: "month-end close", quote: "Eighteen restaurants, one close process. It used to be eighteen.", tone: "bg-chart-5/15" },
{ name: "Marlow Co", sector: "Consumer goods", stat: "4.9 / 5", note: "employee card satisfaction", quote: "Nobody has asked to expense a personal card in a year.", tone: "bg-chart-4/15" },
{ name: "Brightwell", sector: "Healthcare", stat: "100%", note: "audit-ready receipts", quote: "Our auditors asked what we changed. We told them: everything.", tone: "bg-chart-3/20" },
{ name: "Quill & Co", sector: "Publishing", stat: "2 hrs", note: "to onboard 60 people", quote: "Cards for the whole company were live before lunch.", tone: "bg-chart-1/15" },
]
type NorthwindCustomersProps = React.ComponentProps<"div"> & { hrefs?: Partial<NorthwindHrefs> }
/** Northwind customers: a featured case study with results, then a grid of shorter stories. */
function NorthwindCustomers({ hrefs, ...props }: NorthwindCustomersProps) {
return (
<NorthwindShell page="customers" hrefs={hrefs} {...props}>
<main>
<section className="mx-auto max-w-6xl px-4 pt-16 sm:px-6 sm:pt-24">
<p className="text-chart-2 text-sm font-semibold tracking-wide uppercase">Customers</p>
<NorthwindHeading as="h1" className="mt-4 max-w-4xl text-5xl leading-[1.04] sm:text-7xl">Finance teams that got their <em>weekends</em> back.</NorthwindHeading>
</section>
<section aria-labelledby="nw-featured" className="mx-auto max-w-6xl px-4 py-14 sm:px-6">
<article className="bg-primary text-primary-foreground grid overflow-hidden rounded-3xl lg:grid-cols-[1.1fr_1fr]">
<div className="p-8 sm:p-12">
<p className="text-sm font-semibold opacity-70">Case study · Professional services</p>
<h2 id="nw-featured" className={cn("mt-4 text-4xl leading-tight tracking-[-0.02em] text-balance sm:text-5xl", serif)}>Harlow & Pine closes the books in three days, not nine.</h2>
<p className="mt-5 max-w-lg text-lg opacity-85 text-pretty">A 340-person consultancy replaced spreadsheets, a shared inbox and four card programs with one system finance trusts.</p>
<a href="#" className="bg-primary-foreground text-primary focus-visible:ring-ring mt-8 inline-flex h-12 items-center gap-2 rounded-full px-6 text-sm font-medium outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]">Read the story <ArrowUpRight className="size-4 rtl:-scale-x-100" aria-hidden="true" /></a>
</div>
<dl className="grid grid-cols-2 border-t border-current/20 lg:border-t-0 lg:border-s">
{[["9 → 3", "days to close"], ["97%", "receipts matched"], ["$1.2M", "cash found in dormant subscriptions"], ["340", "employees, live in 2 weeks"]].map(([v, l], i) => (
<div key={l} className={cn("flex flex-col justify-end p-6 sm:p-8", i % 2 === 0 && "border-e border-current/20", i < 2 && "border-b border-current/20")}>
<dd className={cn("text-4xl tracking-[-0.03em] sm:text-5xl", serif)}>{v}</dd>
<dt className="mt-2 text-sm opacity-75">{l}</dt>
</div>
))}
</dl>
</article>
</section>
<section aria-labelledby="nw-more" className="mx-auto max-w-6xl px-4 pb-24 sm:px-6">
<NorthwindHeading id="nw-more" className="text-3xl sm:text-4xl">More stories</NorthwindHeading>
<ul className="mt-8 grid gap-5 md:grid-cols-2 lg:grid-cols-3">
{stories.map((s) => (
<li key={s.name} className="bg-card flex flex-col rounded-2xl border p-7">
<div className="flex items-center gap-3">
<span className={cn("flex size-10 items-center justify-center rounded-full text-sm font-semibold", s.tone)} aria-hidden="true">{s.name[0]}</span>
<div><h3 className="font-semibold">{s.name}</h3><p className="text-muted-foreground text-xs">{s.sector}</p></div>
</div>
<p className={cn("mt-6 text-5xl tracking-[-0.03em]", serif)}>{s.stat}</p>
<p className="text-muted-foreground mt-1 text-sm">{s.note}</p>
<blockquote className="mt-6 flex-1 border-t pt-5 text-[15px] text-pretty">“{s.quote}”</blockquote>
</li>
))}
</ul>
</section>
</main>
</NorthwindShell>
)
}
export { NorthwindCustomers, type NorthwindCustomersProps }// Ballmac UI: Northwind about page. https://ui.ballmac.com/templates/template-northwind
import * as React from "react"
import { NorthwindHeading, NorthwindShell, type NorthwindHrefs } from "@/components/ballmac/templates/northwind/northwind-theme"
import { cn } from "@/lib/utils"
const serif = "[font-family:var(--northwind-serif),ui-serif,Georgia,serif]"
const values = [
{ title: "Plain beats clever", text: "If a customer needs a manual to use a feature, we haven’t finished it." },
{ title: "Money is personal", text: "Behind every transaction is someone trying to do their job. We design for them first." },
{ title: "Boring is a feature", text: "Finance software should be dependable enough to forget about. We aim to be forgotten." },
]
const timeline = [
["2019", "Two former controllers start Northwind in a spare bedroom in Portland."],
["2021", "First 100 customers, and our first real month-end close with no spreadsheets."],
["2023", "Launch bill pay and reimbursements. Reach $1B in annual spend managed."],
["2025", "Open offices in Toronto and London. 1,000 customers across 14 countries."],
["2026", "Today: 1,400 finance teams, 140 people, still answering email ourselves."],
]
const team = [
["Ines Alvarado", "Co-founder, CEO", "bg-chart-1/25"], ["Tobias Wren", "Co-founder, CTO", "bg-chart-2/25"], ["Amara Nwosu", "VP Finance", "bg-chart-5/25"],
["Jonas Eklund", "Head of Design", "bg-chart-4/25"], ["Mei Tanaka", "Head of Security", "bg-chart-3/30"], ["Colm Brennan", "Head of Support", "bg-chart-1/25"],
] as const
type NorthwindAboutProps = React.ComponentProps<"div"> & { hrefs?: Partial<NorthwindHrefs> }
/** Northwind about: a long-form mission, values, a timeline and the team. */
function NorthwindAbout({ hrefs, ...props }: NorthwindAboutProps) {
return (
<NorthwindShell page="about" hrefs={hrefs} {...props}>
<main>
<section className="mx-auto max-w-4xl px-4 pt-16 sm:px-6 sm:pt-24">
<p className="text-chart-2 text-sm font-semibold tracking-wide uppercase">About</p>
<NorthwindHeading as="h1" className="mt-4 text-5xl leading-[1.04] sm:text-7xl">We think finance should feel <em>quiet</em>.</NorthwindHeading>
<div className="text-muted-foreground mt-10 space-y-6 text-xl leading-relaxed text-pretty">
<p>Northwind began with a frustration. We had both run finance teams, and we had both spent the last week of every month chasing people for receipts that were, somewhere, already in an inbox.</p>
<p>The tools we used treated spending as a problem to police. We wanted the opposite: software that makes it easy for people to do the right thing, and shows finance what happened without asking.</p>
<p className={cn("text-foreground text-3xl leading-snug", serif)}>The best month-end is the one you barely notice.</p>
</div>
</section>
<section aria-labelledby="nw-values" className="mx-auto max-w-6xl px-4 py-24 sm:px-6">
<NorthwindHeading id="nw-values" className="text-3xl sm:text-5xl">What we believe</NorthwindHeading>
<ol className="mt-10 grid gap-8 md:grid-cols-3">
{values.map((v, i) => (
<li key={v.title} className="border-t-2 border-current pt-5">
<span className={cn("text-chart-2 text-5xl", serif)}>{i + 1}</span>
<h3 className="mt-4 text-xl font-semibold">{v.title}</h3>
<p className="text-muted-foreground mt-2 text-pretty">{v.text}</p>
</li>
))}
</ol>
</section>
<section aria-labelledby="nw-history" className="bg-surface border-y">
<div className="mx-auto max-w-4xl px-4 py-24 sm:px-6">
<NorthwindHeading id="nw-history" className="text-3xl sm:text-5xl">Seven years, briefly</NorthwindHeading>
<ol className="mt-10 divide-y">
{timeline.map(([y, t]) => (
<li key={y} className="grid gap-2 py-6 sm:grid-cols-[7rem_1fr] sm:gap-8"><span className={cn("text-chart-2 text-3xl", serif)}>{y}</span><p className="text-lg text-pretty">{t}</p></li>
))}
</ol>
</div>
</section>
<section aria-labelledby="nw-team" className="mx-auto max-w-6xl px-4 py-24 sm:px-6">
<NorthwindHeading id="nw-team" className="text-3xl sm:text-5xl">The people</NorthwindHeading>
<ul className="mt-10 grid grid-cols-2 gap-6 md:grid-cols-3">
{team.map(([n, r, t]) => (
<li key={n}>
<div className={cn("flex aspect-[4/3] items-end rounded-2xl p-5", t)} aria-hidden="true"><span className={cn("text-5xl tracking-tight", serif)}>{n.split(" ").map((w) => w[0]).join("")}</span></div>
<h3 className="mt-3 font-semibold">{n}</h3>
<p className="text-muted-foreground text-sm">{r}</p>
</li>
))}
</ul>
<div className="bg-primary text-primary-foreground mt-16 flex flex-col items-start justify-between gap-6 rounded-3xl p-8 sm:flex-row sm:items-center sm:p-12">
<div><p className={cn("text-3xl sm:text-4xl", serif)}>We’re hiring in Portland, Toronto and London.</p><p className="mt-2 opacity-80">14 open roles across engineering, design and support.</p></div>
<a href="#" className="bg-primary-foreground text-primary focus-visible:ring-ring inline-flex h-12 shrink-0 items-center rounded-full px-6 text-sm font-medium outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]">See open roles</a>
</div>
</section>
</main>
</NorthwindShell>
)
}
export { NorthwindAbout, type NorthwindAboutProps }// Ballmac UI: Northwind contact page. https://ui.ballmac.com/templates/template-northwind
"use client"
import * as React from "react"
import { Check, Mail, MapPin, MessageCircle } from "lucide-react"
import { NorthwindHeading, NorthwindShell, type NorthwindHrefs } from "@/components/ballmac/templates/northwind/northwind-theme"
import { cn } from "@/lib/utils"
const serif = "[font-family:var(--northwind-serif),ui-serif,Georgia,serif]"
const field = "bg-card focus-visible:ring-ring/50 mt-2 h-12 w-full rounded-xl border px-4 text-base outline-none focus-visible:ring-[3px] aria-[invalid=true]:border-destructive"
type Errors = Partial<Record<"name" | "email" | "size", string>>
type NorthwindContactProps = React.ComponentProps<"div"> & { hrefs?: Partial<NorthwindHrefs> }
/** Northwind contact: a demo request form with inline validation, and the other ways to reach the team. */
function NorthwindContact({ hrefs, ...props }: NorthwindContactProps) {
const [errors, setErrors] = React.useState<Errors>({})
const [sent, setSent] = React.useState<string | null>(null)
const [pending, setPending] = React.useState(false)
const timer = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
React.useEffect(() => () => clearTimeout(timer.current), [])
function submit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const form = e.currentTarget
const f = new FormData(form)
const name = String(f.get("name") ?? "").trim()
const email = String(f.get("email") ?? "").trim()
const size = String(f.get("size") ?? "")
const next: Errors = {}
if (!name) next.name = "Tell us your name."
if (!/^\S+@\S+\.\S+$/.test(email)) next.email = "Enter a work email like you@company.com."
if (!size) next.size = "Choose your team size."
setErrors(next)
if (Object.keys(next).length) {
const first = (["name", "email", "size"] as const).find((k) => next[k])
;(form.elements.namedItem(first ?? "name") as HTMLElement | null)?.focus()
return
}
setPending(true)
timer.current = setTimeout(() => {
setPending(false)
setSent(name.split(" ")[0])
}, 700)
}
return (
<NorthwindShell page="contact" hrefs={hrefs} {...props}>
<main className="mx-auto grid max-w-6xl gap-14 px-4 py-16 sm:px-6 sm:py-24 lg:grid-cols-[1.1fr_1fr] lg:gap-20">
<div>
<NorthwindHeading as="h1" className="text-5xl leading-[1.04] sm:text-7xl">Let’s talk about <em>your</em> spend.</NorthwindHeading>
<p className="text-muted-foreground mt-6 max-w-lg text-lg text-pretty">Tell us a little about your team. We’ll book a 30-minute walkthrough built around how you spend today. No slides.</p>
{sent ? (
<div role="status" className="bg-card mt-10 rounded-3xl border p-8">
<span className="bg-chart-1/15 text-chart-1 flex size-12 items-center justify-center rounded-full"><Check className="size-6" aria-hidden="true" /></span>
<p className={cn("mt-5 text-3xl", serif)}>Thanks, {sent}.</p>
<p className="text-muted-foreground mt-2 text-pretty">We’ll email you within one business day with a few times that work.</p>
</div>
) : (
<form onSubmit={submit} noValidate className="mt-10 space-y-5">
<div>
<label htmlFor="nw-name" className="text-sm font-medium">Full name</label>
<input id="nw-name" name="name" autoComplete="name" aria-invalid={!!errors.name} aria-describedby={errors.name ? "nw-name-err" : undefined} className={field} />
{errors.name && <p id="nw-name-err" className="text-destructive mt-1.5 text-sm">{errors.name}</p>}
</div>
<div>
<label htmlFor="nw-email" className="text-sm font-medium">Work email</label>
<input id="nw-email" name="email" type="email" autoComplete="email" aria-invalid={!!errors.email} aria-describedby={errors.email ? "nw-email-err" : undefined} className={field} />
{errors.email && <p id="nw-email-err" className="text-destructive mt-1.5 text-sm">{errors.email}</p>}
</div>
<div>
<label htmlFor="nw-size" className="text-sm font-medium">Team size</label>
<select id="nw-size" name="size" defaultValue="" aria-invalid={!!errors.size} aria-describedby={errors.size ? "nw-size-err" : undefined} className={field}>
<option value="" disabled>Choose one</option>
<option>1–25 people</option><option>26–100 people</option><option>101–500 people</option><option>500+ people</option>
</select>
{errors.size && <p id="nw-size-err" className="text-destructive mt-1.5 text-sm">{errors.size}</p>}
</div>
<div>
<label htmlFor="nw-note" className="text-sm font-medium">What would you like to fix? <span className="text-muted-foreground font-normal">(optional)</span></label>
<textarea id="nw-note" name="note" rows={4} className={cn(field, "h-auto py-3")} />
</div>
<button type="submit" disabled={pending} className="bg-primary text-primary-foreground focus-visible:ring-ring/50 inline-flex h-13 items-center rounded-full px-8 text-base font-medium outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px] disabled:opacity-60">
{pending ? "Sending…" : "Request a demo"}
</button>
</form>
)}
</div>
<aside className="space-y-4 lg:pt-4" aria-label="Other ways to reach us">
{[
{ icon: Mail, title: "Sales", text: "sales@northwind.example", note: "Replies within one business day." },
{ icon: MessageCircle, title: "Support", text: "help@northwind.example", note: "Existing customers: weekdays, 6am to 6pm Pacific." },
{ icon: MapPin, title: "Portland", text: "410 SW Alder St, Suite 300", note: "Toronto and London offices on request." },
].map((c) => (
<div key={c.title} className="bg-card flex gap-4 rounded-2xl border p-6">
<span className="bg-chart-2/15 text-chart-2 flex size-11 shrink-0 items-center justify-center rounded-full"><c.icon className="size-5" aria-hidden="true" /></span>
<div><h2 className="font-semibold">{c.title}</h2><p className="mt-0.5">{c.text}</p><p className="text-muted-foreground mt-1 text-sm">{c.note}</p></div>
</div>
))}
</aside>
</main>
</NorthwindShell>
)
}
export { NorthwindContact, type NorthwindContactProps }// Ballmac UI: Northwind template route. https://ui.ballmac.com/templates/template-northwind
import type { Metadata } from "next"
import { NorthwindHome } from "@/components/ballmac/templates/northwind/northwind-home"
export const metadata: Metadata = {
title: "Northwind: Spend with clarity",
description: "Cards, approvals and budgets for finance teams.",
}
export default function Page() {
return <NorthwindHome />
}// Ballmac UI: Northwind template route. https://ui.ballmac.com/templates/template-northwind
import type { Metadata } from "next"
import { NorthwindPricing } from "@/components/ballmac/templates/northwind/northwind-pricing"
export const metadata: Metadata = {
title: "Northwind pricing",
description: "Pricing that scales with your team, not your spend.",
}
export default function Page() {
return <NorthwindPricing />
}// Ballmac UI: Northwind template route. https://ui.ballmac.com/templates/template-northwind
import type { Metadata } from "next"
import { NorthwindCustomers } from "@/components/ballmac/templates/northwind/northwind-customers"
export const metadata: Metadata = {
title: "Northwind customers",
description: "How finance teams closed faster with Northwind.",
}
export default function Page() {
return <NorthwindCustomers />
}// Ballmac UI: Northwind template route. https://ui.ballmac.com/templates/template-northwind
import type { Metadata } from "next"
import { NorthwindAbout } from "@/components/ballmac/templates/northwind/northwind-about"
export const metadata: Metadata = {
title: "About Northwind",
description: "Why we think finance should feel quiet.",
}
export default function Page() {
return <NorthwindAbout />
}// Ballmac UI: Northwind template route. https://ui.ballmac.com/templates/template-northwind
import type { Metadata } from "next"
import { NorthwindContact } from "@/components/ballmac/templates/northwind/northwind-contact"
export const metadata: Metadata = {
title: "Contact Northwind",
description: "Book a demo built around how your team spends.",
}
export default function Page() {
return <NorthwindContact />
}Installation
$ pnpm dlx shadcn@latest add @ballmac/template-northwindInstall the dependencies.
$ pnpm add lucide-react radix-uiAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/accordion @ballmac/blur-fade @ballmac/number-ticker @ballmac/segmented-controlCopy the source into your project.
components/ballmac/templates/northwind/northwind-fonts.ts// Ballmac UI: Northwind template fonts. https://ui.ballmac.com/templates/template-northwind import { Figtree, Fraunces } from "next/font/google" export const northwindSerif = Fraunces({ variable: "--northwind-serif", subsets: ["latin"], axes: ["opsz", "SOFT"], display: "swap", preload: false }) export const northwindSans = Figtree({ variable: "--northwind-sans", subsets: ["latin"], display: "swap", preload: false })components/ballmac/templates/northwind/northwind-theme.tsx// Ballmac UI: Northwind template shell. https://ui.ballmac.com/templates/template-northwind "use client" import * as React from "react" import { Menu, X } from "lucide-react" import { northwindSans, northwindSerif } from "@/components/ballmac/templates/northwind/northwind-fonts" import { cn } from "@/lib/utils" type NorthwindPage = "home" | "pricing" | "customers" | "about" | "contact" type NorthwindHrefs = Record<NorthwindPage, string> const defaultHrefs: NorthwindHrefs = { home: "/northwind", pricing: "/northwind/pricing", customers: "/northwind/customers", about: "/northwind/about", contact: "/northwind/contact" } /** Northwind's palette: cream paper, forest ink and one terracotta. Dark mode turns to deep green. */ const northwindCss = ` .northwind-theme{--background:oklch(0.975 0.014 88);--foreground:oklch(0.24 0.035 155);--card:oklch(0.99 0.01 88);--card-foreground:oklch(0.24 0.035 155);--popover:oklch(0.99 0.01 88);--popover-foreground:oklch(0.24 0.035 155);--primary:oklch(0.36 0.075 158);--primary-foreground:oklch(0.975 0.014 88);--secondary:oklch(0.94 0.02 88);--secondary-foreground:oklch(0.24 0.035 155);--muted:oklch(0.94 0.02 88);--muted-foreground:oklch(0.48 0.03 150);--accent:oklch(0.925 0.025 95);--accent-foreground:oklch(0.24 0.035 155);--border:oklch(0.24 0.035 155 / 13%);--input:oklch(0.24 0.035 155 / 18%);--ring:oklch(0.5 0.1 158);--surface:oklch(0.955 0.018 88);--destructive:oklch(0.55 0.2 28);--chart-1:oklch(0.45 0.09 158);--chart-2:oklch(0.5 0.15 45);--chart-3:oklch(0.76 0.13 85);--chart-4:oklch(0.5 0.12 330);--chart-5:oklch(0.52 0.08 240);--radius:0.5rem} .dark .northwind-theme{--background:oklch(0.19 0.025 158);--foreground:oklch(0.95 0.015 88);--card:oklch(0.23 0.03 158);--card-foreground:oklch(0.95 0.015 88);--popover:oklch(0.25 0.03 158);--popover-foreground:oklch(0.95 0.015 88);--primary:oklch(0.84 0.09 150);--primary-foreground:oklch(0.2 0.03 158);--secondary:oklch(0.27 0.032 158);--secondary-foreground:oklch(0.95 0.015 88);--muted:oklch(0.26 0.03 158);--muted-foreground:oklch(0.74 0.03 130);--accent:oklch(0.3 0.035 158);--accent-foreground:oklch(0.95 0.015 88);--border:oklch(0.95 0.015 88 / 14%);--input:oklch(0.95 0.015 88 / 18%);--ring:oklch(0.74 0.12 150);--surface:oklch(0.21 0.027 158);--destructive:oklch(0.7 0.18 28);--chart-1:oklch(0.78 0.11 150);--chart-2:oklch(0.76 0.13 50);--chart-3:oklch(0.84 0.12 90);--chart-4:oklch(0.74 0.12 335);--chart-5:oklch(0.76 0.09 240)} ` const links: { key: NorthwindPage; label: string }[] = [ { key: "home", label: "Product" }, { key: "pricing", label: "Pricing" }, { key: "customers", label: "Customers" }, { key: "about", label: "About" }, ] function NorthwindMark({ className }: { className?: string }) { return ( <svg viewBox="0 0 32 32" aria-hidden="true" className={cn("size-7", className)} fill="none"> <circle cx="16" cy="16" r="14" fill="var(--primary)" /> <path d="M16 6 20.5 16 16 26 11.5 16Z" fill="var(--primary-foreground)" /> <path d="M16 6v20" stroke="var(--primary)" strokeWidth="1.2" /> </svg> ) } /** A serif heading. Wrap one word in `<em>` for the italic accent. */ function NorthwindHeading({ as: Tag = "h2", className, ...props }: React.ComponentProps<"h2"> & { as?: "h1" | "h2" | "h3" }) { return <Tag className={cn("[font-family:var(--northwind-serif),ui-serif,Georgia,serif] font-medium tracking-[-0.025em] text-balance [&_em]:text-chart-2 [&_em]:font-normal", className)} {...props} /> } type NorthwindShellProps = React.ComponentProps<"div"> & { /** The page being shown, so its nav link is marked current. */ page: NorthwindPage /** Override where pages live (used by previews). */ hrefs?: Partial<NorthwindHrefs> } /** Northwind's frame: cream paper, a serif wordmark header and an editorial footer. Wrap every Northwind page in it. */ function NorthwindShell({ page, hrefs: overrides, className, style, children, ...props }: NorthwindShellProps) { const hrefs = { ...defaultHrefs, ...overrides } const [open, setOpen] = React.useState(false) return ( <div data-slot="northwind" className={cn("northwind-theme bg-background text-foreground relative min-h-dvh overflow-x-clip", northwindSerif.variable, northwindSans.variable, className)} style={{ fontFamily: "var(--northwind-sans), ui-sans-serif, system-ui, sans-serif", ...style }} {...props} > <style>{northwindCss}</style> <header className="bg-background/90 sticky top-0 z-40 border-b backdrop-blur-lg"> <div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-4 sm:px-6"> <a href={hrefs.home} className="focus-visible:ring-ring/50 flex items-center gap-2.5 rounded-md outline-none focus-visible:ring-[3px]"> <NorthwindMark /> <span className="text-xl font-medium tracking-tight [font-family:var(--northwind-serif),ui-serif,Georgia,serif]">Northwind</span> </a> <nav aria-label="Main" className="hidden items-center gap-8 md:flex"> {links.map((l) => ( <a key={l.key} href={hrefs[l.key]} aria-current={page === l.key ? "page" : undefined} className="text-muted-foreground hover:text-foreground aria-[current=page]:text-foreground aria-[current=page]:underline decoration-chart-2 focus-visible:ring-ring/50 rounded text-[15px] underline-offset-[10px] outline-none transition-colors focus-visible:ring-[3px] aria-[current=page]:decoration-2"> {l.label} </a> ))} </nav> <div className="flex items-center gap-2"> <a href={hrefs.contact} aria-current={page === "contact" ? "page" : undefined} className="text-muted-foreground hover:text-foreground hidden px-2 text-[15px] sm:block">Contact</a> <a href={hrefs.contact} className="bg-primary text-primary-foreground focus-visible:ring-ring/50 inline-flex h-10 items-center rounded-full px-5 text-sm font-medium outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]">Book a demo</a> <button type="button" aria-label={open ? "Close menu" : "Open menu"} aria-expanded={open} aria-controls="northwind-mobile-menu" onClick={() => setOpen((v) => !v)} className="text-muted-foreground focus-visible:ring-ring/50 inline-flex size-10 items-center justify-center rounded-md outline-none focus-visible:ring-[3px] md:hidden"> {open ? <X className="size-5" aria-hidden="true" /> : <Menu className="size-5" aria-hidden="true" />} </button> </div> </div> {open && ( <nav id="northwind-mobile-menu" aria-label="Mobile" className="border-t px-4 py-2 md:hidden"> {[...links, { key: "contact" as const, label: "Contact" }].map((l) => <a key={l.key} href={hrefs[l.key]} className="hover:bg-accent block rounded-md px-3 py-3 text-base">{l.label}</a>)} </nav> )} </header> {children} <footer className="bg-primary text-primary-foreground"> <div className="mx-auto max-w-6xl px-4 py-16 sm:px-6"> <div className="grid gap-12 lg:grid-cols-[1.5fr_repeat(3,1fr)]"> <div> <p className="text-3xl leading-tight font-medium tracking-tight [font-family:var(--northwind-serif),ui-serif,Georgia,serif]">Know where every dollar goes.</p> <a href={hrefs.contact} className="bg-primary-foreground text-primary focus-visible:ring-ring mt-6 inline-flex h-11 items-center rounded-full px-6 text-sm font-medium outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]">Book a demo</a> </div> {[ { title: "Product", items: [["Cards", "home"], ["Approvals", "home"], ["Reporting", "home"], ["Pricing", "pricing"]] }, { title: "Company", items: [["About", "about"], ["Customers", "customers"], ["Careers", "about"], ["Contact", "contact"]] }, { title: "Resources", items: [["Guides", "home"], ["Security", "home"], ["Status", "home"], ["Press", "about"]] }, ].map((c) => ( <div key={c.title}> <h2 className="text-sm font-semibold opacity-80">{c.title}</h2> <ul className="mt-4 space-y-3"> {c.items.map(([label, key]) => <li key={label}><a href={hrefs[key as NorthwindPage]} className="text-sm opacity-90 hover:underline">{label}</a></li>)} </ul> </div> ))} </div> <p className="mt-14 border-t border-current/20 pt-6 text-xs opacity-80">© 2026 Northwind Financial, Inc. Banking services provided by partner banks, Members FDIC.</p> </div> </footer> </div> ) } export { NorthwindHeading, NorthwindMark, NorthwindShell, defaultHrefs as northwindDefaultHrefs, type NorthwindHrefs, type NorthwindPage, type NorthwindShellProps }components/ballmac/templates/northwind/northwind-tour.tsx// Ballmac UI: Northwind product tour. https://ui.ballmac.com/templates/template-northwind "use client" import * as React from "react" import { BarChart3, Check, CreditCard, Home, ListChecks, Settings, Wallet } from "lucide-react" import { RadioGroup as RadioGroupPrimitive } from "radix-ui" import { NorthwindHeading } from "@/components/ballmac/templates/northwind/northwind-theme" import { cn } from "@/lib/utils" const steps = [ { id: "cards", title: "Issue a card in seconds", text: "Virtual or physical, with a limit, a merchant lock and an owner. Employees never wait on finance to buy what they need." }, { id: "approvals", title: "Approve from anywhere", text: "Requests reach the right person by policy, with the receipt and context attached. One tap from a phone is enough." }, { id: "budgets", title: "Budgets that warn early", text: "Every team sees what is left as they spend, and finance is told at 80 percent, not after the overrun." }, { id: "close", title: "Close the month in days", text: "Receipts are matched, coded and exported to your ledger as spend happens, so close day stops being a project." }, ] as const type StepId = (typeof steps)[number]["id"] const cardRows = [ ["Design tools", "•••• 4821", "$2,400", "Maya R."], ["Team offsite", "•••• 1190", "$8,000", "Dev P."], ["Cloud hosting", "•••• 7745", "$14,500", "Engineering"], ] const queue = [ ["Figma annual", "$1,440", "Maya R."], ["Flight to Lisbon", "$612", "Sam O."], ["Conference pass", "$890", "Dev P."], ] const budgets = [ ["Marketing", 82, "bg-chart-2"], ["Engineering", 64, "bg-chart-1"], ["Operations", 38, "bg-chart-5"], ] function Region({ id, active, children, className }: { id: StepId; active: StepId; children: React.ReactNode; className?: string }) { const on = id === active return ( <div data-region={id} data-active={on} className={cn("bg-card relative rounded-xl border p-3.5 transition-[box-shadow,opacity,border-color] duration-500 motion-reduce:transition-none", on ? "border-chart-2 shadow-[0_0_0_3px_color-mix(in_oklab,var(--chart-2)_22%,transparent)]" : "border-dashed", className)} > {children} </div> ) } /** A numbered pin on the mock. It repeats the step list for pointer users, so it is hidden from keyboard and screen readers. */ function Pin({ n, id, active, onSelect }: { n: number; id: StepId; active: StepId; onSelect: (id: StepId) => void }) { const on = id === active return ( <button type="button" tabIndex={-1} aria-hidden="true" data-pin={id} onClick={() => onSelect(id)} className={cn("absolute -top-3 -start-3 z-10 flex size-7 items-center justify-center rounded-full border-2 text-xs font-semibold shadow-sm outline-none transition-colors", on ? "bg-chart-2 border-card text-white" : "bg-card text-foreground hover:bg-accent")} > {n} </button> ) } /** An interactive product tour: numbered pins on a mock dashboard, tied to a list of steps. */ function NorthwindTour({ className }: { className?: string }) { const [active, setActive] = React.useState<StepId>("cards") const current = steps.find((s) => s.id === active)! return ( <div data-slot="northwind-tour" className={cn("grid items-center gap-10 lg:grid-cols-[0.8fr_1.3fr] lg:gap-14", className)}> <div> <RadioGroupPrimitive.Root value={active} onValueChange={(v) => setActive(v as StepId)} aria-label="Product tour steps" className="space-y-1.5"> {steps.map((s, i) => { const on = s.id === active return ( <RadioGroupPrimitive.Item key={s.id} value={s.id} className={cn("focus-visible:ring-ring/50 group block w-full rounded-xl border border-transparent p-4 text-start outline-none transition-colors focus-visible:ring-[3px]", on ? "bg-card border-border shadow-sm" : "hover:bg-accent/60")} > <span className="flex items-center gap-3"> <span className={cn("flex size-7 shrink-0 items-center justify-center rounded-full text-xs font-semibold transition-colors", on ? "bg-chart-2 text-white" : "bg-secondary")}>{i + 1}</span> <NorthwindHeading as="h3" className="text-xl">{s.title}</NorthwindHeading> </span> {on && <span className="text-muted-foreground mt-2 block ps-10 text-[15px] leading-relaxed text-pretty">{s.text}</span>} </RadioGroupPrimitive.Item> ) })} </RadioGroupPrimitive.Root> </div> <div className="bg-surface relative rounded-2xl border p-2.5 shadow-[0_30px_70px_-40px_oklch(0.3_0.05_155/0.5)] sm:p-3"> <p className="sr-only" aria-live="polite">Step {steps.indexOf(current) + 1} of {steps.length}: {current.title}. {current.text}</p> <div aria-hidden="true" className="@container grid min-w-0 grid-cols-[2.75rem_1fr] gap-2.5 sm:grid-cols-[9rem_1fr]"> <div className="bg-card hidden rounded-xl border p-2.5 sm:block"> <p className="flex items-center gap-2 text-sm font-semibold"><span className="bg-primary size-5 rounded-full" />Acme Co</p> <ul className="mt-4 space-y-1 text-[13px]"> {[[Home, "Home"], [CreditCard, "Cards"], [ListChecks, "Approvals"], [Wallet, "Budgets"], [BarChart3, "Reports"], [Settings, "Settings"]].map(([Icon, label], i) => { const I = Icon as typeof Home return <li key={label as string} className={cn("flex items-center gap-2 rounded-md px-2 py-1.5", i === 1 ? "bg-accent font-medium" : "text-muted-foreground")}><I className="size-3.5" />{label as string}</li> })} </ul> </div> <div className="bg-card flex flex-col items-center gap-3 rounded-xl border py-3 sm:hidden"> {[Home, CreditCard, ListChecks, Wallet].map((I, i) => <I key={i} className={cn("size-4", i === 1 ? "text-foreground" : "text-muted-foreground")} />)} </div> <div className="grid min-w-0 gap-2.5 @lg:grid-cols-2"> <Region id="cards" active={active} className="@lg:col-span-2"> <Pin n={1} id="cards" active={active} onSelect={setActive} /> <p className="text-xs font-semibold">Cards</p> <ul className="mt-2 divide-y text-xs"> {cardRows.map((r) => ( <li key={r[1]} className="grid grid-cols-[1fr_auto] items-center gap-2 py-1.5 @md:grid-cols-[1.2fr_1fr_auto_auto]"> <span className="font-medium">{r[0]}</span> <span className="text-muted-foreground hidden @md:block">{r[1]}</span> <span className="text-muted-foreground hidden @md:block">{r[3]}</span> <span className="tabular-nums">{r[2]}</span> </li> ))} </ul> </Region> <Region id="approvals" active={active}> <Pin n={2} id="approvals" active={active} onSelect={setActive} /> <p className="text-xs font-semibold">Needs approval <span className="bg-chart-2/20 ms-1 rounded-full px-1.5">3</span></p> <ul className="mt-2 space-y-1.5 text-xs"> {queue.map((q) => ( <li key={q[0]} className="bg-secondary/70 flex items-center justify-between gap-2 rounded-lg px-2.5 py-1.5"> <span className="min-w-0 truncate"><span className="font-medium">{q[0]}</span> <span className="text-muted-foreground">{q[2]}</span></span> <span className="flex shrink-0 items-center gap-1.5 tabular-nums">{q[1]}<Check className="text-chart-1 size-3.5" /></span> </li> ))} </ul> </Region> <Region id="budgets" active={active}> <Pin n={3} id="budgets" active={active} onSelect={setActive} /> <p className="text-xs font-semibold">Budgets this quarter</p> <ul className="mt-2 space-y-2.5 text-xs"> {budgets.map(([name, pct, tone]) => ( <li key={name as string}> <div className="flex justify-between"><span>{name}</span><span className={cn("tabular-nums", (pct as number) > 80 && "text-chart-2 font-semibold")}>{pct}%</span></div> <div className="bg-secondary mt-1 h-1.5 rounded-full"><div className={cn("h-full rounded-full", tone as string)} style={{ width: `${pct}%` }} /></div> </li> ))} </ul> </Region> <Region id="close" active={active} className="@lg:col-span-2"> <Pin n={4} id="close" active={active} onSelect={setActive} /> <div className="flex items-center justify-between text-xs"> <p className="font-semibold">October close</p> <p className="text-muted-foreground">Day 2 of 3</p> </div> <div className="bg-secondary mt-2.5 h-2 rounded-full"><div className="bg-chart-1 h-full w-[88%] rounded-full" /></div> <p className="text-muted-foreground mt-2 text-xs">412 of 468 receipts matched · 56 waiting on employees</p> </Region> </div> </div> </div> </div> ) } export { NorthwindTour }components/ballmac/templates/northwind/northwind-home.tsx// Ballmac UI: Northwind home page. https://ui.ballmac.com/templates/template-northwind "use client" import * as React from "react" import { ArrowRight, Check, Quote } from "lucide-react" import { BlurFade } from "@/components/ballmac/blur-fade" import { NumberTicker } from "@/components/ballmac/number-ticker" import { NorthwindHeading, NorthwindShell, type NorthwindHrefs } from "@/components/ballmac/templates/northwind/northwind-theme" import { NorthwindTour } from "@/components/ballmac/templates/northwind/northwind-tour" import { cn } from "@/lib/utils" const serif = "[font-family:var(--northwind-serif),ui-serif,Georgia,serif]" const logos = ["Harlow & Pine", "Fernhill", "Oakline", "Brightwell", "Tidewater", "Marlow Co"] const before = ["Receipts in a shared inbox", "Spreadsheets for every team’s budget", "Cards approved by whoever answers Slack", "Nine days to close the month"] const after = ["Receipts captured and matched at purchase", "Live budgets every team can see", "Policy routes each request to the right person", "Three days to close the month"] function Mini({ children, label }: { children: React.ReactNode; label: string }) { return ( <div role="img" aria-label={label} className="bg-surface rounded-2xl border p-5 sm:p-7"> <div aria-hidden="true">{children}</div> </div> ) } type NorthwindHomeProps = React.ComponentProps<"div"> & { hrefs?: Partial<NorthwindHrefs> } /** The Northwind home page: an editorial hero, an interactive product tour, proof, three product stories and a quote. */ function NorthwindHome({ hrefs, ...props }: NorthwindHomeProps) { const contact = hrefs?.contact ?? "/northwind/contact" return ( <NorthwindShell page="home" hrefs={hrefs} {...props}> <main> <section className="mx-auto max-w-6xl px-4 pt-16 pb-12 sm:px-6 sm:pt-24"> <BlurFade> <p className="text-chart-2 text-sm font-semibold tracking-wide uppercase">Spend management for finance teams</p> <NorthwindHeading as="h1" className="mt-5 max-w-4xl text-5xl leading-[1.02] sm:text-7xl lg:text-[5.5rem]"> Spend with <em>clarity</em>, close with calm. </NorthwindHeading> <div className="mt-8 flex flex-col gap-8 lg:flex-row lg:items-end lg:justify-between"> <p className="text-muted-foreground max-w-xl text-lg text-pretty sm:text-xl"> Northwind gives every team the cards, approvals and budgets they need, and gives finance a ledger that is already right on the first of the month. </p> <div className="flex flex-col gap-3 sm:flex-row"> <a href={contact} className="bg-primary text-primary-foreground focus-visible:ring-ring/50 inline-flex h-13 items-center justify-center gap-2 rounded-full px-7 text-base font-medium outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]"> Book a demo <ArrowRight className="size-4 rtl:rotate-180" aria-hidden="true" /> </a> <a href="#tour" className="hover:bg-accent focus-visible:ring-ring/50 inline-flex h-13 items-center justify-center rounded-full border px-7 text-base font-medium outline-none transition-colors focus-visible:ring-[3px]">Take the tour</a> </div> </div> </BlurFade> </section> <section id="tour" aria-label="Product tour" className="mx-auto max-w-6xl scroll-mt-24 px-4 pb-20 sm:px-6"> <NorthwindTour /> </section> <section aria-label="Customers" className="border-y"> <div className="mx-auto max-w-6xl px-4 py-10 sm:px-6"> <p className="text-muted-foreground text-center text-sm">Trusted by 1,400 finance teams</p> <ul className="mt-6 grid grid-cols-2 gap-x-6 gap-y-5 text-center sm:grid-cols-3 lg:grid-cols-6"> {logos.map((l) => <li key={l} className={cn("text-muted-foreground text-xl font-medium tracking-tight", serif)}>{l}</li>)} </ul> </div> </section> <section aria-label="Results" className="mx-auto max-w-6xl px-4 py-20 sm:px-6"> <dl className="grid gap-10 sm:grid-cols-3"> {[ { v: 62, s: "%", l: "Faster month-end close", d: 0 }, { v: 4.2, s: "B", p: "$", l: "Spend managed each year", d: 1 }, { v: 97, s: "%", l: "Receipts matched automatically", d: 0 }, ].map((x) => ( <div key={x.l} className="border-t-2 border-current pt-5"> <dd className={cn("text-6xl font-medium tracking-[-0.04em] tabular-nums sm:text-7xl", serif)}>{x.p}<NumberTicker value={x.v} format={{ minimumFractionDigits: x.d, maximumFractionDigits: x.d }} />{x.s}</dd> <dt className="text-muted-foreground mt-3">{x.l}</dt> </div> ))} </dl> </section> {/* Three stories */} <section className="mx-auto max-w-6xl space-y-24 px-4 pb-24 sm:px-6"> <div className="grid items-center gap-10 lg:grid-cols-2 lg:gap-16"> <div> <p className="text-chart-2 text-sm font-semibold tracking-wide uppercase">Cards</p> <NorthwindHeading className="mt-3 text-4xl sm:text-5xl">Give every team a card that <em>knows</em> the rules.</NorthwindHeading> <p className="text-muted-foreground mt-5 text-lg text-pretty">Limits, merchants and approval policies travel with the card, so the right purchase just works and the wrong one never leaves the building.</p> <ul className="mt-6 space-y-3">{["Virtual cards for every subscription", "Merchant and category locks", "Instant freeze, from any device"].map((x) => <li key={x} className="flex items-start gap-3"><Check className="text-chart-1 mt-1 size-4 shrink-0" aria-hidden="true" />{x}</li>)}</ul> </div> <Mini label="Three virtual cards with limits"> <div className="space-y-3"> {[["Design tools", "$2,400", "bg-chart-1"], ["Cloud hosting", "$14,500", "bg-chart-2"], ["Team offsite", "$8,000", "bg-chart-5"]].map(([n, a, t]) => ( <div key={n} className="bg-card flex items-center gap-4 rounded-xl border p-4"> <span className={cn("h-10 w-14 rounded-md", t)} /> <div className="flex-1"><p className="font-medium">{n}</p><p className="text-muted-foreground text-sm">Monthly limit</p></div> <p className="font-medium tabular-nums">{a}</p> </div> ))} </div> </Mini> </div> <div className="grid items-center gap-10 lg:grid-cols-2 lg:gap-16"> <Mini label="Before and after comparison"> <div className="grid gap-4 sm:grid-cols-2"> <div><p className="text-muted-foreground text-sm font-semibold">Before</p><ul className="mt-3 space-y-3 text-sm">{before.map((x) => <li key={x} className="text-muted-foreground flex gap-2"><span aria-hidden="true">×</span>{x}</li>)}</ul></div> <div><p className="text-chart-1 text-sm font-semibold">With Northwind</p><ul className="mt-3 space-y-3 text-sm">{after.map((x) => <li key={x} className="flex gap-2"><Check className="text-chart-1 mt-0.5 size-4 shrink-0" />{x}</li>)}</ul></div> </div> </Mini> <div> <p className="text-chart-2 text-sm font-semibold tracking-wide uppercase">Close</p> <NorthwindHeading className="mt-3 text-4xl sm:text-5xl">The month closes itself, <em>nearly</em>.</NorthwindHeading> <p className="text-muted-foreground mt-5 text-lg text-pretty">Every transaction is matched to a receipt, coded to your chart of accounts and synced to your ledger as it happens. What is left on close day is review, not data entry.</p> </div> </div> <div className="grid items-center gap-10 lg:grid-cols-2 lg:gap-16"> <div> <p className="text-chart-2 text-sm font-semibold tracking-wide uppercase">Reporting</p> <NorthwindHeading className="mt-3 text-4xl sm:text-5xl">Answers for the board, <em>before</em> they ask.</NorthwindHeading> <p className="text-muted-foreground mt-5 text-lg text-pretty">Spend by team, vendor and project, always current. Build a view once and share it with a link.</p> </div> <Mini label="Bar chart of spend by team over six months"> <div className="flex h-44 items-end gap-3"> {[48, 56, 52, 70, 64, 82].map((h, i) => <div key={i} className="flex flex-1 flex-col justify-end gap-1"><div className="bg-chart-1 rounded-t-md" style={{ height: `${h * 0.55}%` }} /><div className="bg-chart-2 rounded-t-md" style={{ height: `${h * 0.3}%` }} /></div>)} </div> <div className="text-muted-foreground mt-3 flex justify-between text-xs"><span>May</span><span>Oct</span></div> </Mini> </div> </section> <section className="bg-primary text-primary-foreground"> <figure className="mx-auto max-w-4xl px-4 py-24 text-center sm:px-6"> <Quote className="mx-auto size-8 opacity-50 rtl:-scale-x-100" aria-hidden="true" /> <blockquote className={cn("mt-6 text-3xl leading-snug tracking-[-0.02em] text-balance sm:text-5xl", serif)}> “We went from nine days to three to close the books, and nobody on my team has asked me for a receipt since March.” </blockquote> <figcaption className="mt-8 text-sm opacity-80"><span className="font-semibold">Elena Marchetti</span> · Controller, Harlow & Pine</figcaption> </figure> </section> <section className="mx-auto max-w-6xl px-4 py-24 text-center sm:px-6"> <NorthwindHeading className="mx-auto max-w-3xl text-4xl sm:text-6xl">See it with <em>your</em> numbers.</NorthwindHeading> <p className="text-muted-foreground mx-auto mt-5 max-w-lg text-lg text-pretty">A 30-minute walkthrough, built around how your team spends today.</p> <a href={contact} className="bg-primary text-primary-foreground focus-visible:ring-ring/50 mt-8 inline-flex h-13 items-center gap-2 rounded-full px-7 text-base font-medium outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]">Book a demo <ArrowRight className="size-4 rtl:rotate-180" aria-hidden="true" /></a> </section> </main> </NorthwindShell> ) } export { NorthwindHome, type NorthwindHomeProps }components/ballmac/templates/northwind/northwind-pricing.tsx// Ballmac UI: Northwind pricing page. https://ui.ballmac.com/templates/template-northwind "use client" import * as React from "react" import { Check } from "lucide-react" import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ballmac/accordion" import { SegmentedControl, SegmentedControlItem } from "@/components/ballmac/segmented-control" import { NorthwindHeading, NorthwindShell, type NorthwindHrefs } from "@/components/ballmac/templates/northwind/northwind-theme" import { cn } from "@/lib/utils" type Billing = "annual" | "monthly" const plans = [ { id: "starter", name: "Starter", blurb: "For teams of up to 25 that are moving off spreadsheets.", price: 8, unit: "per person / month", features: ["Virtual and physical cards", "Receipt capture and matching", "Basic approval rules", "Sync to QuickBooks or Xero"] }, { id: "growth", name: "Growth", blurb: "For finance teams that want budgets and policy.", price: 14, unit: "per person / month", featured: true, features: ["Everything in Starter", "Team budgets with alerts", "Multi-step approval policies", "Bill pay and reimbursements", "NetSuite and Sage Intacct sync"] }, { id: "enterprise", name: "Enterprise", blurb: "For multiple entities, countries and auditors.", price: null, unit: "annual contract", features: ["Everything in Growth", "Multi-entity and multi-currency", "SSO, SCIM and audit exports", "Dedicated implementation lead", "99.95% uptime SLA"] }, ] as const const faqs = [ { q: "Do I pay for every employee?", a: "You pay for people who hold a card or approve spend. Employees who only submit reimbursements are free, so rolling out to the whole company doesn’t change your bill." }, { q: "Is there a card fee?", a: "No. Cards are free, and we earn a small share of interchange from the card network. That is why we can keep seat prices low." }, { q: "How long does setup take?", a: "Most teams are issuing cards in a day. Accounting sync and policy setup usually take a week, with a guide at your side." }, { q: "What if we outgrow a plan?", a: "Change plans any time. We prorate to the day, and your cards, receipts and history carry over untouched." }, ] type NorthwindPricingProps = React.ComponentProps<"div"> & { hrefs?: Partial<NorthwindHrefs> } /** Northwind pricing: three plans with an annual saving, an included-everywhere list and questions. */ function NorthwindPricing({ hrefs, ...props }: NorthwindPricingProps) { const [billing, setBilling] = React.useState<Billing>("annual") const contact = hrefs?.contact ?? "/northwind/contact" return ( <NorthwindShell page="pricing" hrefs={hrefs} {...props}> <main> <section className="mx-auto max-w-6xl px-4 pt-16 sm:px-6 sm:pt-24"> <div className="mx-auto max-w-3xl text-center"> <NorthwindHeading as="h1" className="text-5xl leading-[1.04] sm:text-7xl">Pricing that <em>scales</em> with your team, not your spend.</NorthwindHeading> <p className="text-muted-foreground mt-6 text-lg text-pretty">No percentage of volume, no card fees, no surprise add-ons.</p> <SegmentedControl aria-label="Billing period" value={billing} onValueChange={(v) => setBilling(v as Billing)} className="mt-9"> <SegmentedControlItem value="annual">Annual · save 20%</SegmentedControlItem> <SegmentedControlItem value="monthly">Monthly</SegmentedControlItem> </SegmentedControl> </div> <div className="mt-14 grid gap-5 lg:grid-cols-3"> {plans.map((p) => { const price = p.price === null ? null : billing === "annual" ? Math.round(p.price * 0.8) : p.price const featured = "featured" in p && p.featured return ( <article key={p.id} className={cn("flex flex-col rounded-3xl border p-8", featured ? "bg-primary text-primary-foreground border-transparent" : "bg-card")}> <h2 className="text-[26px] font-medium tracking-tight [font-family:var(--northwind-serif),ui-serif,Georgia,serif]">{p.name}</h2> <p className={cn("mt-2 min-h-12 text-[15px] text-pretty", featured ? "opacity-80" : "text-muted-foreground")}>{p.blurb}</p> <p className="mt-6 flex items-baseline gap-2"> {price === null ? <span className="text-5xl font-medium tracking-tight [font-family:var(--northwind-serif),ui-serif,Georgia,serif]">Let’s talk</span> : ( <> <span className="text-6xl font-medium tracking-[-0.04em] tabular-nums [font-family:var(--northwind-serif),ui-serif,Georgia,serif]">${price}</span> <span className={cn("text-sm", featured ? "opacity-70" : "text-muted-foreground")}>{p.unit}</span> </> )} </p> <a href={contact} className={cn("focus-visible:ring-ring/50 mt-7 inline-flex h-12 items-center justify-center rounded-full text-[15px] font-medium outline-none transition-opacity focus-visible:ring-[3px]", featured ? "bg-primary-foreground text-primary hover:opacity-90" : "bg-primary text-primary-foreground hover:opacity-90")}> {price === null ? "Contact sales" : "Book a demo"} </a> <ul className="mt-8 space-y-3.5 border-t border-current/15 pt-7 text-[15px]"> {p.features.map((f) => <li key={f} className="flex items-start gap-3"><Check className={cn("mt-1 size-4 shrink-0", featured ? "" : "text-chart-1")} aria-hidden="true" />{f}</li>)} </ul> </article> ) })} </div> </section> <section aria-labelledby="nw-included" className="mx-auto max-w-6xl px-4 py-24 sm:px-6"> <NorthwindHeading id="nw-included" className="max-w-xl text-3xl sm:text-5xl">Included with every plan.</NorthwindHeading> <ul className="mt-10 grid gap-x-10 gap-y-6 sm:grid-cols-2 lg:grid-cols-3"> {["Unlimited virtual cards", "Real-time spend alerts", "Mobile app for iOS and Android", "Receipt capture by email and text", "CSV and API exports", "Bank-level encryption and 2FA", "US-based support, weekdays", "Free onboarding session"].map((x) => ( <li key={x} className="flex items-start gap-3 border-t pt-4"><Check className="text-chart-1 mt-1 size-4 shrink-0" aria-hidden="true" />{x}</li> ))} </ul> </section> <section className="bg-surface border-y" aria-labelledby="nw-faq"> <div className="mx-auto grid max-w-6xl gap-10 px-4 py-24 sm:px-6 lg:grid-cols-[1fr_1.4fr]"> <NorthwindHeading id="nw-faq" className="text-3xl sm:text-5xl">Questions we hear <em>often</em>.</NorthwindHeading> <Accordion type="single" collapsible> {faqs.map((f) => ( <AccordionItem key={f.q} value={f.q}> <AccordionTrigger className="text-lg">{f.q}</AccordionTrigger> <AccordionContent className="text-muted-foreground text-[15px] text-pretty">{f.a}</AccordionContent> </AccordionItem> ))} </Accordion> </div> </section> </main> </NorthwindShell> ) } export { NorthwindPricing, type NorthwindPricingProps }components/ballmac/templates/northwind/northwind-customers.tsx// Ballmac UI: Northwind customers page. https://ui.ballmac.com/templates/template-northwind import * as React from "react" import { ArrowUpRight } from "lucide-react" import { NorthwindHeading, NorthwindShell, type NorthwindHrefs } from "@/components/ballmac/templates/northwind/northwind-theme" import { cn } from "@/lib/utils" const serif = "[font-family:var(--northwind-serif),ui-serif,Georgia,serif]" const stories = [ { name: "Fernhill", sector: "Architecture", stat: "70%", note: "less time chasing receipts", quote: "Our partners finally see project costs while the project is still running.", tone: "bg-chart-1/15" }, { name: "Oakline", sector: "Logistics", stat: "$310k", note: "saved on duplicate vendors", quote: "We found three vendors we paid under different names within a week.", tone: "bg-chart-2/15" }, { name: "Tidewater", sector: "Hospitality", stat: "11 days → 4", note: "month-end close", quote: "Eighteen restaurants, one close process. It used to be eighteen.", tone: "bg-chart-5/15" }, { name: "Marlow Co", sector: "Consumer goods", stat: "4.9 / 5", note: "employee card satisfaction", quote: "Nobody has asked to expense a personal card in a year.", tone: "bg-chart-4/15" }, { name: "Brightwell", sector: "Healthcare", stat: "100%", note: "audit-ready receipts", quote: "Our auditors asked what we changed. We told them: everything.", tone: "bg-chart-3/20" }, { name: "Quill & Co", sector: "Publishing", stat: "2 hrs", note: "to onboard 60 people", quote: "Cards for the whole company were live before lunch.", tone: "bg-chart-1/15" }, ] type NorthwindCustomersProps = React.ComponentProps<"div"> & { hrefs?: Partial<NorthwindHrefs> } /** Northwind customers: a featured case study with results, then a grid of shorter stories. */ function NorthwindCustomers({ hrefs, ...props }: NorthwindCustomersProps) { return ( <NorthwindShell page="customers" hrefs={hrefs} {...props}> <main> <section className="mx-auto max-w-6xl px-4 pt-16 sm:px-6 sm:pt-24"> <p className="text-chart-2 text-sm font-semibold tracking-wide uppercase">Customers</p> <NorthwindHeading as="h1" className="mt-4 max-w-4xl text-5xl leading-[1.04] sm:text-7xl">Finance teams that got their <em>weekends</em> back.</NorthwindHeading> </section> <section aria-labelledby="nw-featured" className="mx-auto max-w-6xl px-4 py-14 sm:px-6"> <article className="bg-primary text-primary-foreground grid overflow-hidden rounded-3xl lg:grid-cols-[1.1fr_1fr]"> <div className="p-8 sm:p-12"> <p className="text-sm font-semibold opacity-70">Case study · Professional services</p> <h2 id="nw-featured" className={cn("mt-4 text-4xl leading-tight tracking-[-0.02em] text-balance sm:text-5xl", serif)}>Harlow & Pine closes the books in three days, not nine.</h2> <p className="mt-5 max-w-lg text-lg opacity-85 text-pretty">A 340-person consultancy replaced spreadsheets, a shared inbox and four card programs with one system finance trusts.</p> <a href="#" className="bg-primary-foreground text-primary focus-visible:ring-ring mt-8 inline-flex h-12 items-center gap-2 rounded-full px-6 text-sm font-medium outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]">Read the story <ArrowUpRight className="size-4 rtl:-scale-x-100" aria-hidden="true" /></a> </div> <dl className="grid grid-cols-2 border-t border-current/20 lg:border-t-0 lg:border-s"> {[["9 → 3", "days to close"], ["97%", "receipts matched"], ["$1.2M", "cash found in dormant subscriptions"], ["340", "employees, live in 2 weeks"]].map(([v, l], i) => ( <div key={l} className={cn("flex flex-col justify-end p-6 sm:p-8", i % 2 === 0 && "border-e border-current/20", i < 2 && "border-b border-current/20")}> <dd className={cn("text-4xl tracking-[-0.03em] sm:text-5xl", serif)}>{v}</dd> <dt className="mt-2 text-sm opacity-75">{l}</dt> </div> ))} </dl> </article> </section> <section aria-labelledby="nw-more" className="mx-auto max-w-6xl px-4 pb-24 sm:px-6"> <NorthwindHeading id="nw-more" className="text-3xl sm:text-4xl">More stories</NorthwindHeading> <ul className="mt-8 grid gap-5 md:grid-cols-2 lg:grid-cols-3"> {stories.map((s) => ( <li key={s.name} className="bg-card flex flex-col rounded-2xl border p-7"> <div className="flex items-center gap-3"> <span className={cn("flex size-10 items-center justify-center rounded-full text-sm font-semibold", s.tone)} aria-hidden="true">{s.name[0]}</span> <div><h3 className="font-semibold">{s.name}</h3><p className="text-muted-foreground text-xs">{s.sector}</p></div> </div> <p className={cn("mt-6 text-5xl tracking-[-0.03em]", serif)}>{s.stat}</p> <p className="text-muted-foreground mt-1 text-sm">{s.note}</p> <blockquote className="mt-6 flex-1 border-t pt-5 text-[15px] text-pretty">“{s.quote}”</blockquote> </li> ))} </ul> </section> </main> </NorthwindShell> ) } export { NorthwindCustomers, type NorthwindCustomersProps }components/ballmac/templates/northwind/northwind-about.tsx// Ballmac UI: Northwind about page. https://ui.ballmac.com/templates/template-northwind import * as React from "react" import { NorthwindHeading, NorthwindShell, type NorthwindHrefs } from "@/components/ballmac/templates/northwind/northwind-theme" import { cn } from "@/lib/utils" const serif = "[font-family:var(--northwind-serif),ui-serif,Georgia,serif]" const values = [ { title: "Plain beats clever", text: "If a customer needs a manual to use a feature, we haven’t finished it." }, { title: "Money is personal", text: "Behind every transaction is someone trying to do their job. We design for them first." }, { title: "Boring is a feature", text: "Finance software should be dependable enough to forget about. We aim to be forgotten." }, ] const timeline = [ ["2019", "Two former controllers start Northwind in a spare bedroom in Portland."], ["2021", "First 100 customers, and our first real month-end close with no spreadsheets."], ["2023", "Launch bill pay and reimbursements. Reach $1B in annual spend managed."], ["2025", "Open offices in Toronto and London. 1,000 customers across 14 countries."], ["2026", "Today: 1,400 finance teams, 140 people, still answering email ourselves."], ] const team = [ ["Ines Alvarado", "Co-founder, CEO", "bg-chart-1/25"], ["Tobias Wren", "Co-founder, CTO", "bg-chart-2/25"], ["Amara Nwosu", "VP Finance", "bg-chart-5/25"], ["Jonas Eklund", "Head of Design", "bg-chart-4/25"], ["Mei Tanaka", "Head of Security", "bg-chart-3/30"], ["Colm Brennan", "Head of Support", "bg-chart-1/25"], ] as const type NorthwindAboutProps = React.ComponentProps<"div"> & { hrefs?: Partial<NorthwindHrefs> } /** Northwind about: a long-form mission, values, a timeline and the team. */ function NorthwindAbout({ hrefs, ...props }: NorthwindAboutProps) { return ( <NorthwindShell page="about" hrefs={hrefs} {...props}> <main> <section className="mx-auto max-w-4xl px-4 pt-16 sm:px-6 sm:pt-24"> <p className="text-chart-2 text-sm font-semibold tracking-wide uppercase">About</p> <NorthwindHeading as="h1" className="mt-4 text-5xl leading-[1.04] sm:text-7xl">We think finance should feel <em>quiet</em>.</NorthwindHeading> <div className="text-muted-foreground mt-10 space-y-6 text-xl leading-relaxed text-pretty"> <p>Northwind began with a frustration. We had both run finance teams, and we had both spent the last week of every month chasing people for receipts that were, somewhere, already in an inbox.</p> <p>The tools we used treated spending as a problem to police. We wanted the opposite: software that makes it easy for people to do the right thing, and shows finance what happened without asking.</p> <p className={cn("text-foreground text-3xl leading-snug", serif)}>The best month-end is the one you barely notice.</p> </div> </section> <section aria-labelledby="nw-values" className="mx-auto max-w-6xl px-4 py-24 sm:px-6"> <NorthwindHeading id="nw-values" className="text-3xl sm:text-5xl">What we believe</NorthwindHeading> <ol className="mt-10 grid gap-8 md:grid-cols-3"> {values.map((v, i) => ( <li key={v.title} className="border-t-2 border-current pt-5"> <span className={cn("text-chart-2 text-5xl", serif)}>{i + 1}</span> <h3 className="mt-4 text-xl font-semibold">{v.title}</h3> <p className="text-muted-foreground mt-2 text-pretty">{v.text}</p> </li> ))} </ol> </section> <section aria-labelledby="nw-history" className="bg-surface border-y"> <div className="mx-auto max-w-4xl px-4 py-24 sm:px-6"> <NorthwindHeading id="nw-history" className="text-3xl sm:text-5xl">Seven years, briefly</NorthwindHeading> <ol className="mt-10 divide-y"> {timeline.map(([y, t]) => ( <li key={y} className="grid gap-2 py-6 sm:grid-cols-[7rem_1fr] sm:gap-8"><span className={cn("text-chart-2 text-3xl", serif)}>{y}</span><p className="text-lg text-pretty">{t}</p></li> ))} </ol> </div> </section> <section aria-labelledby="nw-team" className="mx-auto max-w-6xl px-4 py-24 sm:px-6"> <NorthwindHeading id="nw-team" className="text-3xl sm:text-5xl">The people</NorthwindHeading> <ul className="mt-10 grid grid-cols-2 gap-6 md:grid-cols-3"> {team.map(([n, r, t]) => ( <li key={n}> <div className={cn("flex aspect-[4/3] items-end rounded-2xl p-5", t)} aria-hidden="true"><span className={cn("text-5xl tracking-tight", serif)}>{n.split(" ").map((w) => w[0]).join("")}</span></div> <h3 className="mt-3 font-semibold">{n}</h3> <p className="text-muted-foreground text-sm">{r}</p> </li> ))} </ul> <div className="bg-primary text-primary-foreground mt-16 flex flex-col items-start justify-between gap-6 rounded-3xl p-8 sm:flex-row sm:items-center sm:p-12"> <div><p className={cn("text-3xl sm:text-4xl", serif)}>We’re hiring in Portland, Toronto and London.</p><p className="mt-2 opacity-80">14 open roles across engineering, design and support.</p></div> <a href="#" className="bg-primary-foreground text-primary focus-visible:ring-ring inline-flex h-12 shrink-0 items-center rounded-full px-6 text-sm font-medium outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]">See open roles</a> </div> </section> </main> </NorthwindShell> ) } export { NorthwindAbout, type NorthwindAboutProps }components/ballmac/templates/northwind/northwind-contact.tsx// Ballmac UI: Northwind contact page. https://ui.ballmac.com/templates/template-northwind "use client" import * as React from "react" import { Check, Mail, MapPin, MessageCircle } from "lucide-react" import { NorthwindHeading, NorthwindShell, type NorthwindHrefs } from "@/components/ballmac/templates/northwind/northwind-theme" import { cn } from "@/lib/utils" const serif = "[font-family:var(--northwind-serif),ui-serif,Georgia,serif]" const field = "bg-card focus-visible:ring-ring/50 mt-2 h-12 w-full rounded-xl border px-4 text-base outline-none focus-visible:ring-[3px] aria-[invalid=true]:border-destructive" type Errors = Partial<Record<"name" | "email" | "size", string>> type NorthwindContactProps = React.ComponentProps<"div"> & { hrefs?: Partial<NorthwindHrefs> } /** Northwind contact: a demo request form with inline validation, and the other ways to reach the team. */ function NorthwindContact({ hrefs, ...props }: NorthwindContactProps) { const [errors, setErrors] = React.useState<Errors>({}) const [sent, setSent] = React.useState<string | null>(null) const [pending, setPending] = React.useState(false) const timer = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined) React.useEffect(() => () => clearTimeout(timer.current), []) function submit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault() const form = e.currentTarget const f = new FormData(form) const name = String(f.get("name") ?? "").trim() const email = String(f.get("email") ?? "").trim() const size = String(f.get("size") ?? "") const next: Errors = {} if (!name) next.name = "Tell us your name." if (!/^\S+@\S+\.\S+$/.test(email)) next.email = "Enter a work email like you@company.com." if (!size) next.size = "Choose your team size." setErrors(next) if (Object.keys(next).length) { const first = (["name", "email", "size"] as const).find((k) => next[k]) ;(form.elements.namedItem(first ?? "name") as HTMLElement | null)?.focus() return } setPending(true) timer.current = setTimeout(() => { setPending(false) setSent(name.split(" ")[0]) }, 700) } return ( <NorthwindShell page="contact" hrefs={hrefs} {...props}> <main className="mx-auto grid max-w-6xl gap-14 px-4 py-16 sm:px-6 sm:py-24 lg:grid-cols-[1.1fr_1fr] lg:gap-20"> <div> <NorthwindHeading as="h1" className="text-5xl leading-[1.04] sm:text-7xl">Let’s talk about <em>your</em> spend.</NorthwindHeading> <p className="text-muted-foreground mt-6 max-w-lg text-lg text-pretty">Tell us a little about your team. We’ll book a 30-minute walkthrough built around how you spend today. No slides.</p> {sent ? ( <div role="status" className="bg-card mt-10 rounded-3xl border p-8"> <span className="bg-chart-1/15 text-chart-1 flex size-12 items-center justify-center rounded-full"><Check className="size-6" aria-hidden="true" /></span> <p className={cn("mt-5 text-3xl", serif)}>Thanks, {sent}.</p> <p className="text-muted-foreground mt-2 text-pretty">We’ll email you within one business day with a few times that work.</p> </div> ) : ( <form onSubmit={submit} noValidate className="mt-10 space-y-5"> <div> <label htmlFor="nw-name" className="text-sm font-medium">Full name</label> <input id="nw-name" name="name" autoComplete="name" aria-invalid={!!errors.name} aria-describedby={errors.name ? "nw-name-err" : undefined} className={field} /> {errors.name && <p id="nw-name-err" className="text-destructive mt-1.5 text-sm">{errors.name}</p>} </div> <div> <label htmlFor="nw-email" className="text-sm font-medium">Work email</label> <input id="nw-email" name="email" type="email" autoComplete="email" aria-invalid={!!errors.email} aria-describedby={errors.email ? "nw-email-err" : undefined} className={field} /> {errors.email && <p id="nw-email-err" className="text-destructive mt-1.5 text-sm">{errors.email}</p>} </div> <div> <label htmlFor="nw-size" className="text-sm font-medium">Team size</label> <select id="nw-size" name="size" defaultValue="" aria-invalid={!!errors.size} aria-describedby={errors.size ? "nw-size-err" : undefined} className={field}> <option value="" disabled>Choose one</option> <option>1–25 people</option><option>26–100 people</option><option>101–500 people</option><option>500+ people</option> </select> {errors.size && <p id="nw-size-err" className="text-destructive mt-1.5 text-sm">{errors.size}</p>} </div> <div> <label htmlFor="nw-note" className="text-sm font-medium">What would you like to fix? <span className="text-muted-foreground font-normal">(optional)</span></label> <textarea id="nw-note" name="note" rows={4} className={cn(field, "h-auto py-3")} /> </div> <button type="submit" disabled={pending} className="bg-primary text-primary-foreground focus-visible:ring-ring/50 inline-flex h-13 items-center rounded-full px-8 text-base font-medium outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px] disabled:opacity-60"> {pending ? "Sending…" : "Request a demo"} </button> </form> )} </div> <aside className="space-y-4 lg:pt-4" aria-label="Other ways to reach us"> {[ { icon: Mail, title: "Sales", text: "sales@northwind.example", note: "Replies within one business day." }, { icon: MessageCircle, title: "Support", text: "help@northwind.example", note: "Existing customers: weekdays, 6am to 6pm Pacific." }, { icon: MapPin, title: "Portland", text: "410 SW Alder St, Suite 300", note: "Toronto and London offices on request." }, ].map((c) => ( <div key={c.title} className="bg-card flex gap-4 rounded-2xl border p-6"> <span className="bg-chart-2/15 text-chart-2 flex size-11 shrink-0 items-center justify-center rounded-full"><c.icon className="size-5" aria-hidden="true" /></span> <div><h2 className="font-semibold">{c.title}</h2><p className="mt-0.5">{c.text}</p><p className="text-muted-foreground mt-1 text-sm">{c.note}</p></div> </div> ))} </aside> </main> </NorthwindShell> ) } export { NorthwindContact, type NorthwindContactProps }app/northwind/page.tsx// Ballmac UI: Northwind template route. https://ui.ballmac.com/templates/template-northwind import type { Metadata } from "next" import { NorthwindHome } from "@/components/ballmac/templates/northwind/northwind-home" export const metadata: Metadata = { title: "Northwind: Spend with clarity", description: "Cards, approvals and budgets for finance teams.", } export default function Page() { return <NorthwindHome /> }app/northwind/pricing/page.tsx// Ballmac UI: Northwind template route. https://ui.ballmac.com/templates/template-northwind import type { Metadata } from "next" import { NorthwindPricing } from "@/components/ballmac/templates/northwind/northwind-pricing" export const metadata: Metadata = { title: "Northwind pricing", description: "Pricing that scales with your team, not your spend.", } export default function Page() { return <NorthwindPricing /> }app/northwind/customers/page.tsx// Ballmac UI: Northwind template route. https://ui.ballmac.com/templates/template-northwind import type { Metadata } from "next" import { NorthwindCustomers } from "@/components/ballmac/templates/northwind/northwind-customers" export const metadata: Metadata = { title: "Northwind customers", description: "How finance teams closed faster with Northwind.", } export default function Page() { return <NorthwindCustomers /> }app/northwind/about/page.tsx// Ballmac UI: Northwind template route. https://ui.ballmac.com/templates/template-northwind import type { Metadata } from "next" import { NorthwindAbout } from "@/components/ballmac/templates/northwind/northwind-about" export const metadata: Metadata = { title: "About Northwind", description: "Why we think finance should feel quiet.", } export default function Page() { return <NorthwindAbout /> }app/northwind/contact/page.tsx// Ballmac UI: Northwind template route. https://ui.ballmac.com/templates/template-northwind import type { Metadata } from "next" import { NorthwindContact } from "@/components/ballmac/templates/northwind/northwind-contact" export const metadata: Metadata = { title: "Contact Northwind", description: "Book a demo built around how your team spends.", } export default function Page() { return <NorthwindContact /> }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
- Home/northwind
- Pricing/northwind/pricing
- Customers/northwind/customers
- About/northwind/about
- Contact/northwind/contact
- Fonts
- Fraunces, Figtree (Google Fonts, loaded with next/font)
- Files
- 13 files · 4 Ballmac blocks and components
Built with
Dependencies
Use with AI
Installs a five-page editorial site for a B2B product, with a hotspot product tour on the home page. Change the palette in northwindCss, the tour steps in northwind-tour.tsx, and the copy in each page file. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Northwind: B2B SaaS (@ballmac/template-northwind) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Marketing sites for finance, HR, legal and other B2B software
- Brands that want a warm, trustworthy, editorial voice instead of a tech look
Not for
- Dark developer tools (use an Orbit-style template)
Registry JSON: https://ui.ballmac.com/r/template-northwind.json
Free to use in personal and commercial projects.