Features
What the product does, as a grid, a list or alternating rows of claims with their proof.
Capability grid
features-01Six capabilities under one claim, each an icon, a title, a sentence and the fact that backs it, on a ruled three-up grid.
import * as React from "react"
import {
BellRingIcon,
CableIcon,
MessageSquareTextIcon,
ShieldCheckIcon,
SigmaIcon,
TrendingUpIcon,
type LucideIcon,
} from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { EYEBROW, FEATURES, HEADLINE, LEDE, type FeatureIcon } from "./features-01.data"
/** The picture each capability is drawn with: vocabulary of the layout, not copy. */
const ICONS: Record<FeatureIcon, LucideIcon> = {
sources: CableIcon,
definitions: SigmaIcon,
ask: MessageSquareTextIcon,
alerts: BellRingIcon,
forecast: TrendingUpIcon,
permissions: ShieldCheckIcon,
}
/**
* The overview: what the product does, as six capabilities under one claim.
* Each is an icon, a title, one sentence and the fact that backs it, and the
* grid is ruled rather than boxed — a rule over each item, so six claims read
* as one page of a catalogue instead of six competing cards. The facts sit on
* the last line of every item, so across a row they land on one baseline.
*
* One column on a phone, two from `sm`, three from `lg`. A server component;
* every word is in `./features-01.data`.
*/
export function Features01() {
const titleId = React.useId()
return (
<section
data-section="features-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">
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-4 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
<p className="mt-5 max-w-2xl text-base/7 text-pretty text-muted-foreground md:text-lg/8">{LEDE}</p>
</div>
<ul className="mt-12 grid gap-x-8 gap-y-10 sm:grid-cols-2 md:mt-16 lg:grid-cols-3 lg:gap-x-12 lg:gap-y-14">
{FEATURES.map((feature) => {
const Icon = ICONS[feature.icon]
return (
// On a phone the icon stands beside the title, not above it:
// six items stacked would otherwise spend a screen on icons.
<li
key={feature.title}
className="grid grid-cols-[auto_minmax(0,1fr)] content-start gap-x-4 border-t border-rule pt-6 sm:flex sm:flex-col"
>
<span className="row-span-3 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>
<h3 className="self-center text-base font-semibold text-balance sm:mt-5 sm:self-auto">{feature.title}</h3>
<p className="mt-2 text-sm/6 text-pretty text-muted-foreground">{feature.body}</p>
<p className="pt-4 sm:mt-auto sm:pt-5">
<Badge variant="secondary" className="h-auto min-h-5 whitespace-normal">
{feature.fact}
</Badge>
</p>
</li>
)
})}
</ul>
</div>
</section>
)
}
export default Features01/**
* Every word `features-01` sets, typed, so the layout can only arrange it.
* The product is the sample company's own — Northwind, the analytics
* workspace every dashboard in the kit is — and each capability carries the
* one fact that makes it more than a claim. Replace the six with your own and
* the grid says them.
*/
/** The picture each capability is drawn with: vocabulary of the layout, not copy. */
export type FeatureIcon = "sources" | "definitions" | "ask" | "alerts" | "forecast" | "permissions"
export type Feature = {
icon: FeatureIcon
title: string
body: string
/** The specific that backs the claim: a number, a limit, a plan. */
fact: string
}
export const EYEBROW = "What Northwind does"
export const HEADLINE = "Everything the weekly review needs, in one workspace"
export const LEDE =
"Northwind connects the tools your company already runs on, keeps one definition of every metric, and tells you when a number moves before anyone has to ask why."
export const FEATURES: readonly Feature[] = [
{
icon: "sources",
title: "Every source, joined on the account",
body: "Billing, CRM, product usage and support are matched to the same customer, so an account is one row wherever it appears.",
fact: "40 read-only connectors",
},
{
icon: "definitions",
title: "One definition per metric",
body: "MRR, churn and net retention are written once, reviewed before they change, and used by every chart that shows them.",
fact: "Versioned, each with an owner",
},
{
icon: "ask",
title: "Questions in plain language",
body: "Ask the way you would ask a colleague and get the chart, the rows behind it and the query that produced them.",
fact: "Every answer cites its source",
},
{
icon: "alerts",
title: "Alerts that say why",
body: "When a figure leaves its usual range, Northwind posts the change and the accounts behind it to email or your team's chat.",
fact: "Checked after every sync",
},
{
icon: "forecast",
title: "Forecasts you can audit",
body: "Revenue projections built from open pipeline and upcoming renewals, with every assumption listed beside the line.",
fact: "Rebuilt every night",
},
{
icon: "permissions",
title: "Permissions down to the row",
body: "Share a dashboard with a partner or a regional team and they see only their own accounts. Every view is logged.",
fact: "SSO and audit log on Business",
},
]npx shadcn@latest add @vibra/features-01Alternating rows with product pictures
features-02Three claims, each with its sentence, three specifics and a link, beside the widget it is about — joined accounts, a forecast with its assumptions, an alert with its causes — alternating sides.
import * as React from "react"
import Link from "next/link"
import { ArrowRightIcon, BellRingIcon, CheckIcon, DatabaseIcon, SendIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { formatCurrency } from "@/lib/format"
import { buttonVariants } from "@/components/ui/button"
import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { LineChart } from "@/components/ui/line-chart"
import { LogoMark } from "@/components/ui/logo-mark"
import { SimpleTable } from "@/components/ui/simple-table"
import { StatusBadge } from "@/components/ui/status-badge"
import { EYEBROW, HEADLINE, LEDE, ROWS, forecastTotal, type AccountRow, type Row } from "./features-02.data"
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: "seats", header: "Active seats", align: "right" as const },
{ key: "tickets", header: "Tickets", align: "right" as const },
{ key: "mrr", header: "MRR", align: "right" as const },
]
/** The accounts widget: one row per customer, its columns from four sources. */
function AccountsPicture({ card }: { card: Extract<Row, { kind: "accounts" }>["card"] }) {
return (
<Card>
<CardHeader>
<CardTitle>{card.title}</CardTitle>
<CardDescription>{card.description}</CardDescription>
</CardHeader>
<CardContent className="p-0">
<SimpleTable columns={ACCOUNT_COLUMNS} rows={[...card.rows]} rowKey="account" size="sm" className="rounded-none border-0" />
</CardContent>
<CardFooter className="items-start text-xs text-muted-foreground">
<DatabaseIcon className="mt-px size-3.5 shrink-0" />
{card.sources}
</CardFooter>
</Card>
)
}
/** The forecast widget: the quarter's figure, its weeks booked then projected, and what it assumes. */
function ForecastPicture({ card }: { card: Extract<Row, { kind: "forecast" }>["card"] }) {
// The band starts on the week the two lines meet, so it runs on from the
// last booked point without a gap.
const from = card.weeks.find((week) => week.projected !== null)?.week ?? ""
const to = card.weeks.at(-1)?.week ?? ""
return (
<Card>
<CardHeader>
<CardTitle>{card.title}</CardTitle>
<CardDescription>{card.description}</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<p className="type-numeral text-3xl">
{formatCurrency(forecastTotal(card.weeks), "USD", { compact: true, maximumFractionDigits: 2 })}
</p>
<p className="text-sm text-muted-foreground tabular-nums">{card.range}</p>
</div>
{/* Booked is the line; projected is drawn as its dashed continuation
over a band that says where the booking stops. */}
<LineChart
data={[...card.weeks]}
index="week"
series={[{ key: "booked", label: "Booked", compareKey: "projected" }]}
compare
baseline="auto"
height={148}
showLegend={false}
showYAxis={false}
showTooltip={false}
annotations={[{ kind: "band", from, to, label: "Projected" }]}
/>
<dl className="grid grid-cols-3 gap-3 border-t border-border pt-4">
{card.assumptions.map((assumption) => (
<div key={assumption.label} className="flex min-w-0 flex-col gap-0.5">
<dt className="text-xs text-muted-foreground">{assumption.label}</dt>
<dd className="text-sm font-medium tabular-nums">{assumption.value}</dd>
</div>
))}
</dl>
</CardContent>
</Card>
)
}
/** The alert as it lands: the change, its usual range, and the accounts behind it. */
function AlertPicture({ card }: { card: Extract<Row, { kind: "alert" }>["card"] }) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BellRingIcon className="size-4 shrink-0 text-muted-foreground" />
{card.title}
</CardTitle>
<CardAction>
<StatusBadge status="investigating" variant="warning" label={card.status} />
</CardAction>
</CardHeader>
<CardContent className="flex flex-col gap-3">
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<p className="type-numeral text-3xl">{card.figure}</p>
<p className="text-sm text-muted-foreground tabular-nums">{card.usual}</p>
</div>
<ul className="flex flex-col border-t border-border">
{card.accounts.map((account) => (
<li key={account.account} className="flex items-center gap-3 border-b border-border py-2.5 last:border-b-0 last:pb-0">
<LogoMark name={account.account} size="sm" className="w-28 shrink-0" />
<span className="min-w-0 flex-1 truncate text-xs text-muted-foreground">{account.reason}</span>
<span className="text-sm font-medium tabular-nums">{account.amount}</span>
</li>
))}
</ul>
</CardContent>
<CardFooter className="text-xs text-muted-foreground">
<SendIcon className="size-3.5 shrink-0" />
{card.sent}
</CardFooter>
</Card>
)
}
function Picture({ row }: { row: Row }) {
if (row.kind === "accounts") return <AccountsPicture card={row.card} />
if (row.kind === "forecast") return <ForecastPicture card={row.card} />
return <AlertPicture card={row.card} />
}
/**
* A row's picture as a figure: hidden and inert to assistive technology, and
* named by its caption the way an image is by its alt text. Each figure takes
* its own id, so two copies of the section never share one.
*/
function RowFigure({ row }: { row: Row }) {
const captionId = React.useId()
return (
<figure aria-labelledby={captionId} className="min-w-0">
<div aria-hidden="true" inert>
<Picture row={row} />
</div>
<figcaption id={captionId} className="sr-only">
{row.caption}
</figcaption>
</figure>
)
}
/**
* What the product does, one claim to a row: the claim, its sentence, three
* specifics and a link on one side, and on the other the part of the product
* it is about — a widget of a Northwind workspace drawn with the kit's own
* Card, table and chart, so it wears the reader's palette and never goes
* stale the way a screenshot does. From `lg` the rows alternate sides; on a
* phone every row reads claim first, then its picture.
*
* Each picture is a figure: hidden and inert to assistive technology and
* described by its caption, the way an image is by its alt text. A server
* component; every word and figure is in `./features-02.data`.
*/
export function Features02() {
const titleId = React.useId()
return (
<section
data-section="features-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 max-w-7xl">
<div className="max-w-3xl">
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-4 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
<p className="mt-5 text-base/7 text-pretty text-muted-foreground md:text-lg/8">{LEDE}</p>
</div>
<div className="mt-14 flex flex-col gap-16 md:mt-20 md:gap-24">
{ROWS.map((row, index) => (
<div key={row.title} className="grid items-center gap-8 lg:grid-cols-2 lg:gap-16">
<div className={cn("flex max-w-xl flex-col items-start", index % 2 === 1 && "lg:order-last lg:justify-self-end")}>
<p className="type-eyebrow">{row.eyebrow}</p>
<h3 className="type-display mt-3 text-2xl text-balance">{row.title}</h3>
<p className="mt-4 text-base/7 text-pretty text-muted-foreground">{row.body}</p>
<ul className="mt-6 flex flex-col gap-2.5 text-sm">
{row.points.map((point) => (
<li key={point} className="flex items-start gap-2.5">
<CheckIcon aria-hidden="true" className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
{point}
</li>
))}
</ul>
<Link href={row.link.href} className={cn(buttonVariants({ variant: "link" }), "group/link mt-6 h-auto px-0")}>
{row.link.label}
<ArrowRightIcon
aria-hidden="true"
data-icon="inline-end"
className="transition-transform duration-(--duration-fast) ease-(--ease-standard) group-hover/link:translate-x-0.5"
/>
</Link>
</div>
<RowFigure row={row} />
</div>
))}
</div>
</div>
</section>
)
}
export default Features02/**
* Every word `features-02` sets, and what each row's picture shows. The
* pictures are three widgets of a Northwind workspace, so their rows and
* figures are copy like the headline: written once here. Replace a row's
* text and its picture together and the section tells your story instead.
*/
import type { LogoName } from "@/components/ui/logo-mark"
export type FeatureLink = { label: string; href: string }
export const EYEBROW = "Northwind Analytics"
export const HEADLINE = "Built for the questions that come up every Monday"
export const LEDE =
"Three things teams move into Northwind first, and what each one looks like once it is there."
/** One joined account, as the accounts widget lists it. */
export type AccountRow = { account: LogoName; plan: string; seats: string; tickets: number; mrr: string }
/** One week of the forecast widget: booked revenue so far, projected revenue after. */
export type ForecastWeek = { week: string; booked: number | null; projected: number | null }
/** One account behind an alert: what it lost and why. */
export type AlertAccount = { account: LogoName; amount: string; reason: string }
export type Row =
| {
kind: "accounts"
eyebrow: string
title: string
body: string
points: readonly string[]
link: FeatureLink
/** What the picture shows, for a reader who cannot see it. */
caption: string
card: { title: string; description: string; sources: string; rows: readonly AccountRow[] }
}
| {
kind: "forecast"
eyebrow: string
title: string
body: string
points: readonly string[]
link: FeatureLink
caption: string
card: {
title: string
description: string
range: string
weeks: readonly ForecastWeek[]
assumptions: readonly { label: string; value: string }[]
}
}
| {
kind: "alert"
eyebrow: string
title: string
body: string
points: readonly string[]
link: FeatureLink
caption: string
card: {
title: string
status: string
figure: string
usual: string
accounts: readonly AlertAccount[]
sent: string
}
}
export const ROWS: readonly Row[] = [
{
kind: "accounts",
eyebrow: "Sources",
title: "Every tool you run on, joined on the customer",
body: "Northwind reads billing, your CRM, product events and the support desk, and matches every record to one account. The plan, the usage and the open tickets land on the same row.",
points: [
"Read-only access to every source",
"Matching rules you can review and override",
"New fields appear without a rebuild",
],
link: { label: "See the connectors", href: "/integrations" },
caption:
"The accounts widget of a Northwind workspace: Meridian, Tidewater, Copperline and Lumen, each with its plan and MRR from billing, its active seats from product events and its open tickets from the support desk.",
card: {
title: "Accounts",
description: "Joined from four sources",
sources: "Plan and MRR from billing, seats from product events, tickets from the support desk",
rows: [
{ account: "Meridian", plan: "Enterprise", seats: "212 of 250", tickets: 3, mrr: "$38.4K" },
{ account: "Tidewater", plan: "Scale", seats: "96 of 120", tickets: 0, mrr: "$21.9K" },
{ account: "Copperline", plan: "Scale", seats: "41 of 120", tickets: 7, mrr: "$17.2K" },
{ account: "Lumen", plan: "Team", seats: "18 of 25", tickets: 1, mrr: "$9.8K" },
],
},
},
{
kind: "forecast",
eyebrow: "Forecasts",
title: "A forecast with its assumptions on the page",
body: "The projection is built from open pipeline and upcoming renewals, and every assumption sits beside the line with the history it came from. Finance argues with the inputs instead of the output.",
points: [
"Renewal rate from your last four quarters",
"Pipeline weighted by stage and by rep",
"Rebuilt every night, kept for every month",
],
link: { label: "How forecasts are built", href: "/docs/forecasts" },
caption:
"The forecast widget of a Northwind workspace: Q4 revenue booked week by week to October 25, then projected to December 27, landing at $1.44M, with the three assumptions the projection rests on.",
card: {
title: "Q4 revenue forecast",
description: "Booked to date, then projected",
range: "Likely between $1.37M and $1.51M",
weeks: [
{ week: "Oct 4", booked: 101_200, projected: null },
{ week: "Oct 11", booked: 104_900, projected: null },
{ week: "Oct 18", booked: 103_300, projected: null },
{ week: "Oct 25", booked: 108_100, projected: 108_100 },
{ week: "Nov 1", booked: null, projected: 109_400 },
{ week: "Nov 8", booked: null, projected: 111_000 },
{ week: "Nov 15", booked: null, projected: 110_200 },
{ week: "Nov 22", booked: null, projected: 112_600 },
{ week: "Nov 29", booked: null, projected: 114_100 },
{ week: "Dec 6", booked: null, projected: 113_500 },
{ week: "Dec 13", booked: null, projected: 115_800 },
{ week: "Dec 20", booked: null, projected: 116_900 },
{ week: "Dec 27", booked: null, projected: 117_400 },
],
assumptions: [
{ label: "Renewal rate", value: "94%" },
{ label: "Stage 3+ win rate", value: "38%" },
{ label: "New pipeline a month", value: "$210K" },
],
},
},
{
kind: "alert",
eyebrow: "Alerts",
title: "When a number moves, the reason comes with it",
body: "Northwind checks every metric after each sync. When one leaves its usual range, the alert names the change, the accounts that caused it and what they have in common.",
points: [
"Ranges learned from each metric's own history",
"Sent to email or your team's chat",
"Muted per metric, and per person",
],
link: { label: "Set up your first alert", href: "/docs/alerts" },
caption:
"An alert from a Northwind workspace: churned revenue reached $9.6K this week against a usual $3.1K to $4.2K, caused by Copperline, Harbour and Alder, each with the amount and the reason.",
card: {
title: "Churned revenue is 2.6× a usual week",
status: "Investigating",
figure: "$9.6K",
usual: "Usual week: $3.1K to $4.2K",
accounts: [
{ account: "Copperline", amount: "$4.1K", reason: "Moved to the Team plan" },
{ account: "Harbour", amount: "$3.2K", reason: "Cancelled at renewal" },
{ account: "Alder", amount: "$2.3K", reason: "Two failed payments" },
],
sent: "Sent to the Revenue channel at 09:14",
},
},
]
/**
* The quarter the forecast lands on: each week counted once, booked where it
* has been and projected where it has not. The week the two lines meet
* carries both, so the dashed line starts where the solid one ends.
*/
export function forecastTotal(weeks: readonly ForecastWeek[]): number {
return weeks.reduce((sum, week) => sum + (week.booked ?? week.projected ?? 0), 0)
}npx shadcn@latest add @vibra/features-02Annotated metric card
features-03One metric card in the middle — its definition, change, month, movements, source and owner — with six numbered callouts either side, each matching a mark on the card.
import * as React from "react"
import { DatabaseIcon, UserRoundCheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { formatCurrency } from "@/lib/format"
import { AreaChart } from "@/components/ui/area-chart"
import { Badge } from "@/components/ui/badge"
import { Card, CardAction, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { MetricDelta } from "@/components/ui/metric-delta"
import { CALLOUTS, CAPTION, EYEBROW, HEADLINE, LEDE, METRIC, monthChange, type Callout } from "./features-03.data"
/** A numbered mark: on the card, and beside the callout it belongs to. Ink on paper, never the accent. */
function Mark({ n, className }: { n: number; className?: string }) {
return (
<span
className={cn(
"inline-flex size-5 shrink-0 items-center justify-center rounded-full bg-foreground text-2xs font-semibold text-background tabular-nums",
className
)}
>
{n}
</span>
)
}
/** A signed amount: "+$18.2K", "−$4.1K" — a real minus, not a hyphen. */
function signed(amount: number): string {
const money = formatCurrency(Math.abs(amount), "USD", { compact: true })
return `${amount < 0 ? "−" : "+"}${money}`
}
/** The card the callouts annotate: the kit's framed Card, marked where each callout points. */
function MetricPicture() {
return (
<Card>
<CardHeader>
<CardTitle>{METRIC.title}</CardTitle>
<CardAction className="flex items-center gap-2">
<Mark n={1} />
<Badge variant="outline">{METRIC.version}</Badge>
</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-3xl">{formatCurrency(METRIC.value, "USD", { compact: true })}</p>
<MetricDelta value={monthChange()} variant="pill" size="sm" />
<span className="text-xs text-muted-foreground">{METRIC.since}</span>
<Mark n={2} />
</div>
<div className="relative">
<Mark n={3} className="absolute top-0 left-0 z-10" />
<AreaChart
data={[...METRIC.days]}
index="day"
series={[{ key: "current", label: "September", compareKey: "previous" }]}
compare
baseline="auto"
height={132}
showLegend={false}
showYAxis={false}
showTooltip={false}
/>
</div>
<div className="flex items-start gap-3 border-t border-border pt-4">
<dl className="grid flex-1 grid-cols-2 gap-x-4 gap-y-3 sm:grid-cols-4">
{METRIC.movements.map((movement) => (
<div key={movement.label} className="flex min-w-0 flex-col gap-0.5">
<dt className="text-xs text-muted-foreground">{movement.label}</dt>
<dd className="text-sm font-medium tabular-nums">{signed(movement.amount)}</dd>
</div>
))}
</dl>
<Mark n={4} />
</div>
</CardContent>
<CardFooter className="flex-wrap justify-between gap-x-4 gap-y-2 text-xs text-muted-foreground">
<span className="inline-flex items-center gap-1.5">
<DatabaseIcon className="size-3.5 shrink-0" />
{METRIC.source}
<Mark n={5} className="ml-1" />
</span>
<span className="inline-flex items-center gap-1.5">
<UserRoundCheckIcon className="size-3.5 shrink-0" />
{METRIC.owner}
<Mark n={6} className="ml-1" />
</span>
</CardFooter>
</Card>
)
}
/**
* Three callouts of the six, as an ordered list starting at `start`. From
* `lg` a column stands either side of the card and each mark faces it: the
* left column sets its marks on the right, its text flush towards the card.
*/
function Callouts({ items, start, side }: { items: readonly Callout[]; start: number; side: "left" | "right" }) {
return (
<ol start={start} className="flex flex-col gap-8 lg:flex-1 lg:justify-around lg:gap-6">
{items.map((callout, index) => (
<li key={callout.title} className={cn("flex items-start gap-3", side === "left" && "lg:flex-row-reverse lg:text-right")}>
<Mark n={start + index} className="mt-0.5" />
<div className="flex min-w-0 flex-col gap-1.5">
<h3 className="text-base font-semibold">{callout.title}</h3>
<p className="text-sm/6 text-pretty text-muted-foreground">{callout.body}</p>
</div>
</li>
))}
</ol>
)
}
/**
* One part of the product, annotated: a metric card in the middle, as a
* Northwind workspace draws it, with six numbered callouts either side, each
* matching a mark on the card — the definition it was computed with, the
* change, the month under it, what moved it, where it came from and who owns
* it. The card is the kit's own Card, MetricDelta and AreaChart, so it wears
* the reader's palette.
*
* The card is a figure, hidden and inert to assistive technology and
* described by its caption; the callouts are one list read in order, 1 to 6.
* On a phone the card comes first and the callouts follow it; on a tablet
* they stand in two columns under it. A server component; every word and
* figure is in `./features-03.data`.
*/
export function Features03() {
const titleId = React.useId()
const captionId = React.useId()
return (
<section
data-section="features-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">
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-4 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
<p className="mt-5 max-w-2xl text-base/7 text-pretty text-muted-foreground md:text-lg/8">{LEDE}</p>
</div>
<div className="mt-12 grid gap-10 md:mt-16 md:grid-cols-2 lg:grid-cols-[minmax(0,1fr)_28rem_minmax(0,1fr)] lg:gap-10">
<figure aria-labelledby={captionId} className="min-w-0 md:col-span-2 md:mx-auto md:w-full md:max-w-lg lg:col-span-1 lg:col-start-2 lg:row-start-1 lg:max-w-none">
<div aria-hidden="true" inert>
<MetricPicture />
</div>
<figcaption id={captionId} className="sr-only">
{CAPTION}
</figcaption>
</figure>
<div className="lg:col-start-1 lg:row-start-1 lg:flex lg:flex-col">
<Callouts items={CALLOUTS.slice(0, 3)} start={1} side="left" />
</div>
<div className="lg:col-start-3 lg:row-start-1 lg:flex lg:flex-col">
<Callouts items={CALLOUTS.slice(3)} start={4} side="right" />
</div>
</div>
</div>
</section>
)
}
export default Features03/**
* Every word `features-03` sets, and the metric card it annotates. The card
* is a picture of one Northwind metric — MRR, as a customer's workspace shows
* it — so its figures are copy like the headline. The six callouts are
* numbered to match the marks on the card: the first three stand on its left,
* the last three on its right. Replace the card and its callouts together.
*/
export const EYEBROW = "Anatomy of a metric"
export const HEADLINE = "Every number on the board comes with its working"
export const LEDE =
"Open any figure in Northwind and its card says what it means, how it moved, where it came from and who answers for it."
export type Callout = { title: string; body: string }
/** In the order of the marks on the card: 1 to 3 on its left, 4 to 6 on its right. */
export const CALLOUTS: readonly Callout[] = [
{
title: "One definition",
body: "The card names the version of MRR it was computed with. Change the definition once and every chart that uses it follows.",
},
{
title: "A like-for-like change",
body: "The change is measured from the first of the month, never a part month against a whole one.",
},
{
title: "This month over the last",
body: "August's same days are drawn behind September's, so a slow week reads as a slow week and not as a loss.",
},
{
title: "What moved it",
body: "New, expansion, contraction and churn are split out on the card, and they add up to the change above them.",
},
{
title: "Where it came from",
body: "The source and the last sync are on every card. Nothing on it was typed in by hand.",
},
{
title: "Who answers for it",
body: "Each metric has an owner, who approves changes to its definition and receives its alerts.",
},
]
/** What the picture is, for a reader who cannot see it. */
export const CAPTION =
"A Northwind metric card for monthly recurring revenue: $412.8K, up 4.1% since September 1, drawn over August's same days, split into new, expansion, contraction and churn, synced from billing and owned by Finance."
/** One day of the month, and the same day of the month before, in dollars. */
export type MetricDay = { day: string; current: number; previous: number }
/** A movement in the month: a signed amount in dollars. */
export type Movement = { label: string; amount: number }
export const METRIC = {
title: "Monthly recurring revenue",
version: "Definition v14",
value: 412_800,
start: 396_600,
since: "since Sep 1",
days: [
{ day: "Sep 1", current: 396_600, previous: 382_000 },
{ day: "Sep 4", current: 398_900, previous: 383_100 },
{ day: "Sep 7", current: 400_200, previous: 384_900 },
{ day: "Sep 10", current: 403_800, previous: 385_600 },
{ day: "Sep 13", current: 405_100, previous: 387_200 },
{ day: "Sep 16", current: 406_900, previous: 388_100 },
{ day: "Sep 19", current: 409_700, previous: 389_000 },
{ day: "Sep 22", current: 411_200, previous: 390_200 },
{ day: "Sep 24", current: 412_800, previous: 390_900 },
] satisfies MetricDay[],
movements: [
{ label: "New", amount: 18_200 },
{ label: "Expansion", amount: 9_600 },
{ label: "Contraction", amount: -4_100 },
{ label: "Churn", amount: -7_500 },
] satisfies Movement[],
source: "Billing, synced 4 min ago",
owner: "Owned by Finance",
} as const
/** The month's change, as the card's pill reads it: the movements add up to it. */
export function monthChange(metric: { value: number; start: number } = METRIC): number {
return metric.start > 0 ? (metric.value - metric.start) / metric.start : 0
}npx shadcn@latest add @vibra/features-03Bento grid of workspace parts
features-04Five unequal tiles — the question box, alerts, a forecast, the sources and the permissions — each with its sentence over a small picture of that part of the product.
import * as React from "react"
import Link from "next/link"
import {
ActivityIcon,
ArrowRightIcon,
BellRingIcon,
BriefcaseIcon,
CheckIcon,
CornerDownLeftIcon,
DatabaseIcon,
LifeBuoyIcon,
MessageSquareTextIcon,
ReceiptTextIcon,
type LucideIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { formatCurrency } from "@/lib/format"
import { buttonVariants } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { LineChart } from "@/components/ui/line-chart"
import { LogoMark } from "@/components/ui/logo-mark"
import { RankList } from "@/components/ui/rank-list"
import {
ALERTS,
ALL_FEATURES,
ASK,
EYEBROW,
FORECAST,
HEADLINE,
LEDE,
PERMISSIONS,
SOURCES,
type SourceIcon,
type Tile,
} from "./features-04.data"
const SOURCE_ICONS: Record<SourceIcon, LucideIcon> = {
billing: ReceiptTextIcon,
crm: BriefcaseIcon,
product: ActivityIcon,
support: LifeBuoyIcon,
}
/** Whole dollars, abbreviated: "$14.2K". */
function money(value: number): string {
return formatCurrency(value, "USD", { compact: true })
}
/**
* One tile: its title and sentence on the frame, its picture on the sheet.
* The picture is decoration beside words that already say what it shows, so
* it is hidden and inert; the title is the tile's `h3`.
*/
function BentoTile({ tile, className, children }: { tile: Tile; className?: string; children: React.ReactNode }) {
return (
<li className={cn("flex min-w-0", className)}>
<Card className="w-full">
<CardHeader>
<CardTitle>
<h3>{tile.title}</h3>
</CardTitle>
<CardDescription className="text-pretty">{tile.description}</CardDescription>
</CardHeader>
<CardContent className="flex flex-col justify-center">
<div aria-hidden="true" inert>
{children}
</div>
</CardContent>
</Card>
</li>
)
}
/**
* What the product does, as a grid of unequal tiles: the question box large,
* then alerts, a forecast, the sources and the permissions, each a small
* picture of that part of a Northwind workspace built from kit parts — a rank
* list, a line chart, rows — rather than a screenshot.
*
* Six columns from `lg`, the first row four and two, the second three twos;
* two up on a tablet, one on a phone, with the question box always first. A
* server component; every word and figure is in `./features-04.data`.
*/
export function Features04() {
const titleId = React.useId()
return (
<section
data-section="features-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>
<h2 id={titleId} className="type-display mt-4 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
</div>
<div className="flex flex-col items-start gap-4 lg:col-span-5">
<p className="text-base/7 text-pretty text-muted-foreground">{LEDE}</p>
<Link href={ALL_FEATURES.href} className={cn(buttonVariants({ variant: "link" }), "group/link h-auto px-0")}>
{ALL_FEATURES.label}
<ArrowRightIcon
aria-hidden="true"
data-icon="inline-end"
className="transition-transform duration-(--duration-fast) ease-(--ease-standard) group-hover/link:translate-x-0.5"
/>
</Link>
</div>
</div>
<ul className="mt-12 grid grid-cols-1 gap-4 md:mt-14 md:grid-cols-2 lg:grid-cols-6">
<BentoTile tile={ASK} className="md:col-span-2 lg:col-span-4">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:gap-8">
<div className="flex flex-col gap-3 lg:flex-1">
<div className="flex min-h-10 items-center gap-2 rounded-lg bg-card px-3 py-2 text-sm ring-1 ring-input">
<MessageSquareTextIcon className="size-4 shrink-0 text-muted-foreground" />
<span className="min-w-0 flex-1 text-pretty">{ASK.question}</span>
<CornerDownLeftIcon className="size-3.5 shrink-0 text-muted-foreground" />
</div>
<p className="text-sm text-pretty">{ASK.answer}</p>
<p className="flex items-start gap-1.5 text-xs text-muted-foreground">
<DatabaseIcon className="mt-px size-3.5 shrink-0" />
{ASK.source}
</p>
</div>
<RankList
size="sm"
className="lg:w-64"
format={money}
items={ASK.rows.map((row) => ({ label: <LogoMark name={row.account} size="sm" />, value: row.value }))}
/>
</div>
</BentoTile>
<BentoTile tile={ALERTS} className="lg:col-span-2">
<ul className="flex flex-col">
{ALERTS.items.map((alert) => (
<li key={alert.text} className="flex items-start gap-2.5 border-b border-border py-2 text-xs first:pt-0 last:border-b-0 last:pb-0">
<BellRingIcon className="mt-px size-3.5 shrink-0 text-muted-foreground" />
<span className="min-w-0 flex-1 text-pretty">{alert.text}</span>
<span className="shrink-0 text-muted-foreground tabular-nums">{alert.when}</span>
</li>
))}
</ul>
</BentoTile>
<BentoTile tile={FORECAST} className="lg:col-span-2">
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-0.5">
<p className="text-xs text-muted-foreground">{FORECAST.label}</p>
<p className="type-numeral text-2xl">{FORECAST.figure}</p>
<p className="text-xs text-muted-foreground tabular-nums">{FORECAST.range}</p>
</div>
{/* Fitted to the quarter's range, not from zero: the tile is
about the shape of the quarter, booked then projected. */}
<LineChart
data={[...FORECAST.weeks]}
index="week"
series={[{ key: "booked", label: "Booked", compareKey: "projected" }]}
compare
baseline="auto"
height={64}
showXAxis={false}
showYAxis={false}
showGrid={false}
showLegend={false}
showTooltip={false}
/>
</div>
</BentoTile>
<BentoTile tile={SOURCES} className="lg:col-span-2">
<ul className="flex flex-col gap-2.5">
{SOURCES.items.map((source) => {
const Icon = SOURCE_ICONS[source.icon]
return (
<li key={source.name} className="flex items-center gap-2.5 text-sm">
<span className="flex size-7 shrink-0 items-center justify-center rounded-md bg-surface ring-1 ring-border">
<Icon className="size-3.5" />
</span>
<span className="min-w-0 flex-1 truncate">{source.name}</span>
<span className="inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground tabular-nums">
<CheckIcon className="size-3.5 text-success" />
{source.synced}
</span>
</li>
)
})}
</ul>
</BentoTile>
<BentoTile tile={PERMISSIONS} className="lg:col-span-2">
<ul className="flex flex-col">
{PERMISSIONS.items.map((grant) => (
<li key={grant.who} className="flex items-center gap-3 border-b border-border py-2 text-sm first:pt-0 last:border-b-0 last:pb-0">
<span className="flex min-w-0 flex-1 flex-col">
<span className="truncate font-medium">{grant.who}</span>
<span className="truncate text-xs text-muted-foreground">{grant.scope}</span>
</span>
<span className="shrink-0 rounded-full bg-secondary px-2 py-0.5 text-xs font-medium">{grant.access}</span>
</li>
))}
</ul>
</BentoTile>
</ul>
</div>
</section>
)
}
export default Features04/**
* Every word `features-04` sets, and what each tile's picture shows. The
* tiles are five parts of a Northwind workspace at thumbnail size, so their
* rows and figures are copy like the headline, written once here. Replace a
* tile's words and its picture together.
*/
import type { LogoName } from "@/components/ui/logo-mark"
export const EYEBROW = "The workspace"
export const HEADLINE = "The parts of Northwind your team will open every day"
export const LEDE =
"Questions, alerts, forecasts, sources and permissions share one model of your customers, so each one answers with the same numbers."
export const ALL_FEATURES = { label: "See every feature", href: "/features" }
export type Tile = { title: string; description: string }
export const ASK = {
title: "Ask in plain language",
description: "Type the question the way you would ask a colleague, and get the answer with the rows behind it.",
question: "Which accounts expanded most this quarter?",
answer: "Meridian, Tidewater and Lumen added $31.4K of MRR between them, 58% of all expansion.",
/** What the answer was counted from: every answer in Northwind cites its source. */
source: "Counted from billing: expansion MRR, Q3 2026 to date",
rows: [
{ account: "Meridian", value: 14_200 },
{ account: "Tidewater", value: 9_800 },
{ account: "Lumen", value: 7_400 },
{ account: "Kestrel", value: 4_100 },
] satisfies { account: LogoName; value: number }[],
}
export const ALERTS = {
title: "Alerts that say why",
description: "A change outside its usual range, with the accounts that caused it.",
items: [
{ text: "Churned revenue is 2.6× a usual week", when: "09:14" },
{ text: "Trial conversion fell to 18% from 24%", when: "Mon" },
{ text: "Support backlog above 40 for two days", when: "Sun" },
],
}
export const FORECAST = {
title: "Forecasts you can audit",
description: "Pipeline and renewals, with the assumptions beside the line.",
label: "Q4 revenue, projected",
figure: "$1.44M",
range: "$1.37M to $1.51M",
/** Revenue by week in thousands: booked to date, then projected; the week they meet carries both. */
weeks: [
{ week: "Oct 4", booked: 101, projected: null },
{ week: "Oct 11", booked: 105, projected: null },
{ week: "Oct 18", booked: 103, projected: null },
{ week: "Oct 25", booked: 108, projected: 108 },
{ week: "Nov 1", booked: null, projected: 109 },
{ week: "Nov 8", booked: null, projected: 111 },
{ week: "Nov 15", booked: null, projected: 110 },
{ week: "Nov 22", booked: null, projected: 113 },
{ week: "Nov 29", booked: null, projected: 114 },
{ week: "Dec 6", booked: null, projected: 114 },
{ week: "Dec 13", booked: null, projected: 116 },
{ week: "Dec 20", booked: null, projected: 117 },
{ week: "Dec 27", booked: null, projected: 117 },
] satisfies { week: string; booked: number | null; projected: number | null }[],
}
export type SourceIcon = "billing" | "crm" | "product" | "support"
export const SOURCES = {
title: "Every source, one account",
description: "Read-only connections, matched on the customer.",
items: [
{ icon: "billing", name: "Billing", synced: "4 min ago" },
{ icon: "crm", name: "CRM", synced: "6 min ago" },
{ icon: "product", name: "Product events", synced: "1 min ago" },
{ icon: "support", name: "Support desk", synced: "9 min ago" },
] satisfies { icon: SourceIcon; name: string; synced: string }[],
}
export const PERMISSIONS = {
title: "Permissions to the row",
description: "Each team and partner sees only the accounts that are theirs.",
items: [
{ who: "Finance", scope: "Every account", access: "Edit" },
{ who: "Sales, EMEA", scope: "Their region", access: "View" },
{ who: "Harbour", scope: "Their own accounts", access: "View" },
],
}npx shadcn@latest add @vibra/features-04Product tour in vertical tabs
features-05Four views — revenue, retention, pipeline and support — as a vertical tab list beside the one chosen, each drawn with the kit's own chart or table.
import * as React from "react"
import { Tour } from "./features-05.client"
import { EYEBROW, HEADLINE, LEDE, STOPS, TOUR_LABEL } from "./features-05.data"
/**
* A product tour in one band: four views of a Northwind workspace — revenue,
* retention, pipeline and support — as a vertical tab list, and beside it the
* view chosen, drawn with the kit's own charts and table so it looks the way
* it will in the reader's workspace. From `lg` the list stands left of the
* view with every summary showing; on a phone it sits above the view and only
* the chosen tab's summary shows.
*
* A server component; the tour is the one island, and every word and figure
* is in `./features-05.data`.
*/
export function Features05() {
const titleId = React.useId()
return (
<section
data-section="features-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-3xl">
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-4 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
<p className="mt-5 text-base/7 text-pretty text-muted-foreground md:text-lg/8">{LEDE}</p>
</div>
<div className="mt-12 md:mt-16">
<Tour stops={STOPS} label={TOUR_LABEL} />
</div>
</div>
</section>
)
}
export default Features05"use client"
import { CircleDollarSignIcon, FilterIcon, LifeBuoyIcon, RepeatIcon, type LucideIcon } from "lucide-react"
import { formatCurrency, formatNumber, formatPercent } from "@/lib/format"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { FunnelChart } from "@/components/ui/funnel-chart"
import { LineChart } from "@/components/ui/line-chart"
import { LogoMark } from "@/components/ui/logo-mark"
import { SimpleTable } from "@/components/ui/simple-table"
import { StackedBarChart } from "@/components/ui/stacked-bar-chart"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import type { Stop, TicketRow } from "./features-05.data"
/** The picture each view is drawn with: vocabulary of the layout, not copy. */
const ICONS: Record<Stop["id"], LucideIcon> = {
revenue: CircleDollarSignIcon,
retention: RepeatIcon,
pipeline: FilterIcon,
support: LifeBuoyIcon,
}
const money = (value: number) => formatCurrency(value, "USD", { compact: true })
const share = (value: number) => formatPercent(value, { maximumFractionDigits: 0 })
const sinceSignup = (month: string | number) => (String(month) === "0" ? "Signup" : `Month ${month}`)
const TICKET_COLUMNS = [
{ key: "account", header: "Account", cell: (row: TicketRow) => <LogoMark name={row.account} size="sm" /> },
{ key: "open", header: "Open", align: "right" as const },
{ key: "oldest", header: "Oldest", align: "right" as const },
{ key: "mrr", header: "MRR", align: "right" as const },
]
/** One view's widget: the kit's chart or table for what the view counts. */
function View({ stop }: { stop: Stop }) {
switch (stop.id) {
case "revenue":
return (
<StackedBarChart
data={[...stop.months]}
index="month"
series={[
{ key: "starter", label: "Starter" },
{ key: "team", label: "Team" },
{ key: "business", label: "Business" },
]}
valueFormatter={money}
height={248}
/>
)
case "retention":
return (
<LineChart
data={[...stop.months]}
index="month"
series={[
{ key: "jan", label: "January" },
{ key: "mar", label: "March" },
{ key: "may", label: "May" },
]}
valueFormatter={share}
indexFormatter={sinceSignup}
baseline="auto"
// The youngest cohort ends early, so its name goes under the plot
// rather than beside a last point the others run past.
legend="bottom"
dots
height={248}
/>
)
case "pipeline":
return <FunnelChart steps={[...stop.stages]} valueFormatter={(value) => formatNumber(value)} />
case "support":
return <SimpleTable columns={TICKET_COLUMNS} rows={[...stop.rows]} rowKey="account" />
}
}
/**
* The tour: the views as a vertical tab list and, beside it, the one chosen.
* Tabs because that is what it is — one choice drives one region — so Up and
* Down move between views and bring each up as they land, Home and End jump
* to the ends, and a reader hears which view is selected. The chosen tab
* wears the brand tint, a rail and the 500 weight, never a stroke; on a
* phone only its summary shows, so the list stays short above the view.
*
* Every view and figure arrives as plain props from the section.
*/
export function Tour({ stops, label }: { stops: readonly Stop[]; label: string }) {
return (
<Tabs orientation="vertical" defaultValue={stops[0]?.id} className="gap-6 max-lg:flex-col lg:gap-10">
<TabsList variant="line" aria-label={label} activateOnFocus className="w-full shrink-0 items-stretch gap-1 lg:w-80">
{stops.map((stop) => {
const Icon = ICONS[stop.id]
return (
<TabsTrigger
key={stop.id}
value={stop.id}
className="group/tab h-auto flex-none flex-col items-start gap-1 rounded-lg py-2.5 pr-3 pl-4 text-left font-normal whitespace-normal group-data-vertical/tabs:after:inset-y-2.5 group-data-vertical/tabs:after:right-auto group-data-vertical/tabs:after:left-0 group-data-vertical/tabs:after:rounded-full hover:bg-muted/50 data-active:bg-brand-muted data-active:hover:bg-brand-muted"
>
<span className="flex items-center gap-2 text-sm group-data-active/tab:font-medium">
<Icon aria-hidden="true" className="size-4 text-muted-foreground group-data-active/tab:text-foreground" />
{stop.title}
</span>
<span className="text-sm text-pretty text-muted-foreground max-lg:hidden max-lg:group-data-active/tab:block">
{stop.summary}
</span>
</TabsTrigger>
)
})}
</TabsList>
{stops.map((stop) => (
<TabsContent key={stop.id} value={stop.id} className="min-w-0 rounded-xl focus-ring">
<Card>
<CardHeader>
<CardTitle>{stop.card.title}</CardTitle>
<CardDescription>{stop.card.description}</CardDescription>
</CardHeader>
{/* One height for every view, so choosing another never moves the page under the reader. */}
<CardContent className="flex min-h-80 flex-col justify-center">
<View stop={stop} />
</CardContent>
</Card>
</TabsContent>
))}
</Tabs>
)
}/**
* Every word `features-05` sets, and the four views its tour shows. Each view
* is a widget of a Northwind workspace, so its figures are copy like the
* headline: written once here, and handed to the tour as plain props. The
* revenue view's last month adds up to the $412.8K MRR the other sections
* print. Swap in views of your own product and the tour shows those.
*/
import type { LogoName } from "@/components/ui/logo-mark"
export const EYEBROW = "Product tour"
export const HEADLINE = "Four views your teams ask for every week"
export const LEDE =
"Each one reads the same model of your customers, so revenue, retention, pipeline and support never disagree about an account."
/** The tab list's name, for assistive technology. */
export const TOUR_LABEL = "Northwind views"
/** One month of MRR, split by plan, in dollars. */
export type PlanMonth = { month: string; starter: number; team: number; business: number }
/** One month since signup: the share of each cohort's accounts still paying. */
export type CohortMonth = { month: string; jan: number | null; mar: number | null; may: number | null }
export type TicketRow = { account: LogoName; open: number; oldest: string; mrr: string }
export type Stop = {
/** The tab's value; stable, so a link or a test can name a view. */
id: "revenue" | "retention" | "pipeline" | "support"
title: string
/** What the view answers, in one line. */
summary: string
card: { title: string; description: string }
} & (
| { id: "revenue"; months: readonly PlanMonth[] }
| { id: "retention"; months: readonly CohortMonth[] }
| { id: "pipeline"; stages: readonly { label: string; value: number }[] }
| { id: "support"; rows: readonly TicketRow[] }
)
export const STOPS: readonly Stop[] = [
{
id: "revenue",
title: "Revenue",
summary: "MRR by plan, month by month, from billing.",
card: { title: "MRR by plan", description: "February to September 2026" },
months: [
{ month: "Feb", starter: 58_200, team: 141_300, business: 142_000 },
{ month: "Mar", starter: 59_000, team: 144_800, business: 145_100 },
{ month: "Apr", starter: 60_100, team: 149_200, business: 149_800 },
{ month: "May", starter: 60_800, team: 152_600, business: 156_200 },
{ month: "Jun", starter: 61_500, team: 158_100, business: 160_400 },
{ month: "Jul", starter: 62_900, team: 161_700, business: 166_300 },
{ month: "Aug", starter: 63_400, team: 166_000, business: 171_800 },
{ month: "Sep", starter: 64_200, team: 170_900, business: 177_700 },
],
},
{
id: "retention",
title: "Retention",
summary: "How each signup month holds on, side by side.",
card: { title: "Accounts still paying", description: "By signup month, months since signup" },
months: [
{ month: "0", jan: 1, mar: 1, may: 1 },
{ month: "1", jan: 0.91, mar: 0.93, may: 0.95 },
{ month: "2", jan: 0.86, mar: 0.89, may: 0.92 },
{ month: "3", jan: 0.83, mar: 0.87, may: 0.9 },
{ month: "4", jan: 0.81, mar: 0.85, may: 0.89 },
{ month: "5", jan: 0.8, mar: 0.84, may: null },
{ month: "6", jan: 0.79, mar: 0.83, may: null },
],
},
{
id: "pipeline",
title: "Pipeline",
summary: "Where deals stall, stage by stage, from your CRM.",
card: { title: "Q3 pipeline", description: "Deals opened July to September" },
stages: [
{ label: "Leads", value: 1_240 },
{ label: "Qualified", value: 486 },
{ label: "Proposal", value: 212 },
{ label: "Won", value: 81 },
],
},
{
id: "support",
title: "Support",
summary: "Open tickets beside the revenue they put at risk.",
card: { title: "Accounts waiting on support", description: "Open tickets, oldest first in each account" },
rows: [
{ account: "Copperline", open: 7, oldest: "6 days", mrr: "$17.2K" },
{ account: "Meridian", open: 3, oldest: "2 days", mrr: "$38.4K" },
{ account: "Fieldstone", open: 2, oldest: "4 days", mrr: "$6.1K" },
{ account: "Lumen", open: 1, oldest: "1 day", mrr: "$9.8K" },
],
},
]npx shadcn@latest add @vibra/features-05Numbered controls beside a sticky audit log
features-06Six security controls as a numbered, ruled list — sign-on, row permissions, the log, read-only sources, regions and the audit — beside a workspace's audit log that stays in view.
import * as React from "react"
import {
DownloadIcon,
EyeIcon,
KeyRoundIcon,
LogInIcon,
RadioTowerIcon,
Share2Icon,
SigmaIcon,
UserMinusIcon,
type LucideIcon,
} from "lucide-react"
import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { StatusBadge } from "@/components/ui/status-badge"
import { AUDIT_LOG, CAPTION, CONTROLS, EYEBROW, HEADLINE, LEDE, type LogIcon } from "./features-06.data"
/** The picture each kind of entry is drawn with: vocabulary of the layout, not copy. */
const LOG_ICONS: Record<LogIcon, LucideIcon> = {
share: Share2Icon,
view: EyeIcon,
access: KeyRoundIcon,
export: DownloadIcon,
signin: LogInIcon,
remove: UserMinusIcon,
definition: SigmaIcon,
}
/** "01" … "06": the item's place, set in the numeral register. */
const ordinal = (index: number) => String(index + 1).padStart(2, "0")
/** The audit log the list is about, as a customer's workspace shows it. */
function AuditPicture() {
return (
<Card>
<CardHeader>
<CardTitle>{AUDIT_LOG.title}</CardTitle>
<CardDescription>{AUDIT_LOG.description}</CardDescription>
<CardAction>
<StatusBadge status="live" variant="success" label="Live" />
</CardAction>
</CardHeader>
<CardContent className="p-0">
<ul className="flex flex-col">
{AUDIT_LOG.entries.map((entry) => {
const Icon = LOG_ICONS[entry.icon]
return (
<li key={`${entry.time}-${entry.actor}`} className="flex items-start gap-3 border-b border-border px-4 py-3 last:border-b-0">
<span className="flex size-7 shrink-0 items-center justify-center rounded-md bg-surface ring-1 ring-border">
<Icon className="size-3.5" />
</span>
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="text-sm text-pretty">{entry.action}</span>
<span className="truncate font-mono text-xs text-muted-foreground">{entry.actor}</span>
</span>
<span className="shrink-0 text-xs text-muted-foreground tabular-nums">{entry.time}</span>
</li>
)
})}
</ul>
</CardContent>
<CardFooter className="justify-between gap-4 text-xs text-muted-foreground">
<span>{AUDIT_LOG.retention}</span>
<span className="inline-flex items-center gap-1.5">
<RadioTowerIcon className="size-3.5 shrink-0" />
{AUDIT_LOG.stream}
</span>
</CardFooter>
</Card>
)
}
/**
* A numbered list of controls beside the one thing a security reviewer asks
* to see: the claim and six controls, each numbered, ruled and set as an
* `h3` with its sentence, and beside them a workspace's audit log that stays
* in view while the list scrolls past it — sticky from `lg`, following the
* list on a phone. The log is the kit's framed Card and a StatusBadge.
*
* The log is a figure, hidden and inert to assistive technology and described
* by its caption. A server component; every word is in `./features-06.data`.
*/
export function Features06() {
const titleId = React.useId()
const captionId = React.useId()
return (
<section
data-section="features-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 gap-12 lg:grid-cols-12 lg:gap-16">
<div className="lg:col-span-6">
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-4 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
<p className="mt-5 text-base/7 text-pretty text-muted-foreground md:text-lg/8">{LEDE}</p>
<ol className="mt-10 flex flex-col md:mt-12">
{CONTROLS.map((control, index) => (
<li key={control.title} className="grid grid-cols-[2.5rem_minmax(0,1fr)] gap-x-4 border-t border-rule py-6 last:pb-0">
<span aria-hidden="true" className="type-numeral pt-px text-sm text-muted-foreground">
{ordinal(index)}
</span>
<div className="flex flex-col gap-1.5">
<h3 className="text-base font-semibold">{control.title}</h3>
<p className="text-sm/6 text-pretty text-muted-foreground">{control.body}</p>
</div>
</li>
))}
</ol>
</div>
<div className="lg:col-span-6">
<figure aria-labelledby={captionId} className="lg:sticky lg:top-24">
<div aria-hidden="true" inert>
<AuditPicture />
</div>
<figcaption id={captionId} className="sr-only">
{CAPTION}
</figcaption>
</figure>
</div>
</div>
</section>
)
}
export default Features06/**
* Every word `features-06` sets, and the audit log its picture shows. The log
* is a picture of a customer's Northwind workspace — Keystone's — so its
* entries are copy like the headline: written once here. Replace the six
* controls and the log with your own.
*/
export const EYEBROW = "Security and control"
export const HEADLINE = "Built to pass the security review the first time"
export const LEDE =
"Your numbers describe your customers. Northwind treats them that way, from the first connection to the last export."
export type Control = { title: string; body: string }
export const CONTROLS: readonly Control[] = [
{
title: "Single sign-on and provisioning",
body: "People sign in through your identity provider over SAML, and SCIM adds and removes them as your directory changes.",
},
{
title: "Permissions down to the row",
body: "Rules decide which accounts each person sees, so a regional team or a partner only ever sees its own.",
},
{
title: "A log of every view and export",
body: "Who opened which dashboard, exported which rows and changed which definition, kept for a year and streamed to your SIEM.",
},
{
title: "Read-only access to your sources",
body: "Northwind never writes to billing or your CRM. Credentials are stored encrypted and can be rotated at any time.",
},
{
title: "Your data stays in its region",
body: "Choose the US or the EU when the workspace is created. Storage, processing and backups stay there.",
},
{
title: "Audited every year",
body: "SOC 2 Type II, reviewed by an independent auditor each year. The report and our questionnaire answers are ready to share.",
},
]
export type LogIcon = "share" | "view" | "access" | "export" | "signin" | "remove" | "definition"
export type LogEntry = { time: string; icon: LogIcon; actor: string; action: string }
/** What the picture is, for a reader who cannot see it. */
export const CAPTION =
"The audit log of a Northwind workspace for Keystone: this morning's share, views, support access grant, export, sign-in, removal by SCIM and definition change, each with its time and who did it."
export const AUDIT_LOG = {
title: "Audit log",
description: "Keystone workspace, today",
entries: [
{ time: "09:52", icon: "share", actor: "dana@keystone.example", action: "Shared Board pack, Q3 with the board until Friday" },
{ time: "09:41", icon: "view", actor: "dana@keystone.example", action: "Viewed Board pack, Q3" },
{ time: "09:38", icon: "access", actor: "ops@keystone.example", action: "Gave Northwind support access until 12:00" },
{ time: "09:12", icon: "export", actor: "leo@keystone.example", action: "Exported 1,284 rows from Accounts" },
{ time: "09:03", icon: "signin", actor: "leo@keystone.example", action: "Signed in with single sign-on" },
{ time: "08:57", icon: "remove", actor: "SCIM", action: "Removed sam@keystone.example" },
{ time: "08:30", icon: "definition", actor: "priya@keystone.example", action: "Changed MRR to definition v14" },
] satisfies LogEntry[],
retention: "Kept for 365 days",
stream: "Streaming to your SIEM",
}npx shadcn@latest add @vibra/features-06Northwind against the spreadsheet
features-07Six weekly jobs, each as a spreadsheet does it and as Northwind does, in a table with the Northwind column tinted — and a way to bring the spreadsheet in.
import * as React from "react"
import Link from "next/link"
import { CheckIcon, FileSpreadsheetIcon, MinusIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { LogoMark } from "@/components/ui/logo-mark"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { COLUMNS, EYEBROW, HEADLINE, IMPORT, JOBS, LEDE } from "./features-07.data"
/** The two answers' marks: a dash for the old way, a check for Northwind's. */
function Answer({ text, after }: { text: string; after?: boolean }) {
const Icon = after ? CheckIcon : MinusIcon
return (
<span className="flex items-start gap-2.5">
<Icon aria-hidden="true" className={cn("mt-0.5 size-4 shrink-0", after ? "text-foreground" : "text-muted-foreground")} />
<span className={cn("text-pretty", !after && "text-muted-foreground")}>{text}</span>
</span>
)
}
/**
* The product against what it replaces: six weekly jobs, each as a
* spreadsheet does it and as Northwind does, side by side. The Northwind
* column is the recommended one, so it wears the brand tint and carries the
* mark in its heading — tinted and named, never colour alone.
*
* From `md` it is a real table — the job heads its row and each answer is
* read with its column — in a sheet on the card plane. On a phone, where
* three columns of sentences cannot fit, each job becomes its own block with
* its two answers labelled. Under both, the way to bring the spreadsheet in.
* A server component; every word is in `./features-07.data`.
*/
export function Features07() {
const titleId = React.useId()
return (
<section
data-section="features-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 max-w-7xl">
<div className="mx-auto flex max-w-3xl flex-col items-center text-center">
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-4 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
<p className="mt-5 max-w-2xl text-base/7 text-pretty text-muted-foreground md:text-lg/8">{LEDE}</p>
</div>
<div className="mx-auto mt-12 max-w-5xl md:mt-16">
<div className="hidden overflow-hidden panel md:block">
<Table className="table-fixed">
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead scope="col" className="h-12 w-[28%] px-5">
{COLUMNS.job}
</TableHead>
<TableHead scope="col" className="h-12 px-5">
<span className="inline-flex items-center gap-2">
<FileSpreadsheetIcon aria-hidden="true" className="size-4" />
{COLUMNS.before}
</span>
</TableHead>
<TableHead scope="col" className="h-12 bg-brand-muted px-5 text-foreground">
<span className="inline-flex items-center gap-2">
<LogoMark name="Northwind" size="sm" aria-hidden="true" />
<span className="sr-only">{COLUMNS.after}</span>
</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{JOBS.map((row) => (
<TableRow key={row.job} className="hover:bg-transparent">
<TableHead scope="row" className="h-auto px-5 py-4 align-top whitespace-normal text-foreground">
{row.job}
</TableHead>
<TableCell className="px-5 py-4 align-top whitespace-normal">
<Answer text={row.before} />
</TableCell>
<TableCell className="bg-brand-muted px-5 py-4 align-top whitespace-normal">
<Answer text={row.after} after />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<ul className="flex flex-col gap-4 md:hidden">
{JOBS.map((row) => (
<li key={row.job} className="panel p-4">
<h3 className="text-base font-semibold">{row.job}</h3>
<dl className="mt-3 flex flex-col gap-3 text-sm">
<div className="flex flex-col gap-1">
<dt className="text-xs text-muted-foreground">{COLUMNS.before}</dt>
<dd>
<Answer text={row.before} />
</dd>
</div>
<div className="flex flex-col gap-1 rounded-lg bg-brand-muted p-3">
<dt className="text-xs font-medium">{COLUMNS.after}</dt>
<dd>
<Answer text={row.after} after />
</dd>
</div>
</dl>
</li>
))}
</ul>
<div className="mt-8 flex flex-col items-start gap-4 border-t border-rule pt-8 md:flex-row md:items-center md:justify-between md:gap-10">
<p className="max-w-2xl text-sm/6 text-pretty text-muted-foreground">{IMPORT.text}</p>
<Link href={IMPORT.action.href} className={cn(buttonVariants({ variant: "outline" }), "shrink-0")}>
{IMPORT.action.label}
</Link>
</div>
</div>
</div>
</section>
)
}
export default Features07/**
* Every word `features-07` sets: six jobs a revenue team does every week,
* each as a spreadsheet does it and as Northwind does. Plain and specific —
* a comparison that overclaims reads as an advertisement, so every Northwind
* line names what it does rather than how it feels. Replace the rows with
* your own product's jobs and the table compares those.
*/
export type Action = { label: string; href: string }
export const EYEBROW = "Northwind or the spreadsheet"
export const HEADLINE = "What the Monday review takes, before and after"
export const LEDE =
"Six jobs every revenue team does each week, done the way most teams do them today and the way they are done in Northwind."
/** The column headings: what the rows compare. */
export const COLUMNS = { job: "The job", before: "In a spreadsheet", after: "In Northwind" }
export type Job = { job: string; before: string; after: string }
export const JOBS: readonly Job[] = [
{
job: "Refreshing the numbers",
before: "Export from four tools and paste them in, every Monday: about three hours.",
after: "Synced every five minutes from each source, with nothing to paste.",
},
{
job: "Agreeing on MRR",
before: "Each tab carries its own formula, and the totals rarely match.",
after: "One definition, versioned, with Finance as its owner.",
},
{
job: "Finding why a number moved",
before: "Filter, pivot, then ask around in chat.",
after: "Open the figure to see the accounts behind the change.",
},
{
job: "Sharing with the board",
before: "A copy of the file, out of date by Tuesday.",
after: "A link that is always current and expires when you say.",
},
{
job: "Controlling who sees what",
before: "Anyone with the file sees every row.",
after: "Permissions down to the row, and every view logged.",
},
{
job: "Looking back",
before: "Whatever was last saved over it.",
after: "Any figure as it stood on any past day.",
},
]
export const IMPORT = {
text: "Bring the spreadsheet with you. Northwind reads its formulas and turns each one into a draft definition for your team to review.",
action: { label: "Import a spreadsheet", href: "/docs/import" } satisfies Action,
}npx shadcn@latest add @vibra/features-07How it is built, as a layered diagram
features-08Three layers — the sources Northwind reads, the model it keeps and the surfaces it serves — drawn from kit parts, with what passes between them written on the rules that join them.
import * as React from "react"
import {
ActivityIcon,
BellRingIcon,
BriefcaseIcon,
Building2Icon,
ChevronDownIcon,
CodeIcon,
DatabaseIcon,
FileSpreadsheetIcon,
LayoutDashboardIcon,
LifeBuoyIcon,
ListChecksIcon,
MessageSquareTextIcon,
PresentationIcon,
ReceiptTextIcon,
SigmaIcon,
type LucideIcon,
} from "lucide-react"
import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { LogoMark } from "@/components/ui/logo-mark"
import { StatusBadge } from "@/components/ui/status-badge"
import { EYEBROW, HANDOFFS, HEADLINE, LEDE, MODEL, SOURCES, SURFACES, type Part, type PartIcon } from "./features-08.data"
const ICONS: Record<PartIcon, LucideIcon> = {
billing: ReceiptTextIcon,
crm: BriefcaseIcon,
product: ActivityIcon,
support: LifeBuoyIcon,
warehouse: DatabaseIcon,
sheet: FileSpreadsheetIcon,
accounts: Building2Icon,
metrics: SigmaIcon,
checks: ListChecksIcon,
dashboards: LayoutDashboardIcon,
alerts: BellRingIcon,
questions: MessageSquareTextIcon,
reports: PresentationIcon,
api: CodeIcon,
}
function PartIconTile({ icon }: { icon: PartIcon }) {
const Icon = ICONS[icon]
return (
<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>
)
}
/** One layer of the diagram: its name and sentence in the left column, its parts beside them. */
function Layer({ title, body, children }: { title: string; body: string; children: React.ReactNode }) {
return (
<div className="grid gap-4 lg:grid-cols-[13rem_minmax(0,1fr)] lg:gap-10">
<div className="lg:pt-2.5">
<h3 className="text-base font-semibold">{title}</h3>
<p className="mt-1 text-sm text-pretty text-muted-foreground">{body}</p>
</div>
{children}
</div>
)
}
/** A row of parts, each a small sheet with its icon, name and what it carries. */
function Parts({ parts, className }: { parts: readonly Part[]; className: string }) {
return (
<ul className={className}>
{parts.map((part) => (
<li key={part.name} className="flex min-w-0 items-center gap-3 panel p-3">
<PartIconTile icon={part.icon} />
<span className="flex min-w-0 flex-col">
<span className="truncate text-sm font-medium">{part.name}</span>
<span className="truncate text-xs text-muted-foreground max-sm:hidden">{part.detail}</span>
</span>
</li>
))}
</ul>
)
}
/** What passes between two layers: a rule down the middle, and the handoff named on it. */
function Handoff({ label }: { label: string }) {
return (
<div className="grid lg:grid-cols-[13rem_minmax(0,1fr)] lg:gap-10">
<div className="max-lg:hidden" />
<div className="flex flex-col items-center py-2">
<span aria-hidden="true" className="h-4 w-px bg-rule" />
<p className="rounded-full bg-background px-3 py-1 text-xs text-muted-foreground ring-1 ring-border tabular-nums">{label}</p>
<span aria-hidden="true" className="h-4 w-px bg-rule" />
<ChevronDownIcon aria-hidden="true" className="-mt-2 size-4 text-faint-foreground" />
</div>
</div>
)
}
/**
* How the product is put together, drawn with the kit rather than described:
* the sources it reads as a grid of small sheets, the model it keeps as a
* framed card with its three figures, and the surfaces it serves as another
* grid — each layer named on the left, with what passes between them written
* on the rule that joins them.
*
* The diagram is the content: every layer is an `h3` over a list, so it reads
* in order without the picture, and only the rules and the chevrons are
* hidden. From `lg` the names stand in a column of their own; on a phone
* each sits over its parts, two to a row, by name alone. A server
* component; every word is in
* `./features-08.data`.
*/
export function Features08() {
const titleId = React.useId()
return (
<section
data-section="features-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 max-w-7xl">
<div className="grid gap-x-16 gap-y-6 lg:grid-cols-12 lg:items-end">
<div className="lg:col-span-6">
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-4 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
</div>
<p className="text-base/7 text-pretty text-muted-foreground lg:col-span-6">{LEDE}</p>
</div>
<div className="mt-12 flex flex-col md:mt-16">
<Layer title={SOURCES.title} body={SOURCES.body}>
<Parts parts={SOURCES.parts} className="grid grid-cols-2 gap-3 lg:grid-cols-3" />
</Layer>
<Handoff label={HANDOFFS.in} />
<Layer title={MODEL.title} body={MODEL.body}>
<Card>
<CardHeader>
<CardTitle>
<LogoMark name="Northwind" size="sm" />
</CardTitle>
<CardDescription>{MODEL.workspace}</CardDescription>
<CardAction>
<StatusBadge status="passing" variant="success" label={MODEL.status} />
</CardAction>
</CardHeader>
<CardContent className="p-0">
<ul className="grid divide-y divide-border sm:grid-cols-3 sm:divide-x sm:divide-y-0">
{MODEL.parts.map((part) => (
<li key={part.name} className="flex items-start gap-3 p-4">
<PartIconTile icon={part.icon} />
<p className="flex min-w-0 flex-col">
<span className="type-numeral text-2xl">{part.value}</span>
<span className="text-sm font-medium">{part.name}</span>
<span className="text-xs text-pretty text-muted-foreground">{part.detail}</span>
</p>
</li>
))}
</ul>
</CardContent>
</Card>
</Layer>
<Handoff label={HANDOFFS.out} />
<Layer title={SURFACES.title} body={SURFACES.body}>
{/* Five across from `lg`; on a phone the odd one out takes the whole last row. */}
<Parts
parts={SURFACES.parts}
className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5 max-sm:[&>li:last-child:nth-child(odd)]:col-span-2"
/>
</Layer>
</div>
</div>
</section>
)
}
export default Features08/**
* Every word `features-08` sets: how Northwind is put together, as three
* layers — the sources it reads, the model it keeps and the places the model
* shows up — and what passes between them. The figures in the model are a
* sample workspace's, copy like the headline. Replace the layers with your
* own architecture and the diagram draws that.
*/
export const EYEBROW = "How it is built"
export const HEADLINE = "Your tools in, one model in the middle, answers out"
export const LEDE =
"Northwind reads the systems you already run, keeps one model of your customers and your metrics, and serves every chart, alert and export from it. Nothing is copied by hand at any step."
/** The picture each part is drawn with: vocabulary of the layout, not copy. */
export type PartIcon =
| "billing"
| "crm"
| "product"
| "support"
| "warehouse"
| "sheet"
| "accounts"
| "metrics"
| "checks"
| "dashboards"
| "alerts"
| "questions"
| "reports"
| "api"
export type Part = { icon: PartIcon; name: string; detail: string }
export type ModelPart = { icon: PartIcon; value: string; name: string; detail: string }
export const SOURCES = {
title: "Your sources",
body: "Read-only, over OAuth or a scoped key.",
parts: [
{ icon: "billing", name: "Billing", detail: "Plans, invoices, refunds" },
{ icon: "crm", name: "CRM", detail: "Accounts, deals, owners" },
{ icon: "product", name: "Product events", detail: "Seats, usage, sessions" },
{ icon: "support", name: "Support desk", detail: "Tickets, response times" },
{ icon: "warehouse", name: "Warehouse", detail: "Any table you choose" },
{ icon: "sheet", name: "Spreadsheets", detail: "Targets and budgets" },
] satisfies Part[],
}
/** What passes from the sources to the model, and from the model out. */
export const HANDOFFS = { in: "Synced every 5 minutes", out: "Served from one model" }
export const MODEL = {
title: "The Northwind model",
body: "Rebuilt after every sync.",
/** The card's own strip: whose model it is, and the state of its checks. */
workspace: "Sample workspace",
status: "All checks passing",
parts: [
{ icon: "accounts", value: "1,284", name: "Accounts", detail: "Each matched across every source" },
{ icon: "metrics", value: "47", name: "Metric definitions", detail: "Versioned, each with an owner" },
{ icon: "checks", value: "212", name: "Checks", detail: "Run on every sync, before anyone looks" },
] satisfies ModelPart[],
}
export const SURFACES = {
title: "Where it shows up",
body: "Every surface reads the same model.",
parts: [
{ icon: "dashboards", name: "Dashboards", detail: "Shared by link" },
{ icon: "alerts", name: "Alerts", detail: "Email and chat" },
{ icon: "questions", name: "Questions", detail: "In plain language" },
{ icon: "reports", name: "Board packs", detail: "Scheduled PDFs" },
{ icon: "api", name: "API and SQL", detail: "For your own tools" },
] satisfies Part[],
}npx shadcn@latest add @vibra/features-08Claim beside four team cards
features-09The claim and its two calls to action beside four linked cards — finance, sales, customer success and product — each with its promise, a sentence and the figures it tracks.
import * as React from "react"
import Link from "next/link"
import { ArrowRightIcon, BoxesIcon, HandshakeIcon, LandmarkIcon, TargetIcon, type LucideIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import {
EYEBROW,
HEADLINE,
LEDE,
PRIMARY_ACTION,
SECONDARY_ACTION,
TEAMS,
TRACKS_LABEL,
type TeamIcon,
} from "./features-09.data"
/** The picture each team is drawn with: vocabulary of the layout, not copy. */
const ICONS: Record<TeamIcon, LucideIcon> = {
finance: LandmarkIcon,
sales: TargetIcon,
success: HandshakeIcon,
product: BoxesIcon,
}
/**
* The claim with the ways in on one side, and on the other the four teams it
* is made for — each a card that is one link to that team's page: its name,
* the promise, one sentence, the figures it tracks and where the link goes.
* The link is stretched over the card, so the card is one target and one tab
* stop, and the focus ring is drawn on the frame, where the card's rounded
* clip cannot cut it off.
*
* From `lg` the claim takes five columns and the cards two by two in the
* other seven; below, the cards follow the claim, two up from `sm`. A server
* component; every word is in `./features-09.data`.
*/
export function Features09() {
const titleId = React.useId()
return (
<section
data-section="features-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 gap-12 lg:grid-cols-12 lg:gap-16">
<div className="flex flex-col items-start lg:col-span-5 lg:py-4">
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-4 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
<p className="mt-5 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>
<ul className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:col-span-7">
{TEAMS.map((team) => {
const Icon = ICONS[team.icon]
return (
<li key={team.team} className="flex min-w-0">
<Card
variant="interactive"
className="relative w-full has-[a:focus-visible]:focus-outline"
>
<CardHeader>
<CardTitle className="flex items-center gap-2.5">
<span className="flex size-7 shrink-0 items-center justify-center rounded-md bg-card ring-1 ring-border">
<Icon aria-hidden="true" className="size-3.5" />
</span>
{team.team}
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-2">
<h3 className="text-base font-semibold text-balance">
{/* The stretched link: its box covers the card, so the
whole card is one target and one tab stop. */}
<Link href={team.link.href} className="outline-none after:absolute after:inset-0">
{team.title}
</Link>
</h3>
<p className="text-sm/6 text-pretty text-muted-foreground">{team.body}</p>
<ul aria-label={TRACKS_LABEL} className="mt-auto flex flex-wrap gap-1.5 pt-2">
{team.tracks.map((figure) => (
<li key={figure} className="rounded-full bg-secondary px-2 py-0.5 text-xs font-medium">
{figure}
</li>
))}
</ul>
</CardContent>
<CardFooter className="justify-between text-sm text-muted-foreground">
<span>{team.link.label}</span>
<ArrowRightIcon
aria-hidden="true"
className="size-4 transition-transform duration-(--duration-fast) ease-(--ease-standard) group-hover/card:translate-x-0.5"
/>
</CardFooter>
</Card>
</li>
)
})}
</ul>
</div>
</section>
)
}
export default Features09/**
* Every word `features-09` sets: the claim, its two calls to action, and the
* four teams Northwind is built for, each with what it gets, the figures it
* tracks and the page that says more. Replace the teams with your own
* audiences and the cards lead to their pages instead.
*/
export type FeatureLink = { label: string; href: string }
export const EYEBROW = "Built for every team"
export const HEADLINE = "One workspace, four teams, the same numbers"
export const LEDE =
"Finance, sales, customer success and product each get the views they ask for, read from one model, so a customer means the same thing in every meeting."
export const PRIMARY_ACTION: FeatureLink = { label: "Start free trial", href: "/sign-up" }
export const SECONDARY_ACTION: FeatureLink = { label: "Book a demo", href: "/contact" }
/** Names each card's row of figures for assistive technology. */
export const TRACKS_LABEL = "Figures it tracks"
export type TeamIcon = "finance" | "sales" | "success" | "product"
export type Team = {
icon: TeamIcon
team: string
title: string
body: string
/** The figures the team opens Northwind for. */
tracks: readonly string[]
link: FeatureLink
}
export const TEAMS: readonly Team[] = [
{
icon: "finance",
team: "Finance",
title: "Close the month on numbers that already agree",
body: "Revenue, deferrals and cash from billing, reconciled against the ledger every night.",
tracks: ["MRR", "Net retention", "Cash collected"],
link: { label: "Northwind for finance", href: "/solutions/finance" },
},
{
icon: "sales",
team: "Sales",
title: "Forecast from the pipeline you actually have",
body: "Stage-weighted pipeline by rep and by region, every deal traced to its account.",
tracks: ["Pipeline", "Win rate", "Quota"],
link: { label: "Northwind for sales", href: "/solutions/sales" },
},
{
icon: "success",
team: "Customer success",
title: "See an account's health before the renewal call",
body: "Usage, tickets and billing on one row per customer, with a score you can read.",
tracks: ["Health", "Expansion", "Churn risk"],
link: { label: "Northwind for customer success", href: "/solutions/success" },
},
{
icon: "product",
team: "Product",
title: "Tie usage to revenue, feature by feature",
body: "Which features paying accounts use, and what the accounts that stopped using them paid.",
tracks: ["Active seats", "Adoption", "Revenue per feature"],
link: { label: "Northwind for product", href: "/solutions/product" },
},
]npx shadcn@latest add @vibra/features-09Specification sheet
features-10Everything the product does in four groups — data, analysis, sharing, security — each line a name, the detail that makes it a fact and the plans that include it, beside a claim that stays in view.
import * as React from "react"
import Link from "next/link"
import { ArrowRightIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { buttonVariants } from "@/components/ui/button"
import { DOCS_LINK, EYEBROW, GROUPS, HEADLINE, LEDE } from "./features-10.data"
/**
* The spec sheet: what the product does, in four groups, each line a name,
* the detail that makes it a fact, and the plans that include it — the band a
* buyer reads before a trial and a reviewer reads before a contract.
*
* Each group is an `h3` over a description list, so every name is read with
* its detail and its plans. From `lg` the claim stands in a column of its own
* and stays in view while the sheet scrolls past it; each line sets its name,
* detail and plans in three columns from `sm`, and stacks them on a phone. A
* server component; every word is in `./features-10.data`.
*/
export function Features10() {
const titleId = React.useId()
return (
<section
data-section="features-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 gap-12 lg:grid-cols-12 lg:gap-16">
<div className="lg:col-span-4">
<div className="flex flex-col items-start lg:sticky lg:top-24">
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-4 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
<p className="mt-5 text-base/7 text-pretty text-muted-foreground">{LEDE}</p>
<Link href={DOCS_LINK.href} className={cn(buttonVariants({ variant: "link" }), "group/link mt-5 h-auto px-0")}>
{DOCS_LINK.label}
<ArrowRightIcon
aria-hidden="true"
data-icon="inline-end"
className="transition-transform duration-(--duration-fast) ease-(--ease-standard) group-hover/link:translate-x-0.5"
/>
</Link>
</div>
</div>
<div className="flex flex-col gap-10 lg:col-span-8">
{GROUPS.map((group) => (
<div key={group.title}>
<h3 className="type-eyebrow text-foreground">{group.title}</h3>
<dl className="mt-3 border-b border-rule">
{group.specs.map((spec) => (
// Name, detail, plans in the source; on a phone the plans
// move up beside the name so each line costs two rows, not three.
<div
key={spec.name}
className="grid grid-cols-[minmax(0,1fr)_auto] items-baseline gap-x-4 gap-y-1 border-t border-rule py-3.5 sm:grid-cols-[11rem_minmax(0,1fr)_auto] sm:gap-x-6"
>
<dt className="col-start-1 row-start-1 text-sm font-medium">{spec.name}</dt>
<dd className="col-span-2 row-start-2 text-sm text-pretty text-muted-foreground sm:col-span-1 sm:col-start-2 sm:row-start-1">
{spec.detail}
</dd>
<dd className="col-start-2 row-start-1 sm:col-start-3">
<Badge variant={spec.plans === "All plans" ? "secondary" : "outline"}>{spec.plans}</Badge>
</dd>
</div>
))}
</dl>
</div>
))}
</div>
</div>
</section>
)
}
export default Features10/**
* Every word `features-10` sets: the specifics a buyer checks before a
* trial — what Northwind does, in groups, each line with the detail that
* makes it a fact and the plans that include it. The plan names are the
* ones the pricing sections sell. Replace the groups with your own product's
* specifics and the sheet lists those.
*/
export type FeatureLink = { label: string; href: string }
export const EYEBROW = "Specifications"
export const HEADLINE = "The details, before you ask for them"
export const LEDE =
"Everything Northwind does, with the limits written down and the plans that include each one. The documentation has the rest."
export const DOCS_LINK: FeatureLink = { label: "Read the documentation", href: "/docs" }
/** Which plans include a line: every plan, the middle one and up, or the top one. */
export type Availability = "All plans" | "Team and up" | "Business"
export type Spec = { name: string; detail: string; plans: Availability }
export type SpecGroup = { title: string; specs: readonly Spec[] }
export const GROUPS: readonly SpecGroup[] = [
{
title: "Data",
specs: [
{ name: "Connectors", detail: "40 sources: billing, CRM, product events, support desks, warehouses and spreadsheets.", plans: "All plans" },
{ name: "Refresh", detail: "Every 5 minutes on Business, hourly on Team, daily on Starter.", plans: "All plans" },
{ name: "History", detail: "Three years backfilled on the first sync, kept for as long as you stay.", plans: "All plans" },
{ name: "Row limits", detail: "None. A workspace is priced by plan, never by rows.", plans: "All plans" },
],
},
{
title: "Analysis",
specs: [
{ name: "Metric definitions", detail: "Versioned, reviewed before they change, each with an owner.", plans: "Team and up" },
{ name: "Questions", detail: "Plain language in, the chart, the rows and the query out.", plans: "Team and up" },
{ name: "Forecasts", detail: "Pipeline and renewals, rebuilt nightly, assumptions listed.", plans: "Team and up" },
{ name: "Alerts", detail: "Ranges learned per metric, sent to email or your team's chat.", plans: "Team and up" },
],
},
{
title: "Sharing",
specs: [
{ name: "Dashboards", detail: "Unlimited, shared by a link that expires when you say.", plans: "All plans" },
{ name: "Board packs", detail: "Any dashboard as a PDF, sent on the schedule you set.", plans: "Team and up" },
{ name: "API and SQL", detail: "Read the model from your own tools, 10,000 requests an hour.", plans: "Business" },
],
},
{
title: "Security",
specs: [
{ name: "Single sign-on", detail: "SAML with any identity provider, and SCIM provisioning.", plans: "Business" },
{ name: "Permissions", detail: "Down to the row, by team, region or partner.", plans: "Business" },
{ name: "Audit log", detail: "Every view, export and change, kept for a year.", plans: "Business" },
{ name: "Data residency", detail: "US or EU, chosen when the workspace is created.", plans: "All plans" },
],
},
]npx shadcn@latest add @vibra/features-10