Prima Get an API key

Module kit / UI design system

alchemy-ui component library

ui-design-system/ui/alchemy-ui-component-librarylive

Kind frontend
Surface SPA
Maturity prod
Reusable extract
Design system alchemy-ui
Stack react-19, typescript, tailwind-3.4, lucide-react, headlessui-2
Depends on _shared/design-system-and-tokens, _shared/react-app-skeleton
Renders no backend module: a design-system row
Entry alchemy_labs/frontend/src/pages/DesignSystemDemo.tsx
Reference implementation
  • alchemy_labs/frontend/src/components/alchemy-ui/index.ts
  • alchemy_labs/frontend/src/components/alchemy-ui/atoms/index.ts
  • alchemy_labs/frontend/src/components/alchemy-ui/atoms/Button.tsx
  • alchemy_labs/frontend/src/components/alchemy-ui/molecules/index.ts
  • alchemy_labs/frontend/src/components/alchemy-ui/molecules/Toast.tsx
  • alchemy_labs/frontend/src/components/alchemy-ui/molecules/Table.tsx
  • alchemy_labs/frontend/src/pages/DesignSystemDemo.tsx
  • alchemy_labs/frontend/tailwind.config.js
Measured on alchemy_labs d1a-token-source fd8558f 2026-09-11
Version 1

Purpose

Foundations: spacing, radius, colours, Heading, Text, Divider foundations server rendered, hydrated in view

spacing

the 4 px scale, as Tailwind emits it

1
0.25rem (4px)
2
0.5rem (8px)
3
0.75rem (12px)
4
1rem (16px)
6
1.5rem (24px)
8
2rem (32px)
12
3rem (48px)
16
4rem (64px)

borderRadius

the steps, plus `pill` from the config

sm

md

lg

xl

2xl

3xl

pill

colors

brand values read from @prima-modules/tokens

brand.purple

#6844C4

brand.orange

#BA573D

brand.cream

#FFF6F1

brand.beige

#FBF6F0

Shell tokens

ink

ink-60

ink-40

paper

line

purple-100

Semantic

success

warning

error

info

Heading

level 1 to 4

Heading 1

Heading 2

Heading 3

Heading 4

Text

size, weight, muted

Large text, for introductions

Regular text, default body copy

Small text, secondary information

Extra small, captions and labels

Normal weight (400)

Medium weight (500)

Semibold weight (600)

Bold weight (700)

Default text colour

Muted text colour

Primary colour text

Divider

variant, label, orientation, contrast

OR
Left
Right

One React component library that every SPA page in the kit is assembled from, so a form on the commerce page and a form on the settings page are the same Input, the same Button and the same Card. The invariant that makes it trustworthy is that the atoms carry no visual value of their own: every colour, radius, shadow and font they render is a Tailwind class emitted from tailwind.config.js (S1, decision D1a), so a token change repaints the whole app and the gallery on the docs page, built from the same token file, repaints with it.

It makes no backend module visible; it is the vocabulary the other frontend recipes render with, and the /design-system page is its own rendering.

When to use

Use it for every surface: SPA rendering (decision D1b, 2026-09-11): a page inside the app shell composes atoms and molecules from the barrel src/components/alchemy-ui/index.ts and writes no button, input, badge or card of its own. Trigger phrases: "add a field to the settings page", "a table of orders", "a status chip", "a loading skeleton", "the toast after saving".

Do not use it on a Prima product surface (chat, portal, prima.li), which render in the prima system, or on an Astro client site (tokens-css, S3). Do not reach for src/components/design-system/: it was deleted on 2026-09-11 (commit fd105ee) and nothing imported it.

Stack assumptions

S1 for the token file, S2 for the shell the atoms render inside. Pinned as measured in alchemy_labs/frontend/package.json at fd8558f (2026-09-11): react ^19.1.1, typescript ^4.9.5, tailwindcss ^3.4.17, lucide-react ^0.563.0 (icons), @headlessui/react ^2.2.9 (the overlays the atoms do not cover), framer-motion ^12.23.24 (used by a few pages, not by the atoms). Class strings only; no CSS-in-JS, no CSS modules.

Data-fetch style: none. The atoms are presentational; DataTable takes rows and columns, Toast takes a message. Copy language of the library page is English (it is an internal showcase); the atoms render whatever copy the page passes, du-Form German on merchant pages.

Data model

No server state. Component props are the contract; the barrel exports them as types:

  • Atoms (atoms/index.ts, 19 modules): Button (variant: primary | secondary | outline | ghost, size: sm | md | lg, fullWidth, loading), Input, Heading (level 1 to 4), Text (size, muted), Textarea, Select (SelectOption), MultiSelect, SmartSelect, Radio and RadioGroup, Checkbox and CheckboxGroup, Toggle, Badge and StatusBadge, SelectCards and MultiSelectCards (SelectableOption), Avatar and AvatarGroup, Spinner plus Skeleton, SkeletonCard, SkeletonListItem, SkeletonProfile, SkeletonTable, SkeletonForm, SkeletonArticle, Divider, AlchemyLoader, AlchemyPageLoader, AlchemyInlineLoader, GlobalBackground, AZSymbol.
  • Molecules (molecules/index.ts, 5 modules): Card with CardHeader, CardContent, CardFooter; InputField (label, help, error around Input); Table with TableHeader, TableBody, TableRow, TableHeadCell, TableCell and DataTable (Column); ScrollNav and FloatingScrollNav (ScrollNavItem); ToastProvider and useToast (ToastType, ToastOptions).
  • The barrel also re-exports ThemeProvider and useTheme from contexts/ThemeContext (index.ts line 15); useTheme().isDark is always false.

Client state: the demo page holds form state for its own specimens (useState at DesignSystemDemo.tsx lines 48 to 56) and a copiedCode id for the copy buttons. Browser storage: alchemy-theme (always light, written by ThemeProvider); nothing else.

API surface

None consumed. The page and the atoms make no request. The one build-time contract is virtual:tailwind-brand (DesignSystemDemo.tsx line 43 imports brand to print the swatch values), generated from tailwind.config.js by vite.config.ts lines 6 to 32.

Application logic

Button buttons server rendered, hydrated in view

Button

variant x size x state; radius is a fixed ratio of height

Variants

Sizes (40, 48, 64 px high; 16, 20, 26 px radius)

States

