# The design system

Source: https://heyparlour.app/docs/design

> One palette, one type ramp, one set of states and one icon, shared by all four interfaces.

Parlour has four interfaces: this site, the menu bar app, the phone page the
CLI serves, and the iOS app. They use three languages and three toolchains,
and none of them declares a colour of its own.

`packages/design` holds one file of tokens and a few emitters that write it
into every surface. The generated files are checked in, so a checkout builds
without running the emitter first. A test fails when any of them has drifted,
so `pnpm check` catches a token change that was not emitted.

```sh
pnpm exec nx run design:emit      # after editing packages/design/src/tokens.ts
pnpm exec nx run design:icons     # after changing the mark, or a colour it uses
pnpm exec nx run design:test      # contrast, and whether anything is stale
```

| Generated file | Read by |
| --- | --- |
| `apps/site/app/tokens.css` | This site |
| `apps/desktop/src/tokens.css` | The menu bar app, which maps it onto shadcn's token names |
| `packages/parlour/src/server/web/tokens.css` | The phone page |
| `apps/ios/Parlour/DesignSystem/Tokens.swift` | The iOS app |
| `apps/ios/.../AccentColor.colorset/Contents.json` | iOS controls the app never styles, in hearth green |
| `apps/ios/.../AppIcon.appiconset/Contents.json` | The iOS app icon set |
| `apps/site/app/icon.svg`, `packages/parlour/src/server/web/icon.svg` | The favicon |

## Ink on a limewashed wall, with one lit thing

Paper, ink, bracken and a hairline rule carry the whole interface. Only two
colours are allowed to mean something, and nothing else is colourful. That is
what makes those two readable at a glance from across a room.

| Token | What it means |
| --- | --- |
| `paper`, `surface` | The wall, and a panel lifted off it by a shade rather than a shadow |
| `ink`, `bracken` | What is said, and what is said quietly |
| `rule`, `inset` | A hairline, and anything recessed into the wall |
| `hearth` | The house is alive: running, listening, speaking, the primary action |
| `lamp` | The one lit thing: the wake word, the page you are on, thinking, a warning |
| `alarm` | Something failed and a person needs to act |

Amber on a pale wall cannot carry text. So `lamp` is only ever a dot or a halo,
and `lampText` is the darker value used for words. Every token that carries
text clears 4.5:1 against its paper in both schemes, and a test checks it.

## The states are part of the design system

Every surface shows the same five states, so they live with the colours rather
than in each app. Each state has a colour, a cadence and a word.

| State | Drawn as | One breath |
| --- | --- | --- |
| `stopped` | An outline in bracken | still |
| `idle` | A steady hearth mark, labelled "ready" | still |
| `listening` | A lit hearth mark | 1100 ms |
| `thinking` | A lit lamp mark | 700 ms |
| `speaking` | A lit hearth mark | 500 ms |

The cadence quickens as the turn nears its answer, so you can tell what the
house is doing without reading a word. The menu bar dot, the phone page's talk
button and the iOS state mark breathe in step, because they read the same
numbers from the same file. Under Reduce Motion the iOS mark stops breathing
and the colour carries the state alone.

## The icons

The house mark (a roof, the walls, a dado rail and one lit lamp in the gable)
is drawn once in `src/icon.ts` from the palette, in four shapes: the favicon
tile, a full bleed square for iOS and the phone page's home screen, the macOS
tile on Apple's grid, and a bare template for the menu bar. `design:icons`
rasterises them into every bitmap a platform wants: the site's favicon, the
phone page's home screen icons, the iOS app icon, and the menu bar app's
`icon.png` and `tray.png`. The PNGs are binary, so the drift test cannot
check them. Regenerate them and look.

## Changing something

Edit `packages/design/src/tokens.ts`, run the emitter, and commit the generated
files with your change. A new surface gets a `Target` in `src/emit.ts` and a
line in the list in `emit.test.ts`. If a surface needs a format none of the
emitters produce, add another emitter next to `css.ts` and `swift.ts` instead
of keeping a hand-maintained copy of the values.
