screen-locker/docs/design-audit/DOCS-audit-flutter-workout.md
Krzysztof kuhy Rudnicki a889341979 Adopt the four markdown namespaces and gate them
Renames this repo's markdown onto README.md / CLAUDE*.md / DOCS*.md /
TODO*.md so a future session can tell a live task from a finished
record without reading the file to find out.

TODO files carry the literal "REMOVE ME AFTER FINISH" and are deleted
when the work lands. Enforced by the md-naming pre-commit hook and CI
workflow, both delegating to the shared gate in kuhyx/utils so this
repo's idea of a valid name cannot drift from every other repo's.

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

8.8 KiB
Raw Blame History

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 ElevatedButtons 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) — Cards 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.