Hero
The opening band: what the product is, in a headline, a line of support and the first call to action.
Centred hero with a logo cloud
hero-01An announcement pill, a headline with its turning phrase on the accent's tint, two calls to action, what the trial costs, and the companies that use it.
import * as React from "react"
import Link from "next/link"
import { ArrowRightIcon, CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { buttonVariants } from "@/components/ui/button"
import { LogoMark } from "@/components/ui/logo-mark"
import {
ANNOUNCEMENT,
CUSTOMERS,
HEADLINE,
LEDE,
PRIMARY_ACTION,
REASSURANCE,
SECONDARY_ACTION,
} from "./hero-01.data"
/**
* The centred opening: what is new, the claim with the phrase it turns on set
* in the accent's tint, one paragraph, the two ways in and what they cost the
* reader — nothing — then the companies that already use it.
*
* A server component with no state: every word comes from `./hero-01.data`,
* and the links name routes of your own site, so it installs as-is and says
* what yours says once the copy is replaced. Its ids come from `useId`, so
* two heroes on one page never share one.
*/
export function Hero01() {
const titleId = React.useId()
const customersId = React.useId()
return (
<section
data-section="hero-01"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto max-w-7xl">
<div className="mx-auto flex max-w-3xl flex-col items-center text-center">
<Link
href={ANNOUNCEMENT.href}
className="group/announcement inline-flex max-w-full items-center gap-2 rounded-full bg-card py-1 pr-3 pl-1 text-sm text-foreground ring-1 ring-border transition-colors duration-(--duration-fast) ease-(--ease-standard) focus-ring hover:bg-muted"
>
<Badge>{ANNOUNCEMENT.tag}</Badge>{" "}
<span className="truncate">{ANNOUNCEMENT.text}</span>
<ArrowRightIcon
aria-hidden="true"
className="size-3.5 shrink-0 text-muted-foreground transition-transform duration-(--duration-fast) ease-(--ease-standard) group-hover/announcement:translate-x-0.5"
/>
</Link>
<h1 id={titleId} className="type-display mt-6 text-4xl text-balance sm:text-5xl md:text-6xl">
{HEADLINE.lead}{" "}
{/* The phrase the claim turns on, on the accent's own tint — the
same mark the site's headline gives its name. */}
<span className="rounded-lg bg-brand-muted px-2 text-brand [box-decoration-break:clone]">
{HEADLINE.emphasis}
</span>
</h1>
<p className="mt-6 max-w-2xl text-base/7 text-pretty text-muted-foreground md:text-lg/8">{LEDE}</p>
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
{/* 40px: the hero's two calls carry more weight than a toolbar's. */}
<Link href={PRIMARY_ACTION.href} className={cn(buttonVariants({ size: "lg" }), "h-10 px-5")}>
{PRIMARY_ACTION.label}
</Link>
<Link
href={SECONDARY_ACTION.href}
className={cn(buttonVariants({ variant: "outline", size: "lg" }), "h-10 px-5")}
>
{SECONDARY_ACTION.label}
</Link>
</div>
<ul className="mt-5 flex flex-wrap justify-center gap-x-5 gap-y-1.5 text-sm text-muted-foreground">
{REASSURANCE.map((item) => (
<li key={item} className="inline-flex items-center gap-1.5">
<CheckIcon aria-hidden="true" className="size-3.5 shrink-0" />
{item}
</li>
))}
</ul>
</div>
<div className="mx-auto mt-16 max-w-5xl md:mt-20">
<p id={customersId} className="text-center text-sm text-muted-foreground">
{CUSTOMERS.caption}
</p>
{/* A grid that always divides the six evenly — two up on a phone,
three on a tablet, one row from lg — where a wrapping row would
leave one mark alone on its last line. */}
<ul
aria-labelledby={customersId}
className="mt-6 grid grid-cols-2 place-items-center gap-x-6 gap-y-7 text-muted-foreground sm:grid-cols-3 lg:grid-cols-6"
>
{CUSTOMERS.names.map((name) => (
<li key={name}>
<LogoMark name={name} />
</li>
))}
</ul>
</div>
</div>
</section>
)
}
export default Hero01/**
* Every word `hero-01` sets, typed, so the layout can only arrange it and
* never say something else. The product is the sample company's own —
* Northwind, the workspace every dashboard in the kit is — and the customers
* are the kit's fictional companies, drawn by `LogoMark`. Replace these values
* with your own and the hero says them.
*/
import type { LogoName } from "@/components/ui/logo-mark"
export type HeroLink = { label: string; href: string }
/** The one line above the headline: a tag, what is new, and where to read about it. */
export type HeroAnnouncement = { tag: string; text: string; href: string }
/** A headline in two parts: the claim, and the phrase it turns on, which a hero may set apart. */
export type HeroHeadline = { lead: string; emphasis: string }
export const PRODUCT = { company: "Northwind", name: "Northwind Analytics" } as const
export const ANNOUNCEMENT: HeroAnnouncement = {
tag: "New",
text: "Cohort reports are in every plan",
href: "/changelog",
}
export const HEADLINE: HeroHeadline = {
lead: "The numbers your team runs on,",
emphasis: "in one place",
}
export const LEDE =
"Northwind brings revenue, product usage and support into one workspace, so the weekly review starts with answers instead of exports."
export const PRIMARY_ACTION: HeroLink = { label: "Start free trial", href: "/sign-up" }
export const SECONDARY_ACTION: HeroLink = { label: "Book a demo", href: "/contact" }
/** What the reader risks by pressing the button: nothing, in three short clauses. */
export const REASSURANCE = ["Free for 14 days", "No card required", "Cancel any time"] as const
/** Who already uses it, as the kit's fictional companies — never a real one. */
export const CUSTOMERS: { caption: string; names: readonly LogoName[] } = {
caption: "Trusted by operations teams at",
names: ["Keystone", "Lumen", "Meridian", "Tidewater", "Copperline", "Fieldstone"],
}npx shadcn@latest add @vibra/hero-01Split hero with a revenue card
hero-02The claim, the lede and two calls to action beside a revenue card built from kit parts: the quarter's figure and change, its weeks against the quarter before, and its split by channel.
import * as React from "react"
import Link from "next/link"
import { CheckIcon, RefreshCwIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { formatCurrency, formatPercent } from "@/lib/format"
import { buttonVariants } from "@/components/ui/button"
import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { MetricDelta } from "@/components/ui/metric-delta"
import { PercentageBar } from "@/components/ui/percentage-bar"
import { RevenueChart } from "./hero-02.client"
import {
EYEBROW,
HEADLINE,
LEDE,
PRIMARY_ACTION,
REASSURANCE,
REVENUE_CARD,
SECONDARY_ACTION,
revenueTotals,
} from "./hero-02.data"
/** A headline figure: whole dollars, abbreviated once it passes a million. */
function money(value: number): string {
return formatCurrency(value, "USD", { compact: true, maximumFractionDigits: 2 })
}
/**
* The split opening: the claim, the lede and the two ways in on one side, and
* on the other the thing the claim is about — a revenue card from a Northwind
* workspace, drawn with the kit's own Card, chart and bar rather than a
* screenshot, so it takes the reader's theme and palette with the rest of the
* page. The card is real content, not decoration: its figure, its change and
* its split are read out like any other card's.
*
* A server component; the chart is the one island, because it takes its
* formatter as a function. Every word and figure is in `./hero-02.data`.
*/
export function Hero02() {
const titleId = React.useId()
const totals = revenueTotals()
return (
<section
data-section="hero-02"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto grid max-w-7xl items-center gap-12 lg:grid-cols-2 lg:gap-16">
<div className="flex max-w-xl flex-col items-start">
<p className="type-eyebrow">{EYEBROW}</p>
<h1 id={titleId} className="type-display mt-4 text-4xl text-balance sm:text-5xl">
{HEADLINE}
</h1>
<p className="mt-6 text-base/7 text-pretty text-muted-foreground md:text-lg/8">{LEDE}</p>
<div className="mt-8 flex flex-wrap items-center gap-3">
<Link href={PRIMARY_ACTION.href} className={cn(buttonVariants({ size: "lg" }), "h-10 px-5")}>
{PRIMARY_ACTION.label}
</Link>
<Link
href={SECONDARY_ACTION.href}
className={cn(buttonVariants({ variant: "outline", size: "lg" }), "h-10 px-5")}
>
{SECONDARY_ACTION.label}
</Link>
</div>
<ul className="mt-5 flex flex-wrap gap-x-5 gap-y-1.5 text-sm text-muted-foreground">
{REASSURANCE.map((item) => (
<li key={item} className="inline-flex items-center gap-1.5">
<CheckIcon aria-hidden="true" className="size-3.5 shrink-0" />
{item}
</li>
))}
</ul>
</div>
<Card className="w-full">
<CardHeader>
<CardTitle>{REVENUE_CARD.title}</CardTitle>
<CardDescription>
{REVENUE_CARD.window}, against {REVENUE_CARD.previousLabel}
</CardDescription>
<CardAction>
<MetricDelta value={totals.delta} variant="pill" size="sm" />
</CardAction>
</CardHeader>
<CardContent className="flex flex-col gap-5">
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<p className="type-numeral text-3xl">{money(totals.current)}</p>
<p className="text-sm text-muted-foreground tabular-nums">
vs {money(totals.previous)} over the same weeks of {REVENUE_CARD.previousLabel}
</p>
</div>
<RevenueChart weeks={REVENUE_CARD.weeks} currentLabel={REVENUE_CARD.currentLabel} />
<div className="flex flex-col gap-2 border-t border-border pt-4">
<p className="type-eyebrow">By channel</p>
<PercentageBar
segments={[...REVENUE_CARD.channels]}
showLegend
showValues
format={(value, percent) => `${money(value)} · ${formatPercent(percent / 100, { maximumFractionDigits: 0 })}`}
/>
</div>
</CardContent>
<CardFooter className="gap-2 text-xs text-muted-foreground">
<RefreshCwIcon aria-hidden="true" className="size-3.5 shrink-0" />
{REVENUE_CARD.sync}
</CardFooter>
</Card>
</div>
</section>
)
}
export default Hero02"use client"
import { formatCurrency } from "@/lib/format"
import { AreaChart } from "@/components/ui/area-chart"
import type { RevenueWeek } from "./hero-02.data"
/** Whole dollars, abbreviated: a card this size reads "$228.9K", not "$228,900.00". */
function dollars(value: number): string {
return formatCurrency(value, "USD", { compact: true })
}
/**
* The card's chart: the quarter so far week by week, with the same weeks of
* the quarter before as a dashed ghost behind it. An island only because the
* chart takes its formatter as a function, which a server component cannot
* hand across; the weeks arrive as plain props.
*/
export function RevenueChart({
weeks,
currentLabel,
}: {
weeks: readonly RevenueWeek[]
currentLabel: string
}) {
return (
<AreaChart
data={[...weeks]}
index="week"
series={[{ key: "current", label: currentLabel, compareKey: "previous" }]}
compare
height={168}
showLegend={false}
showYAxis={false}
valueFormatter={dollars}
/>
)
}/**
* Every word `hero-02` sets, and the figures its card draws. The card is a
* picture of the product — one widget of a Northwind workspace — so its
* numbers are copy like the headline: written once here, as the weeks a
* sample quarter ran, and every total, share and change is counted from those
* weeks by the functions below, so the card can never print a total its own
* chart disagrees with. Replace the weeks with your own and the card follows.
*/
export type HeroLink = { label: string; href: string }
export const EYEBROW = "Northwind Analytics"
export const HEADLINE = "Know where this quarter's revenue is coming from"
export const LEDE =
"Northwind joins your billing, CRM and product usage into one model, so every figure on the board traces back to the deals, plans and accounts behind it."
export const PRIMARY_ACTION: HeroLink = { label: "Start free trial", href: "/sign-up" }
export const SECONDARY_ACTION: HeroLink = { label: "See a sample workspace", href: "/demo" }
/** What the reader risks by pressing the button: nothing, in three short clauses. */
export const REASSURANCE = ["Free for 14 days", "No card required", "Set up in under an hour"] as const
/** One week of the quarter, against the same week of the quarter before, in whole dollars. */
export type RevenueWeek = { week: string; current: number; previous: number }
/** The card: net revenue for the quarter so far, week by week, and where it came from. */
export const REVENUE_CARD = {
title: "Net revenue",
window: "Q3 2026 to date",
currentLabel: "Q3 2026",
previousLabel: "Q2 2026",
weeks: [
{ week: "Jul 6", current: 182_400, previous: 171_900 },
{ week: "Jul 13", current: 176_900, previous: 169_800 },
{ week: "Jul 20", current: 191_300, previous: 174_200 },
{ week: "Jul 27", current: 198_800, previous: 178_500 },
{ week: "Aug 3", current: 205_600, previous: 181_100 },
{ week: "Aug 10", current: 199_400, previous: 179_900 },
{ week: "Aug 17", current: 214_700, previous: 186_400 },
{ week: "Aug 24", current: 221_300, previous: 188_200 },
{ week: "Aug 31", current: 228_900, previous: 192_700 },
] satisfies RevenueWeek[],
/** The quarter's revenue by where the customer came from; the amounts add up to the weeks. */
channels: [
{ label: "Direct", value: 1_054_800 },
{ label: "Partners", value: 491_200 },
{ label: "Marketplace", value: 273_300 },
],
sync: "Synced from billing and your CRM 4 minutes ago",
} as const
/** The quarter so far, and the same weeks of the quarter before. */
export function revenueTotals(weeks: readonly RevenueWeek[] = REVENUE_CARD.weeks) {
const current = weeks.reduce((sum, week) => sum + week.current, 0)
const previous = weeks.reduce((sum, week) => sum + week.previous, 0)
return { current, previous, delta: previous > 0 ? (current - previous) / previous : 0 }
}npx shadcn@latest add @vibra/hero-02Centred hero over a product window
hero-03The claim, the lede, one button and a tour link, then a whole workspace page under them — sidebar, four figures, a weekly chart and the largest accounts — drawn from kit parts.
import * as React from "react"
import Link from "next/link"
import {
ActivityIcon,
ArrowRightIcon,
CalendarIcon,
CircleDollarSignIcon,
FileTextIcon,
LayoutDashboardIcon,
LifeBuoyIcon,
UsersIcon,
type LucideIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { BarChart } from "@/components/ui/bar-chart"
import { buttonVariants } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { LogoMark } from "@/components/ui/logo-mark"
import { SimpleTable } from "@/components/ui/simple-table"
import { StatCard } from "@/components/ui/stat-card"
import { StatusBadge } from "@/components/ui/status-badge"
import {
ACCOUNTS,
CAPTION,
HEADLINE,
LEDE,
PRIMARY_ACTION,
STATS,
TOUR_LINK,
WEEKLY_REVENUE,
WORKSPACE,
type AccountRow,
} from "./hero-03.data"
/** The sidebar's icons, by the page they stand for; a page with none draws a document. */
const PAGE_ICONS: Record<string, LucideIcon> = {
Overview: LayoutDashboardIcon,
Revenue: CircleDollarSignIcon,
Customers: UsersIcon,
"Product usage": ActivityIcon,
Support: LifeBuoyIcon,
}
const ACCOUNT_COLUMNS = [
{ key: "account", header: "Account", cell: (row: AccountRow) => <LogoMark name={row.account} size="sm" /> },
{ key: "plan", header: "Plan", className: "max-sm:hidden" },
{
key: "health",
header: "Health",
cell: (row: AccountRow) => (
<StatusBadge status={row.health} variant={row.health === "Healthy" ? "success" : "warning"} />
),
},
{ key: "mrr", header: "MRR", align: "right" as const },
]
/**
* The centred opening with the product under it: the claim, the lede, one
* button and a quieter link, then a whole workspace page — the inset shell's
* sidebar ground and white sheet, four figures, a chart and a table — built
* from the kit's own Card, StatCard, BarChart and SimpleTable instead of a
* screenshot, so it is sharp at every width and wears the reader's palette.
*
* The window is a picture, so it is one to assistive technology: hidden and
* inert, described by the figure's caption, the way an image is by its alt
* text. Below `md` it drops its sidebar and keeps the page. A server
* component; every word and figure is in `./hero-03.data`.
*/
export function Hero03() {
const titleId = React.useId()
const captionId = React.useId()
return (
<section
data-section="hero-03"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto max-w-7xl">
<div className="mx-auto flex max-w-3xl flex-col items-center text-center">
<h1 id={titleId} className="type-display text-4xl text-balance sm:text-5xl md:text-6xl">
{HEADLINE}
</h1>
<p className="mt-6 max-w-2xl text-base/7 text-pretty text-muted-foreground md:text-lg/8">{LEDE}</p>
<div className="mt-8 flex flex-wrap items-center justify-center gap-x-6 gap-y-3">
<Link href={PRIMARY_ACTION.href} className={cn(buttonVariants({ size: "lg" }), "h-10 px-5")}>
{PRIMARY_ACTION.label}
</Link>
<Link
href={TOUR_LINK.href}
className="group/tour focus-ring inline-flex h-10 items-center gap-1.5 rounded-md text-sm font-medium text-foreground"
>
{TOUR_LINK.label}
<ArrowRightIcon
aria-hidden="true"
className="size-4 transition-transform duration-(--duration-fast) ease-(--ease-standard) group-hover/tour:translate-x-0.5"
/>
</Link>
</div>
</div>
<figure aria-labelledby={captionId} className="mx-auto mt-14 max-w-6xl md:mt-16">
<div
aria-hidden="true"
inert
className="overflow-hidden rounded-xl bg-sidebar ring-1 ring-border md:grid md:grid-cols-[12rem_minmax(0,1fr)]"
>
<div className="hidden flex-col gap-4 p-3 text-sidebar-foreground md:flex">
<LogoMark name="Northwind" size="sm" className="px-2 pt-2" />
<ul className="flex flex-col gap-0.5 text-sm">
{WORKSPACE.pages.map((page) => {
const Icon = PAGE_ICONS[page] ?? FileTextIcon
const current = page === WORKSPACE.page
return (
<li
key={page}
className={cn(
"flex h-8 items-center gap-2 rounded-md px-2",
current && "bg-sidebar-accent font-medium text-sidebar-accent-foreground"
)}
>
<Icon className="size-4 shrink-0" />
{page}
</li>
)
})}
</ul>
</div>
<div className="bg-background md:m-2 md:ml-0 md:rounded-lg md:ring-1 md:ring-border">
<div className="flex h-12 items-center gap-2.5 border-b border-rule px-4">
<span className="text-sm font-semibold">{WORKSPACE.page}</span>
<span className="text-faint-foreground">/</span>
<LogoMark name={WORKSPACE.customer} size="sm" className="text-muted-foreground" />
<span className="ml-auto inline-flex h-7 items-center gap-1.5 rounded-md px-2.5 text-xs text-muted-foreground ring-1 ring-border">
<CalendarIcon className="size-3.5" />
{WORKSPACE.range}
</span>
</div>
<div className="flex flex-col gap-3 p-3 md:gap-4 md:p-4">
<div className="grid grid-cols-2 gap-3 md:gap-4 lg:grid-cols-4">
{STATS.map((stat) => (
<StatCard
key={stat.label}
size="sm"
label={stat.label}
value={stat.value}
delta={stat.delta}
deltaFormat="percent"
positiveIsGood={stat.positiveIsGood}
/>
))}
</div>
<div className="grid gap-3 md:gap-4 lg:grid-cols-5">
<Card className="lg:col-span-3">
<CardHeader>
<CardTitle>Revenue by week</CardTitle>
<CardDescription>Booked, in dollars</CardDescription>
</CardHeader>
<CardContent>
<BarChart
data={WEEKLY_REVENUE}
index="week"
series={[{ key: "revenue", label: "Revenue" }]}
height={184}
showLegend={false}
showYAxis={false}
showTooltip={false}
/>
</CardContent>
</Card>
<Card className="max-sm:hidden lg:col-span-2">
<CardHeader>
<CardTitle>Largest accounts</CardTitle>
<CardDescription>By monthly revenue</CardDescription>
</CardHeader>
<CardContent className="p-0">
<SimpleTable columns={ACCOUNT_COLUMNS} rows={[...ACCOUNTS]} rowKey="account" size="sm" />
</CardContent>
</Card>
</div>
</div>
</div>
</div>
<figcaption id={captionId} className="mt-4 text-center text-sm text-muted-foreground">
{CAPTION}
</figcaption>
</figure>
</div>
</section>
)
}
export default Hero03/**
* Every word `hero-03` sets, and what its product window shows. The window is
* a picture of a Northwind workspace — a customer's Overview page — so its
* figures and rows are copy like the headline, written once here. Replace
* them with a page of your own product and the window shows that instead.
*/
import type { LogoName } from "@/components/ui/logo-mark"
export type HeroLink = { label: string; href: string }
export const HEADLINE = "Every team's numbers, on one page they all trust"
export const LEDE =
"Finance, sales and support read the same figures from the same model, refreshed every few minutes. Nobody rebuilds a report by hand before the Monday review."
export const PRIMARY_ACTION: HeroLink = { label: "Start free trial", href: "/sign-up" }
/** The quiet second way in: a link, not a second button. */
export const TOUR_LINK: HeroLink = { label: "Take the product tour", href: "/tour" }
/** What the picture is, for a reader who cannot see it: the figure's caption. */
export const CAPTION = "The Overview page of a Northwind workspace, shown with sample data."
/** The workspace in the picture: the customer, and the pages of its sidebar. */
export const WORKSPACE: { customer: LogoName; page: string; range: string; pages: readonly string[] } = {
customer: "Keystone",
page: "Overview",
range: "Last 30 days",
pages: ["Overview", "Revenue", "Customers", "Product usage", "Support", "Reports"],
}
export type WindowStat = { label: string; value: string; delta: number; positiveIsGood?: boolean }
export const STATS: readonly WindowStat[] = [
{ label: "MRR", value: "$412.8K", delta: 0.041 },
{ label: "Active accounts", value: "1,284", delta: 0.023 },
{ label: "Churned revenue", value: "$9.6K", delta: -0.124, positiveIsGood: false },
{ label: "Open tickets", value: "38", delta: -0.12, positiveIsGood: false },
]
/** Revenue booked each week, in whole dollars. */
export const WEEKLY_REVENUE = [
{ week: "Jul 13", revenue: 88_200 },
{ week: "Jul 20", revenue: 91_600 },
{ week: "Jul 27", revenue: 90_100 },
{ week: "Aug 3", revenue: 94_800 },
{ week: "Aug 10", revenue: 97_300 },
{ week: "Aug 17", revenue: 96_200 },
{ week: "Aug 24", revenue: 101_900 },
{ week: "Aug 31", revenue: 104_400 },
]
export type AccountRow = { account: LogoName; plan: string; mrr: string; health: "Healthy" | "At risk" }
export const ACCOUNTS: readonly AccountRow[] = [
{ account: "Meridian", plan: "Enterprise", mrr: "$38.4K", health: "Healthy" },
{ account: "Tidewater", plan: "Scale", mrr: "$21.9K", health: "Healthy" },
{ account: "Copperline", plan: "Scale", mrr: "$17.2K", health: "At risk" },
{ account: "Lumen", plan: "Team", mrr: "$9.8K", health: "Healthy" },
]npx shadcn@latest add @vibra/hero-03Hero with a working question panel
hero-04The claim beside its lede and calls, over a panel of suggested questions: each chip is a tab that brings up its answer — the sentence, its source, and the bars, line or rows behind it.
import * as React from "react"
import Link from "next/link"
import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { buttonVariants } from "@/components/ui/button"
import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { AskPanel } from "./hero-04.client"
import { EYEBROW, HEADLINE, LEDE, PANEL, PRIMARY_ACTION, QUESTIONS, SECONDARY_ACTION } from "./hero-04.data"
/**
* The opening that lets the reader try the product before signing up: the
* claim set left over a working panel of suggested questions, each one a tab
* that brings up its answer — a sentence first, then the bars, the line or the
* rows it rests on, then what it was counted from. Every chart is the kit's,
* so an answer looks the way it will in the reader's own workspace.
*
* The questions are the one island; this file is a server component and
* every word is in `./hero-04.data`.
*/
export function Hero04() {
const titleId = React.useId()
return (
<section
data-section="hero-04"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto max-w-7xl">
<div className="grid gap-x-16 gap-y-6 lg:grid-cols-12 lg:items-end">
<div className="lg:col-span-7">
<p className="type-eyebrow">{EYEBROW}</p>
<h1 id={titleId} className="type-display mt-4 text-4xl text-balance sm:text-5xl">
{HEADLINE}
</h1>
</div>
<div className="flex flex-col items-start gap-6 lg:col-span-5">
<p className="text-base/7 text-pretty text-muted-foreground">{LEDE}</p>
<div className="flex flex-wrap items-center gap-3">
<Link href={PRIMARY_ACTION.href} className={cn(buttonVariants({ size: "lg" }), "h-10 px-5")}>
{PRIMARY_ACTION.label}
</Link>
<Link
href={SECONDARY_ACTION.href}
className={cn(buttonVariants({ variant: "outline", size: "lg" }), "h-10 px-5")}
>
{SECONDARY_ACTION.label}
</Link>
</div>
</div>
</div>
<Card className="mt-12 md:mt-16">
<CardHeader>
<CardTitle>{PANEL.title}</CardTitle>
<CardDescription>{PANEL.description}</CardDescription>
<CardAction>
<Badge variant="outline">Sample data</Badge>
</CardAction>
</CardHeader>
<CardContent className="p-0">
<AskPanel questions={QUESTIONS} label={PANEL.title} />
</CardContent>
</Card>
</div>
</section>
)
}
export default Hero04"use client"
import { CornerDownRightIcon, DatabaseIcon, MessageSquareTextIcon } from "lucide-react"
import { formatPercent } from "@/lib/format"
import { HorizontalBarChart } from "@/components/ui/horizontal-bar-chart"
import { LineChart } from "@/components/ui/line-chart"
import { LogoMark } from "@/components/ui/logo-mark"
import { SimpleTable } from "@/components/ui/simple-table"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import type { Answer, Question, RefundRow } from "./hero-04.data"
/** A share or a rate to one decimal: "23.3%". */
function percent(value: number): string {
return formatPercent(value, { maximumFractionDigits: 1 })
}
const REFUND_COLUMNS = [
{ key: "account", header: "Account", cell: (row: RefundRow) => <LogoMark name={row.account} size="sm" /> },
{ key: "plan", header: "Plan" },
{ key: "reason", header: "Reason", className: "max-sm:hidden" },
{ key: "amount", header: "Refunded", align: "right" as const },
]
/** What an answer rests on: the bars, the line or the rows it counted. */
function Evidence({ evidence }: { evidence: Answer }) {
if (evidence.kind === "bars")
return (
<HorizontalBarChart
data={evidence.rows}
index="label"
series={[{ key: "value", label: evidence.series }]}
valueFormatter={percent}
height={156}
showLegend={false}
/>
)
if (evidence.kind === "line")
return (
<LineChart
data={evidence.rows}
index="label"
series={[{ key: "value", label: evidence.series }]}
valueFormatter={percent}
height={184}
showLegend={false}
dots
/>
)
return (
<div className="overflow-hidden rounded-lg ring-1 ring-border">
<SimpleTable columns={REFUND_COLUMNS} rows={evidence.rows} rowKey="account" size="sm" />
</div>
)
}
/**
* The panel's working part: the suggested questions as a row of chips and,
* under them, the answer to the one chosen — the sentence and what it was
* counted from beside its evidence. Tabs because that is what it is: one
* choice drives one region, the arrow keys move between questions and bring
* each answer up as they land, and a reader hears which one is selected. The
* chosen chip wears the brand tint and the 500 weight, never a stroke. Every
* question and answer arrives as plain props from the section.
*/
export function AskPanel({ questions, label }: { questions: Question[]; label: string }) {
return (
<Tabs defaultValue={questions[0]?.id} className="gap-0">
<TabsList
variant="line"
aria-label={label}
activateOnFocus
className="w-full flex-wrap justify-start gap-2 rounded-none border-b border-border p-3 group-data-horizontal/tabs:h-auto md:p-4"
>
{questions.map((item) => (
<TabsTrigger
key={item.id}
value={item.id}
className="h-auto flex-none justify-start gap-2 rounded-full px-3 py-1.5 text-left font-normal whitespace-normal ring-1 ring-border after:hidden hover:bg-muted/50 data-active:bg-brand-muted data-active:font-medium data-active:ring-transparent data-active:hover:bg-brand-muted"
>
<MessageSquareTextIcon aria-hidden="true" />
{item.question}
</TabsTrigger>
))}
</TabsList>
{questions.map((item) => (
<TabsContent
key={item.id}
value={item.id}
className="grid gap-6 rounded-b-lg p-4 focus-ring md:p-6 lg:grid-cols-5 lg:gap-10"
>
<div className="flex flex-col gap-4 lg:col-span-2">
<p className="flex gap-2 text-base font-medium text-pretty lg:text-lg">
<CornerDownRightIcon aria-hidden="true" className="mt-1 size-4 shrink-0 text-muted-foreground" />
{item.answer}
</p>
<p className="flex items-start gap-1.5 text-xs text-muted-foreground">
<DatabaseIcon aria-hidden="true" className="mt-px size-3.5 shrink-0" />
{item.source}
</p>
</div>
<div className="min-w-0 lg:col-span-3">
<Evidence evidence={item.evidence} />
</div>
</TabsContent>
))}
</Tabs>
)
}/**
* Every word `hero-04` sets, and the questions its panel answers. The panel
* shows how an answer looks — the sentence, the chart or the rows behind it,
* and what it was counted from — so each answer is copy, written once here
* with the figures it draws. Swap in questions about your own product and
* the panel asks those instead.
*/
import type { LogoName } from "@/components/ui/logo-mark"
export type HeroLink = { label: string; href: string }
export const EYEBROW = "Ask Northwind"
export const HEADLINE = "Ask a question, get the chart and the rows behind it"
export const LEDE =
"Ask the way you would ask a colleague. Northwind answers from your own billing, CRM and support data, and shows its working: the chart, what it counted and where the figures came from."
export const PRIMARY_ACTION: HeroLink = { label: "Try it on your data", href: "/sign-up" }
export const SECONDARY_ACTION: HeroLink = { label: "How answers are checked", href: "/docs/answers" }
/** The panel's own strip. */
export const PANEL = {
title: "Suggested questions",
description: "Pick one to see the answer as your team would get it.",
}
/** A share, a rate: the bars and the line read out as percentages. */
export type Share = { label: string; value: number }
export type RefundRow = { account: LogoName; plan: string; amount: string; reason: string }
export type Answer =
| { kind: "bars"; series: string; rows: Share[] }
| { kind: "line"; series: string; rows: Share[] }
| { kind: "rows"; rows: RefundRow[] }
export type Question = {
/** The tab's value; stable, so a link or a test can name a question. */
id: string
question: string
/** The answer in one sentence, before the evidence. */
answer: string
/** What the answer was counted from. */
source: string
evidence: Answer
}
export const QUESTIONS: Question[] = [
{
id: "fastest-channel",
question: "Which channel grew fastest this quarter?",
answer: "Partners, up 31% on Q2: more than twice the growth of the next channel.",
source: "Counted from billing: 1,284 paying accounts, Q3 2026 to date",
evidence: {
kind: "bars",
series: "Growth on Q2",
rows: [
{ label: "Partners", value: 0.31 },
{ label: "Direct", value: 0.14 },
{ label: "Marketplace", value: 0.06 },
],
},
},
{
id: "trial-conversion",
question: "How many trials converted in August?",
answer: "142 of 610 trials became paying accounts, 23.3%: the best month this year.",
source: "Counted from product events and billing, January to August 2026",
evidence: {
kind: "line",
series: "Trials converted",
rows: [
{ label: "Jan", value: 0.171 },
{ label: "Feb", value: 0.164 },
{ label: "Mar", value: 0.182 },
{ label: "Apr", value: 0.19 },
{ label: "May", value: 0.187 },
{ label: "Jun", value: 0.204 },
{ label: "Jul", value: 0.219 },
{ label: "Aug", value: 0.233 },
],
},
},
{
id: "refund-sources",
question: "Where did last month's refunds come from?",
answer: "Four accounts made up 61% of August's refunds, and three of them were on annual plans.",
source: "Counted from billing and support tickets, August 2026",
evidence: {
kind: "rows",
rows: [
{ account: "Harbour", plan: "Annual", amount: "$4,200", reason: "Seats unused" },
{ account: "Alder", plan: "Annual", amount: "$3,650", reason: "Moved to Team" },
{ account: "Solstice", plan: "Monthly", amount: "$2,100", reason: "Billing error" },
{ account: "Ironbark", plan: "Annual", amount: "$1,980", reason: "Seats unused" },
],
},
},
]npx shadcn@latest add @vibra/hero-04Hero that leads with its figures
hero-05A large claim and its calls over a row of four figures, each with what it counts, the window it covers and the twelve months it is the end of as a sparkline.
import * as React from "react"
import Link from "next/link"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { Sparkline } from "@/components/ui/sparkline"
import { EYEBROW, HEADLINE, LEDE, METRICS, PRIMARY_ACTION, SECONDARY_ACTION } from "./hero-05.data"
/**
* The opening that leads with its figures: the claim set large on the left,
* then, under one rule, four figures in a row — each the number, what it
* counts, the window it covers and the twelve months it is the end of, drawn
* as a sparkline so a figure is never a claim on its own. Rules, not boxes:
* the row is divided by hairlines, not tiles.
*
* The figures are a description list, so a reader hears each label with its
* number; the number is drawn above its label only visually. A server
* component; every word and figure is in `./hero-05.data`.
*/
export function Hero05() {
const titleId = React.useId()
return (
<section
data-section="hero-05"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto max-w-7xl">
<div className="max-w-4xl">
<p className="type-eyebrow">{EYEBROW}</p>
<h1 id={titleId} className="type-display mt-4 text-4xl text-balance sm:text-5xl md:text-6xl">
{HEADLINE}
</h1>
<p className="mt-6 max-w-2xl text-base/7 text-pretty text-muted-foreground md:text-lg/8">{LEDE}</p>
<div className="mt-8 flex flex-wrap items-center gap-3">
<Link href={PRIMARY_ACTION.href} className={cn(buttonVariants({ size: "lg" }), "h-10 px-5")}>
{PRIMARY_ACTION.label}
</Link>
<Link
href={SECONDARY_ACTION.href}
className={cn(buttonVariants({ variant: "outline", size: "lg" }), "h-10 px-5")}
>
{SECONDARY_ACTION.label}
</Link>
</div>
</div>
<dl className="mt-16 grid grid-cols-2 gap-x-6 gap-y-10 border-t border-rule pt-8 md:mt-20 md:gap-x-8 lg:grid-cols-4 lg:gap-x-0">
{METRICS.map((metric) => (
<div
key={metric.label}
className="flex flex-col lg:border-l lg:border-rule lg:px-6 lg:first:border-l-0 lg:first:pl-0 lg:last:pr-0"
>
<dt className="order-2 mt-2 text-sm font-medium text-foreground">{metric.label}</dt>
<dd className="order-1 type-numeral text-4xl md:text-5xl">{metric.value}</dd>
<dd className="order-3 mt-1 text-xs text-muted-foreground">{metric.note}</dd>
<dd className="order-4 mt-5">
<Sparkline data={metric.trend} type={metric.shape} height={44} />
</dd>
</div>
))}
</dl>
</div>
</section>
)
}
export default Hero05/**
* Every word `hero-05` sets, and the four figures it leads with. Each figure
* carries the twelve months it is the end of, so a number is never a claim on
* its own: the line under it shows how it got there. The figures are copy,
* written once here; the last point of each run is the figure it prints.
*/
export type HeroLink = { label: string; href: string }
export const EYEBROW = "Northwind Analytics"
export const HEADLINE = "The numbers 2,400 companies run their week on"
export const LEDE =
"From a five-person studio to a retailer with three thousand staff, teams count revenue, product usage and support in one place, and answer the board without opening a spreadsheet."
export const PRIMARY_ACTION: HeroLink = { label: "Start free trial", href: "/sign-up" }
export const SECONDARY_ACTION: HeroLink = { label: "Talk to sales", href: "/contact" }
/** A figure and its run: the last point of `trend` is the figure. */
export type Metric = {
value: string
label: string
/** What the figure counts and over which months: the meta line under it. */
note: string
trend: number[]
/** Bars for a count per period, a line for a level. */
shape: "line" | "bar"
}
export const METRICS: readonly Metric[] = [
{
value: "2,400",
label: "Companies reporting on Northwind",
note: "Paying workspaces, month by month for a year",
trend: [1480, 1560, 1630, 1720, 1790, 1850, 1930, 2010, 2090, 2180, 2290, 2400],
shape: "line",
},
{
value: "4.1B",
label: "Events counted last month",
note: "Billing, product and support events, per month",
trend: [2.2, 2.4, 2.5, 2.7, 2.8, 3.0, 3.1, 3.3, 3.5, 3.7, 3.9, 4.1],
shape: "bar",
},
{
value: "11 min",
label: "From sign-up to the first report",
note: "Median for new workspaces, per month",
trend: [26, 24, 23, 21, 19, 18, 17, 15, 14, 13, 12, 11],
shape: "line",
},
{
value: "4 min",
label: "Median first reply from support",
note: "Answered by people, seven days a week",
trend: [9, 8, 8, 7, 7, 6, 6, 5, 5, 5, 4, 4],
shape: "line",
},
]npx shadcn@latest add @vibra/hero-05Testimonial-led hero
hero-06An outcome as the claim, the average rating and how many reviews it comes from, and one customer's words beside it with the three things that changed, each before and after.
import * as React from "react"
import Link from "next/link"
import { ArrowRightIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { avatarFor } from "@/lib/avatars"
import { getInitials } from "@/lib/format"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { buttonVariants } from "@/components/ui/button"
import { LogoMark } from "@/components/ui/logo-mark"
import { Rating } from "@/components/ui/rating"
import { HEADLINE, LEDE, PRIMARY_ACTION, RATING, SECONDARY_ACTION, TESTIMONIAL } from "./hero-06.data"
/**
* The opening that lets a customer make the case: the claim is the outcome
* one of them had, the rating row says how many others agree, and beside it
* the customer says it in their own words — the quote, who said it and where
* they work, then the three things that changed, each as before and after.
*
* The quote is a `figure` holding a `blockquote` and its `figcaption`, so it
* is attributed the way a quotation should be — the person's face, from the
* kit's `avatarFor`, is decoration beside the name that says who it is; each
* result reads "from … to …" to a screen reader where a sighted reader sees an
* arrow. A server component; every word is in `./hero-06.data`.
*/
export function Hero06() {
const titleId = React.useId()
return (
<section
data-section="hero-06"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto grid max-w-7xl items-center gap-12 lg:grid-cols-12 lg:gap-16">
<div className="flex flex-col items-start lg:col-span-5">
<div className="flex flex-wrap items-center gap-x-2.5 gap-y-1">
<Rating value={RATING.value} max={RATING.max} readOnly fractional size="sm" label="Average rating" />
<p className="text-sm text-muted-foreground">
<span className="font-medium text-foreground tabular-nums">{RATING.value}</span> from{" "}
<span className="tabular-nums">{RATING.count}</span> by {RATING.from}
</p>
</div>
<h1 id={titleId} className="type-display mt-5 text-4xl text-balance sm:text-5xl">
{HEADLINE}
</h1>
<p className="mt-6 text-base/7 text-pretty text-muted-foreground md:text-lg/8">{LEDE}</p>
<div className="mt-8 flex flex-wrap items-center gap-3">
<Link href={PRIMARY_ACTION.href} className={cn(buttonVariants({ size: "lg" }), "h-10 px-5")}>
{PRIMARY_ACTION.label}
</Link>
<Link
href={SECONDARY_ACTION.href}
className={cn(buttonVariants({ variant: "outline", size: "lg" }), "h-10 px-5")}
>
{SECONDARY_ACTION.label}
</Link>
</div>
</div>
<figure className="frame flex flex-col p-6 md:p-10 lg:col-span-7">
<LogoMark name={TESTIMONIAL.company} size="lg" className="text-foreground" />
<blockquote className="mt-8 text-xl/8 font-medium text-pretty md:text-2xl/9">
<p>“{TESTIMONIAL.quote}”</p>
</blockquote>
<figcaption className="mt-6 flex items-center gap-3 text-sm">
<Avatar size="lg">
<AvatarImage src={avatarFor(TESTIMONIAL.name)} alt="" />
<AvatarFallback>{getInitials(TESTIMONIAL.name)}</AvatarFallback>
</Avatar>
<p>
<span className="font-medium text-foreground">{TESTIMONIAL.name}</span>
<span className="text-muted-foreground">
, {TESTIMONIAL.role} at {TESTIMONIAL.company}
</span>
</p>
</figcaption>
<dl className="mt-8 grid gap-4 border-t border-border pt-6 sm:grid-cols-3 sm:gap-6">
{TESTIMONIAL.results.map((result) => (
<div key={result.label} className="flex flex-col gap-1.5">
<dt className="text-xs text-muted-foreground">{result.label}</dt>
<dd className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm">
<span className="sr-only">from </span>
<span className="text-muted-foreground tabular-nums">{result.before}</span>
<ArrowRightIcon aria-hidden="true" className="size-3.5 shrink-0 text-muted-foreground" />
<span className="sr-only"> to </span>
<span className="font-semibold text-foreground tabular-nums">{result.after}</span>
</dd>
</div>
))}
</dl>
</figure>
</div>
</section>
)
}
export default Hero06/**
* Every word `hero-06` sets. The hero leads with one customer's words and
* what changed for them, so the quote, the person and the before-and-after
* figures are the copy that matters most here. The customer is one of the
* kit's fictional companies, drawn by `LogoMark`; replace them with a real
* customer who said this, never with one who did not.
*/
import type { LogoName } from "@/components/ui/logo-mark"
export type HeroLink = { label: string; href: string }
/** The score the rating row reads out, and what it is an average of. */
export const RATING = { value: 4.8, max: 5, count: "1,240 reviews", from: "finance and operations teams" }
export const HEADLINE = "Close the month in two days, not six"
export const LEDE =
"Northwind keeps revenue, usage and refunds reconciled while the month runs, so finance starts the close with the numbers already agreed instead of rebuilding them."
export const PRIMARY_ACTION: HeroLink = { label: "Start free trial", href: "/sign-up" }
export const SECONDARY_ACTION: HeroLink = { label: "Read Meridian's story", href: "/customers/meridian" }
/** One customer, in their own words, and the three things that changed. */
export const TESTIMONIAL: {
quote: string
name: string
role: string
company: LogoName
results: readonly { label: string; before: string; after: string }[]
} = {
quote:
"The first week of every month used to go on rebuilding the same reports. Now the numbers are ready when we are, and the board pack takes an afternoon.",
name: "Priya Raman",
role: "VP Finance",
company: "Meridian",
results: [
{ label: "Month-end close", before: "6 days", after: "2 days" },
{ label: "Reports rebuilt by hand", before: "40 a month", after: "None" },
{ label: "Board pack", before: "3 days", after: "An afternoon" },
],
}npx shadcn@latest add @vibra/hero-06Waitlist hero
hero-07A product's status, its claim and lede, one email field that refuses an empty or malformed address and confirms a good one, and who is already on the list.
import * as React from "react"
import { Badge } from "@/components/ui/badge"
import { LogoMark } from "@/components/ui/logo-mark"
import { WaitlistForm } from "./hero-07.client"
import { FORM, HEADLINE, LEDE, STATUS, WAITING } from "./hero-07.data"
/**
* The opening for a product that is not open yet: where it stands, the claim,
* what it will do, and one field that puts the reader on the list — with its
* refusals and its confirmation in its own words — then who is already
* waiting. Centred and narrow, because a waitlist asks for one thing.
*
* The form is the one island; this file is a server component and every word,
* the form's messages included, is in `./hero-07.data`.
*/
export function Hero07() {
const titleId = React.useId()
const waitingId = React.useId()
return (
<section
data-section="hero-07"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto flex max-w-7xl flex-col items-center text-center">
<p className="inline-flex max-w-full flex-wrap items-center justify-center gap-x-2 gap-y-1 text-sm text-muted-foreground">
<Badge variant="outline">{STATUS.tag}</Badge>
{STATUS.text}
</p>
<h1 id={titleId} className="type-display mt-6 max-w-3xl text-4xl text-balance sm:text-5xl md:text-6xl">
{HEADLINE}
</h1>
<p className="mt-6 max-w-2xl text-base/7 text-pretty text-muted-foreground md:text-lg/8">{LEDE}</p>
<div className="mt-10 flex w-full justify-center">
<WaitlistForm copy={FORM} />
</div>
<div className="mt-14 w-full max-w-3xl border-t border-rule pt-8">
<p id={waitingId} className="text-sm text-muted-foreground tabular-nums">
{WAITING.caption}
</p>
<ul
aria-labelledby={waitingId}
className="mt-5 grid grid-cols-2 place-items-center gap-x-6 gap-y-5 text-muted-foreground sm:grid-cols-4"
>
{WAITING.names.map((name) => (
<li key={name}>
<LogoMark name={name} />
</li>
))}
</ul>
</div>
</div>
</section>
)
}
export default Hero07"use client"
import * as React from "react"
import { CircleCheckIcon } from "lucide-react"
import { isEmail } from "@/lib/validation"
import { Button } from "@/components/ui/button"
import { FieldError } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
/** The form's words, handed over by the section: see `FORM` in the data file. */
export type WaitlistCopy = {
label: string
placeholder: string
submit: string
hint: string
empty: string
invalid: string
/** The confirmation, with `{email}` where the accepted address goes. */
joined: string
}
/**
* One field and one button. The browser's own validation is off so the form
* can say what is wrong in its own words: an empty field and a malformed
* address each get a message tied to the field, which is marked invalid and
* takes focus back. An accepted address clears the field and is confirmed in
* the status line under it, which is always mounted so the confirmation is
* announced when it arrives. The refusal and the confirmation never share a
* slot.
*
* It holds no list of its own: wire `onSubmit` to your provider where the
* address is accepted.
*/
export function WaitlistForm({ copy }: { copy: WaitlistCopy }) {
const inputId = React.useId()
const hintId = React.useId()
const errorId = React.useId()
const inputRef = React.useRef<HTMLInputElement>(null)
const [error, setError] = React.useState<string | null>(null)
const [joined, setJoined] = React.useState<string | null>(null)
function submit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const email = String(new FormData(event.currentTarget).get("email") ?? "").trim()
const problem = email === "" ? copy.empty : isEmail(email) ? null : copy.invalid
setError(problem)
if (problem) {
setJoined(null)
inputRef.current?.focus()
return
}
setJoined(email)
event.currentTarget.reset()
}
return (
<form noValidate onSubmit={submit} className="flex w-full max-w-md flex-col gap-2 text-left">
<Label htmlFor={inputId}>{copy.label}</Label>
<div className="flex flex-col gap-2 sm:flex-row">
<Input
ref={inputRef}
id={inputId}
name="email"
type="email"
autoComplete="email"
placeholder={copy.placeholder}
aria-invalid={error ? true : undefined}
aria-describedby={error ? `${errorId} ${hintId}` : hintId}
className="h-10 sm:flex-1"
/>
<Button type="submit" size="lg" className="h-10 px-5">
{copy.submit}
</Button>
</div>
{error ? <FieldError id={errorId}>{error}</FieldError> : null}
<p id={hintId} className="text-xs text-muted-foreground">
{copy.hint}
</p>
<p role="status" aria-live="polite" className="text-sm">
{joined ? (
<span className="inline-flex items-start gap-2 text-foreground">
<CircleCheckIcon aria-hidden="true" className="mt-0.5 size-4 shrink-0 text-success" />
{copy.joined.replace("{email}", joined)}
</span>
) : null}
</p>
</form>
)
}/**
* Every word `hero-07` sets: a product that is not open yet, what it will do,
* and the one field that puts a reader on the list — its label, its promise,
* and what it says when the address is missing, malformed or accepted. The
* form's messages are copy like the headline, so they live here too.
*/
import type { LogoName } from "@/components/ui/logo-mark"
/** Where the product stands, above the headline. */
export const STATUS = { tag: "Private beta", text: "Invites go out every week from October" }
export const HEADLINE = "Forecasts that show their working"
export const LEDE =
"Northwind Forecasts projects revenue, churn and hiring from the numbers you already report on, and lists the assumption behind every line so finance can argue with it."
/**
* The form. `{email}` in `joined` is replaced with the address that was
* accepted, so the confirmation names what was actually sent.
*/
export const FORM = {
label: "Work email",
placeholder: "you@example.com",
submit: "Join the waitlist",
hint: "One email when your invite is ready, and nothing else.",
empty: "Enter your work email to join the waitlist.",
invalid: "Enter an email address with an @ and a domain, like ada@keystone.example.",
joined: "You are on the list. We will write to {email} when your invite is ready.",
}
/** Who is already waiting: a count and a few of the kit's fictional companies. */
export const WAITING: { caption: string; names: readonly LogoName[] } = {
caption: "3,180 teams are on the list, including",
names: ["Kestrel", "Harbour", "Solstice", "Northgate"],
}npx shadcn@latest add @vibra/hero-07Release announcement hero
hero-08Set like a release note: the version, the day it shipped and who gets it in a rail beside the claim, and under the calls the three changes it brings with the plans each reaches.
import * as React from "react"
import Link from "next/link"
import { ArrowRightIcon, BellRingIcon, GaugeIcon, TrendingUpIcon, type LucideIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import {
CHANGES,
HEADLINE,
LEDE,
PRIMARY_ACTION,
RELEASE,
SECONDARY_ACTION,
type ChangeIcon,
} from "./hero-08.data"
/** The picture each change is drawn with: vocabulary of the layout, not copy. */
const ICONS: Record<ChangeIcon, LucideIcon> = {
forecast: TrendingUpIcon,
alert: BellRingIcon,
engine: GaugeIcon,
}
/**
* The opening that is an announcement: set like a release note, with the
* release's own facts in a rail beside the claim — its version large, the
* day it shipped, who gets it and where the notes are — and, under the claim
* and its two calls, what is in it: three changes, each with the plans it
* reaches. On a phone the rail folds into one line above the headline.
*
* A server component; every word is in `./hero-08.data`.
*/
export function Hero08() {
const titleId = React.useId()
return (
<section
data-section="hero-08"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto grid max-w-7xl gap-8 lg:grid-cols-12 lg:gap-16">
<div className="flex flex-wrap items-baseline gap-x-4 gap-y-2 lg:col-span-3 lg:flex-col lg:flex-nowrap lg:items-start lg:gap-0 lg:border-r lg:border-rule lg:pr-10">
<p className="type-eyebrow max-lg:sr-only">{RELEASE.eyebrow}</p>
<p className="type-numeral text-3xl lg:mt-2 lg:text-6xl">{RELEASE.version}</p>
<dl className="flex flex-wrap gap-x-4 gap-y-1 text-sm lg:mt-6 lg:flex-col lg:gap-3">
<div className="flex gap-1.5 lg:flex-col lg:gap-0.5">
<dt className="text-muted-foreground">Shipped</dt>
<dd className="font-medium">{RELEASE.date}</dd>
</div>
<div className="flex gap-1.5 max-sm:hidden lg:flex lg:flex-col lg:gap-0.5">
<dt className="text-muted-foreground">Available</dt>
<dd className="font-medium">{RELEASE.availability}</dd>
</div>
</dl>
<Link
href={RELEASE.notes.href}
className={cn(buttonVariants({ variant: "link" }), "group/notes h-auto px-0 lg:mt-6")}
>
{RELEASE.notes.label}
<ArrowRightIcon
aria-hidden="true"
data-icon="inline-end"
className="transition-transform duration-(--duration-fast) ease-(--ease-standard) group-hover/notes:translate-x-0.5"
/>
</Link>
</div>
<div className="lg:col-span-9">
<h1 id={titleId} className="type-display max-w-4xl text-4xl text-balance sm:text-5xl md:text-6xl">
{HEADLINE}
</h1>
<p className="mt-6 max-w-2xl text-base/7 text-pretty text-muted-foreground md:text-lg/8">{LEDE}</p>
<div className="mt-8 flex flex-wrap items-center gap-3">
<Link href={PRIMARY_ACTION.href} className={cn(buttonVariants({ size: "lg" }), "h-10 px-5")}>
{PRIMARY_ACTION.label}
</Link>
<Link
href={SECONDARY_ACTION.href}
className={cn(buttonVariants({ variant: "outline", size: "lg" }), "h-10 px-5")}
>
{SECONDARY_ACTION.label}
</Link>
</div>
<ul className="mt-12 grid gap-8 border-t border-rule pt-8 md:mt-16 md:grid-cols-3 md:gap-10">
{CHANGES.map((change) => {
const Icon = ICONS[change.icon]
return (
<li key={change.title} className="flex flex-col">
<span className="flex size-9 items-center justify-center rounded-lg bg-surface text-foreground ring-1 ring-border">
<Icon aria-hidden="true" className="size-4" />
</span>
<p className="mt-4 text-sm font-semibold text-foreground">{change.title}</p>
<p className="mt-1.5 text-sm text-pretty text-muted-foreground">{change.body}</p>
<p className="mt-3 text-xs text-muted-foreground">{change.plans}</p>
</li>
)
})}
</ul>
</div>
</div>
</section>
)
}
export default Hero08/**
* Every word `hero-08` sets: a release, and what is in it. The hero is the
* announcement, so the version, the day it shipped and who gets it lead, and
* each change carries the plans it is in. `icon` names a picture the layout
* draws — the words live here, the drawing with the component.
*/
export type HeroLink = { label: string; href: string }
export const RELEASE = {
eyebrow: "Release",
version: "4.0",
/** The day it shipped, as the page prints it: before the kit's "now", which is September 4. */
date: "September 1, 2026",
availability: "In every plan from today",
notes: { label: "Read the release notes", href: "/changelog/4-0" } satisfies HeroLink,
}
export const HEADLINE = "Forecasts, alerts, and reports that open in under a second"
export const LEDE =
"Northwind 4.0 is the largest release since launch. It projects the rest of the quarter from your own history, tells you when a metric moves for a reason worth knowing, and runs every report on a new engine."
export const PRIMARY_ACTION: HeroLink = { label: "Start free trial", href: "/sign-up" }
export const SECONDARY_ACTION: HeroLink = { label: "Watch the four-minute tour", href: "/changelog/4-0#tour" }
export type ChangeIcon = "forecast" | "alert" | "engine"
export type Change = { icon: ChangeIcon; title: string; body: string; plans: string }
export const CHANGES: readonly Change[] = [
{
icon: "forecast",
title: "Forecasts",
body: "The rest of the quarter, projected from your own history, with the assumption behind every line listed under it.",
plans: "Pro and Enterprise",
},
{
icon: "alert",
title: "Anomaly alerts",
body: "A message when a metric moves more than it usually does, with the accounts that moved it attached.",
plans: "Every plan",
},
{
icon: "engine",
title: "A faster engine",
body: "A report over a year of events now opens in under a second, down from eight on the old engine.",
plans: "Every plan",
},
]npx shadcn@latest add @vibra/hero-08Storefront hero with featured products
hero-09A store's claim, its delivery, returns and warranty promises, and three products as a mosaic of interactive cards, the first drawn large, each with its drawing, note and price.
import * as React from "react"
import Link from "next/link"
import { RotateCcwIcon, ShieldCheckIcon, TruckIcon, type LucideIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { Card, CardContent, CardFooter } from "@/components/ui/card"
import { Price } from "@/components/ui/price"
import { ProductArt } from "@/components/ui/product-art"
import {
EYEBROW,
FEATURED,
HEADLINE,
LEDE,
PRIMARY_ACTION,
PROMISES,
SECONDARY_ACTION,
productHref,
type PromiseIcon,
} from "./hero-09.data"
const PROMISE_ICONS: Record<PromiseIcon, LucideIcon> = {
delivery: TruckIcon,
returns: RotateCcwIcon,
warranty: ShieldCheckIcon,
}
/**
* A store's opening: the claim, what every order comes with, and the three
* products the store leads with as a mosaic — the first drawn large beside
* the other two, or above them on a phone — each the kit's interactive card
* with the product drawn by `ProductArt`, its name, what sets it apart and
* its price. No photograph: the drawings take the reader's palette and never
* go stale.
*
* Each card is one stretched link, so a keyboard stops once per product and
* hears its name; the focus ring is drawn on the frame. A server component;
* every word and price is in `./hero-09.data`.
*/
export function Hero09() {
const titleId = React.useId()
return (
<section
data-section="hero-09"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto grid max-w-7xl items-center gap-12 lg:grid-cols-12 lg:gap-16">
<div className="flex flex-col items-start lg:col-span-5">
<p className="type-eyebrow">{EYEBROW}</p>
<h1 id={titleId} className="type-display mt-4 text-4xl text-balance sm:text-5xl">
{HEADLINE}
</h1>
<p className="mt-6 text-base/7 text-pretty text-muted-foreground md:text-lg/8">{LEDE}</p>
<div className="mt-8 flex flex-wrap items-center gap-3">
<Link href={PRIMARY_ACTION.href} className={cn(buttonVariants({ size: "lg" }), "h-10 px-5")}>
{PRIMARY_ACTION.label}
</Link>
<Link
href={SECONDARY_ACTION.href}
className={cn(buttonVariants({ variant: "outline", size: "lg" }), "h-10 px-5")}
>
{SECONDARY_ACTION.label}
</Link>
</div>
<ul className="mt-8 flex flex-col gap-2.5 text-sm text-muted-foreground sm:flex-row sm:flex-wrap sm:gap-x-6">
{PROMISES.map((promise) => {
const Icon = PROMISE_ICONS[promise.icon]
return (
<li key={promise.text} className="inline-flex items-center gap-2">
<Icon aria-hidden="true" className="size-4 shrink-0 text-foreground" />
{promise.text}
</li>
)
})}
</ul>
</div>
<ul className="grid grid-cols-2 gap-3 md:gap-4 lg:col-span-7 lg:grid-cols-3">
{FEATURED.map((product, index) => (
<li key={product.id} className={cn("flex", index === 0 && "col-span-2 lg:row-span-2")}>
<Card
variant="interactive"
className="relative w-full has-[a:focus-visible]:focus-outline"
>
<CardContent className={cn("relative p-0", index === 0 ? "flex-1" : "flex-none")}>
{/* The large tile is wider than tall on a phone and taller
than wide beside the other two; either way its drawing
fits the shorter side. */}
<ProductArt
name={product.name}
category={product.category}
className={cn(
"rounded-none",
index === 0 && "aspect-[4/3] lg:aspect-auto lg:h-full"
)}
/>
</CardContent>
<CardFooter className="mt-0 flex-col items-stretch gap-0.5 pt-2 pb-3">
<p className="text-sm font-semibold text-foreground">
<Link href={productHref(product)} className="outline-none after:absolute after:inset-0">
{product.name}
</Link>
</p>
<p className="text-xs text-muted-foreground max-sm:hidden">{product.note}</p>
<Price amount={product.price} size="sm" className="mt-1" />
</CardFooter>
</Card>
</li>
))}
</ul>
</div>
</section>
)
}
export default Hero09/**
* Every word `hero-09` sets: the store's claim, its promises, and the three
* products it opens on. The products are the sample store's own — the names,
* departments and prices of three rows in its catalogue, written out as copy
* so the hero installs without the store — and each is drawn by `ProductArt`
* from its name. Replace them with the three things your store leads with.
*/
export type HeroLink = { label: string; href: string }
export const EYEBROW = "Northwind Store"
export const HEADLINE = "A better desk setup, delivered in two days"
export const LEDE =
"Desks, chairs and screens chosen for long working days, tested in our own office before they reach the catalogue, and shipped from our warehouse the day you order."
export const PRIMARY_ACTION: HeroLink = { label: "Shop the office range", href: "/collections/office" }
export const SECONDARY_ACTION: HeroLink = { label: "See what's new", href: "/collections/new" }
export type PromiseIcon = "delivery" | "returns" | "warranty"
/** What every order comes with; `icon` names a picture the layout draws. */
export const PROMISES: readonly { icon: PromiseIcon; text: string }[] = [
{ icon: "delivery", text: "Free delivery over $75" },
{ icon: "returns", text: "30-day returns" },
{ icon: "warranty", text: "Two-year warranty" },
]
/** A product the hero shows: the first is drawn large. */
export type FeaturedProduct = { id: string; name: string; category: string; price: number; note: string }
export const FEATURED: readonly FeaturedProduct[] = [
{ id: "prd_0026", name: "Standing Desk", category: "Office", price: 549, note: "Height memory, four presets" },
{ id: "prd_0005", name: "Ergonomic Office Chair", category: "Office", price: 179, note: "Adjustable lumbar support" },
{ id: "prd_0060", name: "27-inch Monitor", category: "Electronics", price: 329, note: "USB-C, one cable to the laptop" },
]
/** Where a product's card leads: its page in your store. */
export function productHref(product: FeaturedProduct): string {
return `/products/${product.id}`
}npx shadcn@latest add @vibra/hero-09Hero with connected sources
hero-10The claim beside a diagram: four sources a company already runs, each with when it last synced, joined by one rule into the figure only they give together, with the two inputs it is divided from.
import * as React from "react"
import Link from "next/link"
import {
ActivityIcon,
ArrowDownIcon,
BriefcaseIcon,
ChevronRightIcon,
LifeBuoyIcon,
ReceiptTextIcon,
type LucideIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { formatCurrency, formatNumber } from "@/lib/format"
import { buttonVariants } from "@/components/ui/button"
import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { LogoMark } from "@/components/ui/logo-mark"
import { MetricDelta } from "@/components/ui/metric-delta"
import {
EYEBROW,
HEADLINE,
LEDE,
PRIMARY_ACTION,
RESULT,
SECONDARY_ACTION,
SOURCES,
SOURCES_LABEL,
result,
type SourceIcon,
} from "./hero-10.data"
const SOURCE_ICONS: Record<SourceIcon, LucideIcon> = {
billing: ReceiptTextIcon,
crm: BriefcaseIcon,
product: ActivityIcon,
support: LifeBuoyIcon,
}
/**
* The opening that explains the product as a diagram: four sources a company
* already runs, each with when it last synced, joined by one rule into the
* figure only the sources together can give — the kit's own Card with its pill
* delta, and the two inputs it is divided from. The claim reads first; from
* `lg` the diagram is drawn on its left.
*
* The sources are a named list and the card a card, so both read as content;
* only the connecting rules are hidden. They are drawn from the list's own
* box — the bus runs from the first row's middle to the last row's — so they
* meet every tile at any height. Below `md` the tiles go two up over an arrow.
* A server component; every word and figure is in `./hero-10.data`.
*/
export function Hero10() {
const titleId = React.useId()
const figure = result()
return (
<section
data-section="hero-10"
data-slot="section"
aria-labelledby={titleId}
className="w-full bg-background px-4 py-16 text-foreground md:px-6 md:py-24"
>
<div className="mx-auto grid max-w-7xl items-center gap-12 lg:grid-cols-12 lg:gap-16">
<div className="flex flex-col items-start lg:col-span-5">
<p className="type-eyebrow">{EYEBROW}</p>
<h1 id={titleId} className="type-display mt-4 text-4xl text-balance sm:text-5xl">
{HEADLINE}
</h1>
<p className="mt-6 text-base/7 text-pretty text-muted-foreground md:text-lg/8">{LEDE}</p>
<div className="mt-8 flex flex-wrap items-center gap-3">
<Link href={PRIMARY_ACTION.href} className={cn(buttonVariants({ size: "lg" }), "h-10 px-5")}>
{PRIMARY_ACTION.label}
</Link>
<Link
href={SECONDARY_ACTION.href}
className={cn(buttonVariants({ variant: "outline", size: "lg" }), "h-10 px-5")}
>
{SECONDARY_ACTION.label}
</Link>
</div>
</div>
<div className="grid items-center gap-4 md:grid-cols-2 md:gap-0 lg:order-first lg:col-span-7">
<div className="relative md:pe-12">
<ul aria-label={SOURCES_LABEL} className="grid grid-cols-2 gap-3 md:auto-rows-fr md:grid-cols-1">
{SOURCES.map((source) => {
const Icon = SOURCE_ICONS[source.icon]
return (
<li
key={source.name}
className="panel relative flex items-start gap-3 p-3 md:after:absolute md:after:top-1/2 md:after:start-full md:after:h-px md:after:w-6 md:after:bg-rule"
>
<span className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-surface ring-1 ring-border">
<Icon aria-hidden="true" className="size-4" />
</span>
<div className="flex min-w-0 flex-col gap-0.5">
<p className="text-sm font-medium">{source.name}</p>
<p className="text-xs text-muted-foreground max-sm:hidden">{source.detail}</p>
<p className="text-xs text-muted-foreground tabular-nums">{source.synced}</p>
</div>
</li>
)
})}
</ul>
<div aria-hidden="true" className="absolute inset-y-0 end-0 hidden w-12 md:block">
<span className="absolute top-[calc((100%-2.25rem)/8)] bottom-[calc((100%-2.25rem)/8)] start-6 w-px bg-rule" />
<span className="absolute top-1/2 end-1 start-6 h-px bg-rule" />
<ChevronRightIcon className="absolute top-1/2 -end-1 size-4 -translate-y-1/2 text-faint-foreground rtl:rotate-180" />
</div>
</div>
<ArrowDownIcon aria-hidden="true" className="mx-auto size-4 text-faint-foreground md:hidden" />
<Card>
<CardHeader>
<CardTitle>{RESULT.title}</CardTitle>
<CardDescription>{RESULT.description}</CardDescription>
<CardAction>
<LogoMark name="Northwind" size="sm" className="text-muted-foreground" />
</CardAction>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
<p className="type-numeral text-4xl">{formatCurrency(figure.value, "USD", { maximumFractionDigits: 0 })}</p>
<MetricDelta value={figure.delta} variant="pill" size="sm" />
<p className="w-full text-xs text-muted-foreground">{RESULT.window}</p>
</div>
<dl className="flex flex-col border-t border-border text-sm">
{RESULT.inputs.map((input) => (
<div key={input.label} className="flex items-baseline gap-3 border-b border-border py-2.5 last:border-b-0 last:pb-0">
<dt className="flex min-w-0 flex-1 flex-col">
<span>{input.label}</span>
<span className="text-xs text-muted-foreground">from {input.source}</span>
</dt>
<dd className="font-medium tabular-nums">
{input.kind === "money"
? formatCurrency(input.value, "USD", { compact: true })
: formatNumber(input.value)}
</dd>
</div>
))}
</dl>
</CardContent>
</Card>
</div>
</div>
</section>
)
}
export default Hero10/**
* Every word `hero-10` sets, and what its diagram shows: four sources a
* company already runs, when each last synced, and the one figure Northwind
* makes of them together. Sources are named by what they are, never by a
* vendor, and `icon` names a picture the layout draws. The figure is counted
* from its two inputs by `result()`, so it cannot disagree with them.
*/
export type HeroLink = { label: string; href: string }
export const EYEBROW = "Integrations"
export const HEADLINE = "Billing, CRM and product data, finally in one report"
export const LEDE =
"Connect the tools your teams already use. Northwind syncs them every few minutes and matches every record to the account it belongs to, so a figure can join two systems without a spreadsheet in between."
export const PRIMARY_ACTION: HeroLink = { label: "Start free trial", href: "/sign-up" }
export const SECONDARY_ACTION: HeroLink = { label: "Browse 60 integrations", href: "/integrations" }
export type SourceIcon = "billing" | "crm" | "product" | "support"
export type Source = { icon: SourceIcon; name: string; detail: string; synced: string }
export const SOURCES_LABEL = "Connected sources"
export const SOURCES: readonly Source[] = [
{ icon: "billing", name: "Billing", detail: "Invoices, plans and refunds", synced: "Synced 2 min ago" },
{ icon: "crm", name: "CRM", detail: "Deals, owners and stages", synced: "Synced 4 min ago" },
{ icon: "product", name: "Product events", detail: "Sign-ins, seats and features", synced: "Streaming" },
{ icon: "support", name: "Support desk", detail: "Tickets and first replies", synced: "Synced 6 min ago" },
]
/**
* The figure only the sources together can give: billing's revenue divided by
* the accounts the product saw active, over the same eight weeks. The figure
* and its change are counted from these inputs by `result()`, never typed.
*/
export const RESULT = {
title: "Revenue per active account",
description: "Billing revenue over the accounts active in the product",
window: "Last 8 weeks",
inputs: [
{ label: "Billing revenue", source: "Billing", value: 408_312, kind: "money" as const },
{ label: "Active accounts", source: "Product events", value: 1_284, kind: "count" as const },
],
/** The same figure over the eight weeks before, for the change. */
previous: 284,
}
/** The figure, and how far it moved on the eight weeks before. */
export function result(inputs = RESULT.inputs, previous = RESULT.previous) {
const [revenue, accounts] = inputs
const value = accounts.value > 0 ? revenue.value / accounts.value : 0
return { value, delta: previous > 0 ? (value - previous) / previous : 0 }
}npx shadcn@latest add @vibra/hero-10