fullWidth

  1. A page imports what it needs from '../components/alchemy-ui' (145 importing files at fd8558f).
  2. It composes: Card around a block, InputField for a labelled input, Button for the action, DataTable for rows, useToast() for the outcome of a write (ToastProvider is mounted once in App.tsx; calling useToast outside it throws).
  3. Visual variation is a prop (variant, size, status), never a className that overrides colour. className exists on every atom for layout (w-full, mt-4), not for identity.
  4. Loading, empty and error are rendered with the atoms: Spinner or a Skeleton* while fetching, a sentence plus the primary Button when empty, StatusBadge and the envelope's message when failed.
  5. /design-system renders every atom in every state on one public page so a change can be seen in one place. FloatingScrollNav builds its dots from the section ids.

Invariants

  • No hex literal in an atom. At fd8558f grep -rli "6844c4" src/components/alchemy-ui prints nothing; the primary Button is bg-brand-purple and focus:ring-brand-purple (Button.tsx lines 42 to 44).
  • Button radius is a fixed ratio of height, about 0.4085 (brand spec: 24 px radius at 58.75 px height): sm h-10 rounded-[16px], md h-12 rounded-[20px], lg h-16 rounded-[26px] (Button.tsx lines 31 to 38). A new size keeps the ratio; a new component that is a pill uses rounded-pill from the config.
  • The library is light-only. dark: variants in the atoms never apply (ThemeContext.tsx line 29 removes .dark); no new ones are written.
  • One library. components/alchemy-ui/ is the only component set; a page that needs a component the library lacks adds an atom there, with a specimen on /design-system, rather than a page-local copy.
  • The /design-system route stays public and data-free.

Frontend

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.

Input, InputField, Textarea inputs server rendered, hydrated in view

Input

inputSize, error, disabled

This field is required

InputField

label, helperText, required, error around Input

Never shared

Already taken

Textarea

resize, inputSize

Select, MultiSelect, SmartSelect, Radio, Checkbox, Toggle, SelectCards selection server rendered, hydrated in view

Select

single value; the list is a portal

Pick one

MultiSelect

value: string[]

SmartSelect

searchable, clearable, loading

Radio, RadioGroup

RadioGroup takes options; Radio is the single control

Checkbox, CheckboxGroup

indeterminate, description, error

Required

Toggle

size, description

SelectCards, MultiSelectCards

variant: cards | chips | list

Badge, StatusBadge, Avatar, AvatarGroup badges-avatars server rendered, hydrated in view

Badge

variant, dot, size

Variants

DefaultPrimarySecondarySuccessWarningErrorInfoOutline

With dot

OnlineAwayOffline

Sizes

SmallMediumLarge

StatusBadge

one per status

ActiveInactivePendingCompletedFailedDraftPublishedArchived

Avatar, AvatarGroup

size, status, shape, max

Sizes

MK
MK
MK
MK
MK
MK

Status and shape

ON
AW
BU
OF
SQ

AvatarGroup, max 4 of 6

MK
JW
LF
NB
+2
Spinner, Skeleton, AlchemyLoader loaders server rendered, hydrated in view

Spinner

size, color, label

Loading

AlchemyLoader, AlchemyInlineLoader

AlchemyPageLoader is the same loader as a fixed full-screen overlay and is not rendered here

⚗️
Small
⚗️
Medium
⚗️
Large
⚗️
Extra large
⚗️
Bubble
⚗️
Pulse
⚗️
Bounce
⚗️
Spin
⚗️⚗️⚗️

Skeleton

variant text | circular | rectangular | rounded; lines

SkeletonCard, SkeletonProfile, SkeletonListItem, SkeletonForm, SkeletonTable, SkeletonArticle

SkeletonCard

SkeletonProfile

SkeletonListItem

SkeletonForm

SkeletonTable

SkeletonArticle

Table, DataTable, Card tables-cards server rendered, hydrated in view

DataTable

columns with render, striped, hoverable, getRowKey, emptyMessage

Name
E-mail
Status
Role
Mira Kellermira@example.comActiveAdmin
Jonas Weberjonas@example.comPendingMember
Lea Freilea@example.comInactiveMember
Noah Brunnernoah@example.comActiveEditor
Name
No people yet

Table, TableHeader, TableBody, TableRow, TableHeadCell, TableCell

the composable half; size sm, selected row, sortable head

Name
Role
Status
Jonas WeberMemberPending
Lea FreiMemberInactive
Mira KellerAdminActive
Noah BrunnerEditorActive

Card, CardHeader, CardContent, CardFooter

variant default | glass; padding none to xl

Welcome back

Sign in to your account

Default variant

Solid card without the glass finish. Good for high-contrast content.

padding none

the content brings its own

ui-design-system/ui/alchemy-ui-component-library/demo.tsx
/**
 * Live demo for ui-design-system/ui/alchemy-ui-component-library.
 *
 * The gallery: every atom and molecule the barrel exports, with its variants
 * and states, grouped into eight frames and titled with its export name. A
 * port of the sections of Labs' DesignSystemDemo.tsx (fd8558f, lines 199 to
 * 832) without the page chrome (sticky header, theme toggle, FloatingScrollNav,
 * the radial gradient). Swatch values are read from @prima-modules/tokens, the
 * way the page reads `brand` from virtual:tailwind-brand.
 *
 * Not rendered: AZSymbol and GlobalBackground (agency brand atoms; a customer
 * deletes the two export lines), AlchemyPageLoader (a fixed full-screen
 * overlay) and FloatingScrollNav (fixed to the viewport edge); each is named in
 * the frame that would hold it.
 */
import React, { useState } from 'react';
import { Rocket, Shield, Sparkles } from 'lucide-react';
import { defineStates } from '@prima-modules/demo';
import { brand } from '@prima-modules/tokens';
import {
  Button,
  Input,
  Heading,
  Text,
  Textarea,
  Select,
  MultiSelect,
  SmartSelect,
  Radio,
  RadioGroup,
  Checkbox,
  CheckboxGroup,
  Toggle,
  Badge,
  StatusBadge,
  SelectCards,
  MultiSelectCards,
  Avatar,
  AvatarGroup,
  Spinner,
  Skeleton,
  SkeletonCard,
  SkeletonListItem,
  SkeletonProfile,
  SkeletonTable,
  SkeletonForm,
  SkeletonArticle,
  Divider,
  AlchemyLoader,
  AlchemyInlineLoader,
  Card,
  CardHeader,
  CardContent,
  CardFooter,
  InputField,
  Table,
  TableHeader,
  TableBody,
  TableRow,
  TableHeadCell,
  TableCell,
  DataTable,
  ScrollNav,
  ToastProvider,
  useToast,
  ThemeProvider,
} from '@prima-modules/alchemy-ui';

