Integrations
The tools the product connects to, as a grid of marks with what each connection does.
Integrations directory
integrations-01Twelve tools under six categories, searchable by name or by what they sync, filtered by chips, with the ones the workspace has connected marked and a Connect button on every card.
import * as React from "react"
import Link from "next/link"
import { IntegrationDirectory } from "./integrations-01.client"
import { CATEGORIES, DIRECTORY, EYEBROW, FOOTNOTE, HEADLINE, INTEGRATIONS, LEDE } from "./integrations-01.data"
const LINK = "rounded-sm font-medium text-foreground underline underline-offset-4 focus-ring hover:text-brand"
/**
* The integrations page's own band: what connects, searchable and filtered by
* category, with the tools the workspace already has marked Connected and a
* way past the end of the list for the tool that is not there.
*
* The directory is the island; this file is a server component, and every
* word and every tool is in `./integrations-01.data`.
*/
export function Integrations01() {
const titleId = React.useId()
return (
<section
data-section="integrations-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="max-w-2xl">
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-3 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
<p className="mt-4 text-base/7 text-pretty text-muted-foreground">{LEDE}</p>
</div>
<div className="mt-10">
<IntegrationDirectory integrations={INTEGRATIONS} categories={CATEGORIES} copy={DIRECTORY} />
</div>
<p className="mt-8 text-sm text-pretty text-muted-foreground">
{FOOTNOTE.lead}{" "}
<Link href={FOOTNOTE.request.href} className={LINK}>
{FOOTNOTE.request.label}
</Link>
, {FOOTNOTE.joiner}{" "}
<Link href={FOOTNOTE.api.href} className={LINK}>
{FOOTNOTE.api.label}
</Link>
.
</p>
</div>
</section>
)
}
export default Integrations01"use client"
import * as React from "react"
import Link from "next/link"
import {
ActivityIcon,
DatabaseIcon,
FlagIcon,
HandshakeIcon,
KeyRoundIcon,
LandmarkIcon,
LifeBuoyIcon,
MailIcon,
MessageCircleIcon,
MessagesSquareIcon,
ReceiptTextIcon,
RefreshCwIcon,
SearchXIcon,
SheetIcon,
type LucideIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
import { EmptyState } from "@/components/ui/empty-state"
import { SearchInput } from "@/components/ui/search-input"
import { StatusBadge } from "@/components/ui/status-badge"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import type { AppIcon, AppTint, DirectoryCopy, Integration, IntegrationCategory } from "./integrations-01.data"
/** The picture each tool is drawn with: vocabulary of the layout, never a vendor's mark. */
const ICONS: Record<AppIcon, LucideIcon> = {
billing: ReceiptTextIcon,
accounting: LandmarkIcon,
crm: HandshakeIcon,
campaigns: MailIcon,
helpdesk: LifeBuoyIcon,
chat: MessageCircleIcon,
events: ActivityIcon,
flags: FlagIcon,
warehouse: DatabaseIcon,
sheets: SheetIcon,
"team-chat": MessagesSquareIcon,
identity: KeyRoundIcon,
}
/** The tile's hue, a chart slot: a preset that re-points the chart palette re-tints every tile. */
const TINTS: Record<AppTint, string> = {
blue: "[--app-tint:var(--chart-2)]",
teal: "[--app-tint:var(--chart-3)]",
amber: "[--app-tint:var(--chart-4)]",
violet: "[--app-tint:var(--chart-5)]",
stone: "[--app-tint:var(--chart-1)]",
}
const ALL = "all"
const CONNECTED = "connected"
/** `"{shown} of {total}"` with the numbers put in. */
function fill(template: string, values: Record<string, string | number>): string {
return template.replace(/\{(\w+)\}/g, (match, key: string) => (key in values ? String(values[key]) : match))
}
/** A tool's picture: its icon on a tile a tenth into the card plane, the ink leaning a fifth towards the hue. */
function AppTile({ icon, tint }: { icon: AppIcon; tint: AppTint }) {
const Icon = ICONS[icon]
return (
<span
aria-hidden="true"
className={cn(
"flex size-10 shrink-0 items-center justify-center rounded-lg [--app-mix:10%] dark:[--app-mix:16%]",
"bg-[color-mix(in_oklab,var(--app-tint)_var(--app-mix),var(--card))] text-[color-mix(in_oklab,var(--foreground)_80%,var(--app-tint))]",
TINTS[tint]
)}
>
<Icon className="size-5" />
</span>
)
}
/**
* The directory's working part: a search field, a row of category chips (with
* Connected among them, counted), a status line that counts what is shown,
* and the tools as a list of panels, each with a Connect button that flips to
* Disconnect in place — the same button, so focus stays on it. Under
* Connected, disconnecting takes the tool off the list, so focus moves on to
* the tool that takes its place, or back to the chips when none is left.
*
* It connects nothing: call your own OAuth flow where `toggle` flips the set.
*/
export function IntegrationDirectory({
integrations,
categories,
copy,
}: {
integrations: readonly Integration[]
categories: readonly IntegrationCategory[]
copy: DirectoryCopy
}) {
const [query, setQuery] = React.useState("")
const [filter, setFilter] = React.useState(ALL)
const [connected, setConnected] = React.useState(
() => new Set(integrations.filter((app) => app.connected).map((app) => app.name))
)
const actions = React.useRef(new Map<string, HTMLButtonElement | null>())
const chips = React.useRef<HTMLDivElement>(null)
// The position of a tool that just left the list, so focus can land on the one after it.
const vacated = React.useRef<number | null>(null)
const labels = new Map(categories.map((category) => [category.id, category.label]))
const needle = query.trim().toLowerCase()
const shown = integrations.filter((app) => {
if (filter === CONNECTED && !connected.has(app.name)) return false
if (filter !== ALL && filter !== CONNECTED && app.category !== filter) return false
if (!needle) return true
return [app.name, labels.get(app.category) ?? "", app.syncs].some((text) => text.toLowerCase().includes(needle))
})
React.useEffect(() => {
const index = vacated.current
if (index === null) return
vacated.current = null
const next = shown[Math.min(index, shown.length - 1)]
const target = next ? actions.current.get(next.name) : chips.current?.querySelector<HTMLElement>("[aria-pressed=true]")
target?.focus()
})
function toggle(app: Integration, index: number) {
const on = connected.has(app.name)
setConnected((current) => {
const next = new Set(current)
if (on) next.delete(app.name)
else next.add(app.name)
return next
})
if (on && filter === CONNECTED) vacated.current = index
}
const chipClass =
"h-7 rounded-full border-border px-3 text-label font-normal text-foreground aria-pressed:border-transparent aria-pressed:bg-brand-muted aria-pressed:font-medium aria-pressed:text-foreground"
return (
<div data-slot="integration-directory" className="flex flex-col gap-6">
<div className="flex flex-col gap-4">
<SearchInput
value={query}
onValueChange={setQuery}
aria-label={copy.searchLabel}
placeholder={copy.searchPlaceholder}
className="max-w-md [&_input]:h-9"
/>
<ToggleGroup
ref={chips}
aria-label={copy.filterLabel}
variant="outline"
size="sm"
spacing={2}
value={[filter]}
// Pressing the chosen chip hands back an empty group; a filter is always chosen.
onValueChange={(next) => {
const [value] = next
if (value) setFilter(value)
}}
className="flex-wrap"
>
<ToggleGroupItem value={ALL} className={chipClass}>
{copy.allLabel}
</ToggleGroupItem>
<ToggleGroupItem value={CONNECTED} className={chipClass}>
{copy.connectedLabel}{" "}
<span className="text-muted-foreground tabular-nums">{connected.size}</span>
</ToggleGroupItem>
{categories.map((category) => (
<ToggleGroupItem key={category.id} value={category.id} className={chipClass}>
{category.label}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
<p role="status" aria-live="polite" className="text-sm text-muted-foreground tabular-nums">
{fill(copy.summary, { shown: shown.length, total: integrations.length, connected: connected.size })}
</p>
{shown.length === 0 ? (
<EmptyState
variant="dashed"
icon={<SearchXIcon />}
title={needle ? fill(copy.emptyTitle, { query: query.trim() }) : copy.emptyConnectedTitle}
description={copy.emptyDescription}
action={
<Link href={copy.emptyAction.href} className={buttonVariants({ variant: "outline" })}>
{copy.emptyAction.label}
</Link>
}
/>
) : (
<ul aria-label={copy.listLabel} className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{shown.map((app, index) => {
const on = connected.has(app.name)
return (
<li key={app.name} data-connected={on || undefined} className="flex flex-col gap-3 panel p-4">
<div className="flex items-center gap-3">
<AppTile icon={app.icon} tint={app.tint} />
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
<h3 className="text-sm font-semibold">{app.name}</h3>
{on ? (
<StatusBadge status="connected" variant="success" size="sm" label={copy.connectedBadge} />
) : null}
</div>
<p className="flex items-center gap-1.5 text-xs text-muted-foreground">
{labels.get(app.category)}{" "}
<span aria-hidden="true">·</span>{" "}
<RefreshCwIcon aria-hidden="true" className="size-3 shrink-0" />
<span className="sr-only">{copy.cadenceLabel}: </span>
{app.cadence}
</p>
</div>
<Button
ref={(node) => {
actions.current.set(app.name, node)
}}
type="button"
size="sm"
variant={on ? "ghost" : "outline"}
aria-label={`${on ? copy.disconnect : copy.connect} ${app.name}`}
onClick={() => toggle(app, index)}
className="shrink-0"
>
{on ? copy.disconnect : copy.connect}
</Button>
</div>
<p className="text-sm text-pretty text-muted-foreground">{app.syncs}</p>
</li>
)
})}
</ul>
)}
</div>
)
}/**
* Every word `integrations-01` sets, and the directory it searches: twelve
* tools filed under six categories, each with what it syncs, how often, and
* whether the sample workspace has it connected. None of them is a real
* product — each is drawn by the layout from a lucide icon (`icon` names it)
* on a tile tinted in one of the chart hues (`tint`), so the page never shows
* a mark it has no right to. Replace the list with your own tools and the
* directory searches, filters and connects those.
*/
export type AppIcon =
| "billing"
| "accounting"
| "crm"
| "campaigns"
| "helpdesk"
| "chat"
| "events"
| "flags"
| "warehouse"
| "sheets"
| "team-chat"
| "identity"
/** A chart hue, or the first chart grey, that the tile is tinted with: decoration, never a status. */
export type AppTint = "blue" | "teal" | "amber" | "violet" | "stone"
export type IntegrationCategory = { id: string; label: string }
export type Integration = {
name: string
icon: AppIcon
tint: AppTint
/** An `IntegrationCategory` id. */
category: string
/** What comes across, in a few words. */
syncs: string
/** How often it syncs, as a reader would say it. */
cadence: string
/** Whether the sample workspace has it connected when the page opens. */
connected: boolean
}
/** What the directory says, handed to its island as plain props. */
export type DirectoryCopy = {
searchLabel: string
searchPlaceholder: string
filterLabel: string
allLabel: string
connectedLabel: string
listLabel: string
/** The status line; `{shown}`, `{total}` and `{connected}` are counted as the reader filters. */
summary: string
cadenceLabel: string
connect: string
disconnect: string
connectedBadge: string
/** `{query}` is what the reader typed. */
emptyTitle: string
emptyConnectedTitle: string
emptyDescription: string
emptyAction: { label: string; href: string }
}
export type DirectoryLink = { label: string; href: string }
export const EYEBROW = "Integrations"
export const HEADLINE = "Connect the tools your numbers come from"
export const LEDE =
"Search the tools Northwind reads from and writes to. Each one connects in a couple of minutes and syncs on its own schedule, so a report is never older than its slowest source."
export const CATEGORIES: readonly IntegrationCategory[] = [
{ id: "finance", label: "Finance" },
{ id: "sales", label: "Sales" },
{ id: "support", label: "Support" },
{ id: "product", label: "Product" },
{ id: "data", label: "Data" },
{ id: "workplace", label: "Workplace" },
]
export const INTEGRATIONS: readonly Integration[] = [
{ name: "Duesbrook", icon: "billing", tint: "blue", category: "finance", syncs: "Invoices, plans and refunds", cadence: "Every 15 min", connected: true },
{ name: "Ledgermere", icon: "accounting", tint: "stone", category: "finance", syncs: "Journals, accounts and the close", cadence: "Daily", connected: false },
{ name: "Closemere", icon: "crm", tint: "amber", category: "sales", syncs: "Deals, owners and pipeline stages", cadence: "Every 5 min", connected: true },
{ name: "Letterbrook", icon: "campaigns", tint: "violet", category: "sales", syncs: "Campaigns, opens and list growth", cadence: "Hourly", connected: false },
{ name: "Replyhouse", icon: "helpdesk", tint: "teal", category: "support", syncs: "Tickets, first replies and ratings", cadence: "Every 5 min", connected: false },
{ name: "Convoline", icon: "chat", tint: "blue", category: "support", syncs: "Chats, wait times and ratings", cadence: "Every 5 min", connected: false },
{ name: "Clickbrook", icon: "events", tint: "violet", category: "product", syncs: "Sign-ins, seats and feature use", cadence: "Real time", connected: true },
{ name: "Switchmoor", icon: "flags", tint: "amber", category: "product", syncs: "Flags, rollouts and who saw them", cadence: "Real time", connected: false },
{ name: "Silomere", icon: "warehouse", tint: "stone", category: "data", syncs: "Tables and views, read and written", cadence: "Hourly", connected: false },
{ name: "Sheetmoor", icon: "sheets", tint: "teal", category: "data", syncs: "Sheets in, any report out", cadence: "On demand", connected: false },
{ name: "Huddlemere", icon: "team-chat", tint: "blue", category: "workplace", syncs: "Alerts and the Monday digest, to a channel", cadence: "As they happen", connected: false },
{ name: "Gatemere", icon: "identity", tint: "stone", category: "workplace", syncs: "Users and groups, provisioned over SCIM", cadence: "Real time", connected: false },
]
export const DIRECTORY: DirectoryCopy = {
searchLabel: "Search integrations",
searchPlaceholder: "Search by name or what it syncs",
filterLabel: "Filter by category",
allLabel: "All",
connectedLabel: "Connected",
listLabel: "Integrations",
summary: "Showing {shown} of {total} · {connected} connected",
cadenceLabel: "Syncs",
connect: "Connect",
disconnect: "Disconnect",
connectedBadge: "Connected",
emptyTitle: "Nothing matches “{query}”",
emptyConnectedTitle: "Nothing connected yet",
emptyDescription: "We build the integrations customers ask for most. Tell us which tool you need.",
emptyAction: { label: "Request an integration", href: "/integrations/request" },
}
/** The line under the directory: the two ways past the end of the list. */
export const FOOTNOTE: { lead: string; request: DirectoryLink; joiner: string; api: DirectoryLink } = {
lead: "Missing a tool?",
request: { label: "Request it", href: "/integrations/request" },
joiner: "or connect anything with",
api: { label: "the REST API", href: "/docs/api" },
}npx shadcn@latest add @vibra/integrations-01Integrations hub
integrations-02Four tools the data comes in from, Northwind in the middle matching their records to accounts, and four it writes back to, each with what crosses the line and how often.
import * as React from "react"
import Link from "next/link"
import {
ActivityIcon,
ArrowDownIcon,
CalendarDaysIcon,
ChevronRightIcon,
DatabaseIcon,
HandshakeIcon,
LifeBuoyIcon,
MessagesSquareIcon,
ReceiptTextIcon,
RefreshCwIcon,
SheetIcon,
type LucideIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { LogoMark } from "@/components/ui/logo-mark"
import {
DESTINATIONS,
EYEBROW,
HEADLINE,
HUB,
LEDE,
PRIMARY_LINK,
SECONDARY_LINK,
SOURCES,
type AppIcon,
type AppTint,
type Connection,
} from "./integrations-02.data"
const ICONS: Record<AppIcon, LucideIcon> = {
billing: ReceiptTextIcon,
crm: HandshakeIcon,
events: ActivityIcon,
helpdesk: LifeBuoyIcon,
warehouse: DatabaseIcon,
"team-chat": MessagesSquareIcon,
sheets: SheetIcon,
calendar: CalendarDaysIcon,
}
const TINTS: Record<AppTint, string> = {
blue: "[--app-tint:var(--chart-2)]",
teal: "[--app-tint:var(--chart-3)]",
amber: "[--app-tint:var(--chart-4)]",
violet: "[--app-tint:var(--chart-5)]",
stone: "[--app-tint:var(--chart-1)]",
}
/** A tool's picture: its icon on a tile a tenth into the card plane, the ink leaning a fifth towards the hue. */
function AppTile({ icon, tint }: { icon: AppIcon; tint: AppTint }) {
const Icon = ICONS[icon]
return (
<span
aria-hidden="true"
className={cn(
"flex size-9 shrink-0 items-center justify-center rounded-lg [--app-mix:10%] dark:[--app-mix:16%]",
"bg-[color-mix(in_oklab,var(--app-tint)_var(--app-mix),var(--card))] text-[color-mix(in_oklab,var(--foreground)_80%,var(--app-tint))]",
TINTS[tint]
)}
>
<Icon className="size-4" />
</span>
)
}
/**
* One side of the hub: a named list of tools, and — from `lg` — the rules that
* join each to a bus and the bus to the hub. The rules 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 row at any height, and they are hidden: the list's name
* says which way the data goes.
*/
function Side({ label, items, flow }: { label: string; items: readonly Connection[]; flow: "in" | "out" }) {
const labelId = React.useId()
const inbound = flow === "in"
return (
<div className="flex flex-col">
{/* 20px and 12px: the hub column is pushed down by exactly this much, so its middle meets the list's. */}
<p id={labelId} className={cn("type-eyebrow mb-3 h-5", !inbound && "lg:ps-12")}>
{label}
</p>
<div className={cn("relative", inbound ? "lg:pe-12" : "lg:ps-12")}>
<ul aria-labelledby={labelId} className="grid gap-3 sm:grid-cols-2 lg:auto-rows-fr lg:grid-cols-1">
{items.map((item) => (
<li
key={item.name}
className={cn(
"relative flex items-center gap-3 panel p-3",
inbound
? "lg:after:absolute lg:after:top-1/2 lg:after:start-full lg:after:h-px lg:after:w-6 lg:after:bg-rule"
: "lg:before:absolute lg:before:top-1/2 lg:before:end-full lg:before:h-px lg:before:w-6 lg:before:bg-rule"
)}
>
<AppTile icon={item.icon} tint={item.tint} />
<div className="flex min-w-0 flex-col gap-0.5">
<p className="text-sm font-medium">{item.name}</p>
<p className="text-xs text-pretty text-muted-foreground">{item.carries}</p>
<p className="flex items-center gap-1.5 text-xs text-muted-foreground">
<RefreshCwIcon aria-hidden="true" className="size-3 shrink-0" />
{item.cadence}
</p>
</div>
</li>
))}
</ul>
<div aria-hidden="true" className={cn("absolute inset-y-0 hidden w-12 lg:block", inbound ? "end-0" : "start-0")}>
<span className="absolute top-[calc((100%-2.25rem)/8)] bottom-[calc((100%-2.25rem)/8)] start-6 w-px bg-rule" />
<span className={cn("absolute top-1/2 h-px bg-rule", inbound ? "end-0 start-6" : "end-6 start-0")} />
<ChevronRightIcon
className={cn(
// The flow runs to the inline end, so the chevron turns with it.
"absolute top-1/2 size-4 -translate-y-1/2 text-faint-foreground rtl:rotate-180",
inbound ? "-end-1.5" : "start-0.5"
)}
/>
</div>
</div>
</div>
)
}
/**
* Integrations as a diagram rather than a wall of marks: the tools a company
* reads its figures from on one side, Northwind in the middle matching their
* records to accounts, and the tools it writes back to on the other — each
* with what crosses the line and how often.
*
* Both sides are lists named by what they are ("Reads from", "Writes to"), so
* the diagram reads as content without its rules; the rules are drawn in
* CSS from each list's own box. Below `lg` the diagram stacks — sources, the
* hub, destinations — with an arrow between each. A server component; every
* word and figure is in `./integrations-02.data`.
*/
export function Integrations02() {
const titleId = React.useId()
return (
<section
data-section="integrations-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="mx-auto max-w-2xl text-center">
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-3 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
<p className="mt-4 text-base/7 text-pretty text-muted-foreground">{LEDE}</p>
</div>
<div className="mx-auto mt-12 grid max-w-5xl gap-4 lg:mt-16 lg:grid-cols-[minmax(0,1fr)_15rem_minmax(0,1fr)] lg:gap-0">
<Side label={SOURCES.label} items={SOURCES.items} flow="in" />
<div className="flex flex-col items-center gap-4 py-2 lg:justify-center lg:py-0 lg:pt-8">
<ArrowDownIcon aria-hidden="true" className="size-4 text-faint-foreground lg:hidden" />
<Card className="w-full max-w-xs">
<CardHeader>
<CardTitle>
<LogoMark name="Northwind" />
</CardTitle>
<CardDescription>{HUB.description}</CardDescription>
</CardHeader>
<CardContent>
<dl className="grid grid-cols-2 gap-3">
{HUB.figures.map((figure) => (
<div key={figure.label} className="flex flex-col-reverse gap-1">
<dt className="text-xs text-pretty text-muted-foreground">{figure.label}</dt>
<dd className="type-numeral text-xl">{figure.value}</dd>
</div>
))}
</dl>
</CardContent>
</Card>
<ArrowDownIcon aria-hidden="true" className="size-4 text-faint-foreground lg:hidden" />
</div>
<Side label={DESTINATIONS.label} items={DESTINATIONS.items} flow="out" />
</div>
<div className="mt-12 flex flex-wrap items-center justify-center gap-3 lg:mt-16">
<Link href={PRIMARY_LINK.href} className={cn(buttonVariants({ size: "lg" }), "h-10 px-5")}>
{PRIMARY_LINK.label}
</Link>
<Link href={SECONDARY_LINK.href} className={cn(buttonVariants({ variant: "outline", size: "lg" }), "h-10 px-5")}>
{SECONDARY_LINK.label}
</Link>
</div>
</div>
</section>
)
}
export default Integrations02/**
* Every word and figure `integrations-02` sets: the tools Northwind reads
* from, the ones it writes to, and what it does in between. The tools are
* fictional — each drawn by the layout from a lucide icon (`icon`) on a tile
* tinted in a chart hue (`tint`), never a vendor's mark — and each says what
* crosses the line and how often, so the diagram explains the product rather
* than decorating it.
*/
export type AppIcon = "billing" | "crm" | "events" | "helpdesk" | "warehouse" | "team-chat" | "sheets" | "calendar"
export type AppTint = "blue" | "teal" | "amber" | "violet" | "stone"
export type Connection = { name: string; icon: AppIcon; tint: AppTint; carries: string; cadence: string }
export type HubLink = { label: string; href: string }
export const EYEBROW = "How it connects"
export const HEADLINE = "In from the tools you run, out to the ones you read"
export const LEDE =
"Northwind reads the systems where each figure starts, matches every record to the account it belongs to, and writes the answer back where your teams already look."
export const SOURCES: { label: string; items: readonly Connection[] } = {
label: "Reads from",
items: [
{ name: "Duesbrook", icon: "billing", tint: "blue", carries: "Invoices and refunds", cadence: "Every 15 min" },
{ name: "Closemere", icon: "crm", tint: "amber", carries: "Deals and their owners", cadence: "Every 5 min" },
{ name: "Clickbrook", icon: "events", tint: "violet", carries: "Sign-ins and feature use", cadence: "Real time" },
{ name: "Replyhouse", icon: "helpdesk", tint: "teal", carries: "Tickets and first replies", cadence: "Every 5 min" },
],
}
export const DESTINATIONS: { label: string; items: readonly Connection[] } = {
label: "Writes to",
items: [
{ name: "Silomere", icon: "warehouse", tint: "stone", carries: "Every table, joined by account", cadence: "Hourly" },
{ name: "Huddlemere", icon: "team-chat", tint: "blue", carries: "An alert when a figure moves", cadence: "As it happens" },
{ name: "Sheetmoor", icon: "sheets", tint: "teal", carries: "The board pack, as a sheet", cadence: "Mondays at 06:00" },
{ name: "Weekmere", icon: "calendar", tint: "violet", carries: "The agenda for each review", cadence: "Before the meeting" },
],
}
/** The middle of the diagram: what Northwind does with what it reads. */
export const HUB = {
title: "Northwind",
description: "Matches every record to an account",
figures: [
{ label: "Accounts matched", value: "1,284" },
{ label: "Records with a match", value: "98.6%" },
],
}
export const PRIMARY_LINK: HubLink = { label: "Browse all integrations", href: "/integrations" }
export const SECONDARY_LINK: HubLink = { label: "How syncing works", href: "/docs/sync" }npx shadcn@latest add @vibra/integrations-02Featured integration
integrations-03One integration in depth: the question it answers, three steps to set it up and what it may touch, beside the records that sync with their direction and cadence and the fields with where each lands.
import * as React from "react"
import Link from "next/link"
import { ArrowLeftIcon, ArrowLeftRightIcon, ArrowRightIcon, HandshakeIcon, ShieldCheckIcon, type LucideIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { buttonVariants } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { SimpleTable } from "@/components/ui/simple-table"
import { StatusBadge } from "@/components/ui/status-badge"
import {
APP,
DIRECTIONS,
EYEBROW,
HEADLINE,
LEDE,
MAPPING,
PERMISSIONS,
PRIMARY_ACTION,
SECONDARY_ACTION,
SETUP,
SYNC,
type FieldRow,
type SyncDirection,
type SyncRow,
} from "./integrations-03.data"
const ARROWS: Record<SyncDirection, LucideIcon> = {
in: ArrowRightIcon,
out: ArrowLeftIcon,
both: ArrowLeftRightIcon,
}
const SYNC_COLUMNS = [
{ key: "object", header: SYNC.columns[0], className: "font-medium" },
{
key: "direction",
header: SYNC.columns[1],
cell: (row: SyncRow) => {
const Arrow = ARROWS[row.direction]
return (
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
<Arrow aria-hidden="true" className="size-3.5 shrink-0" />
{DIRECTIONS[row.direction]}
</span>
)
},
},
{ key: "cadence", header: SYNC.columns[2], className: "text-muted-foreground" },
]
const MAPPING_COLUMNS = [
{ key: "from", header: MAPPING.columns[0], cell: (row: FieldRow) => <code className="font-mono text-xs">{row.from}</code> },
{ key: "to", header: MAPPING.columns[1], cell: (row: FieldRow) => <code className="font-mono text-xs">{row.to}</code> },
]
/**
* One integration in depth, where a directory gives each a line: who it is,
* the question it lets a team answer, the three steps to set it up and what
* it may touch — beside two cards that show exactly what crosses: the records
* with their direction and cadence, and each field with where it lands.
*
* Direction is said in words beside every arrow; field names are code, set in
* the mono face. A server component; every word is in `./integrations-03.data`.
*/
export function Integrations03() {
const titleId = React.useId()
const stepsId = React.useId()
return (
<section
data-section="integrations-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 grid max-w-7xl gap-12 lg:grid-cols-12 lg:gap-16">
<div className="flex flex-col lg:col-span-5">
<p className="type-eyebrow">{EYEBROW}</p>
<div className="mt-5 flex items-center gap-3">
<span
aria-hidden="true"
className="flex size-12 shrink-0 items-center justify-center rounded-xl [--app-tint:var(--chart-4)] [--app-mix:10%] dark:[--app-mix:16%] bg-[color-mix(in_oklab,var(--app-tint)_var(--app-mix),var(--card))] text-[color-mix(in_oklab,var(--foreground)_80%,var(--app-tint))]"
>
<HandshakeIcon className="size-6" />
</span>
<div className="flex min-w-0 flex-col gap-1">
<p className="text-base font-semibold">
{APP.name} <span className="font-normal text-muted-foreground">· {APP.kind}</span>
</p>
<ul className="flex flex-wrap gap-1.5">
{APP.badges.map((badge) => (
<li key={badge}>
<Badge variant="outline">{badge}</Badge>
</li>
))}
</ul>
</div>
</div>
<h2 id={titleId} className="type-display mt-8 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
<p className="mt-4 text-base/7 text-pretty text-muted-foreground">{LEDE}</p>
<h3 id={stepsId} className="mt-8 text-sm font-semibold">
{SETUP.title}
</h3>
<ol aria-labelledby={stepsId} className="mt-3 flex flex-col gap-3">
{SETUP.steps.map((step, index) => (
<li key={step} className="flex gap-3 text-sm/6">
<span
aria-hidden="true"
className="flex size-6 shrink-0 items-center justify-center rounded-full bg-secondary text-xs font-medium tabular-nums"
>
{index + 1}
</span>
<span className="text-pretty">{step}</span>
</li>
))}
</ol>
<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>
<p className="mt-6 flex items-start gap-2 text-sm text-pretty text-muted-foreground">
<ShieldCheckIcon aria-hidden="true" className="mt-0.5 size-4 shrink-0" />
{PERMISSIONS}
</p>
</div>
<div className="flex flex-col gap-4 lg:col-span-7">
<Card>
<CardHeader>
<CardTitle>
<h3>{SYNC.title}</h3>
</CardTitle>
<CardDescription>{SYNC.description}</CardDescription>
</CardHeader>
<CardContent className="p-0">
<SimpleTable columns={SYNC_COLUMNS} rows={SYNC.rows} rowKey="object" className="rounded-none border-0" />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>
<h3>{MAPPING.title}</h3>
</CardTitle>
<CardDescription>{MAPPING.description}</CardDescription>
</CardHeader>
<CardContent className="p-0">
<SimpleTable columns={MAPPING_COLUMNS} rows={MAPPING.rows} rowKey="from" className="rounded-none border-0" />
</CardContent>
<CardFooter className="flex-wrap justify-between gap-2">
<StatusBadge status="healthy" variant="success" label={MAPPING.status} />
<p className="text-xs text-muted-foreground tabular-nums">{MAPPING.note}</p>
</CardFooter>
</Card>
</div>
</div>
</section>
)
}
export default Integrations03/**
* Every word `integrations-03` sets: one integration in depth — who it is,
* what it lets a team answer, what syncs in which direction and how often,
* which field lands where, what it may read and write, and the three steps to
* set it up. Closemere is fictional, drawn by the layout from a lucide icon
* on a tile tinted in a chart hue; replace it with the integration you want
* to put in front of readers.
*/
export type SpotlightLink = { label: string; href: string }
/** Which way a record travels: into Northwind, out to the tool, or both. */
export type SyncDirection = "in" | "out" | "both"
export type SyncRow = { object: string; direction: SyncDirection; cadence: string }
export type FieldRow = { from: string; to: string }
export const EYEBROW = "Featured integration"
export const APP = {
name: "Closemere",
kind: "CRM",
badges: ["Two-way sync", "On every plan"],
} as const
export const HEADLINE = "See which deals turn into revenue"
export const LEDE =
"Closemere holds the pipeline; Northwind already holds what was billed. Connected, every closed deal is matched to its invoices, so a forecast can be checked against what actually came in."
export const SETUP = {
title: "Set up in about four minutes",
steps: [
"Sign in to Closemere as an admin and allow read access.",
"Choose the pipelines to sync. The rest stay private.",
"Match Closemere owners to people in Northwind, or let their email addresses do it.",
],
}
export const PRIMARY_ACTION: SpotlightLink = { label: "Connect Closemere", href: "/integrations/closemere/connect" }
export const SECONDARY_ACTION: SpotlightLink = { label: "Read the setup guide", href: "/docs/integrations/closemere" }
export const PERMISSIONS = "Reads deals, accounts and owners. Writes one field back: revenue to date on each account."
/** What each direction is called, in words: the arrows beside them are decoration. */
export const DIRECTIONS: Record<SyncDirection, string> = {
in: "To Northwind",
out: "To Closemere",
both: "Both ways",
}
export const SYNC: { title: string; description: string; columns: [string, string, string]; rows: SyncRow[] } = {
title: "What syncs",
description: "Records, the way they travel and how often",
columns: ["Record", "Direction", "How often"],
rows: [
{ object: "Deals", direction: "in", cadence: "Every 5 min" },
{ object: "Accounts", direction: "both", cadence: "Every 5 min" },
{ object: "Owners", direction: "in", cadence: "Hourly" },
{ object: "Revenue to date", direction: "out", cadence: "Daily" },
],
}
export const MAPPING: {
title: string
description: string
columns: [string, string]
rows: FieldRow[]
status: string
note: string
} = {
title: "Field mapping",
description: "Where each Closemere field lands",
columns: ["Closemere", "Northwind"],
rows: [
{ from: "deal.amount", to: "pipeline_value" },
{ from: "deal.close_date", to: "expected_close" },
{ from: "deal.stage", to: "stage" },
{ from: "deal.owner_email", to: "account_owner" },
{ from: "account.domain", to: "account_key" },
],
status: "Healthy",
note: "Last sync 3 min ago · 12,480 deals",
}npx shadcn@latest add @vibra/integrations-03Integrations by team
integrations-04The claim and the way to the whole directory beside the tools grouped by the team that owns them, each with what it is, what it syncs and the plan it comes with.
import * as React from "react"
import Link from "next/link"
import {
ActivityIcon,
ArrowRightIcon,
BanknoteIcon,
DatabaseIcon,
FlagIcon,
HandshakeIcon,
LandmarkIcon,
LifeBuoyIcon,
MessageCircleIcon,
PhoneIcon,
ReceiptTextIcon,
SheetIcon,
UsersIcon,
type LucideIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import {
API_NOTE,
EYEBROW,
GROUPS,
HEADLINE,
LEDE,
PLAN_LABEL,
PRIMARY_ACTION,
type AppIcon,
type AppTint,
type ToolGroup,
} from "./integrations-04.data"
const ICONS: Record<AppIcon, LucideIcon> = {
billing: ReceiptTextIcon,
accounting: LandmarkIcon,
crm: HandshakeIcon,
calls: PhoneIcon,
helpdesk: LifeBuoyIcon,
chat: MessageCircleIcon,
events: ActivityIcon,
flags: FlagIcon,
warehouse: DatabaseIcon,
sheets: SheetIcon,
hr: UsersIcon,
payroll: BanknoteIcon,
}
const TINTS: Record<AppTint, string> = {
blue: "[--app-tint:var(--chart-2)]",
teal: "[--app-tint:var(--chart-3)]",
amber: "[--app-tint:var(--chart-4)]",
violet: "[--app-tint:var(--chart-5)]",
stone: "[--app-tint:var(--chart-1)]",
}
/** One team's tools: a list named by its heading, each row ruled off from the next. */
function Group({ group }: { group: ToolGroup }) {
const headingId = React.useId()
return (
<div className="flex flex-col">
<h3 id={headingId} className="border-b border-rule pb-2 text-sm font-semibold">
{group.heading}
</h3>
<ul aria-labelledby={headingId} className="flex flex-col">
{group.tools.map((tool) => {
const Icon = ICONS[tool.icon]
return (
<li key={tool.name} className="flex items-center gap-3 border-b border-border py-3">
<span
aria-hidden="true"
className={cn(
"flex size-8 shrink-0 items-center justify-center rounded-lg [--app-mix:10%] dark:[--app-mix:16%]",
"bg-[color-mix(in_oklab,var(--app-tint)_var(--app-mix),var(--card))] text-[color-mix(in_oklab,var(--foreground)_80%,var(--app-tint))]",
TINTS[tool.tint]
)}
>
<Icon className="size-4" />
</span>
<div className="flex min-w-0 flex-1 flex-col">
<p className="text-sm">
<span className="font-medium">{tool.name}</span>{" "}
<span className="text-muted-foreground">{tool.kind}</span>
</p>
<p className="text-xs text-pretty text-muted-foreground">{tool.syncs}</p>
</div>
<p className="shrink-0 text-right text-xs text-muted-foreground">
<span className="sr-only">{PLAN_LABEL}: </span>
{tool.plan}
</p>
</li>
)
})}
</ul>
</div>
)
}
/**
* What the product connects to as a spec sheet rather than a wall of logos:
* the claim and the way to the whole directory on the left, and on the right
* the tools grouped by the team that owns them — each with what it is, what
* it syncs and the plan it comes with, divided by rules rather than boxed.
*
* Each group is a list named by its heading. Two columns of groups from `md`;
* from `lg` the claim takes four columns and the groups eight. A server
* component; every word is in `./integrations-04.data`.
*/
export function Integrations04() {
const titleId = React.useId()
return (
<section
data-section="integrations-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 grid max-w-7xl gap-12 lg:grid-cols-12 lg:gap-16">
<div className="flex flex-col items-start lg:col-span-4">
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-3 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
<p className="mt-4 text-base/7 text-pretty text-muted-foreground">{LEDE}</p>
<Link href={PRIMARY_ACTION.href} className={cn(buttonVariants({ size: "lg" }), "mt-8 h-10 px-5")}>
{PRIMARY_ACTION.label}
</Link>
<p className="mt-6 text-sm text-muted-foreground">
{API_NOTE.lead}{" "}
<Link
href={API_NOTE.link.href}
className="group/api inline-flex items-center gap-1 rounded-sm font-medium text-foreground underline-offset-4 focus-ring hover:underline"
>
{API_NOTE.link.label}
<ArrowRightIcon
aria-hidden="true"
className="size-3.5 transition-transform duration-(--duration-fast) ease-(--ease-standard) group-hover/api:translate-x-0.5 motion-reduce:transition-none"
/>
</Link>
</p>
</div>
<div className="grid gap-x-10 gap-y-10 md:grid-cols-2 lg:col-span-8">
{GROUPS.map((group) => (
<Group key={group.heading} group={group} />
))}
</div>
</div>
</section>
)
}
export default Integrations04/**
* Every word `integrations-04` sets: the tools Northwind connects to, grouped
* by the team that owns them, each with what it is, what it syncs and the
* plan it comes with. The tools are fictional — drawn by the layout from a
* lucide icon (`icon`) on a tile tinted in a chart hue (`tint`) — so the list
* never borrows a vendor's mark. Replace the groups with your own.
*/
export type AppIcon =
| "billing"
| "accounting"
| "crm"
| "calls"
| "helpdesk"
| "chat"
| "events"
| "flags"
| "warehouse"
| "sheets"
| "hr"
| "payroll"
export type AppTint = "blue" | "teal" | "amber" | "violet" | "stone"
export type ListedTool = { name: string; kind: string; icon: AppIcon; tint: AppTint; syncs: string; plan: string }
export type ToolGroup = { heading: string; tools: readonly ListedTool[] }
export type ListLink = { label: string; href: string }
export const EYEBROW = "Connects to"
export const HEADLINE = "Built on the systems your teams already run"
export const LEDE =
"Northwind connects to the tools finance, sales, support and product keep their records in. Nothing to export and nothing to re-key: connect once and every report stays current."
export const PRIMARY_ACTION: ListLink = { label: "See all integrations", href: "/integrations" }
export const API_NOTE: { lead: string; link: ListLink } = {
lead: "Using something else?",
link: { label: "Build on the REST API", href: "/docs/api" },
}
/** What the plan column is called, for a screen reader that meets the plan on its own. */
export const PLAN_LABEL = "Plan"
export const GROUPS: readonly ToolGroup[] = [
{
heading: "Finance",
tools: [
{ name: "Duesbrook", kind: "Billing", icon: "billing", tint: "blue", syncs: "Invoices, plans and refunds", plan: "Every plan" },
{ name: "Ledgermere", kind: "Accounting", icon: "accounting", tint: "stone", syncs: "Journals and the month-end close", plan: "Growth and up" },
],
},
{
heading: "Sales",
tools: [
{ name: "Closemere", kind: "CRM", icon: "crm", tint: "amber", syncs: "Deals, owners and pipeline stages", plan: "Every plan" },
{ name: "Dialmere", kind: "Calls", icon: "calls", tint: "violet", syncs: "Calls, outcomes and talk time", plan: "Growth and up" },
],
},
{
heading: "Support",
tools: [
{ name: "Replyhouse", kind: "Help desk", icon: "helpdesk", tint: "teal", syncs: "Tickets, first replies and ratings", plan: "Every plan" },
{ name: "Convoline", kind: "Live chat", icon: "chat", tint: "blue", syncs: "Chats, wait times and ratings", plan: "Every plan" },
],
},
{
heading: "Product",
tools: [
{ name: "Clickbrook", kind: "Product events", icon: "events", tint: "violet", syncs: "Sign-ins, seats and feature use", plan: "Every plan" },
{ name: "Switchmoor", kind: "Feature flags", icon: "flags", tint: "amber", syncs: "Rollouts and who saw them", plan: "Scale" },
],
},
{
heading: "Data",
tools: [
{ name: "Silomere", kind: "Warehouse", icon: "warehouse", tint: "stone", syncs: "Every table, read and written", plan: "Scale" },
{ name: "Sheetmoor", kind: "Spreadsheets", icon: "sheets", tint: "teal", syncs: "Sheets in, any report out", plan: "Every plan" },
],
},
{
heading: "People",
tools: [
{ name: "Rostermill", kind: "HR", icon: "hr", tint: "violet", syncs: "Headcount, teams and start dates", plan: "Growth and up" },
{ name: "Wagemoor", kind: "Payroll", icon: "payroll", tint: "teal", syncs: "Payroll cost by team and month", plan: "Growth and up" },
],
},
]npx shadcn@latest add @vibra/integrations-04API and webhooks
integrations-05The pitch to a developer with the four facts they check first, beside one API call shown three ways — the request, the response and a signed webhook — and the events a webhook can carry.
import * as React from "react"
import Link from "next/link"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { ApiSamples } from "./integrations-05.client"
import {
EVENTS,
EYEBROW,
FACTS,
HEADLINE,
LEDE,
PRIMARY_ACTION,
SAMPLES,
SAMPLES_LABEL,
SECONDARY_ACTION,
} from "./integrations-05.data"
/**
* The band for the reader who would rather build it: the pitch, the four
* facts a developer checks first and the two ways in, beside one API call
* shown three ways — the request, the response and a webhook — and the events
* a webhook can carry.
*
* The facts and the events are description lists, the event names code in
* the mono face. The samples are the island (`./integrations-05.client`); this
* file is a server component, and every word and every line of code is in
* `./integrations-05.data`.
*/
export function Integrations05() {
const titleId = React.useId()
return (
<section
data-section="integrations-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 grid max-w-7xl gap-12 lg:grid-cols-12 lg:gap-16">
<div className="flex flex-col lg:col-span-5">
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-3 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
<p className="mt-4 text-base/7 text-pretty text-muted-foreground">{LEDE}</p>
<dl className="mt-8 grid gap-x-6 gap-y-5 border-t border-border pt-6 sm:grid-cols-2">
{FACTS.map((fact) => (
<div key={fact.term} className="flex flex-col gap-1">
<dt className="text-sm font-medium">{fact.term}</dt>
<dd className="text-sm text-pretty text-muted-foreground">{fact.detail}</dd>
</div>
))}
</dl>
<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="flex min-w-0 flex-col gap-8 lg:col-span-7">
<ApiSamples samples={SAMPLES} label={SAMPLES_LABEL} />
<div>
<h3 className="text-sm font-semibold">
{EVENTS.heading}
</h3>
<dl className="mt-3 flex flex-col border-t border-border">
{EVENTS.items.map((event) => (
<div
key={event.name}
className="flex flex-col gap-1 border-b border-border py-2.5 sm:flex-row sm:items-baseline sm:gap-4"
>
<dt className="shrink-0 sm:w-56">
<code className="font-mono text-xs">{event.name}</code>
</dt>
<dd className="text-sm text-pretty text-muted-foreground">{event.description}</dd>
</div>
))}
</dl>
</div>
</div>
</div>
</section>
)
}
export default Integrations05"use client"
import { CodeBlock } from "@/components/ui/code-block"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import type { CodeSample } from "./integrations-05.data"
/**
* One API call three ways — the request, what comes back, and a webhook as it
* arrives — as the kit's Tabs over the kit's CodeBlock. The arrow keys move
* between the tabs and bring each sample up as they land; each sample has its
* own copy button, and its scroll region is named by its title.
*/
export function ApiSamples({ samples, label }: { samples: readonly CodeSample[]; label: string }) {
return (
<Tabs defaultValue={samples[0]?.id} className="gap-3">
<TabsList aria-label={label} activateOnFocus>
{samples.map((sample) => (
<TabsTrigger key={sample.id} value={sample.id} className="px-3">
{sample.label}
</TabsTrigger>
))}
</TabsList>
{samples.map((sample) => (
<TabsContent key={sample.id} value={sample.id} className="rounded-lg focus-ring">
<CodeBlock
code={sample.code}
title={sample.title}
language={sample.language}
// Every sample is given the tallest one's height, so the band does
// not jump as the reader moves between them.
className="bg-card [&_[data-slot=code-block-body]]:min-h-88 [&_[data-slot=code-block-title]]:font-mono [&_[data-slot=code-block-title]]:font-normal"
/>
</TabsContent>
))}
</Tabs>
)
}/**
* Every word `integrations-05` sets: the pitch to a developer, the four facts
* they check first, the code they would write — a request, what comes back
* and a webhook as it arrives — and the events they can subscribe to. The
* host is the reserved example domain and the figures add up: the three plans
* in the response sum to its total. Replace the samples with your own API's.
*/
export type ApiLink = { label: string; href: string }
export type ApiFact = { term: string; detail: string }
/** One tab of the code panel: its label, the file-like title over the code, the language chip and the code. */
export type CodeSample = { id: string; label: string; title: string; language: string; code: string }
export type WebhookEvent = { name: string; description: string }
export const EYEBROW = "For developers"
export const HEADLINE = "Build the integration we haven’t"
export const LEDE =
"Every figure in Northwind is in the REST API, and every change it notices can be sent to you as a webhook, signed so you know it came from us."
export const FACTS: readonly ApiFact[] = [
{ term: "REST and JSON", detail: "Versioned under /v1, paged with cursors" },
{ term: "Signed webhooks", detail: "HMAC-SHA256, retried for 24 hours" },
{ term: "Rate limit", detail: "600 requests a minute for each key" },
{ term: "Sandbox", detail: "Sample data behind every test key" },
]
export const PRIMARY_ACTION: ApiLink = { label: "Read the API reference", href: "/docs/api" }
export const SECONDARY_ACTION: ApiLink = { label: "Create an API key", href: "/settings/api-keys" }
/** What the tab row is called: it switches between three views of one call. */
export const SAMPLES_LABEL = "Code samples"
export const SAMPLES: readonly CodeSample[] = [
{
id: "request",
label: "Request",
title: "GET /v1/metrics/net-revenue",
language: "bash",
code: `curl https://api.example.com/v1/metrics/net-revenue \\
-H "Authorization: Bearer $NORTHWIND_KEY" \\
-G -d period=2026-08 -d group_by=plan`,
},
{
id: "response",
label: "Response",
title: "200 OK",
language: "json",
code: `{
"metric": "net_revenue",
"period": "2026-08",
"currency": "USD",
"total": 408312,
"change": 0.121,
"groups": [
{ "plan": "Starter", "value": 58240 },
{ "plan": "Growth", "value": 186020 },
{ "plan": "Scale", "value": 164052 }
]
}`,
},
{
id: "webhook",
label: "Webhook",
title: "POST https://app.example.com/hooks/northwind",
language: "http",
code: `Northwind-Signature: t=1788536400,v1=5f2b9c61e0a4
Content-Type: application/json
{
"type": "metric.threshold_crossed",
"created": "2026-09-04T15:40:00Z",
"data": {
"metric": "churned_accounts",
"threshold": 12,
"value": 14,
"report": "rep_0428"
}
}`,
},
]
export const EVENTS: { heading: string; items: readonly WebhookEvent[] } = {
heading: "Events you can subscribe to",
items: [
{ name: "report.published", description: "A scheduled report finished and was sent" },
{ name: "metric.threshold_crossed", description: "A metric crossed a line you set" },
{ name: "sync.failed", description: "A source stopped syncing, with the reason" },
{ name: "member.invited", description: "Someone was invited to the workspace" },
],
}npx shadcn@latest add @vibra/integrations-05Request an integration
integrations-06The most-requested tools, where each stands and how many have asked, each with a vote, beside a form that asks for one more and counts a request for a listed tool as a vote.
import * as React from "react"
import { RequestBoard } from "./integrations-06.client"
import { EYEBROW, FORM, HEADLINE, LEDE, REQUESTED } from "./integrations-06.data"
/**
* Where the directory ends: the tools customers are asking for, where each
* stands and how many have asked — with a vote for each — beside a form that
* asks for one more. A request for a tool already on the list counts as a
* vote for it, and the confirmation says so.
*
* The board is the island, handed the band's heading to draw above the list;
* this file is a server component, and every word, every refusal and both
* confirmations are in `./integrations-06.data`.
*/
export function Integrations06() {
const titleId = React.useId()
return (
<section
data-section="integrations-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 max-w-7xl">
<RequestBoard
requested={REQUESTED}
form={FORM}
intro={
<>
<p className="type-eyebrow">{EYEBROW}</p>
<h2 id={titleId} className="type-display mt-3 text-3xl text-balance md:text-4xl">
{HEADLINE}
</h2>
<p className="mt-4 text-base/7 text-pretty text-muted-foreground">{LEDE}</p>
</>
}
/>
</div>
</section>
)
}
export default Integrations06"use client"
import * as React from "react"
import { ArrowBigUpIcon, 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"
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"
import { StatusBadge } from "@/components/ui/status-badge"
import { Textarea } from "@/components/ui/textarea"
import { Toggle } from "@/components/ui/toggle"
import type { RequestForm, RequestList } from "./integrations-06.data"
type Field = "tool" | "category" | "email"
const ORDER: Field[] = ["tool", "category", "email"]
function fill(template: string, values: Record<string, string | number>): string {
return template.replace(/\{(\w+)\}/g, (match, key: string) => (key in values ? String(values[key]) : match))
}
/**
* The request board: the most-requested tools, each with where it stands, how
* many have asked and a toggle that adds the reader's vote, beside the form
* that asks for one more. The form refuses in its own words — every field it
* needs is checked, each refusal is tied to its field, which is marked
* invalid, and focus goes to the first field refused. An accepted request
* clears the form and is confirmed in a status line that is always mounted;
* a tool already on the list counts as a vote instead, and says so.
*
* It sends nothing: call your own endpoint where `submit` confirms.
*/
export function RequestBoard({
intro,
requested,
form,
}: {
intro: React.ReactNode
requested: RequestList
form: RequestForm
}) {
const listHeadingId = React.useId()
const formHeadingId = React.useId()
const ids = { tool: React.useId(), category: React.useId(), details: React.useId(), email: React.useId() }
const errorIds = { tool: React.useId(), category: React.useId(), email: React.useId() }
const noteId = React.useId()
const [votes, setVotes] = React.useState<Record<string, boolean>>({})
const [errors, setErrors] = React.useState<Partial<Record<Field, string>>>({})
const [sent, setSent] = React.useState<string | null>(null)
function submit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const data = new FormData(event.currentTarget)
const tool = String(data.get("tool") ?? "").trim()
const category = String(data.get("category") ?? "")
const email = String(data.get("email") ?? "").trim()
const next: Partial<Record<Field, string>> = {}
if (!tool) next.tool = form.errors.tool
if (!category) next.category = form.errors.category
if (!email) next.email = form.errors.emailEmpty
else if (!isEmail(email)) next.email = form.errors.emailInvalid
setErrors(next)
const first = ORDER.find((field) => next[field])
if (first) {
setSent(null)
// Each checked control is named for its field, so the form finds the first refused one itself.
const control = event.currentTarget.elements.namedItem(first)
if (control instanceof HTMLElement) control.focus()
return
}
const listed = requested.items.find((item) => item.name.toLowerCase() === tool.toLowerCase())
if (listed) setVotes((current) => ({ ...current, [listed.name]: true }))
setSent(fill(listed ? form.voted : form.sent, { tool: listed?.name ?? tool, email }))
event.currentTarget.reset()
}
/** The props every checked field shares: its invalid state and what describes it. */
const described = (field: Field, extra?: string) => ({
"aria-invalid": errors[field] ? true : undefined,
"aria-describedby": [errors[field] ? errorIds[field] : null, extra].filter(Boolean).join(" ") || undefined,
})
return (
<div className="grid gap-12 lg:grid-cols-12 lg:gap-16">
<div className="flex flex-col lg:col-span-5">
{intro}
<h3 id={listHeadingId} className="mt-10 text-sm font-semibold">
{requested.heading}
</h3>
<ul aria-labelledby={listHeadingId} className="mt-3 flex flex-col border-t border-border">
{requested.items.map((item) => {
const voted = Boolean(votes[item.name])
return (
<li key={item.name} className="flex items-center gap-3 border-b border-border py-3">
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<p className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm">
<span className="font-medium">{item.name}</span>{" "}
<StatusBadge status={item.status} variant={item.tone} size="sm" />
</p>
<p className="text-xs text-muted-foreground tabular-nums">
{item.kind} · {fill(requested.countLabel, { count: item.count + (voted ? 1 : 0) })}
</p>
</div>
<Toggle
variant="outline"
size="sm"
pressed={voted}
onPressedChange={(pressed) => setVotes((current) => ({ ...current, [item.name]: pressed }))}
aria-label={fill(requested.voteLabel, { name: item.name })}
className="border-border aria-pressed:border-transparent aria-pressed:bg-brand-muted"
>
<ArrowBigUpIcon aria-hidden="true" />
{requested.vote}
</Toggle>
</li>
)
})}
</ul>
</div>
<form
noValidate
onSubmit={submit}
aria-labelledby={formHeadingId}
className="flex flex-col gap-5 frame p-6 sm:p-8 lg:col-span-7"
>
<h3 id={formHeadingId} className="text-base font-semibold">
{form.title}
</h3>
<div className="grid gap-5 sm:grid-cols-2">
<div className="flex flex-col gap-2">
<Label htmlFor={ids.tool}>{form.toolLabel}</Label>
<Input id={ids.tool} name="tool" autoComplete="off" placeholder={form.toolPlaceholder} className="h-9" {...described("tool")} />
{errors.tool ? <FieldError id={errorIds.tool}>{errors.tool}</FieldError> : null}
</div>
<div className="flex flex-col gap-2">
<Label htmlFor={ids.category}>{form.categoryLabel}</Label>
<NativeSelect id={ids.category} name="category" defaultValue="" className="w-full [&_select]:h-9" {...described("category")}>
<NativeSelectOption value="" disabled>
{form.categoryPlaceholder}
</NativeSelectOption>
{form.categories.map((category) => (
<NativeSelectOption key={category} value={category}>
{category}
</NativeSelectOption>
))}
</NativeSelect>
{errors.category ? <FieldError id={errorIds.category}>{errors.category}</FieldError> : null}
</div>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor={ids.details}>{form.detailsLabel}</Label>
<Textarea id={ids.details} name="details" rows={3} placeholder={form.detailsPlaceholder} className="min-h-20" />
</div>
<div className="flex flex-col gap-2">
<Label htmlFor={ids.email}>{form.emailLabel}</Label>
<Input
id={ids.email}
name="email"
type="email"
autoComplete="email"
placeholder={form.emailPlaceholder}
className="h-9"
{...described("email", noteId)}
/>
{errors.email ? <FieldError id={errorIds.email}>{errors.email}</FieldError> : null}
<p id={noteId} className="text-xs text-muted-foreground">
{form.note}
</p>
</div>
<div className="flex flex-col gap-3">
<Button type="submit" size="lg" className="h-10 self-start px-5">
{form.submit}
</Button>
<p role="status" aria-live="polite" className="min-h-5 text-sm">
{sent ? (
<span className="inline-flex items-start gap-2 text-foreground">
<CircleCheckIcon aria-hidden="true" className="mt-0.5 size-4 shrink-0 text-success" />
{sent}
</span>
) : null}
</p>
</div>
</form>
</div>
)
}/**
* Every word `integrations-06` sets: the most-requested tools with where each
* stands and how many have asked, and the form that adds a request — its
* labels, the choices for what a tool is for, every refusal and both
* confirmations. The tools are fictional. Nothing is sent anywhere: wire the
* island's `submit` to wherever your team keeps requests.
*/
import type { StatusVariant } from "@/components/ui/status-badge"
export type RequestedTool = { name: string; kind: string; count: number; status: string; tone: StatusVariant }
export type RequestForm = {
/** The form's heading, which names it as a landmark. */
title: string
toolLabel: string
toolPlaceholder: string
categoryLabel: string
categoryPlaceholder: string
categories: readonly string[]
detailsLabel: string
detailsPlaceholder: string
emailLabel: string
emailPlaceholder: string
submit: string
note: string
errors: { tool: string; category: string; emailEmpty: string; emailInvalid: string }
/** `{tool}` and `{email}` are put in from the form. */
sent: string
/** Used instead of `sent` when the tool is already on the list: the request counts as a vote. */
voted: string
}
export type RequestList = {
heading: string
/** `{count}` is the number of requests, the reader's vote included. */
countLabel: string
/** The vote button's word, and its full name; `{name}` is the tool. */
vote: string
voteLabel: string
items: readonly RequestedTool[]
}
export const EYEBROW = "Integration requests"
export const HEADLINE = "Missing a tool? Tell us which"
export const LEDE =
"We build the integrations customers ask for most, in the order they ask for them. The three at the top of this list last quarter shipped in August."
export const REQUESTED: RequestList = {
heading: "Most requested",
countLabel: "{count} requests",
vote: "Vote",
voteLabel: "Vote for {name}",
items: [
{ name: "Sumbrook", kind: "Accounting", count: 214, status: "Building", tone: "info" },
{ name: "Tillbrook", kind: "Point of sale", count: 168, status: "Planned", tone: "primary" },
{ name: "Parcelmere", kind: "Shipping", count: 141, status: "Planned", tone: "primary" },
{ name: "Pollwright", kind: "Surveys", count: 97, status: "Considering", tone: "neutral" },
],
}
export const FORM: RequestForm = {
title: "Request an integration",
toolLabel: "Which tool?",
toolPlaceholder: "Its name",
categoryLabel: "What is it for?",
categoryPlaceholder: "Choose one",
categories: ["Finance", "Sales", "Support", "Product", "Data", "People", "Something else"],
detailsLabel: "What should sync? (optional)",
detailsPlaceholder: "The records you need, and which way they should go",
emailLabel: "Work email",
emailPlaceholder: "name@example.com",
submit: "Send request",
note: "We use your address for this request only.",
errors: {
tool: "Tell us the tool’s name.",
category: "Choose what the tool is for.",
emailEmpty: "Enter your work email, so we can tell you when it ships.",
emailInvalid: "Enter an email address in the format name@example.com.",
},
sent: "Thanks. {tool} is on the list, and we’ll write to {email} when it moves.",
voted: "{tool} is already on the list, so we counted your vote. We’ll write to {email} when it ships.",
}npx shadcn@latest add @vibra/integrations-06