Beratung
Ein erstes Gespräch, in dem wir zuhören und Fragen stellen.
ui-design-system/ui/astro-design-system-showcaselive
| Kind | frontend |
|---|---|
| Surface | Astro |
| Maturity | prod |
| Reusable | extract |
| Design system | tokens-css |
| Stack | astro-4, typescript, css-custom-properties, tokens-css |
| Depends on | _shared/design-system-and-tokens, _shared/astro-site-skeleton |
| Renders | no backend module: a design-system row |
| Entry | alchemy.zuerich/alchemy-new/src/pages/design-system.astro |
| Reference implementation |
|
| Measured on | alchemy.zuerich alchemy.zuerich_TRANSMUTATION2026 77291bb 2026-09-11 |
| Also measured on |
|
| Version | 1 |
default server rendered .t-* classes; every size is one --fs-* token, so a client site changes the scale in one file..t-display var(--fs-display) .t-h1 var(--fs-h1) .t-h2 var(--fs-h2) .t-h3 var(--fs-h3) .t-h4 var(--fs-h4) .t-body-1 var(--fs-body-1) .t-body-2 var(--fs-body-2) .t-caption var(--fs-caption) --bg-page page background --bg-surface raised surface --bg-dark dark band --fg-default text --fg-muted secondary text --fg-accent accent, links, primary action --fg-on-dark text on a dark band --border-default hairline --radius-sm --radius --radius-lg --radius-xl A browsable design-system site inside an Astro project: one registry lists every component, a small scaffold renders the catalogue and the previews, and one specimen page per component shows the real component in every state. The invariant that makes it trustworthy is that the docs cannot drift from the site: a specimen renders the actual component file, never a copy, so what the showcase shows is what the site ships.
It makes no backend module visible. It is the rendering reference for every tokens-css and prima Astro surface (S3 says "the site's /design-system route when it has one").
actions server rendered components/button.astro). One component renders <a> with href and <button> without; disabled and loading block interaction.default, on-dark and plain.Use it on any Astro site with more than a handful of shared components, and on every site that a second person will build blocks for: the showcase is where they learn which Button variants exist and what a Card looks like on a dark band. Trigger phrases: "where do I see all the components", "add a specimen", "document the new block", "show every state of the button", "how does the email look on mobile".
Do not use it for a React SPA (the alchemy-ui library has its /design-system page, alchemy-ui-component-library), and do not build it as a separate site: it lives under /design-system of the site it documents so the specimens import the real components by relative path.
S1 for the token rules and S3 for the site the showcase sits inside. Measured on the agency's own marketing site, which is where the pattern lives at 77291bb (2026-09-11): Astro ^4.16.0 (alchemy-new/package.json), TypeScript for the registry, scoped <style> blocks on tokens; no client framework, a little inline JavaScript in the preview scaffolds for scaling and tab switching. prima.li at 9c01b14 no longer has a /design-system route (measured: src/pages lists none), although the wiki records the pattern as first built there; the agency site's copy is the reference.
Data-fetch style: none. Everything is read at build time from src/data/ds-registry.ts. Copy language: English on the reference; a client site writes the showcase in whichever language its builders read.
src/data/ds-registry.ts (212 lines at 77291bb):
DsGroup: 'Actions' | 'Forms' | 'Indicators' | 'Cards' | 'Pages' | 'Documents' | 'Layout' | 'Blocks' | 'Social Media' | 'Email'; DsStatus: 'stable' | 'wip'.DsComponent (lines 11 to 20): slug (URL and specimen page name), name, file (the real component path, printed on the doc header as the source of truth), group, status, summary, documented? (true once pages/design-system/components/<slug>.astro exists), parent? (a slug; nests the entry under its parent in the sidebar).dsComponents (22 to 199): 78 entries, 65 documented, at 77291bb. dsGroups (201): the display order. DsFoundation and dsFoundations (203 to 212): six foundation pages (colour, typography, spacing, layout, radius-shadow, icons).Per-page data lives in the specimen page's frontmatter: the variant list, the state list, the surfaces (On light, On dark, On image), the props table rows and the usage snippet (components/button.astro lines 7 to 33). No client state; the preview scaffolds keep a data-* attribute for the selected device or tab. Browser storage: none.
None at runtime. Build-time contracts: the registry module's exports; the site's Base.astro layout (DsLayout passes navMode="solid" and switches the floating card off, line 14); the /design-system/frame/<name> routes, which the preview scaffolds load in an <iframe> at a true device width.
src/components/ to S1's rules.dsComponents (and the group to dsGroups if new); set parent when it is a variant of an existing component (button-group under button, password and textarea under input, filter-tabs under tab-row).Specimen for anything that fits the column at true size (buttons, inputs, badges, cards); a chrome-less frame/ route plus SocialPreview for a fixed pixel canvas (posts, stories, documents); ViewportPreview (six breakpoints: 360, 680, 768, 1080, 1280, 1920, each at its true width and then scaled to fit) or EmailClientPreview (Desktop and Mobile toggle inside mail-client chrome) for anything that reflows; ResponsivePreview for a true-size scrolling view.pages/design-system/components/<slug>.astro: DsLayout with active={slug}, ComponentDoc slug={slug}, an intro, the previews (Matrix for surfaces by states, StateChart for the forced interaction states), PropsTable rows={props}, a usage snippet, Do and Don't.documented: true; the sidebar link and the index tile switch from "soon" to "Doc" on the next build.design-system.astro) counts documented against total and renders the groups; the sidebar reveals a component's children only while it or one of them is the active page (DsLayout.astro lines 43 to 62), which keeps the nav quiet.Specimen and Matrix slot the imported component; the doc header prints the component's file.documented is false and the entry says "soon"._ds/ and never leak into product code; they may be pragmatic (inline JS, forced-state CSS) because they are tooling.StateChart.astro lines 46 to 52 use :global(.btn) inside .force-hover); they are never added to the shipped component.innerHTML, createElement) is declared :global() in the scaffold's <style>, because Astro's scoped selectors carry a data-astro-cid-* attribute that runtime nodes lack (7 :global() uses across _ds/ at 77291bb).Specimen.astro lines 30 to 45 read --border-default, --radius-lg, --bg-page, --bg-dark); the mail-client and social preview chromes are the dated exception (Gotchas).The component, rebuilt from the demo in the kit with the design system the recipe names, one frame per state. What is in the frames is what the recipe describes and what Primacode builds from; it is not a picture of the running application.
cards server rendered <em> for the serif accent.Ein erstes Gespräch, in dem wir zuhören und Fragen stellen.
Was gebaut wird, in welcher Reihenfolge, und warum.
Die Website, das Formular, die Buchungsseite.
surface="dark" inverts to the ink card; the marker and the body copy follow.Hosting, Updates und ein Ansprechpartner.
Die Nummer als Marker, in Weiss auf der dunklen Karte.
navigation server rendered ---
/**
* Live demo for ui-design-system/ui/astro-design-system-showcase.
*
* The tokens-css components rendered as specimens, the way a specimen page of
* the showcase renders the real component. One component takes `state`; the
* frontmatter exports `states` so the docs host and scripts/check.mjs read the
* same list (RENDERING.md, sections 2.2 and 2.4). No hydration: everything
* here is plain markup with `var(--token)` reads, resolved against the page's
* tokens. On the docs host those are prima.li's values; a client site brings
* its own, which is the recipe's point.
*
* The component bodies beside this file in components/*.astro are the
* measured site's (alchemy.zuerich alchemy-new/src/components/ at 77291bb,
* the commit measured_on names): Button, Eyebrow, Section, Navigation, Footer
* and cards/FeatureCard, with the site's `withBase` and `content.ts` imports
* replaced by literal props; each header lists its adaptations. They live
* here until a second Astro row needs them, at which point they move to a
* workspace package.
*/
import Button from './components/Button.astro';
import Eyebrow from './components/Eyebrow.astro';
import FeatureCard from './components/FeatureCard.astro';
import Footer from './components/Footer.astro';
import Navigation from './components/Navigation.astro';
import Section from './components/Section.astro';
import { wordmark } from './wordmark.ts';
export const states = {
default: { label: 'Overview: type scale, colour aliases, spacing, radius' },
actions: { label: 'Button (three variants by two surfaces, disabled, loading), Eyebrow' },
cards: { label: 'FeatureCard (dot, number, icon; on light and on dark)' },
navigation: { label: 'Navigation, Section with tone, Footer' },
};
interface Props {
state: keyof typeof states;
}
const { state } = Astro.props;
/* Sample data: literal, nothing real. */
const brand = 'Musterfirma';
const links = [
{ label: 'Leistungen', href: '/leistungen' },
{ label: 'Referenzen', href: '/referenzen' },
{ label: 'Über uns', href: '/ueber-uns' },
{ label: 'Blog', href: 'https://blog.example', external: true },
];
const cta = { label: 'Kontakt', href: '/kontakt' };
const year = 2026;
/* The site passes logo files; the demo passes a wordmark as an inline SVG (wordmark.ts). */
const logoDark = wordmark(brand, '#1a1420');
const logoLight = wordmark(brand, '#ffffff');
const footerCols = [
{ title: 'Studio', links: [{ label: 'Leistungen', href: '/leistungen' }, { label: 'Referenzen', href: '/referenzen' }, { label: 'Über uns', href: '/ueber-uns' }] },
{ title: 'Kontakt', links: [{ label: 'Termin buchen', href: '/kontakt' }, { label: 'Blog', href: 'https://blog.example', external: true }] },
{ title: 'Rechtliches', links: [{ label: 'Impressum', href: '/impressum' }, { label: 'Datenschutz', href: '/datenschutz' }] },
];
const footerMeta = [{ label: 'Kontakt', href: '/kontakt' }, { label: 'Newsletter', href: '/newsletter' }];
const variants = ['primary', 'secondary', 'tertiary'] as const;
const surfaces = [
{ key: 'light', label: 'On light' },
{ key: 'dark', label: 'On dark' },
] as const;
const type = [
{ cls: 't-display', name: 'display', token: '--fs-display' },
{ cls: 't-h1', name: 'h1', token: '--fs-h1' },
{ cls: 't-h2', name: 'h2', token: '--fs-h2' },
{ cls: 't-h3', name: 'h3', token: '--fs-h3' },
{ cls: 't-h4', name: 'h4', token: '--fs-h4' },
{ cls: 't-body-1', name: 'body-1', token: '--fs-body-1' },
{ cls: 't-body-2', name: 'body-2', token: '--fs-body-2' },
{ cls: 't-caption', name: 'caption', token: '--fs-caption' },
];
const colours = [
{ token: '--bg-page', role: 'page background' },
{ token: '--bg-surface', role: 'raised surface' },
{ token: '--bg-dark', role: 'dark band' },
{ token: '--fg-default', role: 'text' },
{ token: '--fg-muted', role: 'secondary text' },
{ token: '--fg-accent', role: 'accent, links, primary action' },
{ token: '--fg-on-dark', role: 'text on a dark band' },
{ token: '--border-default', role: 'hairline' },
];
const spacing = ['--space-xs', '--space-sm', '--space-md', '--space-lg', '--space-xl', '--space-2xl', '--space-3xl', '--space-4xl', '--space-section'];
const radii = ['--radius-sm', '--radius', '--radius-lg', '--radius-xl'];
---
<div class="ds-demo" data-ds-state={state}>
{state === 'default' && (
<div class="ds-stack">
<section class="ds-spec">
<h3 class="ds-spec-title">Type scale</h3>
<div class="ds-spec-note">The <code>.t-*</code> classes; every size is one <code>--fs-*</code> token, so a client site changes the scale in one file.</div>
<div class="ds-type">
{type.map((t) => (
<div class="ds-type-row">
<span class={t.cls}>Alles an einem Ort</span>
<code>.{t.cls} <span class="ds-dim">var({t.token})</span></code>
</div>
))}
</div>
</section>
<section class="ds-spec">
<h3 class="ds-spec-title">Colour aliases</h3>
<div class="ds-spec-note">Components read the aliases, never a raw palette value. The swatch shows the value the host page resolves.</div>
<div class="ds-swatches">
{colours.map((c) => (
<div class="ds-swatch">
<span class="ds-swatch-chip" style={`background: var(${c.token});`}></span>
<code>{c.token}</code>
<span class="ds-dim">{c.role}</span>
</div>
))}
</div>
</section>
<section class="ds-spec">
<h3 class="ds-spec-title">Spacing</h3>
<div class="ds-spec-note">The steps, as bars at their own width.</div>
<div class="ds-bars">
{spacing.map((s) => (
<div class="ds-bar-row">
<code>{s}</code>
<span class="ds-bar" style={`width: var(${s});`}></span>
</div>
))}
</div>
</section>
<section class="ds-spec">
<h3 class="ds-spec-title">Radius</h3>
<div class="ds-radii">
{radii.map((r) => (
<div class="ds-radius">
<span class="ds-radius-chip" style={`border-radius: var(${r});`}></span>
<code>{r}</code>
</div>
))}
</div>
</section>
</div>
)}
{state === 'actions' && (
<div class="ds-stack">
<section class="ds-spec">
<h3 class="ds-spec-title">Button</h3>
<div class="ds-spec-note">Three variants on two surfaces, the matrix the site's own specimen page draws (<code>components/button.astro</code>). One component renders <code><a></code> with <code>href</code> and <code><button></code> without; <code>disabled</code> and <code>loading</code> block interaction.</div>
{surfaces.map((sf) => (
<div class:list={['ds-surface', { 'ds-surface--dark': sf.key === 'dark' }]}>
<span class="ds-surface-label">{sf.label}</span>
<div class="ds-row">
{variants.map((v) => <Button variant={v} surface={sf.key}>{v[0].toUpperCase() + v.slice(1)}</Button>)}
<Button variant="primary" surface={sf.key} disabled>Disabled</Button>
<Button variant="primary" surface={sf.key} loading>Loading</Button>
</div>
</div>
))}
<div class="ds-row">
<Button href="/kontakt" variant="primary">As a link</Button>
<Button type="submit" variant="secondary">As submit</Button>
<Button variant="tertiary">
Weiter
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</Button>
</div>
<div class="ds-row ds-row--narrow">
<Button fullWidth>Full width</Button>
</div>
</section>
<section class="ds-spec">
<h3 class="ds-spec-title">Eyebrow</h3>
<div class="ds-spec-note">Sentence case at body size; the tones are <code>default</code>, <code>on-dark</code> and <code>plain</code>.</div>
<div class="ds-row">
<Eyebrow>Leistungen</Eyebrow>
<span class="ds-on-dark"><Eyebrow tone="on-dark">On dark</Eyebrow></span>
<span class="ds-on-dark"><Eyebrow tone="plain">Plain</Eyebrow></span>
</div>
</section>
</div>
)}
{state === 'cards' && (
<div class="ds-stack">
<section class="ds-spec">
<h3 class="ds-spec-title">FeatureCard</h3>
<div class="ds-spec-note">A bordered surface card with a marker at the top: a dot, a number or an icon. The title takes an <code><em></code> for the serif accent.</div>
<div class="ds-grid" data-gallery-focus="true">
<FeatureCard marker="dot" title="Beratung" body="Ein erstes Gespräch, in dem wir zuhören und Fragen stellen." />
<FeatureCard marker="number" number="02" title="Konzept" body="Was gebaut wird, in welcher Reihenfolge, und warum." />
<FeatureCard marker="icon" title="<em>Umsetzung</em>" body="Die Website, das Formular, die Buchungsseite.">
<svg slot="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 17l6-6-6-6M12 19h8"/></svg>
</FeatureCard>
</div>
</section>
<section class="ds-spec">
<h3 class="ds-spec-title">On dark</h3>
<div class="ds-spec-note"><code>surface="dark"</code> inverts to the ink card; the marker and the body copy follow.</div>
<div class="ds-grid">
<FeatureCard surface="dark" marker="dot" title="Betrieb" body="Hosting, Updates und ein Ansprechpartner." />
<FeatureCard surface="dark" marker="number" number="04" title="Mit <em>Zahl</em>" body="Die Nummer als Marker, in Weiss auf der dunklen Karte." />
<FeatureCard surface="dark" marker="none" title="Ohne Marker">
<Button variant="secondary" surface="dark">Termin buchen</Button>
</FeatureCard>
</div>
</section>
</div>
)}
{state === 'navigation' && (
<div class="ds-stack ds-stack--flush">
<section class="ds-spec ds-spec--flush">
<h3 class="ds-spec-title ds-spec-title--inset">Navigation, solid mode</h3>
<div class="ds-spec-note ds-spec-note--inset">The site pins this to the top of every page; the wrapper here contains it. Below 680 px the links fold into the burger.</div>
<div class="ds-nav-stage">
<Navigation navMode="solid" logoLight={logoLight} logoDark={logoDark} logoAlt={brand} links={links} ctaLabel={cta.label} ctaHref={cta.href} />
</div>
</section>
<section class="ds-spec ds-spec--flush">
<h3 class="ds-spec-title ds-spec-title--inset">Section, tone default and dark</h3>
<Section tone="default" spacing="sm">
<Eyebrow>Leistungen</Eyebrow>
<div class="t-h2 ds-section-heading">Ein heller Abschnitt</div>
<div class="t-body-1 ds-section-copy">Die Section bringt Abstand, Breite und Ton; der Inhalt bringt seine eigenen Farben über die on-dark-Varianten.</div>
</Section>
<Section tone="dark" spacing="sm" width="narrow">
<Eyebrow tone="on-dark">Referenzen</Eyebrow>
<div class="t-h2 ds-section-heading">Ein dunkler Abschnitt</div>
<div class="t-body-1 ds-section-copy">Die schmale Breite für einen Abschnitt, der genau eine Sache will.</div>
<div class="ds-section-actions"><Button variant="secondary" surface="dark">Termin buchen</Button><Button variant="tertiary" surface="dark">Mehr erfahren</Button></div>
</Section>
</section>
<section class="ds-spec ds-spec--flush">
<h3 class="ds-spec-title ds-spec-title--inset">Footer</h3>
<Footer brand={brand} logo={logoDark} cols={footerCols} meta={footerMeta} year={year}>
<svg slot="mark" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path fill="currentColor" d="M24 2l5.5 14.5L44 22l-14.5 5.5L24 42l-5.5-14.5L4 22l14.5-5.5z"/></svg>
</Footer>
</section>
</div>
)}
</div>
<style>
/* The specimen chrome: labels and grids around the components. Every value
is a token read, so the chrome follows the host page like the components do.
Notes and headings are div and h3, not p and h2: the docs host's shell
styles p, h1 and h2 inside the frame at a specificity a scoped class here
cannot beat, and the frame's reset undoes that to `inherit` (prima.li
src/index-site/demo-frame.css says why). */
.ds-demo {
font-family: var(--font-body);
color: var(--fg-default);
background: var(--bg-page);
}
.ds-stack { display: flex; flex-direction: column; gap: var(--space-2xl); padding: var(--space-xl); }
.ds-stack--flush { padding: var(--space-xl) 0; }
.ds-spec { display: flex; flex-direction: column; gap: var(--space-md); }
.ds-spec-title { margin: 0; font-size: var(--fs-overline); font-weight: 600; letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--fg-muted); }
.ds-spec-title--inset { padding: 0 var(--space-xl); }
.ds-spec-note--inset { padding: 0 var(--space-xl); }
.ds-spec-note { margin: 0; font-size: var(--fs-body-2); color: var(--fg-muted); max-width: var(--content-narrow); }
.ds-spec code { font-family: var(--work-mono, ui-monospace, monospace); font-size: var(--fs-caption); }
.ds-dim { color: var(--fg-muted); }
.ds-gap { height: var(--space-lg); }
.ds-type { display: flex; flex-direction: column; gap: var(--space-md); }
.ds-type-row { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--space-md); border-bottom: 1px solid var(--border-default); padding-bottom: var(--space-sm); }
.ds-swatches { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: var(--space-md); }
.ds-swatch { display: flex; flex-direction: column; gap: var(--space-xs); font-size: var(--fs-caption); }
.ds-swatch-chip { display: block; height: 56px; border-radius: var(--radius); border: 1px solid var(--border-default); }
.ds-bars { display: flex; flex-direction: column; gap: var(--space-sm); }
.ds-bar-row { display: grid; grid-template-columns: 140px 1fr; align-items: center; gap: var(--space-md); }
.ds-bar { display: block; height: 12px; background: var(--fg-accent); border-radius: var(--radius-sm); }
.ds-radii { display: flex; flex-wrap: wrap; gap: var(--space-lg); }
.ds-radius { display: flex; flex-direction: column; align-items: center; gap: var(--space-xs); }
.ds-radius-chip { display: block; width: 64px; height: 64px; background: var(--fg-accent); }
.ds-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-md); }
.ds-row--narrow { max-width: 320px; }
.ds-on-dark { display: inline-block; background: var(--bg-dark); padding: var(--space-xs) var(--space-sm); border-radius: var(--radius-sm); }
.ds-surface { display: flex; flex-direction: column; gap: var(--space-sm); padding: var(--space-md); border-radius: var(--radius); border: 1px solid var(--border-default); }
.ds-surface--dark { background: var(--bg-dark); color: var(--fg-on-dark); border-color: transparent; }
.ds-surface-label { font-size: var(--fs-caption); color: var(--fg-muted); }
.ds-surface--dark .ds-surface-label { color: var(--fg-on-dark); opacity: 0.6; }
/* The site's Navigation is position: fixed; layout containment makes this
box its containing block, so it pins to the top of the stage and not of
the docs page. The height is the nav's own: the 40px logo plus the
component's vertical padding. */
.ds-nav-stage { position: relative; contain: layout paint; min-height: calc(40px + 2 * var(--space-lg)); background: var(--bg-page); }
.ds-section-heading { margin: var(--space-sm) 0 var(--space-xs); color: inherit; }
.ds-section-copy { margin: 0; max-width: var(--content-narrow); color: inherit; opacity: 0.85; }
.ds-section-actions { display: flex; flex-wrap: wrap; gap: var(--space-md); margin-top: var(--space-lg); }
.ds-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: var(--space-lg); }
</style>
/design-system (index; reads dsComponents, dsGroups)
DsLayout (title, description, active)
Base (the site's layout: navMode="solid", no floating card)
div.dsl
aside.dsl-side [data-lenis-prevent]
a.dsl-home "Design System"
nav.dsl-nav
ul.dsl-toplist Overview, Principles
div.dsl-group "Foundations" > li x dsFoundations (Colour, Typography, Spacing, Layout, Radius & shadow, Icons)
div.dsl-group x dsGroups (Actions, Forms, Indicators, Cards, Layout, Blocks, Pages, Documents, Social Media, Email)
li x top-level component (documented -> link; else "soon", aria-disabled)
ul.dsl-sub x children (parent: slug), shown only while that component or a child is active
main.dsl-main
<slot />
header.ov-hero eyebrow, h1 "The design system.", lede, "65 of 78 components documented"
section.ov-cards Foundations card, Principles card
section.ov-index h2 "Components", one .ov-group per dsGroup, .ov-cmp tile per component (Doc | soon badge)
/design-system/components/<slug> (one page per documented component)
DsLayout > ComponentDoc (group, Stable | WIP, name, "Source <file>")
Specimen (label, bg canvas | dark, code, pad, center) around the REAL component
Matrix (surfaces x states) . StateChart (default, hover, active, focus, disabled forced)
ResponsivePreview | ViewportPreview (six breakpoints, true width, scaled to fit) | SocialPreview | EmailClientPreview
PropsTable (rows: name, type, default, note)
/design-system/foundations/<slug> colour, typography, spacing, layout, radius-shadow, icons
/design-system/frame/<name> chrome-less fixed-size renders for iframes and screenshots (69 files) The rendering is the live demo astro-design-system-showcase/demo.astro: the tokens-css components rendered as specimens, the way a specimen page of the showcase renders the real component, and served by the docs host as plain markup with no hydration (RENDERING.md, sections 2.2 and 3). The component bodies are the reference site's own at 77291bb, copied from alchemy-new/src/components/ into components/*.astro beside the demo: Button (variant primary, secondary, tertiary by surface light, dark; disabled; loading with the spinner; 97 lines), Eyebrow (tones default, on-dark, plain), Section (width, tone default, dark, transparent; spacing), Navigation (159 lines, solid and over-hero modes, the burger below 680 px), Footer (91 lines) and cards/FeatureCard (marker dot, number, icon; surface light, dark), which is the card the site's showcase documents (it has no generic Card component). The only edits are the ones each file's header lists: the site's withBase and content.ts imports are replaced by literal props, the Footer's inline brand name, logo, star and bottom links become props and a slot, and Button reads --fs-button with a body-2 fallback because prima.li's tokens.css lacks that token. Until 2026-09-11 the folder held the wiki blueprint bodies (a three-variant Button, Card, SectionIntro), which are not what this recipe measures; they are gone. The components stay beside the demo until a second Astro row needs them, at which point they move to a workspace package. They read the page's tokens (--fg-accent, --bg-surface, --border-default, --radius-lg, --space-*, --fs-*), so on the docs host they render with prima.li's values; a client site brings its own, which is the recipe's point. The tree in the frontmatter describes the agency site's showcase, the pattern; the demo is the specimens that showcase documents.
States: default (the overview: the type scale from .t-display to .t-caption with the --fs-* token each reads, the colour aliases as swatches, the spacing steps as bars, the four radius steps), actions (Button in the matrix the site's own specimen page draws, three variants on the light and on the dark surface, plus disabled, loading, as link, as submit, with a trailing icon and full width; Eyebrow default, on-dark and plain), cards (FeatureCard with the dot, number and icon markers on light, then on dark with a slotted button), navigation (Navigation in solid mode with the wordmark, four links, one external, the CTA and the burger, contained by the specimen wrapper because the site pins it to the viewport; Section in the default and the dark tone; Footer with three link columns, a mark in its slot, the legal line and the bottom links). The specimen chrome around the components (labels, grids, swatches) is the demo's own and reads the same tokens. Loading and error states do not exist on a static page. The sidebar stacking of the real showcase at 900 px is not in the demo, which has no sidebar; a 390 px frame stacks the specimens in one column. One limit of the docs host, dated 2026-09-11: its shell styles p, h1 and h2 inside the frame at a specificity an Astro scoped class rule does not reach, and the frame's reset undoes that to inherit, so FeatureCard's body copy and the Footer's legal line render at the inherited size and colour on the docs host and at the component's own on a site; the demo's own chrome uses div and h3 for that reason.
Tokens on the page (the site's tokens.css, same shape and the same --c-* values as prima.li's at 9c01b14): --fg-accent for the active link, the "Doc" badge tint via color-mix and the arrows; --bg-surface on the two cards; --border-default dotted canvas on specimens; --font-body and --font-display (the italic serif on "design system."); --fs-* steps; --radius-lg; --space-*.
States: overview (rendered); a component page with Matrix, StateChart and PropsTable; a foundation page; a frame/ route (bare); the "soon" entry (aria-disabled link, tile with a muted badge). Loading and error states do not exist on a static site; a missing specimen page is a 404 the build never links to, because the link is emitted only when documented is true.
A static, public, data-free set of pages. The frame/ routes render real components with sample content and are reachable by URL; they must not embed anything the site would not publish (an email frame renders template copy, not a real message). Nothing is gated. The client's brand assets on the showcase are the client's, in the client's repo; the agency site's values (its violet, its faces under licence) are not copied into a client project.
npx astro build fails on a specimen that imports a missing component or a registry row with a bad group; measured 2026-09-11, 171 pages in 4.6 s at 77291bb.group outside DsGroup or a status outside DsStatus fails astro check (TypeScript).node --test script over ds-registry.ts asserting every documented: true row has a page file and every parent names an existing slug; the S3 overflow sweep over the /design-system/** routes at 390, 768, 1024 and 1280 px (the sidebar stacking at 900 px is the breakpoint to watch).npm run check) reads the demo's export const states from the frontmatter and compares it with rendering.states; tooling/verify/compare.mjs --state actions against the agency site's /design-system/components/button through astro-static.sh is the visual check, and its report is never committed.Ships with the site: astro build emits the showcase as static HTML under dist/design-system/; there is nothing to configure and no env. On a client site the route is public by default; a site that wants it unlisted excludes /design-system from sitemap and robots (the reference does not). Frame routes add one file each to the build. Rollback is the site's rollback (S13).
DsLayout.astro lines 111 to 112 and 125): the sidebar group titles and the "soon" tag are text-transform: uppercase (with letter-spacing: 0), against S1 rule 14 (founder rule of 2026-08-08). ComponentDoc.astro line 24 does the same for the group name on a component page. Pre-rule code; a client site renders them in sentence case./design-system route; the inventory row says the pattern was first built there. The agency site is where it lives now, and this recipe's evidence comes from there.grep -c "slug: '" src/data/ds-registry.ts prints 84, not 78: the six foundation rows share the slug shape. Count inside the dsComponents array (lines 22 to 199) or the numbers will not match the page.SocialPreview.astro line 45, EmailPreview.astro line 35, EmailClientPreview.astro lines 80 to 90: the fake mail-client greys) and one in the WIP badge (ComponentDoc.astro line 27). Tooling, not shipped UI, but a client site names them (--ds-chrome-bg) rather than copying the literals.ViewportPreview.astro lines 15 to 22): the six breakpoints include 1920, which is also the page cap of S1 rule 15; a preview at 1920 shows the capped page with its own auto side margins.:global(); the symptom is dynamically built chips rendering as raw browser buttons.class:list, scoped styles, Astro.slots.has), but a port is measured, not assumed.Branch: alchemy.zuerich (the agency's own Astro marketing site) alchemy.zuerich_TRANSMUTATION2026 at 77291bb (2026-09-11); the showcase last changed in 91c5bcf (the Email group). Paths relative to alchemy-new/:
src/data/ds-registry.ts (212 lines): types (8 to 20), dsComponents (22 to 199), dsGroups (201), foundations (203 to 212).src/pages/design-system.astro (99 lines): the index; src/pages/design-system/principles.astro, foundations.astro, foundations/{color,icons,layout,radius-shadow,spacing,typography}.astro; components/ (66 specimen pages); frame/ (69 chrome-less renders).src/components/_ds/DsLayout.astro (143 lines: sidebar 16 to 79, media query 131), ComponentDoc.astro (34), PropsTable.astro (38), Specimen.astro (63), Matrix.astro (38), StateChart.astro (59; forced states 46 to 52), ViewportPreview.astro (127; devices 15 to 22), ResponsivePreview.astro (93), SocialPreview.astro (73), EmailPreview.astro (72), EmailClientPreview.astro (141).src/pages/design-system/components/button.astro: the canonical specimen page (variants, states, surfaces, props, usage).src/components/Button.astro (97 lines: props 3 to 11), Eyebrow.astro (21), Section.astro (30), Navigation.astro (159; script 138 to 159), Footer.astro (91), cards/FeatureCard.astro (46): the component bodies the demo folder carries, see Frontend.src/styles/tokens.css: the site's token file (the --c-* block matches prima.li's at 9c01b14).guides/design-system-showcase.md (2026-08-24): the mental model, the scaffold, the four preview mechanics, the checklist.Deviations of the reference from this recipe, dated 2026-09-11: uppercase group titles and tags; nine hex literals in the preview chromes and the WIP badge; Astro 4; the agency's own brand values (a client site replaces tokens.css and the fonts).
From alchemy.zuerich/alchemy-new at 77291bb, or from a client site built to this recipe:
sed -n 22,199p src/data/ds-registry.ts | grep -c "^ { slug:" prints 78 and grep -c "documented: true" src/data/ds-registry.ts prints 65; ls src/pages/design-system/components | wc -l prints 66 and ls src/pages/design-system/frame | wc -l prints 69.npx astro build exits 0 and prints 171 page(s) built; ls dist/design-system/index.html dist/design-system/components/button/index.html lists both.grep -c ":global(" src/components/_ds/*.astro | awk -F: '{s+=$2} END {print s}' prints 7.grep -rnE "#[0-9a-fA-F]{6}" src/components/_ds/ | wc -l prints 8 on the reference (the preview chromes and the WIP badge, listed in Gotchas) and 0 on a client site./design-system shows Overview active and every group; (b) click "Button"; the page shows the source path components/Button.astro, a matrix of three surfaces by six states, a props table with seven rows; the sidebar now shows "Button Group" nested under Button; (c) click "Colour" under Foundations; swatches render from tokens; (d) open /design-system/frame/email-welcome directly; no site header, one email; (e) resize to 800 px; the sidebar stacks above the content.npm run check prints no line for this recipe (the four state keys in demo.astro match the frontmatter in order).tooling/verify/astro-static.sh <site> /design-system --serve builds and serves the site on 127.0.0.1:4320, then node tooling/verify/compare.mjs ui-design-system/ui/astro-design-system-showcase --state actions --docs http://127.0.0.1:4321 --app http://127.0.0.1:4320 writes a report with page_errors: []; the verifier reads mismatch_ratio for the button specimens only, since the values on the docs host are prima.li's and the values on the agency site are the agency's (same shape, different palette, as the recipe says).Content synced from the prima-modules repository at 0df2ec8 on 2026-09-14. The recipe file is the source; this page is a rendering of it.