utils/unified-design-system/README.md
Krzysztof kuhy Rudnicki a50d27688f Consolidate unified-design-system docs into this monorepo
Moves the design-system rules, frozen token table, and annotated
component references out of ~/.claude/skills/ and the two Claude
Artifacts into a proper subdir here, matching the existing
gatelock/crdt-sync/guard-lib pattern of one-subdir-per-shared-library.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 21:23:47 +02:00

159 lines
7.2 KiB
Markdown

# unified-design-system
The concrete, frozen design tokens (colors, spacing, type, radius, shadow
policy) that implement `safe-design-rules`' 28 abstract rules identically
across every one of kuhy's repos — Flutter, web/React, and Python/Tkinter.
One palette everywhere (including Flutter launcher-icon glyphs), not just
shared rules. Use whenever building or fixing UI in any of kuhy's repos —
this is what a design audit's violations should be fixed *against*, and what
new UI should be built from instead of inventing new values. Full frozen
value table in [`tokens.md`](tokens.md); annotated component references in
[`components.html`](components.html) (9 components: Button/Card/Input/Chip/
Dialog/Switch/Divider/Checkbox/SnackBar) and [`button.html`](button.html)
(the earlier single-component exemplar that set the doc format).
Read `tokens.md` for the full frozen value table before touching any
theme/CSS/style file.
## Why one palette, not per-app branding
Apps are visually unified down to the exact hex values, not just the same
rules with different accents. This also means the Flutter launcher-icon
family (the `app-icon` skill/generator) uses the same accent for every app's
glyph — apps are distinguished by glyph shape, not color.
## Per-stack implementation pattern
### Flutter
Build `ThemeData` from **explicit** `ColorScheme.dark(...)`/`.light(...)`
using the exact hex values in `tokens.md` — not `ColorScheme.fromSeed`, which
algorithmically derives tones and won't reproduce hand-picked values. Add:
- A `ThemeExtension` for roles M3 doesn't have (success/warning — `error` is
already `danger`), e.g. `AppStatusColors(success:, warning:)`. Only add
this if the app actually has success/warning status indicators — grep for
existing `Colors.green`/`Colors.orange`-style usage first; don't add it
speculatively (it becomes untested dead code otherwise).
- `AppSpacing` / `AppRadius` / `AppTextSize` static const classes mirroring
the scales in `tokens.md`.
- Wire every screen through `Theme.of(context)` — the dominant violation
across every Flutter audit was raw `Colors.*` literals bypassing an
existing (or newly added) theme. Grep for `Colors\.` / `Color(0x` after
wiring the theme and route every hit through a theme role.
```dart
ThemeData buildTheme() => ThemeData(
useMaterial3: true,
colorScheme: const ColorScheme.dark(
surface: Color(0xFF211D1B), // ink
surfaceContainerHighest: Color(0xFF38312E), // ink-raised-2
onSurface: Color(0xFFECEAE9), // text-on-dark
onSurfaceVariant: Color(0xFFAAA09A), // muted-on-dark
outline: Color(0xFF463E3A), // line-dark
primary: Color(0xFFB8862E), // accent
onPrimary: Color(0xFF211D1B), // on-fill — NOT text-on-dark, see below
error: Color(0xFFE2585F), // danger
onError: Color(0xFF211D1B), // on-fill
),
extensions: const [AppStatusColors(
success: Color(0xFF8A9A3C),
onSuccess: Color(0xFF211D1B), // on-fill
warning: Color(0xFFE0A63C),
onWarning: Color(0xFF211D1B), // on-fill
)],
);
```
`onPrimary`/`onError`/`onSuccess`/`onWarning` are all `on-fill` (`#211D1B`,
dark), never `onSurface`'s near-white — see `tokens.md`'s `on-fill` entry:
every filled semantic color under-contrasts with near-white text.
### Web (CSS custom properties)
One `:root` block (plus a light/dark override block as needed) with these
variable names — kept identical across repos so the vocabulary transfers:
```css
:root {
--bg: #211d1b; --surface-1: #2b2624; --surface-2: #38312e;
--border: #463e3a; --text: #eceae9; --text-muted: #aaa09a;
--accent: #b8862e; --success: #8a9a3c; --warning: #e0a63c; --danger: #e2585f;
--on-fill: #211d1b; /* text/icons ON a filled accent/success/warning/danger surface */
--on-scrim: #eceae9; /* text/icons on a dark translucent overlay/backdrop — fixed, not theme-tied */
--radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px;
--space-1: 4px; --space-2: 8px; --space-3: 16px; --space-4: 24px;
--space-5: 32px; --space-6: 48px;
--font-sans: -apple-system, "Segoe UI", Roboto, system-ui, sans-serif;
--font-mono: ui-monospace, "Cascadia Code", monospace;
}
@media (prefers-color-scheme: light) {
:root {
--bg: #f6f4f3; --surface-1: #fcfbfb; --surface-2: #fcfbfb;
--border: #e0dad7; --text: #211d1b; --text-muted: #70625b;
/* --on-fill / --on-scrim stay fixed — the fills and the scrim don't change with page theme */
}
}
body { font-size: 16px; font-family: var(--font-sans); }
button.primary { background: var(--accent); color: var(--on-fill); }
```
Don't reach for a single "on-accent" variable for both cases (an earlier
dufs-cloud draft did, and it broke the moment the accent went from blue to
gold): a filled button needs `--on-fill` (dark), a caption over a photo-tile
scrim needs `--on-scrim` (near-white) — same-looking near-white token, wrong
one silently fails contrast on fills.
Every hardcoded color/spacing/radius/shadow value in component CSS should
route through one of these variables. No `#fff`/`#000`/`rgba(0,0,0,…)`
literals — use `var(--text)`/`var(--bg)` or an `ink`-tinted rgba.
### Python/Tkinter
No `ttk.Style()` in any of these apps currently — introduce (or, for the
screen-locker/wake_alarm/diet_guard family, extend) a shared constants
module/dataclass with the same token names, and have every `tk.Label`/
`tk.Button`/`tk.Frame` read from it instead of inline hex literals:
```python
@dataclass(frozen=True)
class Tokens:
bg: str = "#211D1B"
fg: str = "#ECEAE9"
muted: str = "#AAA09A"
field_bg: str = "#2B2624"
accent: str = "#B8862E"
success: str = "#8A9A3C"
warning: str = "#E0A63C"
danger: str = "#E2585F"
on_fill: str = "#211D1B" # text on an accent/success/warning/danger bg — NOT fg
font_family: str = "Arial"
```
A shared `_button(bg=...)` helper that always sets `fg=self._colors.fg` is
the exact bug this caught: `fg` (near-white) is correct when `bg` is
`field_bg`/`bg` (a dark neutral surface), but any button whose `bg` is
`accent`/`success`/`warning`/`danger` needs `fg=self._colors.on_fill`
instead — pick `fg` from `bg`, don't hardcode it per call site.
For screen-locker/wake_alarm/diet_guard specifically: this lives on
`gatelock.LockConfig` (the shared token source already vendored in this same
`~/utils` monorepo, see [`../gatelock/`](../gatelock/)) rather than a
per-repo duplicate, since all three already depend on it. Extending
`LockConfig` requires production verification
(`/usr/bin/python3 -c "import gatelock"`) since it backs 3 live systemd
services — a dev-venv-only check is not sufficient.
## Do NOT
- Don't use `ColorScheme.fromSeed` for the unified palette — it can't
reproduce hand-picked hex values exactly.
- Don't add shadows to dark surfaces, or mix shadow + border depth on the
same element (see `tokens.md`'s shadow policy).
- Don't invent a new spacing/radius/font value outside the scales in
`tokens.md` — round to the nearest step instead.
- Don't add a `ThemeExtension`/status-color role speculatively — check the
app actually uses that status (success/warning/etc.) before adding it.
- Don't touch `../gatelock` without the production-path verification above;
a broken import there takes down 3 services at once.