Build
How we build
One source, many surfaces. How tokens, components and checks fit together, which rules apply in code, and when a view is done.
Hey Albert runs in the browser and on iPhone, iPad, Mac and Watch. Every surface shows the same rows from the same database. And every surface carries the same design from the same source: packages/design-system.
This chapter is for anyone who writes code: people on the team, outside experts and agents.
One source
Every value of the brand exists exactly once in the repository. Colours, sizes, fonts and motion live in packages/design-system/tokens/quelle/*.json. Everything else refers to them.
| What | Where | Becomes |
|---|---|---|
| Tokens | tokens/quelle/*.json (W3C Design Tokens Community Group format) | tokens/*.css, tailwind/theme.css, swift/Tokens.swift |
| Components | components/<group>/<Name>.jsx with .d.ts and .prompt.md | the model for apps/web/src/bausteine and for SwiftUI |
| Web primitives | bauteile/ui/*.tsx (shadcn on our roles) | forms, dialogs and tables in HQ and on the website |
| Website blocks | bloecke/*.tsx, one to one with packages/content-model | the pages of hey-albert.app from Payload |
| Rules | regeln/regeln.json | this site, brand QA, agents |
| Fonts, icons, logos | assets/ | every surface, served locally |
The order
Every area is built in the same sequence. The foundation stands before anyone sees a screen.
- Schema and RLS. The table with
household_id, an index and four policies, in one migration. - Tests. pgTAP for all five roles:
owner,adult,child,guest,service. - Domain logic. Pure functions without UI, with their own tests.
- Interface. From components of the system, against the seed of the Sonntag family.
Nobody builds against empty screens. The seed household has a real week: Mon 1 to Sun 7 September, and today is Wednesday.
The stack per surface
Web app and HQ
apps/web is Next.js 16 with the App Router. Tokens come straight from the package as CSS variables, imported in the layout from tokens/colors.css and the other token files. Tailwind is used only for layout helpers such as grids, spacing and breakpoints. Colour, type and radius go through roles.
The components in apps/web/src/bausteine are the production versions of the components in the system. Rahmen.tsx is LeisteMac plus Kopfzeile, and Zeichen.tsx is Icon. If a version differs, that is a bug, not a style.
Website
apps/site builds hey-albert.app from Payload. Every block in the content model has exactly one component in packages/design-system/bloecke. RenderBloecke picks by blockType. pnpm test in the package checks that no block is left without a component.
Apple
apps/apple will be SwiftUI Multiplatform with Swift 6 and strict concurrency. The roles live in swift/Tokens.swift: AlbertFarbe, AlbertRaum, AlbertSchrift, AlbertForm, AlbertBewegung. Every colour carries light and dark and resolves at runtime.
Tokens as code
The source is JSON in the W3C Design Tokens Community Group format, one set per file: farben-hell, farben-dunkel, schrift, raum, form, bewegung. Style Dictionary 5 generates three outputs from it without converting any value.
cd packages/design-system
pnpm tokens # generate and check against the frozen reference
pnpm tokens:diff # check only
tokens-diff compares every name and value pair with tokens/referenz/. If a value differs, the run fails. New roles are listed with a reason in tokens/referenz/ergaenzungen.txt.
Generated files start with the line „Erzeugt aus tokens/quelle“ (generated from tokens/quelle). Nobody edits them by hand. The GitHub workflow design.yml regenerates them and checks that nothing differs.
Rules in code
- No hex value in a component. Colour runs through roles (--grund, --karte, --tinte, --akzent).
- No shadows, anywhere. Depth comes from a veil, a stronger outline and tilt.
- Meanings are never swapped: pink = accent and now, yellow = family and ritual, blue = drive, open-pink only as a dashed outline for „no one responsible“. Pink never means „error“.
- Colour is never the only carrier. An initial or name is always present.
- Disabled is a computed role, not opacity.
- Text and controls at least 4.5:1, surfaces and outlines at least 3:1.
- Target sizes: pointer 28, finger 44, child 56 px — never below.
- Focus always via :focus-visible: a 2 px ring in open-pink with 2 px offset.
- 120 ms states, 200 ms reveals, 300 ms breath. No springs, no spinner. With reduced motion everything stands still.
- Skeleton, not spinner. The skeleton has the shape of the content.
- Every change is optimistic and offers „Undo“.
- Fonts are served locally, never from Google’s servers.
- No emoji, no Unicode characters as icon substitutes.
- If an icon is missing, take it from the set; never draw one.
React with Tailwind
The Tailwind theme knows only our roles. Tailwind’s own palette, shadows and radii are switched off, so bg-red-500 or shadow-md simply do not exist.
Disabled is a computed role: disabled:bg-knopf-inaktiv-flaeche disabled:text-knopf-inaktiv-text. Opacity multiplies with whatever lies underneath. Then no computed contrast ratio holds any more.
SwiftUI
Fonts served locally
The four fonts ship in the package as woff2 for the web and TTF for the app. Nothing loads from Google’s servers. Otherwise every visitor’s IP address would go to the US. The Munich Regional Court ruled that unlawful in 2022.
Focus
Focus always shows through :focus-visible, never :focus. The ring is 2 px wide in --rand-fokus with a 2 px offset. Keyboard users see it, and nobody else does.
Checks
| Check | What it finds | Command |
|---|---|---|
| Brand QA | hex values outside the source, shadows, free radii, banned words, exclamation marks, images without alt | pnpm qa in the package |
| Token diff | values that differ from the frozen reference | pnpm tokens:diff |
| Blocks | a block in the content model without a component | pnpm test |
| Storybook | tokens, primitives, every block in de, en, tr, light and dark, phone and desktop | pnpm storybook (port 3112) |
| Visual regression | every story light and dark, phone and desktop, threshold 0.1 % | pnpm vr, new references: pnpm vr:update |
| Contrast | required pairings by WCAG in both modes | python3 tooling/check-kontrast.py |
| Language | sentence length, register, word lists | python3 tooling/check-sprache.py |
Agent design-pruefer | token violations and the definition of done for a view, read-only | in Claude Code before every „done“ |
| Chapters of this site | directives, arguments, language | node apps/brand/skripte/pruefen.ts |
The GitHub workflow design.yml runs on every change under packages/design-system. It generates the tokens, checks the reference, tests the blocks, runs brand QA and types, builds Storybook and compares the images.
A view is done only with
- empty state
- skeleton instead of spinner
- keyboard and :focus-visible
- phone and desktop
- light and dark as a token switch
- optimistic update with „Undo“
- correct register
- no hex value
- target sizes 28/44/56
On the Mac this includes full keyboard control, drag and drop too. In the app it includes Dynamic Type and VoiceOver. Conflicts are visible, but they never block.
A new component
No component exists in one app only. Whatever is built in apps/web, in the Swift app or on the website goes into the system first, or at the latest in the same commit.
components/<group>/<Name>.jsxwith every state: default, pressed, focused, disabled, empty, light and dark, child size where children use it.<Name>.d.tswith German props.<Name>.prompt.md: what it is for, which register, an example with the Sonntag family, the limits.- A
@dsCardcard in the folder and an entry in_ds_manifest.json. - The production version in the app, with the same name and the same props.
pnpm qa, a Storybook story, visual regression.
Changing an existing component means: read .prompt.md first, then the JSX, then the version in the app.
Changing a token
- Change the value in
tokens/quelle/*.json, for colours light and dark. - Run
pnpm tokens. The diff fails and shows the difference. - Record the change with date, context and reason in
docs/entscheidungen.md. - Freeze the reference in
tokens/referenz/again. - Check contrast and visual regression, then commit the generated files too.
A new role needs no decision, but a line with a reason in ergaenzungen.txt. An existing role only changes with a decision.
Git and publishing
Commits come from the account guido77-EnP, in German, with the subject as a sentence. Every git push is confirmed first. Afterwards tooling/deploy-pruefen.sh checks the builds of albert-web, albert-site and the GitHub workflow. A red build is never left standing.
Screens, stories and examples always show the Sonntag family. Real family data never goes into the repository. The seed doubles as the login for Apple’s App Review.