Restaurant page
A menu rendered as real text with dietary tags, and booking above everything else.
Live preview
Open the full pageSource
This exact file renders the preview above.
import { Clock, MapPin, Phone } from 'lucide-react'
import Link from 'next/link'
import MinimalNavbar from '@/components/blocks/navigation/minimal'
import { Container } from '@/components/ui/layout'
import { Badge } from '@/components/ui/badge'
import { ButtonLink } from '@/components/ui/button'
import { Divider } from '@/components/ui/layout'
import OfficesContactSection from '@/components/blocks/sections/contact/offices'
import TestimonialCarousel from '@/components/blocks/sections/testimonials/carousel'
import MinimalFooter from '@/components/blocks/footers/minimal'
import { restaurantMenu } from '@/content/demo'
/**
* Restaurant page
*
* Hospitality has one requirement the other patterns do not: the menu must be
* readable as text on a phone, not a PDF. This pattern renders it as a real
* description list with dietary tags, and puts booking above everything else.
*/
export default function RestaurantPattern() {
return (
<>
<MinimalNavbar />
<main>
<header className="border-b border-line bg-canvas py-section">
<Container size="narrow" className="text-center">
<p className="label-caps text-ink-subtle">Ashfield Row · London</p>
<h1 className="display-type mt-5 text-4xl leading-tight font-semibold text-ink-strong sm:text-5xl">
Ember
</h1>
<p className="mx-auto mt-5 max-w-lg text-md leading-relaxed text-ink-muted sm:text-lg">
A small dining room and a nine-seat counter. One menu, changed when the produce
changes, cooked mostly over fire.
</p>
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
<ButtonLink href="/forms/booking" size="lg">
Book a table
</ButtonLink>
<ButtonLink href="#menu" size="lg" variant="outline">
Read the menu
</ButtonLink>
</div>
<dl className="mx-auto mt-10 flex max-w-lg flex-wrap items-center justify-center gap-x-8 gap-y-3 border-t border-line pt-6 text-sm">
<div className="flex items-center gap-2">
<Clock className="size-4 text-accent" aria-hidden="true" />
<dt className="sr-only">Opening hours</dt>
<dd className="text-ink-muted">Wed–Sat, 17:30–22:00</dd>
</div>
<div className="flex items-center gap-2">
<MapPin className="size-4 text-accent" aria-hidden="true" />
<dt className="sr-only">Address</dt>
<dd className="text-ink-muted">18 Ashfield Row, EC2A</dd>
</div>
<div className="flex items-center gap-2">
<Phone className="size-4 text-accent" aria-hidden="true" />
<dt className="sr-only">Telephone</dt>
<dd>
<a
href="tel:+442079460958"
className="text-ink-muted underline underline-offset-4"
>
+44 20 7946 0958
</a>
</dd>
</div>
</dl>
</Container>
</header>
<section id="menu" className="border-b border-line bg-canvas py-section">
<Container size="narrow">
<div className="text-center">
<h2 className="display-type text-2xl font-semibold text-ink-strong sm:text-3xl">
This week
</h2>
<p className="mt-2 text-sm text-ink-muted">
Prices in pounds. V is vegetarian, GF is gluten free.
</p>
</div>
<div className="mt-10 flex flex-col gap-10">
{restaurantMenu.map((course) => (
<section
key={course.course}
aria-labelledby={`course-${course.course.replace(/\s+/g, '-').toLowerCase()}`}
>
<div className="flex items-baseline justify-between gap-4 border-b border-line pb-2">
<h3
id={`course-${course.course.replace(/\s+/g, '-').toLowerCase()}`}
className="display-type text-lg font-semibold text-ink-strong"
>
{course.course}
</h3>
{course.note ? <p className="text-xs text-ink-subtle">{course.note}</p> : null}
</div>
<dl className="mt-4 flex flex-col gap-5">
{course.dishes.map((dish) => (
<div
key={dish.name}
className="flex flex-wrap items-baseline gap-x-4 gap-y-1"
>
<dt className="min-w-0 flex-1 text-md font-medium text-ink-strong">
{dish.name}
</dt>
<dd className="shrink-0 font-mono text-md text-ink tabular-nums">
{dish.price}
</dd>
<dd className="w-full text-sm text-ink-muted">{dish.description}</dd>
{dish.tags?.length ? (
<dd className="flex w-full flex-wrap gap-1.5">
{dish.tags.map((tag) => (
<Badge key={tag} size="sm">
{tag}
</Badge>
))}
</dd>
) : null}
</div>
))}
</dl>
</section>
))}
</div>
<Divider className="mt-12" />
<p className="mt-6 text-center text-sm text-ink-muted">
Allergen information is available on request.{' '}
<Link href="/forms/booking" className="text-accent underline underline-offset-4">
Book a table
</Link>
.
</p>
</Container>
</section>
<TestimonialCarousel />
<OfficesContactSection />
</main>
<MinimalFooter />
</>
)
}
components/patterns/restaurant.tsx
import { Clock, MapPin, Phone } from 'lucide-react'
import Link from 'next/link'
import MinimalNavbar from '@/components/blocks/navigation/minimal'
import { Container } from '@/components/ui/layout'
import { Badge } from '@/components/ui/badge'
import { ButtonLink } from '@/components/ui/button'
import { Divider } from '@/components/ui/layout'
import OfficesContactSection from '@/components/blocks/sections/contact/offices'
import TestimonialCarousel from '@/components/blocks/sections/testimonials/carousel'
import MinimalFooter from '@/components/blocks/footers/minimal'
import { restaurantMenu } from '@/content/demo'
/**
* Restaurant page
*
* Hospitality has one requirement the other patterns do not: the menu must be
* readable as text on a phone, not a PDF. This pattern renders it as a real
* description list with dietary tags, and puts booking above everything else.
*/
export default function RestaurantPattern() {
return (
<>
<MinimalNavbar />
<main>
<header className="border-b border-line bg-canvas py-section">
<Container size="narrow" className="text-center">
<p className="label-caps text-ink-subtle">Ashfield Row · London</p>
<h1 className="display-type mt-5 text-4xl leading-tight font-semibold text-ink-strong sm:text-5xl">
Ember
</h1>
<p className="mx-auto mt-5 max-w-lg text-md leading-relaxed text-ink-muted sm:text-lg">
A small dining room and a nine-seat counter. One menu, changed when the produce
changes, cooked mostly over fire.
</p>
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
<ButtonLink href="/forms/booking" size="lg">
Book a table
</ButtonLink>
<ButtonLink href="#menu" size="lg" variant="outline">
Read the menu
</ButtonLink>
</div>
<dl className="mx-auto mt-10 flex max-w-lg flex-wrap items-center justify-center gap-x-8 gap-y-3 border-t border-line pt-6 text-sm">
<div className="flex items-center gap-2">
<Clock className="size-4 text-accent" aria-hidden="true" />
<dt className="sr-only">Opening hours</dt>
<dd className="text-ink-muted">Wed–Sat, 17:30–22:00</dd>
</div>
<div className="flex items-center gap-2">
<MapPin className="size-4 text-accent" aria-hidden="true" />
<dt className="sr-only">Address</dt>
<dd className="text-ink-muted">18 Ashfield Row, EC2A</dd>
</div>
<div className="flex items-center gap-2">
<Phone className="size-4 text-accent" aria-hidden="true" />
<dt className="sr-only">Telephone</dt>
<dd>
<a
href="tel:+442079460958"
className="text-ink-muted underline underline-offset-4"
>
+44 20 7946 0958
</a>
</dd>
</div>
</dl>
</Container>
</header>
<section id="menu" className="border-b border-line bg-canvas py-section">
<Container size="narrow">
<div className="text-center">
<h2 className="display-type text-2xl font-semibold text-ink-strong sm:text-3xl">
This week
</h2>
<p className="mt-2 text-sm text-ink-muted">
Prices in pounds. V is vegetarian, GF is gluten free.
</p>
</div>
<div className="mt-10 flex flex-col gap-10">
{restaurantMenu.map((course) => (
<section
key={course.course}
aria-labelledby={`course-${course.course.replace(/\s+/g, '-').toLowerCase()}`}
>
<div className="flex items-baseline justify-between gap-4 border-b border-line pb-2">
<h3
id={`course-${course.course.replace(/\s+/g, '-').toLowerCase()}`}
className="display-type text-lg font-semibold text-ink-strong"
>
{course.course}
</h3>
{course.note ? <p className="text-xs text-ink-subtle">{course.note}</p> : null}
</div>
<dl className="mt-4 flex flex-col gap-5">
{course.dishes.map((dish) => (
<div
key={dish.name}
className="flex flex-wrap items-baseline gap-x-4 gap-y-1"
>
<dt className="min-w-0 flex-1 text-md font-medium text-ink-strong">
{dish.name}
</dt>
<dd className="shrink-0 font-mono text-md text-ink tabular-nums">
{dish.price}
</dd>
<dd className="w-full text-sm text-ink-muted">{dish.description}</dd>
{dish.tags?.length ? (
<dd className="flex w-full flex-wrap gap-1.5">
{dish.tags.map((tag) => (
<Badge key={tag} size="sm">
{tag}
</Badge>
))}
</dd>
) : null}
</div>
))}
</dl>
</section>
))}
</div>
<Divider className="mt-12" />
<p className="mt-6 text-center text-sm text-ink-muted">
Allergen information is available on request.{' '}
<Link href="/forms/booking" className="text-accent underline underline-offset-4">
Book a table
</Link>
.
</p>
</Container>
</section>
<TestimonialCarousel />
<OfficesContactSection />
</main>
<MinimalFooter />
</>
)
}
content/demo.ts
/**
* Shared demo content.
*
* Marketing copy lives here rather than inside JSX, so a section file stays a
* layout decision and nothing else. Every section in the library draws from
* this module, which is also what keeps 110 sections from becoming 110 copies
* of the same paragraph.
*
* All names, companies and figures are fictional.
*/
export interface Feature {
title: string
description: string
/** lucide-react icon name, resolved by the consuming section. */
icon: string
}
export const features: Feature[] = [
{
title: 'Token-driven theming',
description:
'Three orthogonal axes — scheme, palette, density — compose into every surface. Change one and the whole system follows.',
icon: 'Palette',
},
{
title: 'Accessible by construction',
description:
'Focus management, live regions and keyboard contracts are built into the primitives, not bolted on during review.',
icon: 'Accessibility',
},
{
title: 'Server-first rendering',
description:
'Sections are Server Components. Interactivity is scoped to small client islands, so a marketing page ships almost no JavaScript.',
icon: 'Server',
},
{
title: 'Composable sections',
description:
'Every block is a standalone export. Assemble a page from nine of them, or lift one into an existing codebase.',
icon: 'Layers',
},
{
title: 'Real source, always',
description:
'The code viewer reads the same file that renders the preview. Documentation cannot drift from implementation.',
icon: 'Code2',
},
{
title: 'Built to grow',
description:
'Adding a component means adding a catalogue record and a file. No page in the application needs to change.',
icon: 'Sprout',
},
]
export const secondaryFeatures: Feature[] = [
{
title: 'Responsive previews',
description: 'Inspect any block at 390, 768 and 1440 without leaving the page.',
icon: 'Smartphone',
},
{
title: 'Deterministic copy',
description: 'The copy button copies exactly what the viewer displays — no rewriting.',
icon: 'Clipboard',
},
{
title: 'Typed catalogue',
description: 'One record shape across six families, validated by the test suite.',
icon: 'ListTree',
},
{
title: 'Zero runtime CSS',
description: 'Tailwind v4 plus custom properties. No styling library in the bundle.',
icon: 'Feather',
},
{
title: 'Keyboard everything',
description: 'Menus, tabs, dialogs and the palette all follow published ARIA patterns.',
icon: 'Keyboard',
},
{
title: 'Dark authored, not inverted',
description: 'The dark scheme is designed, not derived from a filter.',
icon: 'Moon',
},
]
export interface Stat {
value: string
label: string
detail?: string
}
export const stats: Stat[] = [
{ value: '4', label: 'Composition levels', detail: 'Primitives through complete products' },
{ value: '3', label: 'Theme axes', detail: 'Scheme, palette, density' },
{ value: '0', label: 'Runtime dependencies', detail: 'Beyond React, Next and icons' },
{ value: '100%', label: 'Statically rendered', detail: 'Every catalogue route' },
]
export const companyLogos = [
'Northwind',
'Halcyon',
'Meridian',
'Kestrel',
'Lumen Works',
'Atlas Forge',
'Verdant',
'Ironwood',
]
export interface Testimonial {
quote: string
name: string
role: string
company: string
}
export const testimonials: Testimonial[] = [
{
quote:
'We replaced four half-finished internal libraries with one. The density axis alone paid for the migration — our admin tools and our marketing site finally share components.',
name: 'Priya Raman',
role: 'Principal Engineer',
company: 'Northwind',
},
{
quote:
'The accessibility work is the part I did not expect. Focus return, live regions, manual tab activation — the things that normally get filed as tech debt were already handled.',
name: 'Tomas Lindqvist',
role: 'Head of Design Systems',
company: 'Halcyon',
},
{
quote:
'Being able to read the exact source that renders the preview removed an entire class of "the docs are wrong" tickets.',
name: 'Amara Osei',
role: 'Staff Frontend Engineer',
company: 'Meridian',
},
{
quote:
'Our first landing page took an afternoon. The second took forty minutes, because by then we were composing rather than building.',
name: 'Jun Watanabe',
role: 'Product Lead',
company: 'Kestrel',
},
{
quote:
'Five palettes sounds like a gimmick until a client asks for a different accent on the third day and it is a one-line change.',
name: 'Elena Rossi',
role: 'Creative Director',
company: 'Lumen Works',
},
{
quote:
'The starters are the honest part. They are not screenshots — every route resolves, including the 404s.',
name: 'Marcus Bell',
role: 'Engineering Manager',
company: 'Atlas Forge',
},
]
export interface PricingTier {
name: string
price: string
cadence: string
description: string
features: string[]
cta: string
featured?: boolean
note?: string
}
export const pricingTiers: PricingTier[] = [
{
name: 'Solo',
price: '$0',
cadence: 'forever',
description: 'For personal projects and evaluation.',
features: [
'Every component and section',
'Copy-paste source',
'Light and dark schemes',
'Community support',
],
cta: 'Start building',
},
{
name: 'Team',
price: '$18',
cadence: 'per seat / month',
description: 'For product teams shipping together.',
features: [
'Everything in Solo',
'All five palettes',
'Starter products',
'Figma token export',
'Priority issue triage',
],
cta: 'Start a trial',
featured: true,
note: 'Most teams start here.',
},
{
name: 'Business',
price: '$2,400',
cadence: 'per year',
description: 'For organisations with a design-system function.',
features: [
'Everything in Team',
'Unlimited seats',
'Private component registry',
'Accessibility review sessions',
'Named support contact',
],
cta: 'Contact sales',
},
]
export interface FAQ {
question: string
answer: string
}
export const faqs: FAQ[] = [
{
question: 'Is Foundry a published npm package?',
answer:
'No. Foundry is a template demonstration. Components are copied into your project and owned by you — there is no registry, no install command and no version to upgrade.',
},
{
question: 'How do I change the accent colour?',
answer:
'Every accent in the system resolves from one ramp of custom properties. Redefine those seven values in your stylesheet and every button, focus ring, link and badge follows.',
},
{
question: 'Do the components require JavaScript?',
answer:
'Most do not. Sections, cards, tables, forms and layout primitives render entirely on the server. Only overlays, menus and the command palette are client islands.',
},
{
question: 'What does the density axis actually change?',
answer:
'Control heights, control padding, stack rhythm, card padding, table row height and section padding. It deliberately does not change the type scale, so text stays legible at every density.',
},
{
question: 'Can I use only part of the library?',
answer:
'Yes. Each block is a standalone file whose imports are visible in the code viewer. Copying one section brings the primitives it names and nothing else.',
},
{
question: 'How is the source in the documentation kept accurate?',
answer:
'A build step reads the real files from disk and generates the source registry. The preview and the code you copy come from the same file, and a test fails if the two ever diverge.',
},
{
question: 'Is there a Figma library?',
answer:
'The token manifest is designed to be exported to Figma variables, but no Figma file ships with this demo.',
},
{
question: 'What browsers are supported?',
answer:
'Current versions of Chrome, Edge, Safari and Firefox. The library uses container-free responsive CSS and standard custom properties throughout.',
},
]
export interface TeamMember {
name: string
role: string
bio: string
location: string
}
export const team: TeamMember[] = [
{
name: 'Priya Raman',
role: 'Design systems',
bio: 'Spent six years untangling component libraries before deciding to write one properly.',
location: 'Bengaluru',
},
{
name: 'Tomas Lindqvist',
role: 'Accessibility',
bio: 'Audits by day, writes focus-management utilities by night.',
location: 'Stockholm',
},
{
name: 'Amara Osei',
role: 'Frontend architecture',
bio: 'Believes most performance problems are really architecture problems.',
location: 'Accra',
},
{
name: 'Jun Watanabe',
role: 'Product',
bio: 'Turns "we should document this" into things that are actually documented.',
location: 'Osaka',
},
{
name: 'Elena Rossi',
role: 'Visual design',
bio: 'Editorial background; still measures leading by eye and is usually right.',
location: 'Milan',
},
{
name: 'Marcus Bell',
role: 'Developer experience',
bio: 'Optimises for the twentieth time you use something, not the first.',
location: 'Manchester',
},
{
name: 'Sofia Delgado',
role: 'Documentation',
bio: 'Thinks a component without a usage note is only half shipped.',
location: 'Madrid',
},
{
name: 'Idris Karim',
role: 'Infrastructure',
bio: 'Keeps the build under a minute so nobody is tempted to skip it.',
location: 'Toronto',
},
]
export interface ProcessStep {
title: string
description: string
duration?: string
}
export const processSteps: ProcessStep[] = [
{
title: 'Audit',
description:
'Inventory every surface, control and one-off in the existing product. Nothing is designed before the mess is measured.',
duration: 'Week 1–2',
},
{
title: 'Tokenise',
description:
'Extract the real colour, type and spacing decisions into a token set the whole team can read.',
duration: 'Week 3',
},
{
title: 'Build primitives',
description:
'Ship the twenty components that account for eighty per cent of screens, with states and keyboard contracts.',
duration: 'Week 4–7',
},
{
title: 'Compose',
description:
'Assemble sections and page patterns, then migrate a real route to prove the system holds.',
duration: 'Week 8–10',
},
{
title: 'Document',
description:
'Write the usage guidance while the decisions are fresh, and wire the docs to the source.',
duration: 'Week 11',
},
{
title: 'Hand over',
description:
'Contribution guide, review checklist, and a session on how to add the twenty-first component.',
duration: 'Week 12',
},
]
export interface TimelineEvent {
year: string
title: string
description: string
}
export const timelineEvents: TimelineEvent[] = [
{
year: '2021',
title: 'One repository, four libraries',
description:
'Four product teams, four component sets, four opinions about what "medium" meant.',
},
{
year: '2022',
title: 'The first token pass',
description: 'Colour and spacing consolidated. Everything else stayed where it was.',
},
{
year: '2023',
title: 'Primitives extracted',
description:
'Twenty components, one focus treatment, one control height. Adoption became easier than resistance.',
},
{
year: '2024',
title: 'Sections and patterns',
description: 'Marketing stopped rebuilding heroes and started composing them.',
},
{
year: '2025',
title: 'Density as an axis',
description: 'Admin tools and marketing pages finally shared a component set.',
},
{
year: '2026',
title: 'Foundry 1.0',
description: 'Four levels, one system, documented against its own source.',
},
]
export interface CaseStudy {
slug: string
client: string
title: string
summary: string
sector: string
year: string
metrics: Array<{ label: string; value: string }>
}
export const caseStudies: CaseStudy[] = [
{
slug: 'northwind-console',
client: 'Northwind',
title: 'One console for eleven internal tools',
summary:
'Eleven admin surfaces, each with its own table implementation, consolidated into a single shell with a shared data layer.',
sector: 'Logistics',
year: '2025',
metrics: [
{ label: 'Tools consolidated', value: '11' },
{ label: 'Bundle reduction', value: '61%' },
{ label: 'Time to new screen', value: '2 days' },
],
},
{
slug: 'halcyon-storefront',
client: 'Halcyon',
title: 'A storefront that loads on a train',
summary:
'Rebuilt a client-rendered catalogue as server components with small islands, targeting the worst connection the team could find.',
sector: 'Retail',
year: '2025',
metrics: [
{ label: 'JS shipped', value: '−78%' },
{ label: 'LCP, 3G', value: '1.4s' },
{ label: 'Conversion', value: '+9%' },
],
},
{
slug: 'meridian-docs',
client: 'Meridian',
title: 'Documentation that cannot go stale',
summary:
'Wired the documentation site to the component source so every example is generated from the file it documents.',
sector: 'Developer tools',
year: '2024',
metrics: [
{ label: 'Stale examples', value: '0' },
{ label: 'Docs PRs / month', value: '×3' },
{ label: 'Support tickets', value: '−34%' },
],
},
{
slug: 'kestrel-onboarding',
client: 'Kestrel',
title: 'Onboarding in four steps instead of nine',
summary:
'Reduced a nine-screen signup to four, with real validation, resumable progress and a working keyboard path throughout.',
sector: 'Fintech',
year: '2024',
metrics: [
{ label: 'Steps removed', value: '5' },
{ label: 'Completion', value: '+27%' },
{ label: 'Support contacts', value: '−41%' },
],
},
{
slug: 'lumen-rebrand',
client: 'Lumen Works',
title: 'A rebrand in one pull request',
summary:
'Because every surface resolved from tokens, a full visual rebrand touched one file and shipped in a single review.',
sector: 'Media',
year: '2023',
metrics: [
{ label: 'Files changed', value: '1' },
{ label: 'Screens updated', value: '240+' },
{ label: 'Regressions', value: '0' },
],
},
]
export interface Post {
slug: string
title: string
excerpt: string
category: string
date: string
readingTime: string
author: string
}
export const posts: Post[] = [
{
slug: 'density-as-an-axis',
title: 'Density belongs in your token system',
excerpt:
'Most design systems treat compact mode as a table prop. Making it a theme axis is what finally lets admin tools and marketing pages share components.',
category: 'Design systems',
date: '2026-03-12',
readingTime: '8 min',
author: 'Priya Raman',
},
{
slug: 'manual-tab-activation',
title: 'Why your tabs should not auto-activate',
excerpt:
'Automatic activation feels responsive and quietly punishes keyboard users. A short argument for the boring default.',
category: 'Accessibility',
date: '2026-02-27',
readingTime: '6 min',
author: 'Tomas Lindqvist',
},
{
slug: 'documentation-from-source',
title: 'Generate documentation from the file, not about it',
excerpt:
'A build step that reads the real component and a test that fails on drift removes an entire category of wrong documentation.',
category: 'Tooling',
date: '2026-02-09',
readingTime: '5 min',
author: 'Sofia Delgado',
},
{
slug: 'server-components-marketing',
title: 'Marketing pages do not need a framework runtime',
excerpt:
'What actually needs to be interactive on a landing page, and how small the remaining island can be.',
category: 'Performance',
date: '2026-01-22',
readingTime: '9 min',
author: 'Amara Osei',
},
{
slug: 'variants-that-earn-their-place',
title: 'A variant should represent a decision, not a padding value',
excerpt:
'How to tell a real variant from an inflated catalogue, and why one hundred good components beat three hundred similar ones.',
category: 'Design systems',
date: '2026-01-08',
readingTime: '7 min',
author: 'Elena Rossi',
},
{
slug: 'focus-return',
title: 'The half of focus management everyone forgets',
excerpt:
'Trapping focus in a dialog is the easy part. Putting it back where it came from is what keyboard users actually notice.',
category: 'Accessibility',
date: '2025-12-15',
readingTime: '4 min',
author: 'Tomas Lindqvist',
},
]
export interface Integration {
name: string
category: string
description: string
}
export const integrations: Integration[] = [
{
name: 'Vector',
category: 'Analytics',
description: 'Event streaming with a typed schema registry.',
},
{
name: 'Cinder',
category: 'Monitoring',
description: 'Traces, logs and alerts in one timeline.',
},
{
name: 'Postmark',
category: 'Email',
description: 'Transactional delivery with per-template metrics.',
},
{ name: 'Harbour', category: 'Storage', description: 'Object storage with signed upload URLs.' },
{
name: 'Sentinel',
category: 'Security',
description: 'Dependency and secret scanning on every push.',
},
{ name: 'Ledger', category: 'Billing', description: 'Usage metering and invoice generation.' },
{
name: 'Relay',
category: 'Messaging',
description: 'Webhooks with automatic retry and replay.',
},
{
name: 'Atlas',
category: 'Search',
description: 'Typo-tolerant search with faceted filtering.',
},
{ name: 'Beacon', category: 'Support', description: 'Shared inbox with conversation routing.' },
{ name: 'Quarry', category: 'Data', description: 'Warehouse sync with incremental models.' },
{ name: 'Pilot', category: 'Deployment', description: 'Preview environments per pull request.' },
{ name: 'Lantern', category: 'Documentation', description: 'Docs generated from typed schemas.' },
]
export interface Product {
slug: string
name: string
category: string
priceCents: number
compareAtCents?: number
description: string
material: string
badge?: string
inStock: boolean
rating: number
reviews: number
}
export const products: Product[] = [
{
slug: 'field-shell-jacket',
name: 'Field Shell Jacket',
category: 'outerwear',
priceCents: 32800,
compareAtCents: 41000,
description:
'A three-layer shell cut close enough to wear in a city and sealed well enough not to.',
material: 'Recycled ripstop',
badge: 'Last season pricing',
inStock: true,
rating: 4.7,
reviews: 214,
},
{
slug: 'quarry-overshirt',
name: 'Quarry Overshirt',
category: 'outerwear',
priceCents: 18500,
description: 'Heavyweight cotton twill that behaves like a jacket and packs like a shirt.',
material: 'Organic cotton twill',
inStock: true,
rating: 4.5,
reviews: 132,
},
{
slug: 'meridian-knit',
name: 'Meridian Merino Knit',
category: 'knitwear',
priceCents: 14900,
description: 'Fine-gauge merino with a high neck and no branding anywhere on it.',
material: 'Extrafine merino',
badge: 'Restocked',
inStock: true,
rating: 4.8,
reviews: 341,
},
{
slug: 'harbour-cardigan',
name: 'Harbour Cardigan',
category: 'knitwear',
priceCents: 21000,
description: 'A boxy cardigan with horn buttons and a shawl collar that holds its shape.',
material: 'Lambswool',
inStock: false,
rating: 4.4,
reviews: 88,
},
{
slug: 'atlas-chino',
name: 'Atlas Chino',
category: 'trousers',
priceCents: 12800,
compareAtCents: 15900,
description: 'A straight-leg chino with a proper waistband and pockets that hold a phone.',
material: 'Compact cotton',
inStock: true,
rating: 4.6,
reviews: 507,
},
{
slug: 'ironwood-trouser',
name: 'Ironwood Wool Trouser',
category: 'trousers',
priceCents: 24500,
description: 'Pleated, cuffed, and cut for people who sit down during the day.',
material: 'Wool flannel',
inStock: true,
rating: 4.3,
reviews: 76,
},
{
slug: 'lumen-tote',
name: 'Lumen Tote',
category: 'accessories',
priceCents: 9800,
description: 'Waxed canvas with a leather base and a strap long enough for a coat.',
material: 'Waxed canvas',
badge: 'Best seller',
inStock: true,
rating: 4.9,
reviews: 623,
},
{
slug: 'kestrel-cap',
name: 'Kestrel Six-Panel Cap',
category: 'accessories',
priceCents: 5200,
description: 'Unstructured, cotton-lined, with a brim that survives being sat on.',
material: 'Cotton canvas',
inStock: true,
rating: 4.2,
reviews: 195,
},
{
slug: 'verdant-tee',
name: 'Verdant Heavy Tee',
category: 'basics',
priceCents: 4800,
description: 'A 240gsm tee that holds its collar past the first wash.',
material: 'Organic cotton',
inStock: true,
rating: 4.6,
reviews: 1102,
},
{
slug: 'halcyon-socks',
name: 'Halcyon Ribbed Socks',
category: 'basics',
priceCents: 2200,
description: 'Three pairs. Ribbed, reinforced, and the same length as each other.',
material: 'Merino blend',
inStock: true,
rating: 4.5,
reviews: 289,
},
]
export const productCategories = [
{ slug: 'outerwear', name: 'Outerwear', description: 'Shells, overshirts and coats.' },
{ slug: 'knitwear', name: 'Knitwear', description: 'Merino, lambswool and cotton knits.' },
{ slug: 'trousers', name: 'Trousers', description: 'Chinos, wool and workwear cuts.' },
{ slug: 'accessories', name: 'Accessories', description: 'Bags, caps and small goods.' },
{ slug: 'basics', name: 'Basics', description: 'Tees, socks and everyday layers.' },
]
export interface MenuDish {
name: string
description: string
price: string
tags?: string[]
}
export interface MenuCourse {
course: string
note?: string
dishes: MenuDish[]
}
export const restaurantMenu: MenuCourse[] = [
{
course: 'To begin',
note: 'Served from opening until close.',
dishes: [
{
name: 'Cured trout, fennel, dill oil',
description: 'Two-day cure, shaved fennel, dill pressed the same morning.',
price: '16',
tags: ['GF'],
},
{
name: 'Charred leek, hazelnut, aged sheep cheese',
description: 'Leeks over embers until the outer layer gives up entirely.',
price: '14',
tags: ['V'],
},
{
name: 'Sourdough, cultured butter',
description: 'Four-day starter, butter churned in-house on Tuesdays.',
price: '7',
tags: ['V'],
},
],
},
{
course: 'Mains',
dishes: [
{
name: 'Aged duck breast, quince, bitter leaves',
description: 'Dry-aged fourteen days, quince from the orchard at Ashfield.',
price: '34',
},
{
name: 'Turbot on the bone, brown butter, capers',
description: 'Whole fish for two, carved at the table.',
price: '58',
tags: ['For two'],
},
{
name: 'Barley, wild mushroom, preserved lemon',
description: 'Slow-cooked barley with a mushroom broth reduced for six hours.',
price: '26',
tags: ['V', 'Vegan on request'],
},
],
},
{
course: 'To finish',
dishes: [
{
name: 'Burnt honey tart, crème fraîche',
description: 'Honey taken just past the point most people would stop.',
price: '12',
tags: ['V'],
},
{
name: 'Poached pear, walnut, blue cheese',
description: 'Pears poached in the last of the autumn cider.',
price: '13',
tags: ['V'],
},
{
name: 'Selection of British cheese',
description: 'Three cheeses, oat crackers, quince paste.',
price: '16',
tags: ['V'],
},
],
},
]
export const changelog = [
{
version: '1.0.0',
date: '2026-03-14',
title: 'Foundry 1.0',
summary:
'The first complete release: four composition levels, one token system, one catalogue.',
changes: [
{
kind: 'added' as const,
text: '45 primitives across actions, forms, feedback, overlay, navigation, data and layout.',
},
{
kind: 'added' as const,
text: '15 navigation patterns, 12 header systems and 10 footer systems.',
},
{
kind: 'added' as const,
text: '28 form flows with real validation, first-invalid focus and live error announcement.',
},
{
kind: 'added' as const,
text: '112 sections spanning marketing, commerce, SaaS, content, application and state.',
},
{
kind: 'added' as const,
text: '10 complete page patterns and 10 multi-route starter products.',
},
{
kind: 'added' as const,
text: 'Theme playground with five palettes and three densities, persisted and flash-free.',
},
],
},
{
version: '0.9.0',
date: '2026-02-20',
title: 'Starters and the composition playground',
summary: 'Level 4 arrives, and sections become assemblable rather than merely browsable.',
changes: [
{
kind: 'added' as const,
text: 'Ten starter products, each with working navigation and its own 404 path.',
},
{ kind: 'added' as const, text: 'URL-driven section composition playground.' },
{
kind: 'changed' as const,
text: 'Catalogue records gained `relatedItems`, validated to resolve.',
},
{
kind: 'fixed' as const,
text: 'Drawer no longer restored focus to a detached element after route changes.',
},
],
},
{
version: '0.8.0',
date: '2026-01-30',
title: 'Density becomes an axis',
summary: 'Compact, Default and Relaxed move out of component props and into the token layer.',
changes: [
{
kind: 'added' as const,
text: 'Density tokens for control height, padding, stack rhythm and row height.',
},
{
kind: 'changed' as const,
text: 'Every control now derives its height from `--density-control-height`.',
},
{
kind: 'removed' as const,
text: 'Per-component `compact` props, superseded by the density axis.',
},
],
},
{
version: '0.7.0',
date: '2026-01-12',
title: 'Source registry',
summary: 'Documentation is generated from the files it documents.',
changes: [
{
kind: 'added' as const,
text: 'Build-time source registry; the viewer reads the real file.',
},
{
kind: 'added' as const,
text: 'Drift test that regenerates the registry and fails on mismatch.',
},
{
kind: 'fixed' as const,
text: 'Copy button now falls back to a hidden textarea on insecure origins.',
},
],
},
{
version: '0.6.0',
date: '2025-12-04',
title: 'Accessibility pass',
summary: 'Focus management and live regions moved into the primitives.',
changes: [
{
kind: 'added' as const,
text: 'Focus trap with guaranteed focus return across Dialog, Drawer and the palette.',
},
{ kind: 'changed' as const, text: 'Tabs switched to manual activation.' },
{ kind: 'fixed' as const, text: 'Tooltips now open on keyboard focus, not only on hover.' },
{
kind: 'fixed' as const,
text: 'Toast queue announces in order instead of interrupting itself.',
},
],
},
]
export const faqShort = faqs.slice(0, 4)
components/blocks/sections/contact/offices.tsx
import { Clock } from 'lucide-react'
import { Container } from '@/components/ui/layout'
import { Status } from '@/components/ui/status'
/**
* Office locations
*
* Addresses as real `<address>` elements, with opening hours and a current
* status. The status is the part people came for — knowing whether anyone is
* there right now matters more than the postcode.
*/
const offices = [
{
city: 'London',
lines: ['18 Ashfield Row', 'London EC2A 4NE', 'United Kingdom'],
hours: 'Mon–Fri, 09:00–18:00 GMT',
status: 'operational' as const,
statusLabel: 'Open now',
email: 'london@foundry.example',
},
{
city: 'Lisbon',
lines: ['Rua da Prata 42', '1100-414 Lisboa', 'Portugal'],
hours: 'Mon–Fri, 09:00–18:00 WET',
status: 'operational' as const,
statusLabel: 'Open now',
email: 'lisbon@foundry.example',
},
{
city: 'Singapore',
lines: ['9 Battery Road', 'Singapore 049910', 'Singapore'],
hours: 'Mon–Fri, 09:00–18:00 SGT',
status: 'idle' as const,
statusLabel: 'Closed until 09:00',
email: 'singapore@foundry.example',
},
]
export default function OfficesContactSection() {
return (
<section className="border-b border-line bg-surface-sunken py-section">
<Container>
<div className="max-w-2xl">
<h2 className="display-type text-2xl font-semibold text-ink-strong sm:text-3xl">
Three fictional offices.
</h2>
</div>
<ul className="mt-10 grid gap-4 lg:grid-cols-3">
{offices.map((office) => (
<li key={office.city} className="rounded-xl border border-line bg-surface p-6">
<div className="flex items-center justify-between gap-3">
<h3 className="display-type text-lg font-semibold text-ink-strong">
{office.city}
</h3>
<Status appearance="pill" kind={office.status} label={office.statusLabel} />
</div>
<address className="mt-4 text-sm leading-relaxed text-ink-muted not-italic">
{office.lines.map((line) => (
<span key={line} className="block">
{line}
</span>
))}
</address>
<p className="mt-4 flex items-center gap-2 text-xs text-ink-subtle">
<Clock className="size-3.5" aria-hidden="true" />
{office.hours}
</p>
<a
href={`mailto:${office.email}`}
className="mt-3 inline-block text-sm text-accent underline underline-offset-4"
>
{office.email}
</a>
</li>
))}
</ul>
</Container>
</section>
)
}
Demo source — adapt to your project. Foundry is not published as a package.
Usage
Hospitality has one requirement the other patterns do not: the menu must be readable as text on a phone, not a PDF. It is a description list here, with dietary tags on each dish.
- Opening hours, address and phone belong in the header, not the footer.
- Never ship a menu as an image or a PDF — it is unreadable, unsearchable and inaccessible.
Variants and states
Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.
- Practical-details header
- Full menu as a description list
- Dietary tags
- Locations with opening status
Accessibility
- Menu semantics
- Each course is a labelled section; dishes are a description list pairing name, price and description.
- Contact details
- Phone is a tel link and the address is a real address element.
Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.
Related
All patternsRestaurant starter
A hospitality site whose menu is real, searchable text with dietary tags — never a PDF — with booking above everything else.
starter4 variantsBooking wizard
Party size, then availability, then contact — with unavailable slots shown rather than hidden.
advancedFeatured4 variantsOffice locations
Addresses with opening hours and a live open-or-closed status.
starter3 variants