diet-guard/DESIGN_AUDIT_TODO.md
Krzysztof kuhy Rudnicki 3bb21c00d6
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Add a design-rules audit against the new safe-design-rules checklist
Report-only findings for a future cleanup pass; no code changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 15:25:20 +02:00

152 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Design audit — diet-guard
Generated against safe-design-rules (anthonyhobday.com/sideprojects/saferules).
Report only — nothing in this repo was changed by the audit itself.
This repo has two UI surfaces, audited separately below.
---
## Flutter app (`app/`)
Theme entry point: `app/lib/main.dart:52` —
`ThemeData(colorSchemeSeed: Colors.teal, useMaterial3: true)`. This is the
**entire** theme definition: no `darkTheme`, no `textTheme` override, no
component themes (`elevatedButtonTheme`, `cardTheme`, etc.), no
`ThemeExtension`. Everything downstream of this one line either inherits
Material 3's generated defaults or bypasses the theme with raw `Colors.*`
literals in individual widgets — sampled across `screens/` and `widgets/`
(11 of 21 files).
### Violations
- **Rule 1 (near-black/near-white)** — `app/lib/widgets/day_status_calendar.dart:79` uses `Colors.black` for the "not logged" cell fill, and `:87`/`:121` use pure `Colors.white` for text — both bypass the muted near-black/near-white the seeded `ColorScheme` would otherwise supply.
- **Rule 2 (saturate neutrals)** — `app/lib/widgets/day_status_calendar.dart:78,104` (`Colors.grey.shade800`) and `app/lib/widgets/slot_selector_row.dart:49` (`Colors.grey`) use Material's generic, zero-chroma grey instead of a grey tinted toward the teal seed.
- **Rule 4 (everything deliberate)** — no centralized token beyond the one-line `ThemeData` call; `day_status_calendar.dart` (8 raw `Colors.*` refs), `slot_selector_row.dart` (4 raw refs, lines 46-49), and `settings_screen.dart:567` each invent status/error colors independently instead of reading `Theme.of(context).colorScheme`.
- **Rule 4 (inconsistency, concrete pair)** — the same semantic "error" state is styled two different ways in the same app: `history_screen.dart:603` uses `Theme.of(context).colorScheme.error`, but `settings_screen.dart:567` hardcodes `Colors.red` for the same purpose.
- **Rule 9 (distinct brightness values)** — `day_status_calendar.dart:80-82` mixes `Colors.red.shade400` (lighter) with `Colors.amber.shade700` / `Colors.green.shade700` (darker) for three parallel status swatches meant to read as one family — inconsistent shade selection, not a deliberate brightness ramp.
- **Rule 11 (mathematically related measurements)** — corner radii are inconsistent across near-identical use cases: `BorderRadius.circular(8)` in `day_status_calendar.dart:105` and `photo_attach_field.dart:95` vs `BorderRadius.circular(6)` in `history_screen.dart:638` for the same "thumbnail clip" purpose, with no shared base unit.
- **Rule 20 (body text ≥16px)** — `day_status_calendar.dart:123` (`fontSize: 14`), `:146` (`fontSize: 11`), `:181` (`fontSize: 12`) are all below the 16px minimum, with no larger default applied elsewhere to compensate for this widget.
- **Rule 28 (icon contrast lower than text)** — `slot_selector_row.dart:54` (`Icon(..., color: color)`) and `:57` (`labelStyle: TextStyle(color: color)`) give the check icon and its adjacent label text the *same* full-strength color — the icon should read lighter/lower-contrast than the text, not equal.
### Passes worth recording
- **Rule 3** — clear hierarchy exists elsewhere: `history_screen.dart:603` correctly pulls the delete/error action from `colorScheme.error`.
- **Rule 18 (container brightness limits)** — only one nested-container case was found (`day_status_calendar.dart`'s `grey.shade800` panel on the default light scaffold); no evidence of a violation.
- **Rule 23 (two typefaces max)** — no `fontFamily` or `google_fonts` import anywhere in `app/lib/` — the whole app uses exactly one (system default) typeface.
- **Rule 26 / 27 (shadows/depth)** — no `BoxShadow`, `elevation:`, or `Card(` usage found in the sampled files; zero depth technique used, so it's trivially consistent (though see Notes — this also means rule 15/16/17 have nothing to evaluate).
- **Rule 28 (partial pass)** — `day_status_calendar.dart:113,127` (`Colors.white70` chevron icons) sit next to `:121` (`Colors.white` full-opacity month label) — here the icon *is* correctly lower-contrast than the text.
### Not applicable
- Rule 5 (optical alignment) — no asymmetric icon/shape widgets found requiring hand-tuned centering.
- Rule 6 (letter-spacing/line-height by size) — no manual `letterSpacing`/`height` found anywhere in `screens/`/`widgets/`; the app relies entirely on Material 3's built-in type scale, which already encodes this rule.
- Rule 7 (container border contrast) — the one border found (`day_status_calendar.dart:172-174`, `Colors.white70` on `Colors.black`) has adequate contrast against both surfaces; no counter-evidence.
- Rule 8 (everything aligns) — default `MainAxisAlignment.spaceBetween`/`spaceAround` usage throughout; no counter-evidence gathered.
- Rule 10 (warm or cool, not both) — the app doesn't saturate its neutrals at all (see Rule 2 violation above), so warm/cool consistency has nothing to check.
- Rule 12 (order by visual weight) — no clear violation or pass identified from the sampled files; would need a full-screen layout review.
- Rule 13 (12-column grid) — mobile-first single-column screens; no grid system in use.
- Rule 14 (space between high-contrast points) — insufficient evidence gathered from the sample.
- Rule 15 (closer = lighter) — no elevation/z-stacking found to evaluate (see Rule 26/27 pass note).
- Rule 16 (shadow blur ≈ 2× distance) — no `BoxShadow` in the codebase.
- Rule 17 (simple on complex) — flat design throughout; no complex background/foreground pairing to break the rule.
- Rule 19 (outer padding ≥ inner padding) — holds everywhere checked, e.g. `day_status_calendar.dart:102` (`EdgeInsets.all(12)` outer) vs `:168` (`EdgeInsets.symmetric(vertical: 2)` cell margin).
- Rule 21 (line length ~70 chars) — no long-form body text in the sampled screens (mostly labels/numbers).
- Rule 22 (button padding h=2×v) — no explicit `ButtonStyle.padding` override found anywhere; buttons run on Material 3 defaults, which already approximate this ratio.
- Rule 24 (nest corners) — no nested corner-radius container pairs found to check a concentric relationship.
- Rule 25 (avoid adjacent hard divides) — insufficient evidence gathered from the sample.
### Notes
- The theme is effectively unstyled beyond a seed color. Every rule above that reads "no evidence"/"insufficient evidence" is really a symptom of the same root cause: there is no `theme.dart`, no `ThemeExtension` for semantic colors (success/warn/danger), and no component themes, so status colors (green/red/amber/grey) get re-invented per-widget instead of being defined once. Centralizing those into a `ThemeExtension` (e.g. `DietGuardStatusColors`) would fix the Rule 4/9/28 violations above in one pass and make Rule 15/16/17/18/26/27 actually checkable against a real design system instead of "nothing to evaluate."
- `day_status_calendar.dart`'s status colors (`red.shade400`/`amber.shade700`/`green.shade700`) do not literally match the Python gatelock's calendar palette (`#ff5555`/`#e0c33c`/`#33cc66` — see Python section below) even though the widget's own doc comment (`day_status_calendar.dart:9`) says it "mirrors" that view. Worth reconciling into one shared value if visual parity between the phone app and the PC lock screen actually matters.
---
## Python/Tkinter gatelock UI (`diet_guard/`, not `app/`)
Files audited: `_gatelock_ui.py` (474 lines), `_gatelock_mealflow.py` (349),
`_gatelock_calendar.py` (466), `_gatelock_core.py` (196),
`_gatelock_nutrition.py` (230), `_gatelock.py` (211), `_gatelock_support.py`
(82), `_calendar_view.py` (151).
**Correction to prior research going in:** there *is* a color-constants block
(`_gatelock_ui.py:27-32`: `BG`, `FG`, `_ACCENT`, `ERR`, `_FIELD_BG`,
`_MUTED`), so "no color/theme constants module" is not quite right — the
real problem is worse than absence: it's **triplication**. `_gatelock_ui.py`
is the closest thing to a canonical module, but `_gatelock_calendar.py:58-60`
and `_calendar_view.py:25-33` each **redefine their own copies** of the same
values instead of importing them (see Violations below). `_gatelock_calendar.py:55-57`
even has a comment justifying this: *"BG/FG/ERR come from `_gatelock_ui`'s
public exports; the rest are private to that module, so equivalents are
defined locally rather than reaching across the boundary."* That's a
structural decision (avoid touching another module's private names) that
produces exactly the drift risk a shared module is supposed to prevent.
Confirmed **no `ttk.Style()` call anywhere** in the 8 files (0 grep hits).
Hex-literal count is also off from the prior estimate: **25 occurrences of
12 distinct hex values** (not ~4) across the 3 files that define palette
constants. Inline `font=` literal count is confirmed accurate: **35
occurrences**.
### The shared `LockConfig.bg` token
`~/utils/gatelock/gatelock/_window.py:71` — `LockConfig.bg: str = "#1a1a1a"`
is the one real shared design token in this family (used by `screen-locker`,
`wake_alarm`, and `diet_guard` alike via `GateRoot`/`LockWindow`).
- `diet_guard/_gatelock_ui.py:27` — `BG = "#1a1a1a"` **re-hardcodes the same
literal instead of importing `gatelock.LockConfig.bg`**. It happens to
match today, but nothing keeps it in sync — if `LockConfig`'s default ever
changes, this constant silently goes stale.
- `diet_guard/_gatelock.py:147` — `LockConfig(mode=..., bg=BG)` then passes
that duplicated literal back into `LockConfig` explicitly. This call is
redundant today (it matches `LockConfig`'s own default) and masks the
duplication: it *looks* like `diet_guard` is deliberately overriding the
background, when it's actually just echoing the default via a second,
unlinked constant.
- Fix direction (report-only, not applied): `_gatelock_ui.py` should import
`LockConfig.bg` (or a `LockConfig()` default instance's `.bg`) rather than
defining its own `BG` literal, and `_gatelock.py:147` should drop the
explicit `bg=BG` kwarg entirely once it's provably redundant.
### Violations
- **Rule 1 (near-black/near-white)** — `_calendar_view.py:25` `_NOT_LOGGED_FILL = "#000000"` is pure black; `_gatelock_ui.py:434` and `:459` (`fg="white"`) are pure white. This is inconsistent with the rest of the palette, which correctly uses `#1a1a1a`/`#e0e0e0` (near-black/near-white, not pure).
- **Rule 2 (saturate neutrals)** — every neutral in the shared palette is a zero-chroma grey (equal R/G/B hex pairs): `BG "#1a1a1a"`, `_FIELD_BG "#2a2a2a"`, `_MUTED "#9a9a9a"` (`_gatelock_ui.py:27,31,32`), plus the duplicate `LockConfig.bg = "#1a1a1a"` (`gatelock/_window.py:71`). None carry any tint — exactly the "generic gray" this rule warns against.
- **Rule 4 (everything deliberate / centralization)** — `_MUTED = "#9a9a9a"` is defined **three times** verbatim: `_gatelock_ui.py:32`, `_gatelock_calendar.py:58`, `_calendar_view.py:33`. `"#003322"` (status text color) is likewise defined 3×: `_gatelock_ui.py:191,421`, `_gatelock_calendar.py:154`, `_calendar_view.py:32`. `_FIELD_BG "#2a2a2a"` and `_ACCENT "#00ff88"`/`"#00cc66"` are each duplicated across `_gatelock_ui.py` and `_gatelock_calendar.py`. No `ttk.Style()` centralizes any of this.
- **Rule 11 (mathematically related measurements)** — font sizes in use: 10, 11, 12, 13, 14, 15, 16, 22, 30 (`_gatelock_ui.py` and `_gatelock_calendar.py`, e.g. `_gatelock_ui.py:130,145,161,285,371,378`) — no common ratio or base unit. Spacing is similarly mixed: `ipady=3` (`_gatelock_ui.py:147,216`) vs `ipady=2` (`:256,330`; `_gatelock_calendar.py:148`) used interchangeably for the same "entry field" role.
- **Rule 20 (body text ≥16px)** — the dominant font size is 12pt (9 occurrences in `_gatelock_ui.py` alone: lines 161,185,209,225,249,260,334,343,... ) with many labels at 11-13pt; only headline-style text (22, 30) clears a 16px-equivalent minimum.
### Passes worth recording
- **Rule 3 (high contrast for important elements)** — `_gatelock_ui.py:416-425` ("Log & Continue", the primary action) uses bright `_ACCENT #00ff88`; `:429-438` ("Fetch from sync", secondary) uses a muted `#334455`/`#445566` pair. Clear, deliberate hierarchy.
- **Rule 9 (distinct brightness values)** — `_calendar_view.py:28-30`: `GREEN #33cc66` / `YELLOW #e0c33c` / `RED #ff5555` are visually distinct hues at roughly matched brightness — better-executed than the Flutter app's equivalent (see Flutter Rule 9 violation above).
- **Rule 18 (container brightness limits)** — `BG "#1a1a1a"` (~10% luma) vs `_FIELD_BG "#2a2a2a"` (~16% luma), a ~6% delta — comfortably within the ~12% dark-interface guideline (`_gatelock_ui.py:27,31`).
- **Rule 19 (outer padding ≥ inner padding)** — outer frame padding (e.g. `padx=6` at `_gatelock_ui.py:144`) is consistently ≥ field-level `ipady=2-3`.
- **Rule 23 (two typefaces max)** — exactly two families used: "Arial" (34 occurrences) and "Courier" (1 occurrence, `_gatelock_ui.py:292`).
- **Rule 26 / 27 (no shadows / single depth technique)** — `relief=` (Tk's raised/sunken/groove bevel, its closest thing to a shadow) is never used in any of the 8 files (0 grep hits); zero depth technique applied, trivially consistent.
### Not applicable
- Rule 5 (optical alignment) — no asymmetric icon/shape widgets in this Tk UI.
- Rule 6 (letter-spacing/line-height by size) — Tk's `font=` tuples have no letter-spacing control and no independent line-height axis; not expressible in this toolkit.
- Rule 7 (container border contrast) — `highlightthickness` varies 0/1/2 (`_gatelock_ui.py:174,193,229`; `_gatelock_calendar.py:235`) but the one case with a matched-to-background `highlightbackground` (`_calendar_view.py:108-115`, future-day cells) is intentional per its own docstring (`day_status_calendar.dart:9`-equivalent design: blend future days into the background), not a rule violation.
- Rule 8 (everything aligns) — grid/pack-based layout; no counter-evidence gathered.
- Rule 10 (warm or cool, not both) — the neutrals aren't saturated at all (Rule 2 violation above), so there's no warm/cool choice to be consistent about.
- Rule 12 (order by visual weight) — not assessed from the sampled files.
- Rule 13 (12-column grid) — fixed-size fullscreen dialog, not a column-grid layout.
- Rule 14 (space between high-contrast points) — insufficient evidence gathered.
- Rule 15 (closer = lighter) — Tk has no z-stacking/elevation concept; nothing to evaluate.
- Rule 16 (shadow blur ≈ 2× distance) — Tk has no drop-shadow primitive; unused.
- Rule 17 (simple on complex) — flat single-color backgrounds throughout; no complex imagery to conflict with.
- Rule 21 (line length ~70 chars) — `wraplength=900` (pixels, e.g. `_gatelock_ui.py:410,446`) is a pixel wrap, not a character-count guarantee; can't verify without rendering.
- Rule 22 (button padding h=2×v) — no `tk.Button` in the sampled files sets explicit `ipadx`/`ipady`; only `Entry` widgets do (grep-confirmed). Buttons run on Tk's own default padding, unexamined by this rule.
- Rule 24 (nest corners) — Tk widgets here are flat-relief rectangles; no corner-radius concept applies.
- Rule 25 (avoid adjacent hard divides) — insufficient evidence gathered.
- Rule 28 (icon contrast lower than text) — this UI uses unicode glyphs embedded in button `text=` (e.g. `"⟳ Fetch from sync"` at `_gatelock_ui.py:431`, `"✕ Close Demo"` at `:456`) rather than a separately-styled icon widget; there's no independent icon-vs-text contrast axis to check.
### Notes
- The real fix for the Python surface's Rule 4 violations is structural: promote `_gatelock_ui.py`'s 6 constants (plus `_calendar_view.py`'s status-color dict) into one importable module that every gatelock file reads from, and have that module import `gatelock.LockConfig`'s `bg` default rather than re-literal-ing `"#1a1a1a"`. That single change collapses 3 duplicate `_MUTED`/`"#003322"` definitions and closes the `LockConfig.bg` drift risk in one pass.
- Because `LockConfig.bg = "#1a1a1a"` is shared by `screen-locker` and `wake_alarm` too, if those repos independently re-hardcode `"#1a1a1a"` the same way `diet_guard` does, this is a family-wide pattern, not a `diet_guard`-only one — worth a follow-up grep across those repos if/when this gets fixed.