From 377ec1735694357ec3d94e9fce835e7be65250eb Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Mon, 2 Nov 2026 09:18:40 +0100 Subject: [PATCH] =?UTF-8?q?Design=20audit=20closed:=206px=20gaps=20?= =?UTF-8?q?=E2=86=92=208=20on=20the=204/8=20grid;=20track=20fit-to-screen?= =?UTF-8?q?=20TODO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filter-sheet tests now scroll Apply into view (tapApply): the sheet is scrollable and Apply legitimately sits below the fold on a 600px viewport. Co-Authored-By: Claude Opus 5.5 --- TODO-design-audit.md | 91 ------------------------------- TODO-fit-to-screen-layout.md | 27 +++++++++ lib/ui/note_actions_sheet.dart | 2 +- lib/ui/note_date_range_field.dart | 2 +- lib/ui/notes_filter_sheet.dart | 2 +- pubspec.yaml | 2 +- test/notes_list_filter_test.dart | 12 ++-- test/notes_list_harness.dart | 9 +++ 8 files changed, 46 insertions(+), 101 deletions(-) delete mode 100644 TODO-design-audit.md create mode 100644 TODO-fit-to-screen-layout.md diff --git a/TODO-design-audit.md b/TODO-design-audit.md deleted file mode 100644 index ef7e838..0000000 --- a/TODO-design-audit.md +++ /dev/null @@ -1,91 +0,0 @@ -# Design audit — todo - -Generated against safe-design-rules (anthonyhobday.com/sideprojects/saferules). -Report only — nothing in this repo was changed by the audit itself. - -## Flutter app (lib/) - -Theme entry point: `lib/main.dart:36-39` — a single inline `ThemeData` with -`ColorScheme.fromSeed(seedColor: Colors.deepPurple)` and `useMaterial3: true`. -No dedicated theme file, no `darkTheme`, no `textTheme`/component-theme -overrides, no custom shadows or corner radii anywhere in `lib/` (confirmed by -grepping for `BoxShadow`, `elevation`, `BorderRadius`, `Card(` — zero hits -outside framework defaults). This means almost every token rule is satisfied -*by deferring to Material 3's generated palette/type scale*, which is itself a -coherent, deliberately-designed system — that counts as Pass, not as an -absence-of-system finding (contrast the web/Tkinter case where hardcoded -per-file values have no source of truth at all). - -### Violations - -- **Rule 4 (everything deliberate) / color consistency** — `lib/ui/settings_screen.dart:410` — - `Text(_error!, style: const TextStyle(color: Colors.red))` hardcodes a raw - `Colors.red` instead of the theme's error color. Every other error-state - text in the app uses `theme.colorScheme.error` - (`lib/ui/capture_screen.dart:522`, `lib/ui/notes_list_screen.dart:375`) — - this is the one place that breaks the pattern → change to - `Theme.of(context).colorScheme.error`. -- **Rule 11 (mathematically related measurements)** — the app's spacing is - overwhelmingly on a 4px scale (4/8/12/16/20/24/96 all appear repeatedly), - but a few values break it: `SizedBox(height: 6)` at - `lib/ui/notes_list_screen.dart:411,574,646`; `SizedBox(height: 10)` at - `lib/ui/markdown_view.dart:29`; `EdgeInsets.only(top: 8, bottom: 2)` / - `EdgeInsets.only(bottom: 4)` / `EdgeInsets.only(left: 4, top: 2)` at - `lib/ui/markdown_view.dart:35,49,78` (the `2`s). Round these to the nearest - 4px step (6→8, 10→8 or 12, 2→4) for consistency. -- **Rule 21 (line length ~70 chars)** — `lib/ui/markdown_view.dart` (the - rendered-note view) and the raw-mode `TextField` in - `lib/ui/note_editor.dart:520-535` have no max-width constraint; both sit - directly in a `SingleChildScrollView`/`Expanded` that fills the available - width. Per this repo's own `CLAUDE.md`, the desktop build is a Chrome - `--app` window (potentially 4K), so body text lines run the full window - width, far past the 60-80 char readable range. Constrain text columns with - a `ConstrainedBox(maxWidth: ~640)` (or similar) centered in the available - space on wide viewports; mobile widths are already narrow enough to be fine. - -### Pass (defers to Material 3 defaults from `main.dart:36-39`, or otherwise verified) - -- **Rule 1** (near-black/near-white) — M3's generated tonal palette, not pure black/white. -- **Rule 2** (saturate neutrals) — M3's seed-derived neutrals carry the seed hue. -- **Rule 3** (high contrast for important elements) — `FilledButton`/`IconButton` get M3's on-primary contrast automatically. -- **Rule 6** (letter-spacing/line-height by size) — M3 `TextTheme` scale. -- **Rule 7** (container border contrast) — `OutlineInputBorder()` used throughout (e.g. `lib/ui/settings_screen.dart:244,252,268,278`) resolves via theme, no custom borders. -- **Rule 8** (align with something else) — screen-level body padding is a consistent `EdgeInsets.all(16)` across `lib/ui/capture_screen.dart:451`, `lib/ui/note_detail_screen.dart:67`, `lib/ui/settings_screen.dart:221`. -- **Rule 9** (distinct brightness per palette color) — M3 tonal palette assigns each role a distinct tone. -- **Rule 10** (warm OR cool, not both) — single seed color (`Colors.deepPurple`) drives the whole palette. -- **Rule 14** (space between high-contrast points) — standard Material component spacing, no bounding-box vs. visual-edge mismatches found. -- **Rule 17** (simple on complex) — all surfaces are plain `Scaffold`/`ListView` backgrounds, no background imagery/texture. -- **Rule 19** (outer padding ≥ inner padding) — e.g. `lib/ui/settings_screen.dart:318-331` (Export/Import button row: 12px gap between buttons) sits inside the screen's 16px outer padding; same pattern in `capture_screen.dart`'s priority/status row (12px gap inside 16px body padding). -- **Rule 20** (body text ≥16px) — `bodyLarge` (16px, M3 default) is used for all primary editable/read text (`lib/ui/note_editor.dart:528`, `lib/ui/markdown_view.dart:82-95`); `bodySmall` (12px) is reserved for secondary captions/timestamps only (`lib/ui/capture_screen.dart:511`, `lib/ui/settings_screen.dart:231`), which is the sanctioned M3 caption use, not body text. -- **Rule 22** (button padding horizontal = 2x vertical) — no custom `ButtonStyle` padding overrides found; `FilledButton`/`OutlinedButton` use M3 defaults, which follow this ratio. -- **Rule 23** (two typefaces max) — single default Material/Roboto family; `pubspec.yaml` declares no custom fonts, no `google_fonts` dependency. -- **Rule 25** (avoid adjacent hard divides) — the one `Divider` in the list (`lib/ui/notes_list_screen.dart:232`, `height: 1`) separates list rows only, no stacked borders; the settings-screen `Divider()` (`lib/ui/settings_screen.dart:308`) has 8-24px spacing on both sides, not adjacent to another edge. -- **Rule 27** (don't mix depth techniques) — no custom shadow/elevation overrides anywhere in `lib/`; consistent flat M3 default depth throughout. -- **Rule 28** (icon contrast vs. text) — icons use the default M3 `IconTheme` (tied to `onSurfaceVariant`-class roles), no hardcoded full-contrast icon colors placed next to text were found. - -### Not applicable - -- **Rule 5** (optical alignment) — no custom-drawn shapes/icons with off-center mass; all icons are stock `Icons.*` glyphs, whose optical centering Material already handles. -- **Rule 12** (order by visual weight) — standard `AppBar` + list/form layouts, not a composition with deliberately varied visual weights to order. -- **Rule 13** (12-column grid) — single-column mobile/narrow-desktop layouts throughout; no multi-column grid exists to check. -- **Rule 15** (closer elements lighter, light+dark) — no custom z-stacked/elevated surfaces with tuned lightness, and no `darkTheme` exists to check the dark-mode half of this rule. -- **Rule 16** (shadow blur ≈ 2x distance) — zero custom `BoxShadow` usage in the codebase (confirmed by grep); nothing to check. -- **Rule 18** (container brightness limits, light/dark) — no custom nested-container brightness steps defined; M3 default surface tones apply, and there is no dark theme to check the 12%-dark-interface half. -- **Rule 24** (nest corners properly) — no custom corner radii anywhere; all components use M3 default shapes. -- **Rule 26** (no shadows in dark interfaces) — no dark theme exists, and no shadows exist in light mode either, so this rule has nothing to trigger on. - -### Notes - -- No dedicated theme file exists (`ThemeData` is inline in `lib/main.dart`, - 32 `.dart` files total) — this is fine at the current size but worth - extracting to `lib/ui/theme.dart` if the theme grows beyond the current - 3-line `ThemeData(...)`. -- No `darkTheme` is provided. The 28 rules don't mandate one, so its absence - is not a violation — but if a dark theme is added later, re-check rules 15, - 18, and 26 (shadow/lightness/brightness-limit rules only bite once a dark - surface exists). -- The single hardcoded-color violation (`Colors.red`) is also the app's only - spot where a raw `Colors.*` constant appears outside `main.dart`'s seed — - fixing it removes the last non-theme-derived color reference in the app. - -REMOVE ME AFTER FINISH diff --git a/TODO-fit-to-screen-layout.md b/TODO-fit-to-screen-layout.md new file mode 100644 index 0000000..fb93da0 --- /dev/null +++ b/TODO-fit-to-screen-layout.md @@ -0,0 +1,27 @@ +# Fit every main screen on one phone screen + +## what +Todo's main screen is the notes list (ListView.separated). Convert it to a dense note-tile grid that fits the screen, paged by swipe. + +## where +/home/kuhy/src/todo; screens to convert: +- lib/ui/notes_list_screen.dart: Notes — ListView.separated of notes -> 2-column title tiles with priority/status color, filter chips, paged; body/meta in long-press sheet (note_actions_sheet.dart) +Exempt: note editor and capture form (free-text editor), settings, GitHub mirror and cloud-image settings (settings) + +## must +- no vertical scrolling on converted screens at 1080x2400 with status bar + nav inset +- secondary metadata/actions behind long-press or a tap sheet, not dropped +- must not: change data/sync/storage code +- optional: reuse `FitGrid` from ~/src/utils/design_system (first tag after design_system-v0.2.1); bump the app's design_system pin to that tag + +## done +A widget test per converted screen at Pixel 6a geometry (`physicalSize = Size(1080, 2400)`, `devicePixelRatio = 2.625`, `padding = FakeViewPadding(top: 132, bottom: 63)`, real nav bar + mini-player/bottom chrome pumped) asserts no vertical Scrollable has maxScrollExtent > 0 and the primary actions are hit-testable; `flutter test --reporter=compact` exits 0. + +## verify +Pixel 6a 23181JEGR08034, release build via ~/.claude/scripts/phone_deploy.sh, screenshot from phone_vd.sh per converted screen. + +## read first +- ~/.claude/skills/build-mobile-app/SKILL.md (R7) +- ~/src/tunedeck Home tab implementation (reference pattern) + +REMOVE ME AFTER FINISH diff --git a/lib/ui/note_actions_sheet.dart b/lib/ui/note_actions_sheet.dart index b2629d8..b26a67b 100644 --- a/lib/ui/note_actions_sheet.dart +++ b/lib/ui/note_actions_sheet.dart @@ -137,7 +137,7 @@ class _EnumChips extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: Theme.of(context).textTheme.labelLarge), - const SizedBox(height: 6), + const SizedBox(height: 8), Wrap( spacing: 8, children: [ diff --git a/lib/ui/note_date_range_field.dart b/lib/ui/note_date_range_field.dart index 257ebc5..2779a04 100644 --- a/lib/ui/note_date_range_field.dart +++ b/lib/ui/note_date_range_field.dart @@ -66,7 +66,7 @@ class DateRangeField extends StatelessWidget { Text(_rangeLabel(), style: theme.textTheme.bodySmall), ], ), - const SizedBox(height: 6), + const SizedBox(height: 8), Wrap( spacing: 8, children: [ diff --git a/lib/ui/notes_filter_sheet.dart b/lib/ui/notes_filter_sheet.dart index 79c970c..10b8dae 100644 --- a/lib/ui/notes_filter_sheet.dart +++ b/lib/ui/notes_filter_sheet.dart @@ -182,7 +182,7 @@ class _MultiChips extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: Theme.of(context).textTheme.labelLarge), - const SizedBox(height: 6), + const SizedBox(height: 8), Wrap( spacing: 8, children: [ diff --git a/pubspec.yaml b/pubspec.yaml index 4088a2a..4f2075a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.48+1 +version: 1.0.49+1 environment: sdk: ^3.12.2 diff --git a/test/notes_list_filter_test.dart b/test/notes_list_filter_test.dart index 2d11e44..d567efa 100644 --- a/test/notes_list_filter_test.dart +++ b/test/notes_list_filter_test.dart @@ -21,7 +21,7 @@ void main() { // Created. await tester.tap(find.text('Today').first); await tester.pump(); - await tester.tap(find.text('Apply')); + await tapApply(tester); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); @@ -80,7 +80,7 @@ void main() { await tester.pump(); await tester.tap(find.text('Clear all')); await tester.pump(); - await tester.tap(find.text('Apply')); + await tapApply(tester); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); @@ -97,7 +97,7 @@ void main() { await tester.pump(); await tester.tap(find.text('Any').first); // clear that range await tester.pump(); - await tester.tap(find.text('Apply')); + await tapApply(tester); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); @@ -120,7 +120,7 @@ void main() { await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); - await tester.tap(find.text('Apply')); + await tapApply(tester); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); @@ -140,7 +140,7 @@ void main() { // Second "Today" belongs to the Last-updated section. await tester.tap(find.text('Today').last); await tester.pump(); - await tester.tap(find.text('Apply')); + await tapApply(tester); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); @@ -168,7 +168,7 @@ void main() { await tester.tap(find.text('Save')); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); - await tester.tap(find.text('Apply')); + await tapApply(tester); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); diff --git a/test/notes_list_harness.dart b/test/notes_list_harness.dart index dd63389..aa34b70 100644 --- a/test/notes_list_harness.dart +++ b/test/notes_list_harness.dart @@ -47,3 +47,12 @@ Future pumpList( await tester.pump(); // flush the initial stream emit return repo; } + +/// Taps the filter sheet's Apply button. It is the last item of a scrollable +/// sheet, so on a short viewport it legitimately sits below the fold: +/// scroll it into view first, as a user would. +Future tapApply(WidgetTester tester) async { + await tester.ensureVisible(find.text('Apply')); + await tester.pump(); + await tester.tap(find.text('Apply')); +}