/* ---- sample data (literal, nothing real) ---------------------------- */

const people = [
  { id: 1, name: 'Mira Keller', email: 'mira@example.com', status: 'active', role: 'Admin' },
  { id: 2, name: 'Jonas Weber', email: 'jonas@example.com', status: 'pending', role: 'Member' },
  { id: 3, name: 'Lea Frei', email: 'lea@example.com', status: 'inactive', role: 'Member' },
  { id: 4, name: 'Noah Brunner', email: 'noah@example.com', status: 'active', role: 'Editor' },
];
type Person = (typeof people)[number];

const frameworks = [
  { value: 'react', label: 'React' },
  { value: 'vue', label: 'Vue' },
  { value: 'astro', label: 'Astro' },
  { value: 'svelte', label: 'Svelte' },
];

const countries = [
  { value: 'ch', label: 'Switzerland', description: 'Europe' },
  { value: 'de', label: 'Germany', description: 'Europe' },
  { value: 'at', label: 'Austria', description: 'Europe' },
  { value: 'jp', label: 'Japan', description: 'Asia' },
  { value: 'ca', label: 'Canada', description: 'North America' },
];

const channels = [
  { value: 'email', label: 'E-mail notifications' },
  { value: 'sms', label: 'SMS notifications' },
  { value: 'push', label: 'Push notifications' },
];

/* ---- the contract ------------------------------------------------- */

export type Section =
  | 'foundations'
  | 'buttons'
  | 'inputs'
  | 'selection'
  | 'badges-avatars'
  | 'loaders'
  | 'tables-cards'
  | 'navigation-toasts';

export interface Props {
  section: Section;
}

export const states = defineStates<Props>({
  foundations: { label: 'Foundations: spacing, radius, colours, Heading, Text, Divider', props: { section: 'foundations' }, height: 1500 },
  buttons: { label: 'Button', props: { section: 'buttons' }, height: 520 },
  inputs: { label: 'Input, InputField, Textarea', props: { section: 'inputs' }, height: 760 },
  selection: { label: 'Select, MultiSelect, SmartSelect, Radio, Checkbox, Toggle, SelectCards', props: { section: 'selection' }, height: 1500 },
  'badges-avatars': { label: 'Badge, StatusBadge, Avatar, AvatarGroup', props: { section: 'badges-avatars' }, height: 760 },
  loaders: { label: 'Spinner, Skeleton, AlchemyLoader', props: { section: 'loaders' }, height: 1500 },
  'tables-cards': { label: 'Table, DataTable, Card', props: { section: 'tables-cards' }, height: 1100 },
  'navigation-toasts': { label: 'ScrollNav, ToastProvider and useToast', props: { section: 'navigation-toasts' }, height: 520 },
});

/* ---- scaffolding ---------------------------------------------------- */

/** One specimen block, titled with the export name it shows. */
function Specimen({ name, note, children }: { name: string; note?: string; children: React.ReactNode }) {
  return (
    <section className="space-y-4">
      <div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
        <Heading level={3} className="!tracking-normal">
          <code className="font-mono text-[0.9em]">{name}</code>
        </Heading>
        {note && (
          <Text size="sm" muted>
            {note}
          </Text>
        )}
      </div>
      <Card variant="glass" padding="lg">
        {children}
      </Card>
    </section>
  );
}

function Row({ label, children }: { label: string; children: React.ReactNode }) {
  return (
    <div>
      <Text weight="semibold" className="mb-3">
        {label}
      </Text>
      <div className="flex flex-wrap items-center gap-4">{children}</div>
    </div>
  );
}

/* ---- the eight frames ---------------------------------------------- */

