mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 20:07:18 +01:00
Some checks are pending
crdt_sync_dart tests / test (push) Waiting to run
crdt_sync_flutter tests / test (push) Waiting to run
crdt-sync pre-commit / pre-commit (push) Waiting to run
crdt-sync tests / test (push) Waiting to run
dependency freshness / dependencies on newest stable (push) Waiting to run
design_system tests / test (push) Waiting to run
file length / file-length (push) Waiting to run
freedays pre-commit / pre-commit (push) Waiting to run
freedays tests / test (push) Waiting to run
gatelock pre-commit / pre-commit (push) Waiting to run
gatelock tests / test (push) Waiting to run
github_device_auth tests / test (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
music_theory tests / test (push) Waiting to run
palette drift / palette (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
sync_settings_ui tests / test (push) Waiting to run
web_ui tests / test (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwtowrowDXx6ccmWVPkCaF
173 lines
8.0 KiB
Markdown
173 lines
8.0 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 [`DOCS-tokens.md`](DOCS-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 `DOCS-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 `DOCS-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 `DOCS-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 `DOCS-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
|
|
`~/src/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.
|
|
|
|
## Pointer-free + small-screen
|
|
|
|
The rules are in [`DOCS-operability.md`](DOCS-operability.md); the per-stack code that
|
|
satisfies them — Flutter, web and Python/Tkinter — is in
|
|
[`DOCS-operability-patterns.md`](DOCS-operability-patterns.md).
|
|
|
|
## Do NOT
|
|
|
|
- Don't use `ColorScheme.fromSeed` for the unified palette — it can't
|
|
reproduce hand-picked hex values exactly.
|
|
- Don't pass a positive font size to Tk — that's points, ~37% oversized.
|
|
- Don't make long-press, hover, or drag the only route to an action.
|
|
- Don't set `highlightthickness=0` on anything focusable, or set
|
|
`highlightbackground` when you meant `highlightcolor`.
|
|
- Don't centre-anchor content that can overflow — it clips symmetrically, taking
|
|
the header and the submit button with it.
|
|
- Don't tune a layout against a 1080p screen, and don't test responsiveness only
|
|
at phone-portrait sizes; neither exercises 1366x768 or 1024x600.
|
|
- Don't add shadows to dark surfaces, or mix shadow + border depth on the
|
|
same element (see `DOCS-tokens.md`'s shadow policy).
|
|
- Don't invent a new spacing/radius/font value outside the scales in
|
|
`DOCS-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.
|