Skip to content
Hey AlbertBrand and design system
As MarkdownDeutschDE

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.

WhatWhereBecomes
Tokenstokens/quelle/*.json (W3C Design Tokens Community Group format)tokens/*.css, tailwind/theme.css, swift/Tokens.swift
Componentscomponents/<group>/<Name>.jsx with .d.ts and .prompt.mdthe model for apps/web/src/bausteine and for SwiftUI
Web primitivesbauteile/ui/*.tsx (shadcn on our roles)forms, dialogs and tables in HQ and on the website
Website blocksbloecke/*.tsx, one to one with packages/content-modelthe pages of hey-albert.app from Payload
Rulesregeln/regeln.jsonthis site, brand QA, agents
Fonts, icons, logosassets/every surface, served locally

The order

Every area is built in the same sequence. The foundation stands before anyone sees a screen.

  1. Schema and RLS. The table with household_id, an index and four policies, in one migration.
  2. Tests. pgTAP for all five roles: owner, adult, child, guest, service.
  3. Domain logic. Pure functions without UI, with their own tests.
  4. 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

CheckWhat it findsCommand
Brand QAhex values outside the source, shadows, free radii, banned words, exclamation marks, images without altpnpm qa in the package
Token diffvalues that differ from the frozen referencepnpm tokens:diff
Blocksa block in the content model without a componentpnpm test
Storybooktokens, primitives, every block in de, en, tr, light and dark, phone and desktoppnpm storybook (port 3112)
Visual regressionevery story light and dark, phone and desktop, threshold 0.1 %pnpm vr, new references: pnpm vr:update
Contrastrequired pairings by WCAG in both modespython3 tooling/check-kontrast.py
Languagesentence length, register, word listspython3 tooling/check-sprache.py
Agent design-pruefertoken violations and the definition of done for a view, read-onlyin Claude Code before every „done“
Chapters of this sitedirectives, arguments, languagenode 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.

  1. components/<group>/<Name>.jsx with every state: default, pressed, focused, disabled, empty, light and dark, child size where children use it.
  2. <Name>.d.ts with German props.
  3. <Name>.prompt.md: what it is for, which register, an example with the Sonntag family, the limits.
  4. A @dsCard card in the folder and an entry in _ds_manifest.json.
  5. The production version in the app, with the same name and the same props.
  6. 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

  1. Change the value in tokens/quelle/*.json, for colours light and dark.
  2. Run pnpm tokens. The diff fails and shows the difference.
  3. Record the change with date, context and reason in docs/entscheidungen.md.
  4. Freeze the reference in tokens/referenz/ again.
  5. 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.