function Foundations() {
  return (
    <>
      <Specimen name="spacing" note="the 4 px scale, as Tailwind emits it">
        <div className="space-y-3">
          {[
            { key: '1', value: '0.25rem', px: '4px' },
            { key: '2', value: '0.5rem', px: '8px' },
            { key: '3', value: '0.75rem', px: '12px' },
            { key: '4', value: '1rem', px: '16px' },
            { key: '6', value: '1.5rem', px: '24px' },
            { key: '8', value: '2rem', px: '32px' },
            { key: '12', value: '3rem', px: '48px' },
            { key: '16', value: '4rem', px: '64px' },
          ].map((item) => (
            <div key={item.key} className="flex items-center gap-4">
              <div className="w-12 text-sm font-mono text-stone-500">{item.key}</div>
              <div className="h-6 bg-brand-purple rounded" style={{ width: item.value }} />
              <div className="text-sm font-mono text-zinc-700">
                {item.value} <span className="text-stone-500">({item.px})</span>
              </div>
            </div>
          ))}
        </div>
      </Specimen>

      <Specimen name="borderRadius" note="the steps, plus `pill` from the config">
        <div className="flex flex-wrap gap-6">
          {[
            { key: 'sm', value: '0.125rem' },
            { key: 'md', value: '0.375rem' },
            { key: 'lg', value: '0.5rem' },
            { key: 'xl', value: '0.75rem' },
            { key: '2xl', value: '1rem' },
            { key: '3xl', value: '1.5rem' },
            { key: 'pill', value: '9999px' },
          ].map((item) => (
            <div key={item.key} className="flex flex-col items-center gap-2">
              <div className="w-16 h-16 bg-brand-purple" style={{ borderRadius: item.value }} />
              <Text size="xs" muted>
                {item.key}
              </Text>
            </div>
          ))}
        </div>
      </Specimen>

      <Specimen name="colors" note="brand values read from @prima-modules/tokens">
        <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
          {(
            [
              ['brand.purple', 'bg-brand-purple', brand.purple],
              ['brand.orange', 'bg-brand-orange', brand.orange],
              ['brand.cream', 'bg-brand-cream border border-line', brand.cream],
              ['brand.beige', 'bg-brand-beige border border-line', brand.beige],
            ] as const
          ).map(([name, cls, hex]) => (
            <div key={name} className="space-y-2">
              <div className={`h-20 rounded-xl ${cls}`} />
              <Text size="sm" weight="medium">
                {name}
              </Text>
              <Text size="xs" muted>
                {hex}
              </Text>
            </div>
          ))}
        </div>
        <Divider className="my-6" />
        <Text weight="semibold" className="mb-4">
          Shell tokens
        </Text>
        <div className="grid grid-cols-3 md:grid-cols-6 gap-4">
          {(
            [
              ['ink', 'bg-ink'],
              ['ink-60', 'bg-ink-60'],
              ['ink-40', 'bg-ink-40'],
              ['paper', 'bg-paper border border-line'],
              ['line', 'bg-line'],
              ['purple-100', 'bg-purple-100'],
            ] as const
          ).map(([name, cls]) => (
            <div key={name} className="space-y-2">
              <div className={`h-12 rounded-lg ${cls}`} />
              <Text size="xs" muted>
                {name}
              </Text>
            </div>
          ))}
        </div>
        <Divider className="my-6" />
        <Text weight="semibold" className="mb-4">
          Semantic
        </Text>
        <div className="grid grid-cols-4 gap-4">
          {(
            [
              ['success', 'bg-success'],
              ['warning', 'bg-warning'],
              ['error', 'bg-error'],
              ['info', 'bg-info'],
            ] as const
          ).map(([name, cls]) => (
            <div key={name} className="space-y-2">
              <div className={`h-12 rounded-lg ${cls}`} />
              <Text size="xs" muted>
                {name}
              </Text>
            </div>
          ))}
        </div>
      </Specimen>

      <Specimen name="Heading" note="level 1 to 4">
        <div className="space-y-2">
          <Heading level={1}>Heading 1</Heading>
          <Heading level={2}>Heading 2</Heading>
          <Heading level={3}>Heading 3</Heading>
          <Heading level={4}>Heading 4</Heading>
        </div>
      </Specimen>

      <Specimen name="Text" note="size, weight, muted">
        <div className="space-y-3">
          <Text size="lg">Large text, for introductions</Text>
          <Text>Regular text, default body copy</Text>
          <Text size="sm">Small text, secondary information</Text>
          <Text size="xs">Extra small, captions and labels</Text>
          <Divider />
          <Text weight="normal">Normal weight (400)</Text>
          <Text weight="medium">Medium weight (500)</Text>
          <Text weight="semibold">Semibold weight (600)</Text>
          <Text weight="bold">Bold weight (700)</Text>
          <Divider />
          <Text>Default text colour</Text>
          <Text muted>Muted text colour</Text>
          <Text className="text-brand-purple">Primary colour text</Text>
        </div>
      </Specimen>

      <Specimen name="Divider" note="variant, label, orientation, contrast">
        <div className="space-y-4">
          <Divider />
          <Divider variant="dashed" />
          <Divider variant="dotted" />
          <Divider label="OR" />
          <Divider contrast="high" />
          <div className="flex items-center gap-4 h-12">
            <span>Left</span>
            <Divider orientation="vertical" />
            <span>Right</span>
          </div>
        </div>
      </Specimen>
    </>
  );
}

function Buttons() {
  return (
    <Specimen name="Button" note="variant x size x state; radius is a fixed ratio of height">
      <div className="space-y-8">
        <Row label="Variants">
          <Button variant="primary">Primary</Button>
          <Button variant="secondary">Secondary</Button>
          <Button variant="outline">Outline</Button>
          <Button variant="ghost">Ghost</Button>
        </Row>
        <Row label="Sizes (40, 48, 64 px high; 16, 20, 26 px radius)">
          <Button size="sm">Small</Button>
          <Button size="md">Medium</Button>
          <Button size="lg">Large</Button>
        </Row>
        <Row label="States">
          <Button>Default</Button>
          <Button loading>Loading</Button>
          <Button disabled>Disabled</Button>
        </Row>
        <Row label="fullWidth">
          <Button fullWidth>Continue</Button>
        </Row>
      </div>
    </Specimen>
  );
}

function Inputs() {
  return (
    <div className="max-w-md space-y-10">
      <Specimen name="Input" note="inputSize, error, disabled">
        <div className="space-y-4">
          <Input placeholder="Small input" inputSize="sm" />
          <Input placeholder="Medium input" inputSize="md" />
          <Input placeholder="Large input" inputSize="lg" />
          <Input placeholder="Invalid input" error="This field is required" />
          <Input placeholder="Disabled input" disabled />
        </div>
      </Specimen>
      <Specimen name="InputField" note="label, helperText, required, error around Input">
        <div className="space-y-4">
          <InputField label="E-mail address" placeholder="you@example.com" type="email" helperText="Never shared" required />
          <InputField label="Workspace name" placeholder="My workspace" error="Already taken" />
        </div>
      </Specimen>
      <Specimen name="Textarea" note="resize, inputSize">
        <div className="space-y-4">
          <Textarea placeholder="Write your message here" />
          <Textarea placeholder="Non-resizable" resize="none" inputSize="sm" />
        </div>
      </Specimen>
    </div>
  );
}

