screen-locker/docs/design-audit/flutter-workout-app.md
Krzysztof kuhy Rudnicki 40a2f28734 Split DESIGN_AUDIT_TODO.md into one file per audited surface
341 lines -> a 19-line index plus three topic files under docs/design-audit/:
the Python/Tkinter lock UI, the Flutter workout app, and the shared gatelock
library. Findings are unchanged -- same 6 entries, same three section headers,
verified by counting both before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 21:27:19 +02:00

147 lines
8.8 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.

## Flutter app (stronglift_replacement/workout_app/)
Design-token entry point: `lib/main.dart:68-74` declares
`ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo,
brightness: Brightness.dark), useMaterial3: true)` — a real Material 3 seed
theme exists. However, **`Theme.of(context)` / `colorScheme` is referenced
zero times anywhere in `lib/screens/` or `lib/widgets/`** (confirmed via
repo-wide grep). The theme is declared but never consumed — every one of the
24 .dart files under `lib/` hardcodes colors directly instead.
- `Colors.*` literals: **~210 occurrences** across `screens/` and
`widgets/`, 27 distinct named colors. Most common: `Colors.white` (98×),
`Colors.grey.shade800` (18×), `Colors.redAccent` (12×),
`Colors.greenAccent` (11×), `Colors.grey.shade900` (10×).
- Raw hex `Color(0xFF...)` literals bypassing `Colors.*` entirely:
`manual_workout_screen.dart:173,201` (`Color(0xFFFF4444)`) and
`manual_workout_screen.dart:238,246` (`Color(0xFF88CCFF)`) — these are the
**exact same hex values** independently hardcoded on the Python/Tkinter
side (`#ff4444` and `#88ccff`, see e.g. `_sick_dialog.py:70`,
`_manual_workout_dialog.py:58`), coincidentally kept in sync only because
no one has touched either yet.
- `fontSize:`/`fontWeight:` literals: 72 occurrences; `history_screen.dart`
alone uses 12 distinct font sizes (9, 11, 12, 13, 14, 15, 16, 17, 18, 20,
22, 26).
### Violations
- **Rule 4 (everything deliberate / single source of truth)** — `main.dart`
builds a `ColorScheme.fromSeed`, but it is dead code from a theming
standpoint: no screen or widget reads it. Every screen re-derives its own
ad hoc dark palette from `Colors.grey.shade900/800/700` plus assorted
accent colors, so a single rebrand (e.g. changing the seed color) would
require manually editing all 24 files instead of one.
- **Rule 1 (near-black/white, not pure)** — `Colors.white` (pure `#FFFFFF`)
used 98 times for primary text/icons, e.g. `home_screen.dart:113,122,128,
211,248,267`, `settings_screen.dart:231,278,430`,
`history_screen.dart:209,410`. `Colors.black` also appears at
`rep_circle.dart:76` (`Colors.black87`, near-pure) as text-on-white in the
neutral rep-circle state. None of these are the theme's actual
near-white/near-black — they're Flutter's pure-value constants.
- **Rule 2 (saturate your neutrals)** — `Colors.grey.shade900/800/700/400`
are Material's standard zero-saturation grays (equal R=G=B), used as the
entire background/surface system in place of `colorScheme.surface`
(which, being seeded from indigo, *would* be saturated). Bypassing the
theme produces exactly the generic-gray outcome the theme was set up to
avoid.
- **Rule 9 (distinct brightness values)** — `Colors.redAccent` (errors) and
`Colors.orangeAccent`/`Colors.orange.shade800` (pending/resume state) are
both used as status-signal colors of similar brightness in adjacent UI
(e.g. `home_screen.dart:222` orangeAccent for active-session text next to
`Colors.greenAccent` for success at line 200 — three status colors of
comparable weight competing in one card).
- **Rule 18 (container brightness limits, dark ≤~12%)** — The repeated
`Scaffold(backgroundColor: Colors.grey.shade900)` /
`AppBar(backgroundColor: Colors.grey.shade800)` pairing (Material
`shade900`=`#212121`, luma 12.9%; `shade800`=`#424242`, luma 25.9% — a
~13-point jump) appears in every screen's `Scaffold`/`AppBar`:
`home_screen.dart:104,106`, `settings_screen.dart:275,277`,
`history_screen.dart:206,208`, `workout_screen.dart:495,498`,
`manual_workout_screen.dart:154,156`. This exceeds the ~12% dark-interface
guideline every time it's used.
- **Rule 20 (body text ≥16px)** — Extensive small body text below 16px:
`fontSize: 9` at `history_screen.dart:612`, `fontSize: 11` (11 occurrences,
e.g. `home_screen.dart:311`, `history_screen.dart:333,486,612`,
`settings_screen.dart:388`), `fontSize: 12` (14 occurrences),
`fontSize: 13` (7 occurrences) — well under the 16px minimum for
supporting/secondary text throughout.
- **Rule 22 (button padding: horizontal ≈2x vertical)** — `home_screen.dart:
242` and `:261` set `padding: const EdgeInsets.symmetric(vertical: 14)`
on full-width `ElevatedButton`s with no `horizontal:` argument, which
defaults `horizontal` to `0.0` — the inverse of "horizontal ≈2x vertical".
- **Rule 26 (no shadows in dark interfaces)** — The app declares
`brightness: Brightness.dark` (`main.dart:71`), yet `rep_circle.dart:
105-111` adds an explicit `BoxShadow(color: Colors.black26, blurRadius: 4,
offset: Offset(0, 2))` to the rep-tracking circle — the one shadow in the
whole app, in a UI this rule says shouldn't have any.
### Not applicable
- Rule 5 (optical alignment) — standard Material icons/widgets are already
optically centered by the framework; no custom asymmetric glyphs.
- Rule 6 (letter-spacing/line-height by size) — the only `letterSpacing`
uses found (`home_screen.dart:311` → 1.1, `history_screen.dart:333` → 1.3,
`settings_screen.dart:388` → 1.4) are all on the same small `fontSize: 11`
eyebrow-label text, consistently given positive tracking as the rule
recommends for small text — no counter-example at larger sizes to check
against, so marked N/A rather than a forced Pass.
- Rule 7 (border contrast) — `Border.all(color: color.withValues(alpha:
0.4))` (`settings_screen.dart:491`) derives border color from the same
status color as the fill, by design (a tinted pill), not a
container-edge border this rule targets.
- Rule 8 (align with something else) — standard `Column`/`Row`/`ListView`
layouts; no cross-screen alignment grid to check.
- Rule 12 (order by visual weight) — screens are single-column flows
(cards stacked top-to-bottom); no left/right or center/edge weight
arrangement to evaluate.
- Rule 13 (12-column grid) — mobile single-column layouts throughout; no
multi-column grid system in use.
- Rule 14 (space between contrast edges) — cannot be verified statically
without rendering the widget tree.
- Rule 15 (closer elements lighter) — `Card`s at `shade800` sit on
`shade900` backgrounds (lighter-on-darker as they "lift"), which is
directionally correct, but see Rule 18 above for the magnitude issue.
- Rule 17 (simple on complex) — flat-colored cards/containers throughout;
nothing complex to conflict with.
- Rule 19 (outer padding ≥ inner padding) — spot-checked
`home_screen.dart:143` (`EdgeInsets.all(24)` outer) vs. its nested
`_WorkoutCard` internal padding (`EdgeInsets.all(16)`,
`home_screen.dart:188`) — outer ≥ inner, consistent with the rule; not
exhaustively checked across all 24 files.
- Rule 21 (line length ~70 chars) — Flutter `Text` wraps by default at
widget width; no unbounded-line risk equivalent to the Tk surface.
- Rule 23 (two typefaces max) — no custom `fontFamily` set anywhere; the
whole app uses the Material/system default typeface. Trivially compliant.
- Rule 24 (nest corners) — `BorderRadius.circular(8)` is reused verbatim
across unrelated containers (`home_screen.dart:301`,
`calendar_widget.dart:63`, `history_screen.dart:402,643,705,772`,
`settings_screen.dart:490,548,587`) but none of these visibly nest inside
another rounded container in the surrounding code, so there's no
outer/inner radius relationship to check.
- Rule 25 (avoid adjacent hard divides) — only one `Divider` found
(`exercise_tile.dart:115`), not adjacent to another border/divide.
- Rule 27 (don't mix depth techniques) — the app consistently uses flat
`Container`/`Card` fills with no shadow except the single instance flagged
under Rule 26; effectively one depth style (none) throughout, so no
mixing to flag.
- Rule 28 (lower icon contrast next to text) — icons consistently use
`Colors.white54`/`Colors.white38`/`Colors.white70` (already
lower-contrast than adjacent full-white/near-white text) rather than full
`Colors.white`, e.g. `exercise_tile.dart:150,160,167,177,248,252`,
`history_screen.dart:331,486,507` — this one is actually already followed
reasonably consistently. Pass.
### Notes
- Rule 4 is the dominant finding for this surface too: an unused
`ColorScheme.fromSeed` plus ~210 ad hoc `Colors.*`/`Color(0x...)` literals
means every other color/contrast rule (1, 2, 9, 18) is failing as a direct
consequence of the same root cause, not independently. Routing all 24
files through `Theme.of(context).colorScheme` (or a small shared
`AppColors`/`AppTextStyles` class) would be the highest-leverage single
fix.
- The coincidental hex overlap between `Color(0xFFFF4444)`/`Color(0xFF88CCFF)`
(Flutter) and `#ff4444`/`#88ccff` (Python/Tkinter) shows the two UI
surfaces are visually related by convention only — there is no shared
token file either could import from, on either language side.