
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Use-case set spanning a filterable industry grid, persona-switched tabs, self-select role cards, and a numbered end-to-end scenario walkthrough.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/use-cases.json$npx shadcn@latest add https://uipkge.dev/r/react/use-cases.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/use-cases.json$bunx shadcn@latest add https://uipkge.dev/r/react/use-cases.jsonnpx shadcn@latest add @uipkge-react/use-casesInstalls to:components/blocks/use-cases/'use client'
import { useMemo, useState } from 'react'
import { ArrowUpRight, Banknote, Factory, HeartPulse, ShoppingCart, Ship, Truck } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
const useCases = [
{
icon: Banknote,
segment: 'Financial services',
title: 'Close the books without a spreadsheet relay',
body: 'Ledger-reconciled metrics that lock on period close, with an auditable trail for every restatement.',
metric: '5 days off close',
},
{
icon: HeartPulse,
segment: 'Healthcare',
title: 'Report on care quality without exposing PHI',
body: 'Row-level scoping evaluated per query, so a shared dashboard never leaks a chart the viewer cannot open.',
metric: 'HIPAA-scoped by default',
},
{
icon: ShoppingCart,
segment: 'Retail',
title: 'One margin definition across every channel',
body: 'Store, marketplace, and wholesale roll up to the same certified metric instead of three competing exports.',
metric: '3 channels, 1 number',
},
{
icon: Truck,
segment: 'Logistics',
title: 'Track service levels against the contract',
body: 'On-time thresholds encoded per customer contract, so breach reporting stops being a manual comparison.',
metric: '98.2% SLA visibility',
},
{
icon: Factory,
segment: 'Manufacturing',
title: 'Yield reporting that survives a line change',
body: 'Definitions version with the production line, so last quarter’s numbers stay comparable after a retool.',
metric: '12 lines reconciled',
},
{
icon: Ship,
segment: 'Logistics',
title: 'Landed cost per shipment, not per guess',
body: 'Freight, duty, and demurrage join at query time against the booking rather than a monthly allocation.',
metric: 'Per-shipment costing',
},
]
const ALL = 'All'
const segments = [ALL, ...new Set(useCases.map((useCase) => useCase.segment))]
export function UseCasesIndustryGrid() {
const [active, setActive] = useState(ALL)
const visible = useMemo(
() => (active === ALL ? useCases : useCases.filter((useCase) => useCase.segment === active)),
[active],
)
return (
<section data-slot="use-cases-industry-grid" className="bg-background">
<div className="mx-auto max-w-6xl px-6 py-20 lg:py-28">
<div className="flex flex-wrap items-end justify-between gap-6">
<div className="max-w-2xl">
<Badge variant="secondary">By industry</Badge>
<h2 className="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">Where teams start</h2>
<p className="text-muted-foreground mt-3 text-lg">
Six rollouts we run often enough to have opinions about. Filter to yours.
</p>
</div>
<ToggleGroup
value={active}
type="single"
variant="outline"
size="sm"
className="flex-nowrap overflow-x-auto"
aria-label="Filter use cases by industry"
// Single-select emits '' when the active item is pressed again;
// fall back to ALL so the grid can never end up empty.
onValueChange={(value: string) => setActive(value || ALL)}
>
{segments.map((segment) => (
<ToggleGroupItem key={segment} value={segment}>
{segment}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
<div className="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{visible.map((useCase) => (
<Card key={useCase.title} className="group">
<CardContent className="flex h-full flex-col p-6">
<useCase.icon className="text-muted-foreground size-5" aria-hidden="true" />
<p className="text-muted-foreground mt-4 font-mono text-xs tracking-[0.14em] uppercase">
{useCase.segment}
</p>
<h3 className="mt-2 text-base leading-snug font-semibold">{useCase.title}</h3>
<p className="text-muted-foreground mt-2 text-sm leading-relaxed">{useCase.body}</p>
<div className="border-border mt-auto flex items-center justify-between gap-3 border-t pt-4">
<span className="text-sm font-medium">{useCase.metric}</span>
<Button variant="ghost" size="sm" className="h-auto px-2 py-1">
Read
<ArrowUpRight
className="ml-1 size-3.5 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5"
aria-hidden="true"
/>
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</div>
</section>
)
}
'use client'
import { ArrowRight, Check } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
const personas = [
{
value: 'analytics',
role: 'Data & analytics',
pain: 'Every request becomes a bespoke SQL ticket, and nobody trusts the number in the deck.',
outcomes: [
'Publish one certified metric definition per number',
'Retire the ad-hoc query queue without losing coverage',
'Trace any figure back to the column it came from',
],
metric: { value: '71%', label: 'fewer ad-hoc data requests after one quarter' },
cta: 'See the analyst workflow',
},
{
value: 'finance',
role: 'Finance',
pain: 'Close depends on four spreadsheets and one person who knows how they link.',
outcomes: [
'Reconcile against the ledger before anything publishes',
'Lock a period so restated figures cannot drift',
'Hand auditors a change log instead of a folder',
],
metric: { value: '5 days', label: 'cut from the average monthly close' },
cta: 'See the close workflow',
},
{
value: 'revops',
role: 'RevOps',
pain: 'Pipeline reporting disagrees with the CRM, so forecast reviews start with an argument.',
outcomes: [
'One pipeline definition shared by sales and finance',
'Stage changes tracked with the rep and the timestamp',
'Forecast snapshots you can diff week over week',
],
metric: { value: '2.4pp', label: 'tighter forecast accuracy band' },
cta: 'See the forecast workflow',
},
{
value: 'engineering',
role: 'Engineering',
pain: 'The reporting layer is an undocumented service nobody wants to own.',
outcomes: [
'Definitions live in the repo, reviewed like any other change',
'No bespoke ETL to babysit at 3am',
'Warehouse cost ceilings enforced per team',
],
metric: { value: '0', label: 'bespoke pipelines to maintain' },
cta: 'See the engineering setup',
},
]
export function UseCasesPersonaTabs() {
return (
<section data-slot="use-cases-persona-tabs" className="bg-background">
<div className="mx-auto max-w-5xl px-6 py-20 lg:py-28">
<div className="max-w-2xl">
<Badge variant="secondary">Use cases</Badge>
<h2 className="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">Pick the seat you sit in</h2>
<p className="text-muted-foreground mt-3 text-lg">
Same platform, four very different first weeks. Start with the one that matches your job.
</p>
</div>
<Tabs defaultValue="analytics" className="mt-10">
{/* flex-nowrap + horizontal scroll: the trigger row keeps its height
whether there are three roles or thirty. */}
<TabsList variant="segmented" className="flex-nowrap overflow-x-auto">
{personas.map((persona) => (
<TabsTrigger key={persona.value} value={persona.value} variant="segmented">
{persona.role}
</TabsTrigger>
))}
</TabsList>
{personas.map((persona) => (
<TabsContent key={persona.value} value={persona.value} className="mt-8">
{/* min-h keeps the panel from collapsing between tabs with shorter copy. */}
<div className="grid min-h-[19rem] gap-8 lg:grid-cols-[1.3fr_1fr]">
<div>
<p className="text-muted-foreground text-sm">Today</p>
<p className="mt-1.5 text-lg leading-snug font-medium text-balance">{persona.pain}</p>
<p className="text-muted-foreground mt-7 text-sm">With uipkge</p>
<ul className="mt-3 space-y-3">
{persona.outcomes.map((outcome) => (
<li key={outcome} className="flex items-start gap-2.5 text-sm">
<Check className="text-primary mt-0.5 size-4 shrink-0" aria-hidden="true" />
<span>{outcome}</span>
</li>
))}
</ul>
<Button className="mt-7">
{persona.cta}
<ArrowRight className="ml-2 size-4" aria-hidden="true" />
</Button>
</div>
<Card className="self-start">
<CardContent className="p-6">
<p className="font-display text-4xl font-bold tracking-tight">{persona.metric.value}</p>
<p className="text-muted-foreground mt-2 text-sm leading-relaxed">{persona.metric.label}</p>
</CardContent>
</Card>
</div>
</TabsContent>
))}
</Tabs>
</div>
</section>
)
}
'use client'
import { ArrowUpRight } from 'lucide-react'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
// Naming the metric each role is judged on is what lets someone self-select
// without reading all four cards.
const roles = [
{
initials: 'FC',
role: 'Financial controller',
job: 'Close the books without a reconciliation relay.',
week: 'Reconcile revenue and margin against the ledger, then lock the period.',
metric: 'Days to close',
},
{
initials: 'AL',
role: 'Analytics lead',
job: 'Stop being a query queue for the rest of the company.',
week: 'Publish the ten numbers that generate the most tickets as certified metrics.',
metric: 'Ad-hoc requests per month',
},
{
initials: 'RO',
role: 'RevOps',
job: 'Make forecast review about the forecast.',
week: 'Agree one pipeline definition with finance and snapshot it weekly.',
metric: 'Forecast accuracy band',
},
{
initials: 'DE',
role: 'Data engineer',
job: 'Stop owning an undocumented reporting service.',
week: 'Move definitions into the repo and delete the bespoke extract jobs.',
metric: 'Pipelines maintained',
},
]
export function UseCasesRoleCards() {
return (
<section data-slot="use-cases-role-cards" className="bg-background">
<div className="mx-auto max-w-6xl px-6 py-20 lg:py-28">
<div className="max-w-2xl">
<Badge variant="secondary">By role</Badge>
<h2 className="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">Find the card with your job on it</h2>
<p className="text-muted-foreground mt-3 text-lg">
Each one names the work of the first week and the number that role is actually judged on.
</p>
</div>
<div className="mt-10 grid gap-4 sm:grid-cols-2">
{roles.map((role) => (
<Card key={role.role} className="group">
<CardContent className="flex h-full flex-col p-6">
<div className="flex items-center gap-3">
<Avatar className="size-9">
<AvatarFallback className="text-xs">{role.initials}</AvatarFallback>
</Avatar>
<p className="text-sm font-semibold">{role.role}</p>
</div>
<p className="mt-4 text-base leading-snug font-medium text-balance">{role.job}</p>
<Separator className="my-4" />
<p className="text-muted-foreground font-mono text-xs tracking-[0.14em] uppercase">First week</p>
<p className="text-muted-foreground mt-2 text-sm leading-relaxed">{role.week}</p>
<div className="border-border mt-auto flex items-center justify-between gap-3 border-t pt-4">
<span className="text-xs">
<span className="text-muted-foreground">Measured on </span>
<span className="font-medium">{role.metric}</span>
</span>
<Button variant="ghost" size="sm" className="h-auto shrink-0 px-2 py-1">
Read
<ArrowUpRight
className="ml-1 size-3.5 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5"
aria-hidden="true"
/>
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</div>
</section>
)
}
'use client'
import { ArrowRight } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
// One concrete scenario beats four abstract ones: the reader can check it
// against their own Tuesday.
const steps = [
{
actor: 'Controller',
action: 'Notices Q1 revenue in the board deck is £41k above the close pack.',
recorded: 'Nothing yet — this is the part that used to take a day to explain.',
},
{
actor: 'Controller',
action: 'Opens the metric and reads its history instead of asking the analytics team.',
recorded: 'Definition v127, changed 14 Feb by finance-analytics, reviewed by A. Reyes.',
},
{
actor: 'Analyst',
action: 'Sees the deck was built on v126, before the trailing window changed.',
recorded: 'Consumer list shows which dashboards still resolve the older version.',
},
{
actor: 'Analyst',
action: 'Republishes the deck against the current definition; the gap disappears.',
recorded: 'Publish event, with the before and after figures attached.',
},
{
actor: 'Auditor',
action: 'Six months later, asks why Q1 was restated.',
recorded: 'The whole chain above, exported as a single PDF in under a minute.',
},
]
export function UseCasesScenarioWalkthrough() {
return (
<section data-slot="use-cases-scenario-walkthrough" className="bg-background">
<div className="mx-auto max-w-4xl px-6 py-20 lg:py-28">
<div className="max-w-2xl">
<Badge variant="secondary">A Tuesday</Badge>
<h2 className="mt-4 text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
Two numbers disagree. Here is the whole sequence.
</h2>
<p className="text-muted-foreground mt-3 text-lg">
Not a feature list — the actual path, including what the system writes down at each step.
</p>
</div>
<ol className="mt-12 space-y-4">
{steps.map((step, index) => (
<li key={step.action} className="grid grid-cols-[auto_1fr] gap-x-5">
<div className="flex flex-col items-center">
<span className="border-border bg-card text-muted-foreground relative z-10 flex size-9 shrink-0 items-center justify-center rounded-full border font-mono text-xs font-semibold">
{String(index + 1).padStart(2, '0')}
</span>
{index < steps.length - 1 && <span className="bg-border w-px grow" aria-hidden="true" />}
</div>
<Card className="mb-4">
<CardContent className="p-5">
<Badge variant="outline">{step.actor}</Badge>
<p className="mt-3 text-sm leading-relaxed">{step.action}</p>
<Separator className="my-3" />
<p className="text-muted-foreground text-xs leading-relaxed">
<span className="font-medium">Recorded: </span>
{step.recorded}
</p>
</CardContent>
</Card>
</li>
))}
</ol>
<div className="border-border mt-6 flex flex-wrap items-center justify-between gap-4 border-t pt-6">
<p className="text-muted-foreground text-sm">Total elapsed, start to resolution: about nine minutes.</p>
<Button>
Walk through it yourself
<ArrowRight className="ml-2 size-4" aria-hidden="true" />
</Button>
</div>
</div>
</section>
)
}
Raw manifest:https://uipkge.dev/r/react/use-cases.json