function Selection() {
  const [selectValue, setSelectValue] = useState('');
  const [multiValue, setMultiValue] = useState<string[]>(['react']);
  const [smartValue, setSmartValue] = useState('');
  const [radioValue, setRadioValue] = useState('option1');
  const [checkboxValues, setCheckboxValues] = useState<string[]>(['email']);
  const [toggleValue, setToggleValue] = useState(true);
  const [cardValue, setCardValue] = useState('pro');
  const [chipValue, setChipValue] = useState<string[]>(['react', 'astro']);

  return (
    <>
      <Specimen name="Select" note="single value; the list is a portal">
        <div className="max-w-md space-y-4">
          <Select options={frameworks} value={selectValue} onChange={setSelectValue} placeholder="Choose a framework" />
          <Select options={frameworks} value="vue" placeholder="Disabled" disabled />
          <Select options={frameworks} value="" placeholder="With error" error="Pick one" />
        </div>
      </Specimen>
      <Specimen name="MultiSelect" note="value: string[]">
        <div className="max-w-md">
          <MultiSelect options={frameworks} value={multiValue} onChange={setMultiValue} placeholder="Select frameworks" />
        </div>
      </Specimen>
      <Specimen name="SmartSelect" note="searchable, clearable, loading">
        <div className="max-w-md space-y-4">
          <SmartSelect
            options={countries}
            value={smartValue}
            onChange={(v) => setSmartValue(v as string)}
            placeholder="Search for a country"
            searchable
            clearable
          />
          <SmartSelect options={countries} value="" onChange={() => {}} placeholder="Loading" loading />
        </div>
      </Specimen>
      <Specimen name="Radio, RadioGroup" note="RadioGroup takes options; Radio is the single control">
        <div className="grid md:grid-cols-2 gap-8">
          <RadioGroup
            name="demo-radio"
            options={[
              { value: 'option1', label: 'Option 1', description: 'The first option' },
              { value: 'option2', label: 'Option 2', description: 'The second option' },
              { value: 'option3', label: 'Option 3', description: 'Disabled', disabled: true },
            ]}
            value={radioValue}
            onChange={setRadioValue}
          />
          <div className="space-y-3">
            <Radio name="single" label="Small" size="sm" defaultChecked />
            <Radio name="single" label="Medium" size="md" />
            <Radio name="single" label="Large" size="lg" />
          </div>
        </div>
      </Specimen>
      <Specimen name="Checkbox, CheckboxGroup" note="indeterminate, description, error">
        <div className="grid md:grid-cols-2 gap-8">
          <CheckboxGroup options={channels} value={checkboxValues} onChange={setCheckboxValues} />
          <div className="space-y-4">
            <Checkbox label="I agree to the terms" description="By ticking this box you accept the terms" />
            <Checkbox
              label="Select all"
              indeterminate={checkboxValues.length > 0 && checkboxValues.length < channels.length}
              checked={checkboxValues.length === channels.length}
              onChange={(checked) => setCheckboxValues(checked ? channels.map((o) => o.value) : [])}
            />
            <Checkbox label="With error" error="Required" />
          </div>
        </div>
      </Specimen>
      <Specimen name="Toggle" note="size, description">
        <div className="flex flex-wrap gap-8">
          <Toggle label="Notifications" description="Receive e-mail notifications" checked={toggleValue} onChange={setToggleValue} />
          <Toggle label="Small" size="sm" />
          <Toggle label="Large" size="lg" defaultChecked />
          <Toggle label="Disabled" disabled />
        </div>
      </Specimen>
      <Specimen name="SelectCards, MultiSelectCards" note="variant: cards | chips | list">
        <div className="space-y-10">
          <SelectCards
            options={[
              { value: 'starter', label: 'Starter', description: 'For small projects', icon: <Sparkles className="w-5 h-5" /> },
              { value: 'pro', label: 'Professional', description: 'For growing businesses', icon: <Rocket className="w-5 h-5" /> },
              { value: 'enterprise', label: 'Enterprise', description: 'Advanced features', icon: <Shield className="w-5 h-5" /> },
            ]}
            value={cardValue}
            onChange={setCardValue}
            variant="cards"
            columns={3}
          />
          <MultiSelectCards options={[...frameworks, { value: 'next', label: 'Next.js' }]} value={chipValue} onChange={setChipValue} variant="chips" />
          <div className="max-w-md">
            <SelectCards
              options={[
                { value: 'email', label: 'E-mail', description: 'Get updates by e-mail' },
                { value: 'sms', label: 'SMS', description: 'Receive text messages' },
                { value: 'push', label: 'Push', description: 'Browser notifications' },
              ]}
              value="sms"
              onChange={() => {}}
              variant="list"
            />
          </div>
        </div>
      </Specimen>
    </>
  );
}

function BadgesAvatars() {
  return (
    <>
      <Specimen name="Badge" note="variant, dot, size">
        <div className="space-y-6">
          <Row label="Variants">
            <Badge>Default</Badge>
            <Badge variant="primary">Primary</Badge>
            <Badge variant="secondary">Secondary</Badge>
            <Badge variant="success">Success</Badge>
            <Badge variant="warning">Warning</Badge>
            <Badge variant="error">Error</Badge>
            <Badge variant="info">Info</Badge>
            <Badge variant="outline">Outline</Badge>
          </Row>
          <Row label="With dot">
            <Badge variant="success" dot>
              Online
            </Badge>
            <Badge variant="warning" dot>
              Away
            </Badge>
            <Badge variant="error" dot>
              Offline
            </Badge>
          </Row>
          <Row label="Sizes">
            <Badge size="sm">Small</Badge>
            <Badge size="md">Medium</Badge>
            <Badge size="lg">Large</Badge>
          </Row>
        </div>
      </Specimen>
      <Specimen name="StatusBadge" note="one per status">
        <div className="flex flex-wrap items-center gap-3">
          {(['active', 'inactive', 'pending', 'completed', 'failed', 'draft', 'published', 'archived'] as const).map((s) => (
            <StatusBadge key={s} status={s} />
          ))}
        </div>
      </Specimen>
      <Specimen name="Avatar, AvatarGroup" note="size, status, shape, max">
        <div className="space-y-6">
          <Row label="Sizes">
            {(['xs', 'sm', 'md', 'lg', 'xl', '2xl'] as const).map((s) => (
              <Avatar key={s} name="Mira Keller" size={s} />
            ))}
          </Row>
          <Row label="Status and shape">
            <Avatar name="Online" status="online" size="lg" />
            <Avatar name="Away" status="away" size="lg" />
            <Avatar name="Busy" status="busy" size="lg" />
            <Avatar name="Offline" status="offline" size="lg" />
            <Avatar name="Square" shape="square" size="lg" />
          </Row>
          <Row label="AvatarGroup, max 4 of 6">
            <AvatarGroup avatars={people.map((p) => ({ name: p.name })).concat([{ name: 'Sara Meier' }, { name: 'Tim Huber' }])} max={4} size="md" />
          </Row>
        </div>
      </Specimen>
    </>
  );
}

