Tokens come out of garage_ui/tool/export_design_tokens_test.dart, the components are CSS on top of them, and the cards cover foundations, components and whole hub and Arcs & Angles screens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
126 lines
4.7 KiB
Markdown
126 lines
4.7 KiB
Markdown
# Garage design system
|
|
|
|
The look of every Garage app — the hub, the payment portal, Arcs & Angles — as
|
|
tokens, CSS and preview cards, for designing new Garage screens.
|
|
|
|
It's generated from the Flutter package, not drawn separately:
|
|
`tokens.css` and `tokens.json` come out of
|
|
`garage_ui/tool/export_design_tokens_test.dart`, and `components.css` only
|
|
uses those tokens. Don't edit the token files by hand — change garage_ui and
|
|
rerun:
|
|
|
|
```sh
|
|
cd garage_ui && flutter test tool/export_design_tokens_test.dart
|
|
```
|
|
|
|
```
|
|
design_system/
|
|
tokens.css, tokens.json colours, density, type, shape — generated
|
|
components.css the components, built on the tokens
|
|
foundations/ colour, type, spacing, shape, density
|
|
components/ buttons, fields, selects, lists, dialogs, …
|
|
patterns/ whole screens from the hub and Arcs & Angles
|
|
```
|
|
|
|
|
|
## The one rule: let the theme speak
|
|
|
|
The colour scheme, the density and the component's variant decide every
|
|
colour, font size, weight, icon size, padding, radius and gap. Designing a
|
|
Garage screen is choosing **which component** and **which variant**. Then
|
|
leave it alone.
|
|
|
|
- No custom colours, font sizes or weights on a component or its contents.
|
|
- No icon sizes or colours — an icon takes them from where it sits.
|
|
- No hand-picked padding, gaps or radii. Use the `--gap-*` and `--radius-*`
|
|
tokens, and let components that already carry spacing keep it.
|
|
- No home-made badges, pills, warning boxes or popovers built from a box with
|
|
a fill and a border. If a component doesnt exist, that's a gap in the system,
|
|
not an invitation.
|
|
- No shadows. Everything is flat — surfaces are separated by a step in
|
|
lightness and a hairline border.
|
|
|
|
If something looks wrong, the answer is a different variant or a different
|
|
component.
|
|
|
|
|
|
## Setting up a screen
|
|
|
|
```html
|
|
<html data-scheme="carbon" data-density="product">
|
|
<link rel="stylesheet" href="components.css">
|
|
```
|
|
|
|
- **Scheme:** `carbon` (default — true black, no accent hue; what the Garage
|
|
apps run), `dark`, `light`.
|
|
- **Density:** `product` (default — web and product apps, 12px text, 33px
|
|
controls), `compact` (dense desktop tools like Arcs & Angles, 10px text, 23px
|
|
controls), `normal` (compact but roomier). Pick one per app. Don't mix.
|
|
|
|
Type is Geist, and Geist Mono for numbers and readouts. Body text is weight
|
|
300, control labels 400, primary buttons 600.
|
|
|
|
|
|
## Layout
|
|
|
|
Every Garage screen is the same shape: a `chrome` ground, `panel`s standing on
|
|
it, and `panel-gap` gutters between them.
|
|
|
|
```html
|
|
<div class="shell">
|
|
<nav class="panel sunken">…rail…</nav>
|
|
<main class="panel">…workspace…</main>
|
|
</div>
|
|
```
|
|
|
|
Headers and footers are `chrome-bar`s. Only one panel is highlighted
|
|
(`panel active`) at a time.
|
|
|
|
A hub page is a centred column inside the workspace panel: a `page-heading`,
|
|
then either a stack of `properties-section`s or `sub-heading` + `settings-list`
|
|
forms. See `patterns/`.
|
|
|
|
|
|
## Choosing components
|
|
|
|
**Buttons**
|
|
|
|
| Variant | For |
|
|
|---|---|
|
|
| `primary` | the one action on the view, or the thing that's on / chosen |
|
|
| `secondary` | a neutral filled control; the selected nav item; a button welded to a field |
|
|
| `outline` | a normal action that isn't the main one — Cancel, Retry |
|
|
| `ghost` | chrome, toolbars, nav items, row actions |
|
|
| `ghost-destructive` | remove / revoke / delete as a row action |
|
|
| `destructive` | the confirm button of a destructive dialog |
|
|
| `link`, `text` | inline in copy |
|
|
|
|
Icon-only buttons are `btn icon-only`. A nav item is ghost with a leading icon,
|
|
left-aligned, secondary when selected — never primary. Toolbar toggles are
|
|
primary when on, outline when off.
|
|
|
|
**Fields**
|
|
|
|
- Free text: the default outline `field`.
|
|
- Values inside a properties section: `field secondary`. A read-only value is a
|
|
read-only secondary field — **never bare text** in a value slot.
|
|
- Selects: secondary in a properties section, `ghost` in a settings list,
|
|
outline elsewhere.
|
|
- Numbers in an editor: secondary, centred, unit as a muted suffix.
|
|
|
|
**Lists**
|
|
|
|
- `properties-section` — a boxed card of label/value rows. Inspectors,
|
|
account pages, one card per record in a list. Controls inside are secondary.
|
|
- `settings-list` — unboxed rows ruled between entries, for forms. Controls
|
|
inside are ghost; text fields are outline.
|
|
|
|
**Subtitles** are short static descriptions of what something is. Never live
|
|
data — no counts, dates, names, emails or statuses. Facts go in rows.
|
|
|
|
**Dialogs** are `dialog` with the actions in their band: outline Cancel, then
|
|
a primary or destructive confirm. Max width is fixed; don't widen it.
|
|
|
|
**Loading** is a spinner. No skeletons. **Empty** is one muted line.
|
|
**Error** is the message, muted, and an outline Retry.
|