Featured post with list
One lead article plus a scannable list of five.
Live preview
Source
This exact file renders the preview above.
import Link from 'next/link'
import { Container } from '@/components/ui/layout'
import { Badge } from '@/components/ui/badge'
import { formatShortDate } from '@/lib/format'
import { posts } from '@/content/demo'
/**
* Featured post with a list
*
* One lead article plus a scannable list. The asymmetry does the editorial
* work: a uniform grid of six posts tells a reader nothing about which one the
* editor thinks matters.
*/
export default function FeaturedPlusListBlog() {
const [lead, ...rest] = posts
return (
<section className="border-b border-line bg-canvas py-section">
<Container>
<div className="flex flex-wrap items-end justify-between gap-4">
<h2 className="display-type text-2xl font-semibold text-ink-strong sm:text-3xl">
Writing
</h2>
<Link
href="/starters/blog/preview/stories"
className="text-sm font-semibold text-accent underline underline-offset-4"
>
All articles
</Link>
</div>
<div className="mt-10 grid gap-10 lg:grid-cols-[1.3fr_1fr] lg:gap-16">
{lead ? (
<article>
<Link href={`/starters/blog/preview/stories/${lead.slug}`} className="group block">
<div
className="grid-paper aspect-[16/9] w-full rounded-xl border border-line bg-surface-sunken"
aria-hidden="true"
/>
<div className="mt-5 flex flex-wrap items-center gap-2">
<Badge tone="accent">{lead.category}</Badge>
<time dateTime={lead.date} className="text-xs text-ink-subtle">
{formatShortDate(lead.date)}
</time>
<span className="text-xs text-ink-subtle">· {lead.readingTime}</span>
</div>
<h3 className="display-type mt-3 text-xl leading-snug font-semibold text-ink-strong group-hover:text-accent sm:text-2xl">
{lead.title}
</h3>
<p className="mt-2 text-sm leading-relaxed text-ink-muted">{lead.excerpt}</p>
<p className="mt-3 text-xs text-ink-subtle">{lead.author}</p>
</Link>
</article>
) : null}
<ul className="divide-y divide-[var(--color-border-subtle)] border-t border-line">
{rest.slice(0, 5).map((post) => (
<li key={post.slug}>
<Link
href={`/starters/blog/preview/stories/${post.slug}`}
className="group block py-5"
>
<div className="flex flex-wrap items-center gap-2">
<span className="label-caps text-accent">{post.category}</span>
<time dateTime={post.date} className="text-xs text-ink-subtle">
{formatShortDate(post.date)}
</time>
</div>
<h3 className="mt-1.5 text-md font-semibold text-ink-strong group-hover:text-accent">
{post.title}
</h3>
<p className="mt-1 text-sm text-ink-muted">{post.excerpt}</p>
</Link>
</li>
))}
</ul>
</div>
</Container>
</section>
)
}
components/blocks/sections/blog/featured-plus-list.tsx
import Link from 'next/link'
import { Container } from '@/components/ui/layout'
import { Badge } from '@/components/ui/badge'
import { formatShortDate } from '@/lib/format'
import { posts } from '@/content/demo'
/**
* Featured post with a list
*
* One lead article plus a scannable list. The asymmetry does the editorial
* work: a uniform grid of six posts tells a reader nothing about which one the
* editor thinks matters.
*/
export default function FeaturedPlusListBlog() {
const [lead, ...rest] = posts
return (
<section className="border-b border-line bg-canvas py-section">
<Container>
<div className="flex flex-wrap items-end justify-between gap-4">
<h2 className="display-type text-2xl font-semibold text-ink-strong sm:text-3xl">
Writing
</h2>
<Link
href="/starters/blog/preview/stories"
className="text-sm font-semibold text-accent underline underline-offset-4"
>
All articles
</Link>
</div>
<div className="mt-10 grid gap-10 lg:grid-cols-[1.3fr_1fr] lg:gap-16">
{lead ? (
<article>
<Link href={`/starters/blog/preview/stories/${lead.slug}`} className="group block">
<div
className="grid-paper aspect-[16/9] w-full rounded-xl border border-line bg-surface-sunken"
aria-hidden="true"
/>
<div className="mt-5 flex flex-wrap items-center gap-2">
<Badge tone="accent">{lead.category}</Badge>
<time dateTime={lead.date} className="text-xs text-ink-subtle">
{formatShortDate(lead.date)}
</time>
<span className="text-xs text-ink-subtle">· {lead.readingTime}</span>
</div>
<h3 className="display-type mt-3 text-xl leading-snug font-semibold text-ink-strong group-hover:text-accent sm:text-2xl">
{lead.title}
</h3>
<p className="mt-2 text-sm leading-relaxed text-ink-muted">{lead.excerpt}</p>
<p className="mt-3 text-xs text-ink-subtle">{lead.author}</p>
</Link>
</article>
) : null}
<ul className="divide-y divide-[var(--color-border-subtle)] border-t border-line">
{rest.slice(0, 5).map((post) => (
<li key={post.slug}>
<Link
href={`/starters/blog/preview/stories/${post.slug}`}
className="group block py-5"
>
<div className="flex flex-wrap items-center gap-2">
<span className="label-caps text-accent">{post.category}</span>
<time dateTime={post.date} className="text-xs text-ink-subtle">
{formatShortDate(post.date)}
</time>
</div>
<h3 className="mt-1.5 text-md font-semibold text-ink-strong group-hover:text-accent">
{post.title}
</h3>
<p className="mt-1 text-sm text-ink-muted">{post.excerpt}</p>
</Link>
</li>
))}
</ul>
</div>
</Container>
</section>
)
}
lib/format.ts
/** Small formatting helpers shared across catalogue and starter surfaces. */
export function titleCase(value: string): string {
return value
.split(/[-_\s]+/)
.filter(Boolean)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
export function pluralise(count: number, singular: string, plural = `${singular}s`): string {
return `${count} ${count === 1 ? singular : plural}`
}
const currencyFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
})
export function formatCurrency(cents: number): string {
return currencyFormatter.format(cents / 100)
}
const compactFormatter = new Intl.NumberFormat('en-US', {
notation: 'compact',
maximumFractionDigits: 1,
})
export function formatCompact(value: number): string {
return compactFormatter.format(value)
}
/**
* Dates in Foundry are authored as ISO date strings so that server and client
* renders agree byte-for-byte. Formatting is pinned to `en-US` + UTC for the
* same reason — no hydration drift from the visitor's locale or timezone.
*/
export function formatDate(iso: string): string {
const date = new Date(`${iso}T00:00:00Z`)
if (Number.isNaN(date.getTime())) return iso
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: 'UTC',
}).format(date)
}
export function formatShortDate(iso: string): string {
const date = new Date(`${iso}T00:00:00Z`)
if (Number.isNaN(date.getTime())) return iso
return new Intl.DateTimeFormat('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
}).format(date)
}
export function initials(name: string): string {
const parts = name.trim().split(/\s+/).filter(Boolean)
if (parts.length === 0) return '?'
if (parts.length === 1) return (parts[0] ?? '?').slice(0, 2).toUpperCase()
return `${(parts[0] ?? '')[0] ?? ''}${(parts[parts.length - 1] ?? '')[0] ?? ''}`.toUpperCase()
}
export function slugify(value: string): string {
return value
.toLowerCase()
.normalize('NFKD')
.replace(/[^\w\s-]/g, '')
.trim()
.replace(/[\s_]+/g, '-')
.replace(/-+/g, '-')
}
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)
Demo source — adapt to your project. Foundry is not published as a package.
Usage
The asymmetry does the editorial work: a uniform grid of six posts tells a reader nothing about which one the editor thinks matters.
- Reserve the media aspect ratio so the grid does not reflow on load.
- One lead. Two leads is a grid with extra steps.
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.
- Lead article with media
- Five-item list
- Two-column at lg
Accessibility
- Article semantics
- The lead is an article element with its own heading.
- Time elements
- Dates carry a machine-readable datetime attribute.
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 sectionsArticle card grid
Three columns of equal cards with reserved media areas.
starter3 variantsEditorial contents index
A numbered contents page set in the display serif.
starter3 variantsContent starter
A magazine with six article routes, four category indexes and four author pages — twenty-one routes from six definitions.
intermediate4 variants