function Loaders() {
  return (
    <>
      <Specimen name="Spinner" note="size, color, label">
        <div className="flex items-center gap-6">
          <Spinner size="xs" />
          <Spinner size="sm" />
          <Spinner size="md" />
          <Spinner size="lg" />
          <Spinner size="xl" />
          <Spinner size="md" label="Loading" />
          <span className="bg-ink rounded-lg p-3">
            <Spinner size="md" color="white" />
          </span>
        </div>
      </Specimen>
      <Specimen name="AlchemyLoader, AlchemyInlineLoader" note="AlchemyPageLoader is the same loader as a fixed full-screen overlay and is not rendered here">
        <div className="space-y-8">
          <div className="flex items-end gap-8">
            <AlchemyLoader size="sm" label="Small" />
            <AlchemyLoader size="md" label="Medium" />
            <AlchemyLoader size="lg" label="Large" />
            <AlchemyLoader size="xl" label="Extra large" />
          </div>
          <Divider contrast="high" />
          <div className="flex items-end gap-8">
            <AlchemyLoader size="md" variant="bubble" label="Bubble" />
            <AlchemyLoader size="md" variant="pulse" label="Pulse" />
            <AlchemyLoader size="md" variant="bounce" label="Bounce" />
            <AlchemyLoader size="md" variant="spin" label="Spin" />
          </div>
          <Divider contrast="high" />
          <div className="flex items-center gap-6">
            <AlchemyInlineLoader size="xs" />
            <AlchemyInlineLoader size="sm" />
            <AlchemyInlineLoader size="md" />
          </div>
        </div>
      </Specimen>
      <Specimen name="Skeleton" note="variant text | circular | rectangular | rounded; lines">
        <div className="space-y-4 max-w-md">
          <div className="flex items-center gap-4">
            <Skeleton variant="circular" width={48} height={48} />
            <div className="flex-1 space-y-2">
              <Skeleton variant="text" width="60%" />
              <Skeleton variant="text" width="40%" />
            </div>
          </div>
          <Skeleton variant="rectangular" height={60} />
          <Skeleton variant="rounded" height={100} />
          <Skeleton variant="text" lines={3} />
        </div>
      </Specimen>
      <Specimen name="SkeletonCard, SkeletonProfile, SkeletonListItem, SkeletonForm, SkeletonTable, SkeletonArticle">
        <div className="grid md:grid-cols-2 gap-6">
          <div>
            <Text size="sm" muted className="mb-2">
              SkeletonCard
            </Text>
            <SkeletonCard />
          </div>
          <div>
            <Text size="sm" muted className="mb-2">
              SkeletonProfile
            </Text>
            <SkeletonProfile />
          </div>
          <div>
            <Text size="sm" muted className="mb-2">
              SkeletonListItem
            </Text>
            <div className="border border-stone-200 rounded-xl">
              <SkeletonListItem />
              <SkeletonListItem />
              <SkeletonListItem />
            </div>
          </div>
          <div>
            <Text size="sm" muted className="mb-2">
              SkeletonForm
            </Text>
            <SkeletonForm fields={2} />
          </div>
          <div className="md:col-span-2">
            <Text size="sm" muted className="mb-2">
              SkeletonTable
            </Text>
            <SkeletonTable rows={3} />
          </div>
          <div className="md:col-span-2">
            <Text size="sm" muted className="mb-2">
              SkeletonArticle
            </Text>
            <SkeletonArticle />
          </div>
        </div>
      </Specimen>
    </>
  );
}

