mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-11 12:47:44 +01:00
tokens.md 260 -> 219 + operability.md 69
nielsen-audit.md 258 -> 154 + shipped-record.md 122
README.md 257 -> 172 + operability-patterns.md 95
Worked the requested order rather than cutting straight to a split.
(a) Automate: the categorical ramp's four constraints were restated in prose
with the measured margins hand-copied (ΔE 34.6/26.0/25.6/27.4, etc).
ramp_check.py prints every one of those numbers and fails if any
regresses, so the prose now points at the command instead of duplicating
its output. The two consequences the numbers do not state — why the ramp
is mid-toned, and why lightness falls monotonically — stay in prose.
(b) Dedup: measured, not assumed. Zero paragraphs are shared between the
three files; README already delegates the rules to tokens.md and shows
only per-stack code. Nothing to remove.
(c/d) Split on meaning: tokens.md is "frozen values", so the behavioural
requirements that *use* those tokens moved to operability.md, with the
per-stack code for them in operability-patterns.md. nielsen-audit.md is a
live grid, so completed phases moved to shipped-record.md — following the
existing phase3-record.md convention, not a new one.
The focus-ring token row had to stay in tokens.md: palette_check.py parses
that file, and moving the row made the check fail with "MISSING md:focus-ring"
— a token it cannot see is a token nothing compares across the four stacks.
The rationale lives in operability.md and the row links to it.
palette_check and ramp_check both at 0 failing checks; every relative link
between the docs resolves.
`bash scripts/check_file_length.sh --all` now exits 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
173 lines
7.9 KiB
Markdown
173 lines
7.9 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.
|
|
|
|
## Pointer-free + small-screen
|
|
|
|
The rules are in [`operability.md`](operability.md); the per-stack code that
|
|
satisfies them — Flutter, web and Python/Tkinter — is in
|
|
[`operability-patterns.md`](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 `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.
|