function TablesCards() {
  const [sort, setSort] = useState<'asc' | 'desc' | null>('asc');
  const rows = [...people].sort((a, b) => (sort === 'desc' ? b.name.localeCompare(a.name) : a.name.localeCompare(b.name)));
  return (
    <>
      <Specimen name="DataTable" note="columns with render, striped, hoverable, getRowKey, emptyMessage">
        <div className="space-y-6" data-gallery-focus="true">
          <DataTable<Person>
            data={people}
            columns={[
              { key: 'name', header: 'Name', sortable: true },
              { key: 'email', header: 'E-mail', sortable: true },
              { key: 'status', header: 'Status', render: (p) => <StatusBadge status={p.status as 'active' | 'pending' | 'inactive'} /> },
              { key: 'role', header: 'Role', align: 'right' },
            ]}
            striped
            hoverable
            getRowKey={(p) => p.id}
          />
          <DataTable<Person> data={[]} columns={[{ key: 'name', header: 'Name' }]} emptyMessage="No people yet" />
        </div>
      </Specimen>
      <Specimen name="Table, TableHeader, TableBody, TableRow, TableHeadCell, TableCell" note="the composable half; size sm, selected row, sortable head">
        <Table size="sm" hoverable>
          <TableHeader>
            <TableRow>
              <TableHeadCell sortable sortDirection={sort} onSort={() => setSort(sort === 'asc' ? 'desc' : 'asc')}>
                Name
              </TableHeadCell>
              <TableHeadCell>Role</TableHeadCell>
              <TableHeadCell align="right" width={120}>
                Status
              </TableHeadCell>
            </TableRow>
          </TableHeader>
          <TableBody>
            {rows.map((p, i) => (
              <TableRow key={p.id} selected={i === 0}>
                <TableCell>{p.name}</TableCell>
                <TableCell>{p.role}</TableCell>
                <TableCell align="right">
                  <StatusBadge status={p.status as 'active' | 'pending' | 'inactive'} size="sm" />
                </TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </Specimen>
      <Specimen name="Card, CardHeader, CardContent, CardFooter" note="variant default | glass; padding none to xl">
        <div className="grid md:grid-cols-2 gap-6">
          <Card variant="glass" padding="xl">
            <CardHeader title="Welcome back" subtitle="Sign in to your account" />
            <CardContent className="mt-6">
              <div className="space-y-4">
                <Input placeholder="E-mail" type="email" />
                <Input placeholder="Password" type="password" />
              </div>
            </CardContent>
            <CardFooter className="mt-6">
              <Button fullWidth>Continue</Button>
            </CardFooter>
          </Card>
          <Card variant="default" padding="lg">
            <Heading level={4} className="mb-4">
              Default variant
            </Heading>
            <Text muted className="mb-6">
              Solid card without the glass finish. Good for high-contrast content.
            </Text>
            <Button variant="outline">Learn more</Button>
          </Card>
          <Card variant="default" padding="none" className="md:col-span-2">
            <CardHeader title="padding none" subtitle="the content brings its own" centered className="p-4" />
          </Card>
        </div>
      </Specimen>
    </>
  );
}

function ToastButtons() {
  const toast = useToast();
  return (
    <div className="flex flex-wrap gap-3">
      <Button size="sm" onClick={() => toast.success('Saved')}>
        success
      </Button>
      <Button size="sm" variant="outline" onClick={() => toast.error('Could not save', { duration: 6000 })}>
        error
      </Button>
      <Button size="sm" variant="ghost" onClick={() => toast.info('Three items exported')}>
        info
      </Button>
      <Button size="sm" variant="secondary" onClick={() => toast.showToast('Sticky until dismissed', 'info', { duration: 0 })}>
        sticky
      </Button>
    </div>
  );
}

function NavigationToasts() {
  return (
    <>
      <Specimen name="ScrollNav" note="FloatingScrollNav is the same list fixed to the viewport edge and is not rendered here">
        <div className="space-y-4">
          <ScrollNav
            items={[
              { id: 'demo-a', label: 'Overview' },
              { id: 'demo-b', label: 'Details' },
              { id: 'demo-c', label: 'History' },
            ]}
          />
          <div className="grid grid-cols-3 gap-3">
            {['demo-a', 'demo-b', 'demo-c'].map((id) => (
              <div key={id} id={id} className="rounded-xl border border-line bg-white p-4">
                <Text size="sm" muted>
                  #{id}
                </Text>
              </div>
            ))}
          </div>
        </div>
      </Specimen>
      <Specimen name="ToastProvider, useToast" note="the toasts stack at the bottom right of the viewport; error toasts stay 6 s, the rest 4 s, 0 is sticky">
        <ToastProvider>
          <ToastButtons />
        </ToastProvider>
      </Specimen>
    </>
  );
}

/* ---- the component -------------------------------------------------- */

export default function Demo({ section }: Props) {
  return (
    <ThemeProvider>
      <div className="min-h-full bg-stone-100 text-ink font-sans">
        <div className="max-w-5xl mx-auto px-6 py-8 space-y-12">
          {section === 'foundations' && <Foundations />}
          {section === 'buttons' && <Buttons />}
          {section === 'inputs' && <Inputs />}
          {section === 'selection' && <Selection />}
          {section === 'badges-avatars' && <BadgesAvatars />}
          {section === 'loaders' && <Loaders />}
          {section === 'tables-cards' && <TablesCards />}
          {section === 'navigation-toasts' && <NavigationToasts />}
        </div>
      </div>
    </ThemeProvider>
  );
}
Component tree, from the recipe
<Route /design-system> (public, no PrivateRoute, App.tsx line 216)
  DesignSystemDemo
    div.min-h-screen.bg-stone-100 (fixed radial-gradient overlay in brand purple)
    FloatingScrollNav (position=right, one dot per section id)
    header.sticky (Heading level=3 "Alchemy UI", Text "Design System", theme toggle: no-op)
    main.max-w-5xl
      section#hero        Heading level=1, Text, Button primary + outline
      section#spacing     the 4 px scale, one bar per step
      section#radius      the radius steps
      section#colors      swatches read from `brand` (virtual:tailwind-brand)
      section#typography  Heading levels 1 to 4, Text sizes
      section#buttons     Button x variants (primary, secondary, outline, ghost) x sizes (sm, md, lg) x loading
      section#inputs      Input, InputField (label, help, error)
      section#textarea    Textarea
      section#selects     Select, MultiSelect, SmartSelect
      section#radio-checkbox  RadioGroup, Checkbox, CheckboxGroup
      section#toggle      Toggle
      section#select-cards    SelectCards, MultiSelectCards
      section#badges      Badge, StatusBadge
      section#avatars     Avatar, AvatarGroup
      section#loaders     Spinner, Skeleton x 6 variants, AlchemyLoader
      section#dividers    Divider
      section#tables      DataTable
      section#cards       Card > CardHeader, CardContent, CardFooter
Design system
alchemy-ui
Packages
@prima-modules/tokens 0.1.0, from alchemy_labs fd8558f, @prima-modules/alchemy-ui 0.1.0, from alchemy_labs fd8558f, @prima-modules/demo 0.1.0, from prima-modules 5d496a7
Content
Content synced from the prima-modules repository at 0df2ec8 on 2026-09-14.

The rendering is the live gallery alchemy-ui-component-library/demo.tsx, built from @prima-modules/alchemy-ui (the atoms and molecules extracted verbatim from fd8558f) and @prima-modules/tokens (the preset generated from tailwind.config.js at the same commit), rendered by the docs host as one frame per state (RENDERING.md, section 3). The tree in the frontmatter describes the Labs page at /design-system; the demo is that page's sections without its chrome (no sticky header, no theme toggle, no FloatingScrollNav, no radial gradient), one section per state, so the docs page shows eight labelled frames instead of one picture whose fold hid most of them.

Every export of the barrel appears under its export name: foundations (spacing, radius, the colour swatches read from @prima-modules/tokens, Heading, Text, Divider), buttons (Button in every variant, size and state), inputs (Input, InputField, Textarea), selection (Select, MultiSelect, SmartSelect, Radio and RadioGroup, Checkbox and CheckboxGroup, Toggle, SelectCards and MultiSelectCards), badges-avatars (Badge, StatusBadge in all eight statuses, Avatar, AvatarGroup), loaders (Spinner, Skeleton and the six composed skeletons, AlchemyLoader, AlchemyInlineLoader), tables-cards (DataTable populated and empty, the composable Table* set, Card with CardHeader, CardContent, CardFooter) and navigation-toasts (ScrollNav, ToastProvider with useToast behind four buttons). Not rendered: AZSymbol and GlobalBackground (agency brand atoms, named in the demo's header; a customer deletes the two export lines), AlchemyPageLoader and FloatingScrollNav (fixed to the viewport, they would escape the frame; each is named in the frame that would hold it). ThemeProvider wraps every frame as it wraps the Labs page; isDark is false. Sample data is fictional; nothing is fetched.

Tokens the page uses, all from tailwind.config.js: brand.purple (buttons, active dots, swatches), stone-100 ground with a radial gradient in the purple's rgb, ink and stone-* for text, shadow-lift on cards, rounded-[16px|20px|26px] on buttons, rounded-pill on chips, font-sans (ES Rebond Grotesque). At 390 px each frame is one column; buttons keep their height and radius.

States rendered: every Button variant in default, loading and disabled; Input in three sizes, with a value, in error and disabled; Skeleton in four shapes plus the six composed patterns; Spinner in five sizes; StatusBadge in each status. Hover, focus, the open menus of the selects and the toasts are live in the frames (hydrate: visible).

Security & tenancy

Nothing here holds data. The route is public by design (App.tsx line 216); it renders no user, no workspace, no key. The theme toggle in the page header calls toggleTheme, which is a no-op, so the button is decorative (see Gotchas). Surface gating does not apply; the page renders for every build surface.

Tests

  • No colocated unit test for the atoms at fd8558f (measured: ls src/components/alchemy-ui/**/__tests__ finds nothing). The render check is the page itself plus npm run build (tsc --noEmit && vite build), which passed on fd105ee and fd8558f with vitest at 160 passed and the one pre-existing App.test.tsx failure.
  • What a new app adds: one vitest file per atom that renders each variant with renderWithProviders (S2 src/test-utils/index.tsx) and asserts the class the variant maps to (bg-brand-purple for primary), and one jsdom test for useToast asserting it throws outside ToastProvider.
  • The Playwright gate does not visit /design-system (measured: none of the five frontend/e2e/*.spec.ts names it). In this kit the render smoke (node tooling/verify/render-smoke.mjs) renders every state of the demo with renderToString and asserts no throw and non-empty output; tooling/verify/compare.mjs against /design-system is the visual check, run by the verifier and never committed.

Deploy & ops

Ships inside the Labs bundle (S2 Deploy & ops): vite build with the Tailwind content globs purging unused classes. Nothing to configure; no env. A token used only inside a computed class string is purged by Tailwind and renders unstyled: name it in safelist or write the full class in source. Rollback is the bundle's rollback.

Gotchas

ScrollNav, ToastProvider and useToast navigation-toasts server rendered, hydrated in view

ScrollNav

FloatingScrollNav is the same list fixed to the viewport edge and is not rendered here

#demo-a

#demo-b

#demo-c

ToastProvider, useToast

the toasts stack at the bottom right of the viewport; error toasts stay 6 s, the rest 4 s, 0 is sticky

  • 2026-09-11 (measured at b1089b6, fixed at fd8558f): the atoms hard-coded the brand purple as arbitrary values (bg-[#6844C4]) in 130 files; tokens.ts in the library folder had zero consumers and a different shade scale. D1a deleted tokens.ts (and the index.ts line 5 re-export that made "zero consumers" true only of the values) and replaced the literals with bg-brand-purple and siblings. Nothing visibly changed (built CSS diffed rule by rule).
  • 2026-09-11 (measured at fd8558f): Button.tsx primary still hovers to bg-[#6359c7] (line 43) and its dead dark: hover to #8a7de8; neither is a brand token. Name a brand.purple-hover in the config before touching it, or accept the literal as the dated exception it is.
  • 2026-09-11 (measured): the page header's theme toggle renders a moon icon and calls a no-op; isDark is always false. The dark specimens the code path describes (bg-zinc-900, the darker gradient) are unreachable. A customer copying the page removes the toggle.
  • 2026-09-11 (measured): AZSymbol and GlobalBackground are agency brand atoms (the mark and the ambient background). A customer build swaps them for its own mark or deletes the specimen; they are in the barrel so the delete is one export line.
  • 2026-09-11 (measured, DesignSystemDemo.tsx line 154): the page ground is bg-stone-100, not the shell's bg-paper; the showcase predates the shell tokens. Inside the shell every page uses paper.
  • 2026-09-11 (measured): the sidecar of the first harness capture of this page said authenticated: true because the harness counted the consent entry as a session; shot.mjs was corrected the same day. The captures themselves were deleted on 2026-09-11 when the rendering became the live demo (RENDERING.md); the harness stays as the verifier.
  • 2026-09-11 (rendering trap): ScrollNav reads window.scrollY and document.getElementById in an effect, Select and SmartSelect portal their menus to document.body, and ToastProvider stacks its toasts fixed at the bottom right of whatever page hosts it. All three are fine under hydrate: visible (the effects run after hydration) and none may run at module top level, which the gate checks by importing the demo in Node.

Reference implementation

Branch that ships: alchemy_labs feat/service-desk at b1089b6 with the D1a commits on top on d1a-token-source (fd105ee, fd8558f, 2026-09-11); the branch is the one this recipe was measured on and is expected to merge to the shipping line. Paths relative to frontend/:

  • src/components/alchemy-ui/index.ts (barrel, ThemeProvider re-export at 15), atoms/index.ts (19 exports), molecules/index.ts (5 exports).
  • src/components/alchemy-ui/atoms/Button.tsx: props (4 to 10), base styles (23 to 29), the size and radius table (31 to 38), variants (40 to 70).
  • src/components/alchemy-ui/molecules/Toast.tsx: ToastProvider, useToast; molecules/Table.tsx: Table* and DataTable.
  • src/pages/DesignSystemDemo.tsx (843 lines): imports (4 to 43), state (48 to 56), page chrome (153 to 196), the sections (199 to 832).
  • src/App.tsx line 216: <Route path="/design-system" element={<DesignSystemDemo />} />, outside PrivateRoute.
  • tailwind.config.js: the token source (S1).

Deviations of the reference from this recipe, dated 2026-09-11: the two non-token hover literals in Button.tsx; dead dark: variants throughout the atoms; the decorative theme toggle; no unit tests for the atoms; stone-100 ground on the showcase page; one pixel-literal font size in the atoms (text-[10px] for the xs avatar, Avatar.tsx line 123) that S1 rule 1 forbids.

Acceptance checks

From alchemy_labs/frontend at fd8558f, or from a new app built to this recipe:

  1. grep -rli "6844c4" src/components/alchemy-ui | wc -l prints 0; grep -c "bg-brand-purple" src/components/alchemy-ui/atoms/Button.tsx prints 2 (the light and the dead dark variant).
  2. grep -c "export" src/components/alchemy-ui/atoms/index.ts prints 19; grep -c "^export" src/components/alchemy-ui/molecules/index.ts prints 5.
  3. npm run build exits 0.
  4. Browser walk-through: open /design-system signed out; the page renders with no redirect to /login; the floating dots on the right jump to each section; in the inspector the three buttons of the "Buttons" section measure 40, 48 and 64 px high with radii 16, 20 and 26 px; the header's moon button changes nothing; document.documentElement.classList.contains('dark') is false.
  5. Demo gate: npm run check prints no line for this recipe (the demo exists, exports the eight states in the frontmatter's order, and carries no screenshot key); node tooling/verify/render-smoke.mjs ui-design-system/ui/alchemy-ui-component-library prints ok for all eight states and 0 failures.
  6. Verifier: with the docs index build and the Labs dev server running, node tooling/verify/compare.mjs ui-design-system/ui/alchemy-ui-component-library --state buttons --docs http://127.0.0.1:4321 --app http://127.0.0.1:3020 --session-file tooling/verify/.labs-local/anon.json writes tooling/verify/out/ui-design-system--alchemy-ui-component-library/buttons/report.json with page_errors: []; the mismatch_ratio is read by the verifier for the Buttons section only (the app page's animated gradient and the sections above the fold are excluded by construction, since the frame is one section), and the report is never committed.

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.