diff --git a/CLAUDE.md b/CLAUDE.md index ca90a1e..5c629a1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,1101 +1,189 @@ # CLAUDE.md — diet_guard -## What this does +A log-to-unlock gate. Every ~30 min `diet-guard-gate.timer` checks whether a +meal slot (08:00, 12:00, 16:00, 20:00) elapsed unlogged; if so it opens a +fullscreen Tk window that blocks the desktop until the user logs what they +ate. It also tracks a daily calorie/macro budget, seeded from biometrics at +`init` and freely editable afterward on either device. A History tab +(`_gatelock_calendar.py`) and the phone's Calendar screen show adherence, +streaks, a YTD tally and weekly/monthly averages, all derived from +`_daystatus.py` / `day_status_service.dart` and `_averages.py` / +`average_service.dart`. -A log-to-unlock gate: every ~30 minutes, `diet-guard-gate.timer` runs -`diet-guard-gate.service`, which checks whether a meal slot (08:00, 12:00, -16:00, 20:00) has elapsed without a logged meal. If so, it opens a fullscreen -Tk window that blocks the desktop until the user logs what they ate (with -autocomplete from a local food-name "bank", optionally seeded from Open Food -Facts via `requests`). It also tracks a daily calorie/macro budget, seeded -from biometrics at `init` time but freely editable afterward on either -device (PC gate or phone app), synced last-edit-wins like the food log (see -"Cross-device sync"). A second tab in the gate window ("History", built by -`_gatelock_calendar.py`) and a matching screen in the phone app show a -budget-adherence calendar, logging/adherence streaks, a year-to-date -tally, and weekly/monthly average intake, all derived from the same -day-status classification (`_daystatus.py` / `day_status_service.dart`) and -the averages built on it (`_averages.py` / `average_service.dart`). +`docs/design.md` holds the original spec (slot timing, the Tue/Wed/Thu +catch-up rule). -See `docs/design.md` for the original feature spec (meal-slot timing logic, -the Tue/Wed/Thu "filled most of the day" catch-up rule). Note that the -multi-item **meal builder** described there was removed on 2026-07-26 as -unused, on both the gate and the phone; only its `components` *storage* -remains, so already-logged composite meals keep their per-item macros. Nothing -creates a new one. The phone's "repeat last meal" button and the -temptation-bundling reward prompt were removed in the same pass (the reward -was replaced by a post-log progress card). +## Commands + +- Tests: `python -m pytest diet_guard/tests/ --cov=diet_guard --cov-branch --cov-fail-under=100` +- Lint: `pre-commit run --all-files` +- Test the lock (safe, closeable): `python -m diet_guard gate --demo` +- One sync tick: `python -m diet_guard sync` +- Averages: `python -m diet_guard averages` +- Install for production: `bash install.sh` +- App tests: `cd app && flutter test` +- Desktop app: `cd app && bash run.sh` +- Install desktop app: `cd app && bash install_arch.sh` +- Phone build: `cd app && flutter build apk --release` ## Scheduling -`diet-guard-gate.timer` — wall-clock `OnCalendar=*-*-* *:00/30:00`, -`Persistent=true`. Deliberately wall-clock rather than boot-relative: an -earlier boot-relative timer interacted badly with fullscreen games grabbing -keyboard/mouse input around the same point in their session, so this is -pinned to the clock instead of "N minutes after boot/login." - -`diet-guard-gate.service` is `Type=oneshot`, fires every tick, and exits 0 -immediately if no lock is due — cheap enough to run that often. It needs -`DISPLAY`/`XAUTHORITY` because it opens a Tk window; see the inline comments -in the unit file for why `XAUTHORITY` is pinned explicitly (a `Persistent=true` -catch-up run at session start can beat the display manager writing -`~/.Xauthority`) and why a real fix lives in Python (`wait_for_display()`) -rather than in the unit file. +`diet-guard-gate.timer` is wall-clock (`OnCalendar=*-*-* *:00/30:00`, +`Persistent=true`), not boot-relative — a boot-relative timer collided with +fullscreen games grabbing input. `diet-guard-gate.service` is `Type=oneshot` +and exits 0 immediately when no lock is due. It needs `DISPLAY`/`XAUTHORITY`; +see the unit file's comments and `wait_for_display()`. ## Cross-device sync -`diet-guard-sync.timer` fires `python -m diet_guard sync` every ~15 minutes -(headless, no `DISPLAY` needed — separate from the gate timer on purpose, see -the unit file's comment for why). It pulls every other device's pushed log -from the private `kuhyx/syncs` GitHub repo (under `diet-guard-sync/`, used as dumb file -storage via the REST Contents API, not a git clone), merges with the local -log (`_sync_merge.merge_logs`: union by `id`, tombstone wins, legacy -`(time, desc)` dedup for pre-`id` entries), **re-signs every persisted entry** -regardless of origin, rebuilds the food bank, then pushes this device's own -merged log back up. +`diet-guard-sync.timer` runs `python -m diet_guard sync` every ~15 min, +headless. It pulls every other device's log from `kuhyx/syncs` +(`diet-guard-sync/`, dumb file storage via the REST Contents API), merges +(`_sync_merge.merge_logs`: union by `id`, tombstone wins, legacy +`(time, desc)` dedup), **re-signs every persisted entry**, rebuilds the food +bank, pushes back. -**The phone's periodic tick must sync unconditionally.** `checkAndNotify` -(`due_slot_check.dart`) once gated its sync on `due.isNotEmpty` — "only pull -when a slot looks overdue". That is a trap: logging every meal promptly on the -phone means nothing is ever due, so the phone never published, and the PC's -gate kept locking for slots that *were* already logged (the only way through -being to spam "nothing"). It publishes first, then computes due slots from the -merged result, so a meal pulled from another device suppresses a nag instead of -triggering one. Do not re-gate the sync on the notification's condition. +- **Re-sign on every merge, not just phone-origin entries.** `_entry_is_valid()` + drops unsigned entries once a machine has the HMAC key, and the phone never + holds that key — skipping the re-sign loses every phone-logged meal on the + next read. +- **The phone's periodic tick syncs unconditionally.** Do not re-gate it on + `due.isNotEmpty` (`due_slot_check.dart`); logging promptly means nothing is + ever due, so the phone never publishes and the PC nags for logged slots. + Publish first, then compute due slots from the merged result. +- **WorkManager background isolates must call `initSyncDeviceId()` themselves.** + A fresh isolate has its own static state; without it `currentSyncDeviceId` + falls back to the compile-time role constant and publishes to + `devices/phone/`. Guarded by `background_sync_service_test.dart`. +- **Catch `RemoteSyncError`, never `GitHubSyncError`.** `FirebaseSyncError`/ + `FirebaseAuthError` are siblings, not subclasses. `ConfigError` subclasses + `Exception` directly and is covered by neither — `_client_for_run` translates + it to `SyncError`. +- Needs **one** backend, not both: `~/.config/crdt-sync/` (Firebase, primary) + or a PAT at `~/.config/diet_guard/sync_token`, mode 600. Neither → the tick + logs `sync not configured` and no-ops. +- **Both halves of the food bank sync.** The derived bank (`food_bank.json`) + merges by **`count`** (max-count-wins, idempotent — the right merge for a + derived counter). The curated bank (`food_bank_manual.json`) is one CRDT + record per normalized name, LWW by `editedAt`. -**Anything running in a WorkManager background isolate must call -`initSyncDeviceId()` itself.** A fresh isolate has its own static state, so -without it `currentSyncDeviceId` silently falls back to the compile-time -`'phone'`/`'desktop'` role constant and the tick publishes to -`devices/phone/` instead of the persisted uuid. Because `sync_state` holds a -single `pushedRev` with no notion of which identity wrote it, the next -*foreground* push then sees `unchanged` and skips writing the uuid directory -entirely. `background_sync_service.dart` does this alongside its other -singleton re-inits; `background_sync_service_test.dart` guards it. +## Production dependency installation -**Catch `RemoteSyncError`, never `GitHubSyncError`, on any sync path.** -`FirebaseSyncError`/`FirebaseAuthError` are *siblings* of `GitHubSyncError` -under `RemoteSyncError`, not subclasses. Catching the GitHub type alone lets -every per-request failure of the *primary* backend escape — which made -`pull_shared_log` (the gate's "Fetch from sync" button, documented to fail -closed with a reason string) raise a traceback instead. -`ConfigError` is the one to watch: it subclasses `Exception` **directly**, so -`RemoteSyncError` does not cover it either. Anything that builds a Firebase -client must translate it (`_client_for_run` raises `SyncError`), or it escapes -every caller's catch tuple by a different route. - -Re-signing on every merge (not just phone-origin entries) is the -non-negotiable step: `_entry_is_valid()` drops any unsigned entry once a -machine has the shared HMAC key, and the phone never holds that key, so -skipping the re-sign would silently lose every phone-logged meal on the very -next read. - -Needs **one** backend configured, not both. Either `~/.config/crdt-sync/` -(Firebase, now the primary) or a fine-grained GitHub PAT scoped to `syncs`'s -contents (read/write) at `~/.config/diet_guard/sync_token`, mode 600 — -a one-time manual setup `install.sh` does **not** automate. A -Firebase-configured machine with no PAT syncs against Firebase alone -(`_client_for_run`); only when *neither* exists is the tick a harmless no-op -that logs `sync not configured`. The PAT used to be required unconditionally, -which left a Firebase-only PC unable to sync at all. - -**Both halves of the food bank sync.** The derived bank -(`food_bank.json`) is still rebuilt locally from the merged log -(`_foodbank.rebuild_food_bank`), but it is then merged and pushed too, so the -two devices agree *immediately* instead of only after each has replayed the -log. Its merge clock is the record's own **`count`**, not a wall time: LWW -therefore means *max-count-wins*, which is the correct merge for a derived -counter (the device that has seen more of the log has the higher count) and -is idempotent, because the count only moves when the log does. That is what -sidesteps needing a real counter CRDT. - -The **hand-curated** bank also syncs -(`food_bank_manual.json`, `_foodbank_manual.py` / the manual half of -`foodbank_service.dart`). Those are foods the user added without ever eating -them, so they are not derivable from any log; before they synced they were -the last piece of device-local state in the app. One CRDT record per -normalized name, last-writer-wins by an `editedAt` stamp, union across -devices. `lookup_food`/`search_foods` merge them under the log-derived -records, so a food curated on the phone autocompletes in the PC gate. - -## Production dependency installation — read this before adding any dependency - -`diet-guard-gate.service` runs `/usr/bin/python` directly — **not** a venv. -Any new non-stdlib dependency (this package itself, `gatelock`, `requests`, -anything added later) must be installed into system Python's *user* -site-packages, the same place `python-kasa` already lives: +`diet-guard-gate.service` runs `/usr/bin/python`, **not** a venv. Any new +non-stdlib dependency must go into system Python's user site-packages: ```bash /usr/bin/python3 -m pip install --user --break-system-packages -e . ``` -`install.sh` already does this. **If you add a dependency and only install it -into a dev venv, the production service will silently fail with -`ModuleNotFoundError` on its next tick** — this exact gap caused a 3-day -diet_guard production outage (2026-06-19 to 2026-06-22) when `gatelock` was -added but only `pip install`-ed into `.venv`. Always verify against -`/usr/bin/python3 -c "import "`, not just the dev venv, before -considering a dependency change done. +Verify against `/usr/bin/python3 -c "import "`, not the dev venv. +Installing only into `.venv` caused a 3-day production outage (2026-06-19). -**Always run `install.sh` (or `pip install -e`) from a durable clone, not a -scratch directory.** `install.sh` does `pip install -e "$REPO_DIR"` — -editable, so a later `git pull` in that same clone updates the running -production code with no reinstall needed. The clone must live somewhere -permanent (this repo's convention: `~/diet-guard`, mirroring `~/screen-locker` -for the screen-locker package) — if you `pip install -e` from `/tmp/...` or -run `pip install "diet_guard @ git+https://..."` as a one-off, you get a -non-editable snapshot frozen at that commit, and the next `git push` here -silently does **not** reach the running service. +Run `install.sh` from a **durable** clone (`~/diet-guard`), never a scratch +dir — it does `pip install -e`, so the clone must persist for `git pull` to +reach the running service. ## Operational gotchas -- **The budget is a plain, freely-editable synced file, by design.** - `~/.local/share/diet_guard/.budget` (`diet_guard/_budget.py`) has no seal - or signing — this replaces a previous `chattr +i` anti-impulse-editing - mechanism, removed deliberately so the same value can be edited from - either device. Edit it from the gate's History tab, the phone app's - Calendar screen, or `settings_screen.dart`; every write stamps an edit - timestamp so `diet-guard-sync.timer`'s `budget.json` sync (mirroring the - food-log sync) resolves concurrent edits last-writer-wins. Do not - reintroduce the seal as a "fix" — the removal is the feature. +- **The budget is a plain, freely-editable synced file.** No seal, no signing — + this deliberately replaced a `chattr +i` mechanism so the value is editable + from either device. Do not reintroduce the seal as a "fix"; the removal is + the feature. Every write stamps an edit timestamp; `budget.json` resolves + concurrent edits LWW. - **Past days are judged against the budget that applied then.** - `~/.local/share/diet_guard/.budget_history` (`diet_guard/_budget_history.py`, - mirrored by `app/lib/services/budget_schedule.dart`) is a forward-only list - of `(effective_from, kcal)` entries; `status_map`/`statusMap` resolve each - day through it, so lowering the budget never retroactively breaks the - adherence streak or the YTD tally. It syncs as extra `hist:` - *fields* on the existing `budget` CRDT record rather than as a second - document: `merge_record` is per-field LWW over the **union** of field names - and both devices push the *merged* log, so a device that predates the - feature relays those fields untouched. `write_budget` / - `saveDailyKcalGoal` must seed the pre-write value to `1970-01-01` **before** - recording the new one for today — reversing that makes every past day adopt - the new budget, which is the exact bug this exists to prevent. -- **Biometrics are used once and discarded.** `init` asks for biometrics to - compute the daily budget, then the only persisted output is the computed - budget number — never the biometrics themselves. -- **PC and phone share one source of truth for every piece of data.** The - food log, the budget, the budget history, body weight (`w`, its own - `weight` CRDT field), and the curated food bank all round-trip through - `kuhyx/syncs`. The derived food bank is a local cache of the synced log, - not separate state. There is deliberately nothing left that only one - device knows — if you add a new stored field, sync it or explain in a - comment why it physically cannot be shared. -- **State lives entirely under `~/.local/share/diet_guard/`** — no - cross-repo file coupling (unlike wake_alarm, which reads - `~/screen-locker/screen_locker/workout_log.json`). Safe to reason about in - isolation, with one exception: `diet-guard-sync.timer` reads/writes the - private `kuhyx/syncs` GitHub repo (see "Cross-device sync" above) - and `~/.config/diet_guard/sync_token`. + `_budget_history.py` / `budget_schedule.dart` is a forward-only list of + `(effective_from, kcal)`. `write_budget` / `saveDailyKcalGoal` must seed the + pre-write value to `1970-01-01` **before** recording today's — reversing that + makes every past day adopt the new budget. Covered by + `test_budget_history.py`; it syncs as `hist:` fields on the + existing `budget` record, so devices predating the feature relay them + untouched. +- **Biometrics are used once and discarded** — only the computed budget is + persisted. +- **PC and phone share one source of truth for everything**: food log, budget, + budget history, body weight (`w`), curated food bank. If you add a stored + field, sync it or comment why it physically cannot be. +- **State lives entirely under `~/.local/share/diet_guard/`** — no cross-repo + file coupling. Exception: the sync timer touches `kuhyx/syncs` and + `~/.config/diet_guard/sync_token`. +- **Every device pushes under a persisted per-install uuid**, not a role + constant. `_device.py` / `sync_device_id.dart` (`crdt.nodeId`). The old role + constant survives as the legacy id so `devices//` is skipped as this + device's own — dropping it makes every tick re-merge our own pre-migration + log. The app resolves it asynchronously in `main.dart` before + `LogStorageService.init()`; don't move that call later. Tests must redirect + `_device.SYNC_DEVICE_ID_FILE` (conftest does). ## The companion app's desktop target is a web build -`app/` builds for two platforms, and only two: **Android** (the phone) and -**web** (the Linux desktop). There is no `app/linux/` — Flutter's GTK embedder -manages only ~20fps at 3840x2160 (measured on a stock `flutter create` app, so -it is the toolkit, not this code), where the same Dart in Chrome sustains -~144fps; see `~/todo/docs/desktop-performance-findings.md`. On the desktop the -app is the web build served by `bin/diet_guard_desktop.dart` and shown in a +`app/` builds for **Android** and **web** only. There is no `app/linux/` — +Flutter's GTK embedder manages ~20fps at 3840x2160 where the same Dart in +Chrome sustains ~144fps (`~/todo/docs/desktop-performance-findings.md`). The +desktop app is the web build served by `bin/diet_guard_desktop.dart` in a Chrome `--app` window. -Consequences worth knowing before touching `app/`: +Enforced by `app/test/repo_invariants_test.dart`: no `app/linux/`, `dart:io` +only in `*_io.dart` seams and `lib/desktop/`, port 8732 and the +`diet-guard-desktop` Chrome profile pinned (IndexedDB is keyed by origin, so +changing either hides the entire local food log). -- **`dart:io` does not fail a web *compile*** — it becomes a stub that throws - at runtime, so the symptom is a blank white window, not a build error. Every - platform-specific edge goes behind a conditional export - (`document_store*.dart`, `blob_store*.dart`, `background_tasks*.dart`, - `notification_backend*.dart`, `token_vault*.dart`, - `github_client_factory*.dart`, `attached_image*.dart`). Branch on `kIsWeb` - *before* any `Platform.is…`, which itself throws on web. -- **The port and the Chrome `--user-data-dir` are fixed on purpose** - (`lib/services/desktop_wrapper.dart`). IndexedDB is keyed by origin and - lives in that profile: changing either hides the entire local food log. -- **The wrapper holds the GitHub token**, not the browser, and proxies +- Branch on `kIsWeb` *before* any `Platform.is…`, which itself throws on web. + `dart:io` does not fail a web compile — it throws at runtime, so the symptom + is a blank white window, not a build error. +- The wrapper holds the GitHub token, not the browser, and proxies `api.github.com` plus the CORS-less device flow (`lib/desktop/github_proxy.dart`). -- **Every device pushes under a persisted per-install uuid**, not the old - fixed `pc`/`phone`/`desktop` role constants. A role constant collides the - moment a second machine takes the same role, and a reinstall silently - inherits the previous install's CRDT identity. The id lives in - `~/.local/share/diet_guard/.device_id` on the PC (`diet_guard/_device.py`) - and under SharedPreferences `crdt.nodeId` in the app - (`lib/services/sync_device_id.dart`), the same key `todo` and - `home_inventory` use. - - The old role constant survives as the **legacy id** - (`SYNC_LEGACY_DEVICE_ID` / `legacySyncDeviceId`), passed to `sync_log` / - `syncLog` so `devices//` is skipped as *this device's own*. Drop - it to `None`/null only after that path has been deleted — until then, - removing it makes every tick re-download and re-merge this device's own - pre-migration log as though a peer wrote it. - - The app resolves its id **asynchronously at startup** - (`initSyncDeviceId()` in `main.dart`, before `LogStorageService.init()`). - HLC stamping is synchronous, so `currentSyncDeviceId` falls back to the - compile-time role constant until that completes: a caller that stamps too - early gets a valid-but-pre-migration id rather than a crash. Don't move - that call later. - - **Tests must redirect `_device.SYNC_DEVICE_ID_FILE`** (conftest already - does). Without it a test mints a uuid into the real state dir and this - machine's live sync identity is decided by whichever test ran first. -- **Desktop reminders only exist while the window is open.** A browser has no - WorkManager equivalent; the PC's real backstop is `diet-guard-gate.timer`, - which locks the screen. Don't "fix" this with a background service without - deciding you want a new daemon. -- `flutter create --platforms linux` would silently restore the slow path. - Don't run it in `app/`. - -## Commands - -- Run tests: `python -m pytest diet_guard/tests/ --cov=diet_guard --cov-branch --cov-fail-under=100` -- Lint: `pre-commit run --all-files` -- Test the lock manually (safe, closeable): `python -m diet_guard gate --demo` -- Run one sync tick manually: `python -m diet_guard sync` -- Weekly/monthly averages: `python -m diet_guard averages` -- Install for production: `bash install.sh` -- App tests: `cd app && flutter test` -- Run the desktop app from the repo: `cd app && bash run.sh` -- Install the desktop app (Arch package + .desktop entry): - `cd app && bash install_arch.sh` -- Build the phone app: `cd app && flutter build apk --release` +- Desktop reminders only exist while the window is open; the PC's real backstop + is `diet-guard-gate.timer`. +- Never run `flutter create --platforms linux` in `app/`. ## Averages (`_averages.py` / `average_service.dart`) -Three rules are load-bearing; changing any of them silently changes what -"under budget" means, so re-read this before touching them: +Three load-bearing rules; changing any silently changes what "under budget" +means: - **Denominator is logged days, not elapsed days.** An unlogged day is not a - zero-kcal day. Averaging gaps in as zeros makes a badly-tracked week look - like the best week of the year, which is the exact failure the gate exists - to prevent. + zero-kcal day. - **The yardstick is the mean of the per-day budgets over those same logged - days**, resolved through the `BudgetSchedule` -- never `daily_budget()`. - Using today's number would retroactively reclassify past weeks on every - budget edit, the same bug `_budget_history.py` was written to fix. -- **Today is excluded.** Every period ends at `last_complete_day()`, i.e. - yesterday. A period ending "now" mixes finished days with one that is three - hours old, and a half-logged today drags the mean far enough to flip the - band. The visible cost is that "this week" is empty on a Monday; that is - correct, and it reports `no logged days yet` rather than a fake average. + days**, resolved through `BudgetSchedule` — never `daily_budget()`. +- **Today is excluded**; every period ends at `last_complete_day()`. "This + week" is empty on a Monday, and reports `no logged days yet` rather than a + fake average. Band boundaries reuse `_daystatus.OVER_BUDGET_YELLOW_CEILING` / -`kOverBudgetYellowCeiling` **by import, not by copy** -- two literals would let -the averages line and the calendar disagree about what "over" means. - -The MCP `get_averages` tool returns `avg_kcal` and the band but deliberately -drops `avg_budget`, keeping the module's budget-secrecy invariant. The CLI, -the gate, and the phone all show both numbers, since the budget is hidden only -from the network, not from the user. +`kOverBudgetYellowCeiling` **by import, not by copy**. The MCP `get_averages` +tool drops `avg_budget` (budget secrecy on the network only); the CLI, gate and +phone show both. ## Do NOT -- Don't relax the meal-slot logic without re-reading `docs/design.md` — the - Tue/Wed/Thu catch-up rule is deliberate, not accidental complexity. The - off-hours **clamp** in `slot_for_log`/`slotForLog` is likewise deliberate - (a meal before 08:00 counts toward 08:00, after 22:00 toward 20:00) and - must stay byte-identical across Python and Dart; do NOT "fix" it by - widening `elapsed_slots`, which would make every slot fall due at 23:00. -- Don't re-add the meal builder, the "repeat last meal" button, the reward - prompt, or **meal photos** — all four were removed deliberately as unused. - The photo removal took `image_picker`, the blob stores, the wrapper's - `/blobs/` route and the `CAMERA` permission with it; re-adding photos means - re-adding all of that *plus* syncing image blobs, since a device-local - attachment would violate the shared-source-of-truth rule above. Do not strip the - `components` field either: nothing writes it any more, but historical - composite entries and the food bank still read it, and it is part of the - sync wire format. -- Don't add a dependency without doing the production install-path check - above. -- Don't reintroduce a seal/lock/`chattr +i` on the budget file as a "fix" — - it was deliberately removed so the budget is freely editable and synced - between devices; see "Operational gotchas" above. -- Don't re-add a Linux embedder target to `app/`, and don't change the - wrapper's fixed port or Chrome profile path — see "The companion app's - desktop target is a web build" above. +- Don't relax the meal-slot logic without re-reading `docs/design.md`. The + off-hours clamp in `slot_for_log`/`slotForLog` (before 08:00 → 08:00, after + 22:00 → 20:00) must stay byte-identical across Python and Dart; do NOT widen + `elapsed_slots`, which would make every slot fall due at 23:00. +- Don't re-add the meal builder, "repeat last meal", the reward prompt, or + **meal photos** — all removed deliberately, enforced by + `app/test/repo_invariants_test.dart`. Photos also took `image_picker`, the + blob stores, the `/blobs/` route and the `CAMERA` permission; re-adding them + means syncing image blobs too. +- Don't strip the `components` field: nothing writes it, but historical + composite entries and the food bank read it, and it is part of the sync wire + format. +- Don't add a dependency without the production install-path check above. +- Don't reintroduce a seal/`chattr +i` on the budget file. +- Don't re-add a Linux embedder target, and don't change the wrapper's port or + Chrome profile path. +- Don't exceed **250 lines** in any file. Enforced by the `file-length` + pre-commit hook and `.github/workflows/file-length.yml`; the cap and its + exemptions live in `~/utils/file_length/config.py`. ## Flutter/Dart AI rules -_Appended from [flutter/flutter docs/rules/rules.md](https://github.com/flutter/flutter/blob/main/docs/rules/rules.md) via `/home/kuhy/.claude/CLAUDE.md` Flutter AI tooling setup. Re-fetch periodically to stay current._ - -## AI rules for Flutter - -You are an expert in Flutter and Dart development. Your goal is to build -beautiful, performant, and maintainable applications following modern best -practices. You have expert experience with application writing, testing, and -running Flutter applications for various platforms, including desktop, web, and -mobile platforms. - -### Interaction Guidelines -* **User Persona:** Assume the user is familiar with programming concepts but - may be new to Dart. -* **Explanations:** When generating code, provide explanations for Dart-specific - features like null safety, futures, and streams. -* **Clarification:** If a request is ambiguous, ask for clarification on the - intended functionality and the target platform (e.g., command-line, web, - server). -* **Dependencies:** When suggesting new dependencies from `pub.dev`, explain - their benefits. -* **Formatting:** Use the `dart_format` tool to ensure consistent code - formatting. -* **Fixes:** Use the `dart_fix` tool to automatically fix many common errors, - and to help code conform to configured analysis options. -* **Linting:** Use the Dart linter with a recommended set of rules to catch - common issues. Use the `analyze_files` tool to run the linter. - -### Project Structure -* **Standard Structure:** Assumes a standard Flutter project structure with - `lib/main.dart` as the primary application entry point. - -### Flutter style guide -* **SOLID Principles:** Apply SOLID principles throughout the codebase. -* **Concise and Declarative:** Write concise, modern, technical Dart code. - Prefer functional and declarative patterns. -* **Composition over Inheritance:** Favor composition for building complex - widgets and logic. -* **Immutability:** Prefer immutable data structures. Widgets (especially - `StatelessWidget`) should be immutable. -* **State Management:** Separate ephemeral state and app state. Use a state - management solution for app state to handle the separation of concerns. -* **Widgets are for UI:** Everything in Flutter's UI is a widget. Compose - complex UIs from smaller, reusable widgets. -* **Navigation:** Use a modern routing package like `auto_route` or `go_router`. - For more guidelines around navigation, see the section on [routing](#routing). - -### Package Management -* **Pub Tool:** To manage packages, use the `pub` tool, if available. -* **External Packages:** If a new feature requires an external package, use the - `pub_dev_search` tool, if it is available. Otherwise, identify the most - suitable and stable package from pub.dev. -* **Adding Dependencies:** To add a regular dependency, use the `pub` tool, if - it is available. Otherwise, run `flutter pub add `. -* **Adding Dev Dependencies:** To add a development dependency, use the `pub` - tool, if it is available, with `dev:`. Otherwise, run `flutter - pub add dev:`. -* **Dependency Overrides:** To add a dependency override, use the `pub` tool, if - it is available, with `override::1.0.0`. Otherwise, run `flutter - pub add override::1.0.0`. -* **Removing Dependencies:** To remove a dependency, use the `pub` tool, if it - is available. Otherwise, run `dart pub remove `. - -### Code Quality -* **Code structure:** Adhere to maintainable code structure and separation of - concerns (e.g., UI logic separate from business logic). -* **Naming conventions:** Avoid abbreviations and use meaningful, consistent, - descriptive names for variables, functions, and classes. -* **Conciseness:** Write code that is as short as it can be while remaining - clear. -* **Simplicity:** Write straightforward code. Code that is clever or - obscure is difficult to maintain. -* **Error Handling:** Anticipate and handle potential errors. Don't let your - code fail silently. -* **Styling:** - * Line length: Lines should be 80 characters or fewer. - * Use `PascalCase` for classes, `camelCase` for - members/variables/functions/enums, and `snake_case` for files. -* **Functions:** - * Keep functions short and with a single purpose. - Strive for less than 20 lines. -* **Testing:** Write code with testing in mind. Use the `file`, `process`, and - `platform` packages, if appropriate, so you can inject in-memory and fake - versions of the objects. -* **Logging:** Use the `logging` package instead of `print`. - -### Dart Best Practices -* **Effective Dart:** Follow the official Effective Dart guidelines - (https://dart.dev/effective-dart) -* **Class Organization:** Define related classes within the same library file. - For large libraries, export smaller, private libraries from a single top-level - library. -* **Library Organization:** Group related libraries in the same folder. -* **API Documentation:** Add documentation comments to all public APIs, - including classes, constructors, methods, and top-level functions. -* **Comments:** Write clear comments for complex or non-obvious code. Avoid - over-commenting. -* **Trailing Comments:** Don't add trailing comments. -* **Async/Await:** Ensure proper use of `async`/`await` for asynchronous - operations with robust error handling. - * Use `Future`s, `async`, and `await` for asynchronous operations. - * Use `Stream`s for sequences of asynchronous events. -* **Null Safety:** Write code that is soundly null-safe. Leverage Dart's null - safety features. Avoid `!` unless the value is guaranteed to be non-null. -* **Pattern Matching:** Use pattern matching features where they simplify the - code. -* **Records:** Use records to return multiple types in situations where defining - an entire class is cumbersome. -* **Switch Statements:** Prefer using exhaustive `switch` statements or - expressions, which don't require `break` statements. -* **Exception Handling:** Use `try-catch` blocks for handling exceptions, and - use exceptions appropriate for the type of exception. Use custom exceptions - for situations specific to your code. -* **Arrow Functions:** Use arrow syntax for simple one-line functions. - -### Flutter Best Practices -* **Immutability:** Widgets (especially `StatelessWidget`) are immutable; when - the UI needs to change, Flutter rebuilds the widget tree. -* **Composition:** Prefer composing smaller widgets over extending existing - ones. Use this to avoid deep widget nesting. -* **Private Widgets:** Use small, private `Widget` classes instead of private - helper methods that return a `Widget`. -* **Build Methods:** Break down large `build()` methods into smaller, reusable - private Widget classes. -* **List Performance:** Use `ListView.builder` or `SliverList` for long lists to - create lazy-loaded lists for performance. -* **Isolates:** Use `compute()` to run expensive calculations in a separate - isolate to avoid blocking the UI thread, such as JSON parsing. -* **Const Constructors:** Use `const` constructors for widgets and in `build()` - methods whenever possible to reduce rebuilds. -* **Build Method Performance:** Avoid performing expensive operations, like - network calls or complex computations, directly within `build()` methods. - -### API Design Principles -When building reusable APIs, such as a library, follow these principles. - -* **Consider the User:** Design APIs from the perspective of the person who will - be using them. The API should be intuitive and easy to use correctly. -* **Documentation is Essential:** Good documentation is a part of good API - design. It should be clear, concise, and provide examples. - -### Application Architecture -* **Separation of Concerns:** Aim for separation of concerns similar to MVC/MVVM, with defined Model, - View, and ViewModel/Controller roles. -* **Logical Layers:** Organize the project into logical layers: - * Presentation (widgets, screens) - * Domain (business logic classes) - * Data (model classes, API clients) - * Core (shared classes, utilities, and extension types) -* **Feature-based Organization:** For larger projects, organize code by feature, - where each feature has its own presentation, domain, and data subfolders. This - improves navigability and scalability. - -### Lint Rules - -Include the package in the `analysis_options.yaml` file. Use the following -`analysis_options.yaml` file as a starting point: - -```yaml -include: package:flutter_lints/flutter.yaml - -linter: - rules: - # Add additional lint rules here: - # avoid_print: false - # prefer_single_quotes: true -``` - -#### State Management -* **Built-in Solutions:** Prefer Flutter's built-in state management solutions. - Do not use a third-party package unless explicitly requested. -* **Streams:** Use `Streams` and `StreamBuilder` for handling a sequence of - asynchronous events. -* **Futures:** Use `Futures` and `FutureBuilder` for handling a single - asynchronous operation that will complete in the future. -* **ValueNotifier:** Use `ValueNotifier` with `ValueListenableBuilder` for - simple, local state that involves a single value. - - ```dart - // Define a ValueNotifier to hold the state. - final ValueNotifier _counter = ValueNotifier(0); - - // Use ValueListenableBuilder to listen and rebuild. - ValueListenableBuilder( - valueListenable: _counter, - builder: (context, value, child) { - return Text('Count: $value'); - }, - ); - ``` - -* **ChangeNotifier:** For state that is more complex or shared across multiple - widgets, use `ChangeNotifier`. -* **ListenableBuilder:** Use `ListenableBuilder` to listen to changes from a - `ChangeNotifier` or other `Listenable`. -* **MVVM:** When a more robust solution is needed, structure the app using the - Model-View-ViewModel (MVVM) pattern. -* **Dependency Injection:** Use simple manual constructor dependency injection - to make a class's dependencies explicit in its API, and to manage dependencies - between different layers of the application. -* **Provider:** If a dependency injection solution beyond manual constructor - injection is explicitly requested, `provider` can be used to make services, - repositories, or complex state objects available to the UI layer without tight - coupling (note: this document generally defaults against third-party packages - for state management unless explicitly requested). - -#### Data Flow -* **Data Structures:** Define data structures (classes) to represent the data - used in the application. -* **Data Abstraction:** Abstract data sources (e.g., API calls, database - operations) using Repositories/Services to promote testability. - -#### Routing -* **GoRouter:** Use the `go_router` package for declarative navigation, deep - linking, and web support. -* **GoRouter Setup:** To use `go_router`, first add it to your `pubspec.yaml` - using the `pub` tool's `add` command. - - ```dart - // 1. Add the dependency - // flutter pub add go_router - - // 2. Configure the router - final GoRouter _router = GoRouter( - routes: [ - GoRoute( - path: '/', - builder: (context, state) => const HomeScreen(), - routes: [ - GoRoute( - path: 'details/:id', // Route with a path parameter - builder: (context, state) { - final String id = state.pathParameters['id']!; - return DetailScreen(id: id); - }, - ), - ], - ), - ], - ); - - // 3. Use it in your MaterialApp - MaterialApp.router( - routerConfig: _router, - ); - ``` -* **Authentication Redirects:** Configure `go_router`'s `redirect` property to - handle authentication flows, ensuring users are redirected to the login screen - when unauthorized, and back to their intended destination after successful - login. - -* **Navigator:** Use the built-in `Navigator` for short-lived screens that do - not need to be deep-linkable, such as dialogs or temporary views. - - ```dart - // Push a new screen onto the stack - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const DetailsScreen()), - ); - - // Pop the current screen to go back - Navigator.pop(context); - ``` - -#### Data Handling & Serialization -* **JSON Serialization:** Use `json_serializable` and `json_annotation` for - parsing and encoding JSON data. -* **Field Renaming:** When encoding data, use `fieldRename: FieldRename.snake` - to convert Dart's camelCase fields to snake_case JSON keys. - - ```dart - // In your model file - import 'package:json_annotation/json_annotation.dart'; - - part 'user.g.dart'; - - @JsonSerializable(fieldRename: FieldRename.snake) - class User { - final String firstName; - final String lastName; - - User({required this.firstName, required this.lastName}); - - factory User.fromJson(Map json) => _$UserFromJson(json); - Map toJson() => _$UserToJson(this); - } - ``` - - -#### Logging -* **Structured Logging:** Use the `log` function from `dart:developer` for - structured logging that integrates with Dart DevTools. - - ```dart - import 'dart:developer' as developer; - - // For simple messages - developer.log('User logged in successfully.'); - - // For structured error logging - try { - // ... code that might fail - } catch (e, s) { - developer.log( - 'Failed to fetch data', - name: 'myapp.network', - level: 1000, // SEVERE - error: e, - stackTrace: s, - ); - } - ``` - -### Code Generation -* **Build Runner:** If the project uses code generation, ensure that - `build_runner` is listed as a dev dependency in `pubspec.yaml`. -* **Code Generation Tasks:** Use `build_runner` for all code generation tasks, - such as for `json_serializable`. -* **Running Build Runner:** After modifying files that require code generation, - run the build command: - - ```shell - dart run build_runner build --delete-conflicting-outputs - ``` - -### Testing -* **Running Tests:** To run tests, use the `run_tests` tool if it is available, - otherwise use `flutter test`. -* **Unit Tests:** Use `package:test` for unit tests. -* **Widget Tests:** Use `package:flutter_test` for widget tests. -* **Integration Tests:** Use `package:integration_test` for integration tests. -* **Assertions:** Prefer using `package:checks` for more expressive and readable - assertions over the default `matchers`. - -#### Testing Best practices -* **Convention:** Follow the Arrange-Act-Assert (or Given-When-Then) pattern. -* **Unit Tests:** Write unit tests for domain logic, data layer, and state - management. -* **Widget Tests:** Write widget tests for UI components. -* **Integration Tests:** For broader application validation, use integration - tests to verify end-to-end user flows. -* **integration_test package:** Use the `integration_test` package from the - Flutter SDK for integration tests. Add it as a `dev_dependency` in - `pubspec.yaml` by specifying `sdk: flutter`. -* **Mocks:** Prefer fakes or stubs over mocks. If mocks are absolutely - necessary, use `mockito` or `mocktail` to create mocks for dependencies. While - code generation is common for state management (e.g., with `freezed`), try to - avoid it for mocks. -* **Coverage:** Aim for high test coverage. - -### Visual Design & Theming -* **UI Design:** Build beautiful and intuitive user interfaces that follow - modern design guidelines. -* **Responsiveness:** Ensure the app is mobile responsive and adapts to - different screen sizes, working perfectly on mobile and web. -* **Navigation:** If there are multiple pages for the user to interact with, - provide an intuitive and easy navigation bar or controls. -* **Typography:** Stress and emphasize font sizes to ease understanding, e.g., - hero text, section headlines, list headlines, keywords in paragraphs. -* **Background:** Keep backgrounds flat and simple — no noise texture or - gradients. Follow the `unified-design-system` skill (safe-design-rules - rule 17: simple-on-complex); let content and typography carry visual - interest instead of background texture. -* **Shadows:** Follow the `unified-design-system` skill's shadow policy: no - shadows at all in dark UI (differentiate elevation with lighter surface - fills, not shadows); in light UI, shadows are for floating/overlay - elements only (menus, dialogs), never for inline cards, and never mixed - with a border on the same surface. -* **Icons:** Incorporate icons to enhance the user’s understanding and the - logical navigation of the app. -* **Interactive Elements:** Buttons/controls signal state via color and fill - changes, not a "glow" shadow — one depth technique per surface, per - `unified-design-system`. - -#### Theming -* **Centralized Theme:** Define a centralized `ThemeData` object to ensure a - consistent application-wide style. -* **Light and Dark Themes:** Implement support for both light and dark themes, - ideal for a user-facing theme toggle (`ThemeMode.light`, `ThemeMode.dark`, - `ThemeMode.system`). -* **Color Scheme Generation:** Do NOT use `ColorScheme.fromSeed` for this - app's palette — it can't reproduce hand-picked values exactly. Build - `ThemeData` from explicit `ColorScheme.dark(...)`/`.light(...)` using the - frozen, identical-everywhere tokens in the `unified-design-system` skill - (`~/.claude/skills/unified-design-system/references/tokens.md`). - - ```dart - final ThemeData lightTheme = ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.deepPurple, - brightness: Brightness.light, - ), - // ... other theme properties - ); - ``` -* **Color Palette:** Include a wide range of color concentrations and hues in - the palette to create a vibrant and energetic look and feel. -* **Component Themes:** Use specific theme properties (e.g., `appBarTheme`, - `elevatedButtonTheme`) to customize the appearance of individual Material - components. -* **Custom Fonts:** For custom fonts, use the `google_fonts` package. Define a - `TextTheme` to apply fonts consistently. - - ```dart - // 1. Add the dependency - // flutter pub add google_fonts - - // 2. Define a TextTheme with a custom font - final TextTheme appTextTheme = TextTheme( - displayLarge: GoogleFonts.oswald(fontSize: 57, fontWeight: FontWeight.bold), - titleLarge: GoogleFonts.roboto(fontSize: 22, fontWeight: FontWeight.w500), - bodyMedium: GoogleFonts.openSans(fontSize: 14), - ); - ``` - -#### Assets and Images -* **Image Guidelines:** If images are needed, make them relevant and meaningful, - with appropriate size, layout, and licensing (e.g., freely available). Provide - placeholder images if real ones are not available. -* **Asset Declaration:** Declare all asset paths in your `pubspec.yaml` file. - - ```yaml - flutter: - uses-material-design: true - assets: - - assets/images/ - ``` - -* **Local Images:** Use `Image.asset` for local images from your asset - bundle. - - ```dart - Image.asset('assets/images/placeholder.png') - ``` -* **Network images:** Use NetworkImage for images loaded from the network. -* **Cached images:** For cached images, use NetworkImage a package like - `cached_network_image`. -* **Custom Icons:** Use `ImageIcon` to display an icon from an `ImageProvider`, - useful for custom icons not in the `Icons` class. -* **Network Images:** Use `Image.network` to display images from a URL, and - always include `loadingBuilder` and `errorBuilder` for a better user - experience. - - ```dart - // When using network images, always provide an errorBuilder. - Image.network( - 'https://picsum.photos/200/300', - loadingBuilder: (context, child, progress) { - if (progress == null) return child; - return const Center(child: CircularProgressIndicator()); - }, - errorBuilder: (context, error, stackTrace) { - return const Icon(Icons.error); - }, - ) - ``` - -### UI Theming and Styling Code - -* **Responsiveness:** Use `LayoutBuilder` or `MediaQuery` to create responsive - UIs. -* **Text:** Use `Theme.of(context).textTheme` for text styles. -* **Text Fields:** Configure `textCapitalization`, `keyboardType`, and - `placeholder`. - -### Material Theming Best Practices - -#### Embrace `ThemeData` and Material 3 - -* **Use `ColorScheme.fromSeed()`:** Use this to generate a complete, harmonious - color palette for both light and dark modes from a single seed color. -* **Define Light and Dark Themes:** Provide both `theme` and `darkTheme` to your - `MaterialApp` to support system brightness settings seamlessly. -* **Centralize Component Styles:** Customize specific component themes (e.g., - `elevatedButtonTheme`, `cardTheme`, `appBarTheme`) within `ThemeData` to - ensure consistency. -* **Dark/Light Mode and Theme Toggle:** Implement support for both light and - dark themes using `theme` and `darkTheme` properties of `MaterialApp`. The - `themeMode` property can be dynamically controlled (e.g., via a - `ChangeNotifierProvider`) to allow for toggling between `ThemeMode.light`, - `ThemeMode.dark`, or `ThemeMode.system`. - -```dart -// main.dart -MaterialApp( - theme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.deepPurple, - brightness: Brightness.light, - ), - textTheme: const TextTheme( - displayLarge: TextStyle(fontSize: 57.0, fontWeight: FontWeight.bold), - bodyMedium: TextStyle(fontSize: 14.0, height: 1.4), - ), - ), - darkTheme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.deepPurple, - brightness: Brightness.dark, - ), - ), - home: const MyHomePage(), -); -``` - -#### Implement Design Tokens with `ThemeExtension` - -For custom styles that aren't part of the standard `ThemeData`, use -`ThemeExtension` to define reusable design tokens. - -* **Create a Custom Theme Extension:** Define a class that extends - `ThemeExtension` and include your custom properties. -* **Implement `copyWith` and `lerp`:** These methods are required for the - extension to work correctly with theme transitions. -* **Register in `ThemeData`:** Add your custom extension to the `extensions` - list in your `ThemeData`. -* **Access Tokens in Widgets:** Use `Theme.of(context).extension()!` - to access your custom tokens. - -```dart -// 1. Define the extension -@immutable -class MyColors extends ThemeExtension { - const MyColors({required this.success, required this.danger}); - - final Color? success; - final Color? danger; - - @override - ThemeExtension copyWith({Color? success, Color? danger}) { - return MyColors(success: success ?? this.success, danger: danger ?? this.danger); - } - - @override - ThemeExtension lerp(ThemeExtension? other, double t) { - if (other is! MyColors) return this; - return MyColors( - success: Color.lerp(success, other.success, t), - danger: Color.lerp(danger, other.danger, t), - ); - } -} - -// 2. Register it in ThemeData -theme: ThemeData( - extensions: const >[ - MyColors(success: Colors.green, danger: Colors.red), - ], -), - -// 3. Use it in a widget -Container( - color: Theme.of(context).extension()!.success, -) -``` - -#### Styling with `WidgetStateProperty` - -* **`WidgetStateProperty.resolveWith`:** Provide a function that receives a - `Set` and returns the appropriate value for the current state. -* **`WidgetStateProperty.all`:** A shorthand for when the value is the same for - all states. - -```dart -// Example: Creating a button style that changes color when pressed. -final ButtonStyle myButtonStyle = ButtonStyle( - backgroundColor: WidgetStateProperty.resolveWith( - (Set states) { - if (states.contains(WidgetState.pressed)) { - return Colors.green; // Color when pressed - } - return Colors.red; // Default color - }, - ), -); -``` - -### Layout Best Practices - -#### Building Flexible and Overflow-Safe Layouts - -##### For Rows and Columns - -* **`Expanded`:** Use to make a child widget fill the remaining available space - along the main axis. -* **`Flexible`:** Use when you want a widget to shrink to fit, but not - necessarily grow. Don't combine `Flexible` and `Expanded` in the same `Row` or - `Column`. -* **`Wrap`:** Use when you have a series of widgets that would overflow a `Row` - or `Column`, and you want them to move to the next line. - -##### For General Content - -* **`SingleChildScrollView`:** Use when your content is intrinsically larger - than the viewport, but is a fixed size. -* **`ListView` / `GridView`:** For long lists or grids of content, always use a - builder constructor (`.builder`). -* **`FittedBox`:** Use to scale or fit a single child widget within its parent. -* **`LayoutBuilder`:** Use for complex, responsive layouts to make decisions - based on the available space. - -#### Layering Widgets with Stack - -* **`Positioned`:** Use to precisely place a child within a `Stack` by anchoring it to the edges. -* **`Align`:** Use to position a child within a `Stack` using alignments like `Alignment.center`. - -#### Advanced Layout with Overlays - -* **`OverlayPortal`:** Use this widget to show UI elements (like custom - dropdowns or tooltips) "on top" of everything else. It manages the - `OverlayEntry` for you. - - ```dart - class MyDropdown extends StatefulWidget { - const MyDropdown({super.key}); - - @override - State createState() => _MyDropdownState(); - } - - class _MyDropdownState extends State { - final _controller = OverlayPortalController(); - - @override - Widget build(BuildContext context) { - return OverlayPortal( - controller: _controller, - overlayChildBuilder: (BuildContext context) { - return const Positioned( - top: 50, - left: 10, - child: Card( - child: Padding( - padding: EdgeInsets.all(8.0), - child: Text('I am an overlay!'), - ), - ), - ); - }, - child: ElevatedButton( - onPressed: _controller.toggle, - child: const Text('Toggle Overlay'), - ), - ); - } - } - ``` - -### Color Scheme Best Practices - -#### Contrast Ratios - -* **WCAG Guidelines:** Aim to meet the Web Content Accessibility Guidelines - (WCAG) 2.1 standards. -* **Minimum Contrast:** - * **Normal Text:** A contrast ratio of at least **4.5:1**. - * **Large Text:** (18pt or 14pt bold) A contrast ratio of at least **3:1**. - -#### Palette Selection - -* **Primary, Secondary, and Accent:** Define a clear color hierarchy. -* **The 60-30-10 Rule:** A classic design rule for creating a balanced color scheme. - * **60%** Primary/Neutral Color (Dominant) - * **30%** Secondary Color - * **10%** Accent Color - -#### Complementary Colors - -* **Use with Caution:** They can be visually jarring if overused. -* **Best Use Cases:** They are excellent for accent colors to make specific - elements pop, but generally poor for text and background pairings as they can - cause eye strain. - -#### Example Palette - -* **Primary:** #0D47A1 (Dark Blue) -* **Secondary:** #1976D2 (Medium Blue) -* **Accent:** #FFC107 (Amber) -* **Neutral/Text:** #212121 (Almost Black) -* **Background:** #FEFEFE (Almost White) - -### Font Best Practices - -#### Font Selection - -* **Limit Font Families:** Stick to one or two font families for the entire - application. -* **Prioritize Legibility:** Choose fonts that are easy to read on screens of - all sizes. Sans-serif fonts are generally preferred for UI body text. -* **System Fonts:** Consider using platform-native system fonts. -* **Google Fonts:** For a wide selection of open-source fonts, use the - `google_fonts` package. - -#### Hierarchy and Scale - -* **Establish a Scale:** Define a set of font sizes for different text elements - (e.g., headlines, titles, body text, captions). -* **Use Font Weight:** Differentiate text effectively using font weights. -* **Color and Opacity:** Use color and opacity to de-emphasize less important - text. - -#### Readability - -* **Line Height (Leading):** Set an appropriate line height, typically **1.4x to - 1.6x** the font size. -* **Line Length:** For body text, aim for a line length of **45-75 characters**. -* **Avoid All Caps:** Do not use all caps for long-form text. - -#### Example Typographic Scale - -```dart -// In your ThemeData -textTheme: const TextTheme( - displayLarge: TextStyle(fontSize: 57.0, fontWeight: FontWeight.bold), - titleLarge: TextStyle(fontSize: 22.0, fontWeight: FontWeight.bold), - bodyLarge: TextStyle(fontSize: 16.0, height: 1.5), - bodyMedium: TextStyle(fontSize: 14.0, height: 1.4), - labelSmall: TextStyle(fontSize: 11.0, color: Colors.grey), -), -``` - -### Documentation - -* **`dartdoc`:** Write `dartdoc`-style comments for all public APIs. - - -#### Documentation Philosophy - -* **Comment wisely:** Use comments to explain why the code is written a certain - way, not what the code does. The code itself should be self-explanatory. -* **Document for the user:** Write documentation with the reader in mind. If you - had a question and found the answer, add it to the documentation where you - first looked. This ensures the documentation answers real-world questions. -* **No useless documentation:** If the documentation only restates the obvious - from the code's name, it's not helpful. Good documentation provides context - and explains what isn't immediately apparent. -* **Consistency is key:** Use consistent terminology throughout your - documentation. - -#### Commenting Style - -* **Use `///` for doc comments:** This allows documentation generation tools to - pick them up. -* **Start with a single-sentence summary:** The first sentence should be a - concise, user-centric summary ending with a period. -* **Separate the summary:** Add a blank line after the first sentence to create - a separate paragraph. This helps tools create better summaries. -* **Avoid redundancy:** Don't repeat information that's obvious from the code's - context, like the class name or signature. -* **Don't document both getter and setter:** For properties with both, only - document one. The documentation tool will treat them as a single field. - -#### Writing Style - -* **Be brief:** Write concisely. -* **Avoid jargon and acronyms:** Don't use abbreviations unless they are widely - understood. -* **Use Markdown sparingly:** Avoid excessive markdown and never use HTML for - formatting. -* **Use backticks for code:** Enclose code blocks in backtick fences, and - specify the language. - -#### What to Document - -* **Public APIs are a priority:** Always document public APIs. -* **Consider private APIs:** It's a good idea to document private APIs as well. -* **Library-level comments are helpful:** Consider adding a doc comment at the - library level to provide a general overview. -* **Include code samples:** Where appropriate, add code samples to illustrate usage. -* **Explain parameters, return values, and exceptions:** Use prose to describe - what a function expects, what it returns, and what errors it might throw. -* **Place doc comments before annotations:** Documentation should come before - any metadata annotations. - -### Accessibility (A11Y) -Implement accessibility features to empower all users, assuming a wide variety -of users with different physical abilities, mental abilities, age groups, -education levels, and learning styles. - -* **Color Contrast:** Ensure text has a contrast ratio of at least **4.5:1** - against its background. -* **Dynamic Text Scaling:** Test your UI to ensure it remains usable when users - increase the system font size. -* **Semantic Labels:** Use the `Semantics` widget to provide clear, descriptive - labels for UI elements. -* **Screen Reader Testing:** Regularly test your app with TalkBack (Android) and - VoiceOver (iOS). +_Vendored from [flutter/flutter docs/rules/rules.md](https://github.com/flutter/flutter/blob/main/docs/rules/rules.md) +via `~/.claude/CLAUDE.md` Flutter AI tooling setup. Re-fetch periodically. +Split across five files to stay under the 250-line cap._ + +- [Language, style and architecture](docs/flutter-rules-language.md) +- [Architecture, lint rules, state and data](docs/flutter-rules-state-and-data.md) +- [Code generation, testing and theming](docs/flutter-rules-testing-and-assets.md) +- [UI: theming, layout and overlays](docs/flutter-rules-ui.md) +- [Colour, type, documentation and accessibility](docs/flutter-rules-design.md) diff --git a/app/pubspec.yaml b/app/pubspec.yaml index ccc9f71..731d8ba 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -2,7 +2,7 @@ name: diet_guard_app description: "Companion phone app for diet_guard: log meals on the go." publish_to: 'none' -version: 1.0.12+1 +version: 1.0.13+1 environment: sdk: ^3.12.2 diff --git a/app/test/repo_invariants_test.dart b/app/test/repo_invariants_test.dart new file mode 100644 index 0000000..e0c40e7 --- /dev/null +++ b/app/test/repo_invariants_test.dart @@ -0,0 +1,157 @@ +/// Structural rules that used to live only as prose in CLAUDE.md. +/// +/// Each rule here was previously a paragraph asking a future reader not to do +/// something. A paragraph does not fail a commit, so these are tests instead: +/// the rule and its enforcement are the same artifact, and the *why* lives in +/// the test name rather than in a document that can drift from the code. +library; + +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +/// `app/` from the repo root, resolved relative to the package directory so +/// the test works regardless of the working directory `flutter test` uses. +Directory get _appDir { + var dir = Directory.current; + while (!File('${dir.path}/pubspec.yaml').existsSync()) { + final parent = dir.parent; + if (parent.path == dir.path) { + fail('could not locate app/ from ${Directory.current.path}'); + } + dir = parent; + } + return dir; +} + +List _dartFilesIn(String relative) { + final dir = Directory('${_appDir.path}/$relative'); + if (!dir.existsSync()) return const []; + return dir + .listSync(recursive: true) + .whereType() + .where((f) => f.path.endsWith('.dart')) + .toList(); +} + +void main() { + group('the desktop target is a web build, not a GTK embedder', () { + // Flutter's Linux embedder manages ~20fps at 3840x2160 where the same Dart + // in Chrome sustains ~144fps. `flutter create --platforms linux` would + // silently restore the slow path, and nothing about the resulting tree + // looks wrong at a glance -- so the absence of the directory is asserted. + test('app/linux/ does not exist', () { + expect( + Directory('${_appDir.path}/linux').existsSync(), + isFalse, + reason: 'A Linux embedder target was re-added. The desktop app is the ' + 'web build served by bin/diet_guard_desktop.dart; see CLAUDE.md.', + ); + }); + }); + + group('dart:io stays behind the platform seam', () { + // dart:io does not fail a web *compile* -- it becomes a stub that throws at + // runtime, so the symptom of a bad import is a blank white window rather + // than a build error. Widget tests run on the VM and would not catch it. + test('only *_io.dart halves and lib/desktop/ import dart:io', () { + final offenders = []; + for (final file in _dartFilesIn('lib')) { + final relative = file.path.split('/app/lib/').last; + final isSeam = relative.endsWith('_io.dart'); + final isDesktopOnly = relative.startsWith('desktop/'); + if (isSeam || isDesktopOnly) continue; + if (file.readAsStringSync().contains("import 'dart:io'")) { + offenders.add(relative); + } + } + expect( + offenders, + isEmpty, + reason: 'These files import dart:io outside the conditional-export ' + 'seam, which compiles for web and throws at runtime (blank white ' + 'window). Move the platform edge into a *_io.dart / *_web.dart ' + 'pair, and branch on kIsWeb before any Platform.is… check.', + ); + }); + }); + + group('deliberately removed features stay removed', () { + // The meal builder, "repeat last meal" and the temptation-bundling reward + // prompt were each removed as unused. Re-adding one is a decision, not an + // accident -- this test exists so it cannot happen by accident. + test('no meal builder, repeat-last-meal or reward prompt', () { + final banned = RegExp( + r'MealBuilder|mealBuilder|repeatLastMeal|rewardPrompt|temptationBundl', + ); + final offenders = []; + for (final file in _dartFilesIn('lib')) { + if (banned.hasMatch(file.readAsStringSync())) { + offenders.add(file.path.split('/app/lib/').last); + } + } + expect( + offenders, + isEmpty, + reason: 'A removed feature came back. See the "Do NOT" section of ' + 'CLAUDE.md before re-adding any of these.', + ); + }); + + // Photos took image_picker, the blob stores, the wrapper's /blobs/ route + // and the CAMERA permission with them. Re-adding photos means re-adding + // all of that *plus* syncing image blobs, since a device-local attachment + // would violate the one-source-of-truth rule. + test('no image_picker dependency', () { + final pubspec = File('${_appDir.path}/pubspec.yaml').readAsStringSync(); + expect( + pubspec.contains('image_picker'), + isFalse, + reason: 'Meal photos were removed deliberately. A device-local image ' + 'attachment also breaks the shared-source-of-truth rule.', + ); + }); + + test('no CAMERA permission in the Android manifest', () { + final manifest = File( + '${_appDir.path}/android/app/src/main/AndroidManifest.xml', + ); + expect( + manifest.readAsStringSync().contains('android.permission.CAMERA'), + isFalse, + reason: 'The CAMERA permission came back with no photo feature to ' + 'justify it.', + ); + }); + }); + + group('the desktop wrapper origin is fixed', () { + // IndexedDB is keyed by origin and lives in the Chrome profile. Changing + // either the port or the --user-data-dir hides the entire local food log + // behind a different origin, which reads as data loss to the user. + test('the wrapper port is still 8732', () { + final source = File( + '${_appDir.path}/lib/services/desktop_wrapper.dart', + ).readAsStringSync(); + expect( + source.contains('desktopWrapperPort = 8732'), + isTrue, + reason: 'The wrapper port changed. IndexedDB is keyed by origin, so a ' + 'new port looks like a different app with no history at all. ' + '8730 is ~/todo and 8731 is ~/habit_stack; do not collide.', + ); + }); + + test('the Chrome profile directory is still diet-guard-desktop', () { + final source = File( + '${_appDir.path}/bin/diet_guard_desktop.dart', + ).readAsStringSync(); + expect( + source.contains("'diet-guard-desktop', 'profile'"), + isTrue, + reason: 'The Chrome --user-data-dir moved. IndexedDB lives in that ' + 'profile, so relocating it hides the entire local food log.', + ); + }); + }); +} diff --git a/docs/flutter-rules-design.md b/docs/flutter-rules-design.md new file mode 100644 index 0000000..e4e310a --- /dev/null +++ b/docs/flutter-rules-design.md @@ -0,0 +1,150 @@ + + +# Colour, type, documentation and accessibility + +_Vendored Flutter/Dart AI rules. See [CLAUDE.md](../CLAUDE.md)._ + +### Color Scheme Best Practices + +#### Contrast Ratios + +* **WCAG Guidelines:** Aim to meet the Web Content Accessibility Guidelines + (WCAG) 2.1 standards. +* **Minimum Contrast:** + * **Normal Text:** A contrast ratio of at least **4.5:1**. + * **Large Text:** (18pt or 14pt bold) A contrast ratio of at least **3:1**. + +#### Palette Selection + +* **Primary, Secondary, and Accent:** Define a clear color hierarchy. +* **The 60-30-10 Rule:** A classic design rule for creating a balanced color scheme. + * **60%** Primary/Neutral Color (Dominant) + * **30%** Secondary Color + * **10%** Accent Color + +#### Complementary Colors + +* **Use with Caution:** They can be visually jarring if overused. +* **Best Use Cases:** They are excellent for accent colors to make specific + elements pop, but generally poor for text and background pairings as they can + cause eye strain. + +#### Example Palette + +* **Primary:** #0D47A1 (Dark Blue) +* **Secondary:** #1976D2 (Medium Blue) +* **Accent:** #FFC107 (Amber) +* **Neutral/Text:** #212121 (Almost Black) +* **Background:** #FEFEFE (Almost White) + +### Font Best Practices + +#### Font Selection + +* **Limit Font Families:** Stick to one or two font families for the entire + application. +* **Prioritize Legibility:** Choose fonts that are easy to read on screens of + all sizes. Sans-serif fonts are generally preferred for UI body text. +* **System Fonts:** Consider using platform-native system fonts. +* **Google Fonts:** For a wide selection of open-source fonts, use the + `google_fonts` package. + +#### Hierarchy and Scale + +* **Establish a Scale:** Define a set of font sizes for different text elements + (e.g., headlines, titles, body text, captions). +* **Use Font Weight:** Differentiate text effectively using font weights. +* **Color and Opacity:** Use color and opacity to de-emphasize less important + text. + +#### Readability + +* **Line Height (Leading):** Set an appropriate line height, typically **1.4x to + 1.6x** the font size. +* **Line Length:** For body text, aim for a line length of **45-75 characters**. +* **Avoid All Caps:** Do not use all caps for long-form text. + +#### Example Typographic Scale + +```dart +// In your ThemeData +textTheme: const TextTheme( + displayLarge: TextStyle(fontSize: 57.0, fontWeight: FontWeight.bold), + titleLarge: TextStyle(fontSize: 22.0, fontWeight: FontWeight.bold), + bodyLarge: TextStyle(fontSize: 16.0, height: 1.5), + bodyMedium: TextStyle(fontSize: 14.0, height: 1.4), + labelSmall: TextStyle(fontSize: 11.0, color: Colors.grey), +), +``` + +### Documentation + +* **`dartdoc`:** Write `dartdoc`-style comments for all public APIs. + + +#### Documentation Philosophy + +* **Comment wisely:** Use comments to explain why the code is written a certain + way, not what the code does. The code itself should be self-explanatory. +* **Document for the user:** Write documentation with the reader in mind. If you + had a question and found the answer, add it to the documentation where you + first looked. This ensures the documentation answers real-world questions. +* **No useless documentation:** If the documentation only restates the obvious + from the code's name, it's not helpful. Good documentation provides context + and explains what isn't immediately apparent. +* **Consistency is key:** Use consistent terminology throughout your + documentation. + +#### Commenting Style + +* **Use `///` for doc comments:** This allows documentation generation tools to + pick them up. +* **Start with a single-sentence summary:** The first sentence should be a + concise, user-centric summary ending with a period. +* **Separate the summary:** Add a blank line after the first sentence to create + a separate paragraph. This helps tools create better summaries. +* **Avoid redundancy:** Don't repeat information that's obvious from the code's + context, like the class name or signature. +* **Don't document both getter and setter:** For properties with both, only + document one. The documentation tool will treat them as a single field. + +#### Writing Style + +* **Be brief:** Write concisely. +* **Avoid jargon and acronyms:** Don't use abbreviations unless they are widely + understood. +* **Use Markdown sparingly:** Avoid excessive markdown and never use HTML for + formatting. +* **Use backticks for code:** Enclose code blocks in backtick fences, and + specify the language. + +#### What to Document + +* **Public APIs are a priority:** Always document public APIs. +* **Consider private APIs:** It's a good idea to document private APIs as well. +* **Library-level comments are helpful:** Consider adding a doc comment at the + library level to provide a general overview. +* **Include code samples:** Where appropriate, add code samples to illustrate usage. +* **Explain parameters, return values, and exceptions:** Use prose to describe + what a function expects, what it returns, and what errors it might throw. +* **Place doc comments before annotations:** Documentation should come before + any metadata annotations. + +### Accessibility (A11Y) +Implement accessibility features to empower all users, assuming a wide variety +of users with different physical abilities, mental abilities, age groups, +education levels, and learning styles. + +* **Color Contrast:** Ensure text has a contrast ratio of at least **4.5:1** + against its background. +* **Dynamic Text Scaling:** Test your UI to ensure it remains usable when users + increase the system font size. +* **Semantic Labels:** Use the `Semantics` widget to provide clear, descriptive + labels for UI elements. +* **Screen Reader Testing:** Regularly test your app with TalkBack (Android) and + VoiceOver (iOS). diff --git a/docs/flutter-rules-language.md b/docs/flutter-rules-language.md new file mode 100644 index 0000000..d8efbd1 --- /dev/null +++ b/docs/flutter-rules-language.md @@ -0,0 +1,140 @@ + + +# Language, style and architecture + +_Vendored Flutter/Dart AI rules. See [CLAUDE.md](../CLAUDE.md)._ + +### Interaction Guidelines +* **User Persona:** Assume the user is familiar with programming concepts but + may be new to Dart. +* **Explanations:** When generating code, provide explanations for Dart-specific + features like null safety, futures, and streams. +* **Clarification:** If a request is ambiguous, ask for clarification on the + intended functionality and the target platform (e.g., command-line, web, + server). +* **Dependencies:** When suggesting new dependencies from `pub.dev`, explain + their benefits. +* **Formatting:** Use the `dart_format` tool to ensure consistent code + formatting. +* **Fixes:** Use the `dart_fix` tool to automatically fix many common errors, + and to help code conform to configured analysis options. +* **Linting:** Use the Dart linter with a recommended set of rules to catch + common issues. Use the `analyze_files` tool to run the linter. + +### Project Structure +* **Standard Structure:** Assumes a standard Flutter project structure with + `lib/main.dart` as the primary application entry point. + +### Flutter style guide +* **SOLID Principles:** Apply SOLID principles throughout the codebase. +* **Concise and Declarative:** Write concise, modern, technical Dart code. + Prefer functional and declarative patterns. +* **Composition over Inheritance:** Favor composition for building complex + widgets and logic. +* **Immutability:** Prefer immutable data structures. Widgets (especially + `StatelessWidget`) should be immutable. +* **State Management:** Separate ephemeral state and app state. Use a state + management solution for app state to handle the separation of concerns. +* **Widgets are for UI:** Everything in Flutter's UI is a widget. Compose + complex UIs from smaller, reusable widgets. +* **Navigation:** Use a modern routing package like `auto_route` or `go_router`. + For more guidelines around navigation, see the section on [routing](#routing). + +### Package Management +* **Pub Tool:** To manage packages, use the `pub` tool, if available. +* **External Packages:** If a new feature requires an external package, use the + `pub_dev_search` tool, if it is available. Otherwise, identify the most + suitable and stable package from pub.dev. +* **Adding Dependencies:** To add a regular dependency, use the `pub` tool, if + it is available. Otherwise, run `flutter pub add `. +* **Adding Dev Dependencies:** To add a development dependency, use the `pub` + tool, if it is available, with `dev:`. Otherwise, run `flutter + pub add dev:`. +* **Dependency Overrides:** To add a dependency override, use the `pub` tool, if + it is available, with `override::1.0.0`. Otherwise, run `flutter + pub add override::1.0.0`. +* **Removing Dependencies:** To remove a dependency, use the `pub` tool, if it + is available. Otherwise, run `dart pub remove `. + +### Code Quality +* **Code structure:** Adhere to maintainable code structure and separation of + concerns (e.g., UI logic separate from business logic). +* **Naming conventions:** Avoid abbreviations and use meaningful, consistent, + descriptive names for variables, functions, and classes. +* **Conciseness:** Write code that is as short as it can be while remaining + clear. +* **Simplicity:** Write straightforward code. Code that is clever or + obscure is difficult to maintain. +* **Error Handling:** Anticipate and handle potential errors. Don't let your + code fail silently. +* **Styling:** + * Line length: Lines should be 80 characters or fewer. + * Use `PascalCase` for classes, `camelCase` for + members/variables/functions/enums, and `snake_case` for files. +* **Functions:** + * Keep functions short and with a single purpose. + Strive for less than 20 lines. +* **Testing:** Write code with testing in mind. Use the `file`, `process`, and + `platform` packages, if appropriate, so you can inject in-memory and fake + versions of the objects. +* **Logging:** Use the `logging` package instead of `print`. + +### Dart Best Practices +* **Effective Dart:** Follow the official Effective Dart guidelines + (https://dart.dev/effective-dart) +* **Class Organization:** Define related classes within the same library file. + For large libraries, export smaller, private libraries from a single top-level + library. +* **Library Organization:** Group related libraries in the same folder. +* **API Documentation:** Add documentation comments to all public APIs, + including classes, constructors, methods, and top-level functions. +* **Comments:** Write clear comments for complex or non-obvious code. Avoid + over-commenting. +* **Trailing Comments:** Don't add trailing comments. +* **Async/Await:** Ensure proper use of `async`/`await` for asynchronous + operations with robust error handling. + * Use `Future`s, `async`, and `await` for asynchronous operations. + * Use `Stream`s for sequences of asynchronous events. +* **Null Safety:** Write code that is soundly null-safe. Leverage Dart's null + safety features. Avoid `!` unless the value is guaranteed to be non-null. +* **Pattern Matching:** Use pattern matching features where they simplify the + code. +* **Records:** Use records to return multiple types in situations where defining + an entire class is cumbersome. +* **Switch Statements:** Prefer using exhaustive `switch` statements or + expressions, which don't require `break` statements. +* **Exception Handling:** Use `try-catch` blocks for handling exceptions, and + use exceptions appropriate for the type of exception. Use custom exceptions + for situations specific to your code. +* **Arrow Functions:** Use arrow syntax for simple one-line functions. + +### Flutter Best Practices +* **Immutability:** Widgets (especially `StatelessWidget`) are immutable; when + the UI needs to change, Flutter rebuilds the widget tree. +* **Composition:** Prefer composing smaller widgets over extending existing + ones. Use this to avoid deep widget nesting. +* **Private Widgets:** Use small, private `Widget` classes instead of private + helper methods that return a `Widget`. +* **Build Methods:** Break down large `build()` methods into smaller, reusable + private Widget classes. +* **List Performance:** Use `ListView.builder` or `SliverList` for long lists to + create lazy-loaded lists for performance. +* **Isolates:** Use `compute()` to run expensive calculations in a separate + isolate to avoid blocking the UI thread, such as JSON parsing. +* **Const Constructors:** Use `const` constructors for widgets and in `build()` + methods whenever possible to reduce rebuilds. +* **Build Method Performance:** Avoid performing expensive operations, like + network calls or complex computations, directly within `build()` methods. + +### API Design Principles +When building reusable APIs, such as a library, follow these principles. + +* **Consider the User:** Design APIs from the perspective of the person who will + be using them. The API should be intuitive and easy to use correctly. +* **Documentation is Essential:** Good documentation is a part of good API + design. It should be clear, concise, and provide examples. diff --git a/docs/flutter-rules-state-and-data.md b/docs/flutter-rules-state-and-data.md new file mode 100644 index 0000000..8abd659 --- /dev/null +++ b/docs/flutter-rules-state-and-data.md @@ -0,0 +1,183 @@ + + +# Architecture, lint rules, state and data + +_Vendored Flutter/Dart AI rules. See [CLAUDE.md](../CLAUDE.md)._ + +### Application Architecture +* **Separation of Concerns:** Aim for separation of concerns similar to MVC/MVVM, with defined Model, + View, and ViewModel/Controller roles. +* **Logical Layers:** Organize the project into logical layers: + * Presentation (widgets, screens) + * Domain (business logic classes) + * Data (model classes, API clients) + * Core (shared classes, utilities, and extension types) +* **Feature-based Organization:** For larger projects, organize code by feature, + where each feature has its own presentation, domain, and data subfolders. This + improves navigability and scalability. + +### Lint Rules + +Include the package in the `analysis_options.yaml` file. Use the following +`analysis_options.yaml` file as a starting point: + +```yaml +include: package:flutter_lints/flutter.yaml + +linter: + rules: + # Add additional lint rules here: + # avoid_print: false + # prefer_single_quotes: true +``` + +#### State Management +* **Built-in Solutions:** Prefer Flutter's built-in state management solutions. + Do not use a third-party package unless explicitly requested. +* **Streams:** Use `Streams` and `StreamBuilder` for handling a sequence of + asynchronous events. +* **Futures:** Use `Futures` and `FutureBuilder` for handling a single + asynchronous operation that will complete in the future. +* **ValueNotifier:** Use `ValueNotifier` with `ValueListenableBuilder` for + simple, local state that involves a single value. + + ```dart + // Define a ValueNotifier to hold the state. + final ValueNotifier _counter = ValueNotifier(0); + + // Use ValueListenableBuilder to listen and rebuild. + ValueListenableBuilder( + valueListenable: _counter, + builder: (context, value, child) { + return Text('Count: $value'); + }, + ); + ``` + +* **ChangeNotifier:** For state that is more complex or shared across multiple + widgets, use `ChangeNotifier`. +* **ListenableBuilder:** Use `ListenableBuilder` to listen to changes from a + `ChangeNotifier` or other `Listenable`. +* **MVVM:** When a more robust solution is needed, structure the app using the + Model-View-ViewModel (MVVM) pattern. +* **Dependency Injection:** Use simple manual constructor dependency injection + to make a class's dependencies explicit in its API, and to manage dependencies + between different layers of the application. +* **Provider:** If a dependency injection solution beyond manual constructor + injection is explicitly requested, `provider` can be used to make services, + repositories, or complex state objects available to the UI layer without tight + coupling (note: this document generally defaults against third-party packages + for state management unless explicitly requested). + +#### Data Flow +* **Data Structures:** Define data structures (classes) to represent the data + used in the application. +* **Data Abstraction:** Abstract data sources (e.g., API calls, database + operations) using Repositories/Services to promote testability. + +#### Routing +* **GoRouter:** Use the `go_router` package for declarative navigation, deep + linking, and web support. +* **GoRouter Setup:** To use `go_router`, first add it to your `pubspec.yaml` + using the `pub` tool's `add` command. + + ```dart + // 1. Add the dependency + // flutter pub add go_router + + // 2. Configure the router + final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const HomeScreen(), + routes: [ + GoRoute( + path: 'details/:id', // Route with a path parameter + builder: (context, state) { + final String id = state.pathParameters['id']!; + return DetailScreen(id: id); + }, + ), + ], + ), + ], + ); + + // 3. Use it in your MaterialApp + MaterialApp.router( + routerConfig: _router, + ); + ``` +* **Authentication Redirects:** Configure `go_router`'s `redirect` property to + handle authentication flows, ensuring users are redirected to the login screen + when unauthorized, and back to their intended destination after successful + login. + +* **Navigator:** Use the built-in `Navigator` for short-lived screens that do + not need to be deep-linkable, such as dialogs or temporary views. + + ```dart + // Push a new screen onto the stack + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const DetailsScreen()), + ); + + // Pop the current screen to go back + Navigator.pop(context); + ``` + +#### Data Handling & Serialization +* **JSON Serialization:** Use `json_serializable` and `json_annotation` for + parsing and encoding JSON data. +* **Field Renaming:** When encoding data, use `fieldRename: FieldRename.snake` + to convert Dart's camelCase fields to snake_case JSON keys. + + ```dart + // In your model file + import 'package:json_annotation/json_annotation.dart'; + + part 'user.g.dart'; + + @JsonSerializable(fieldRename: FieldRename.snake) + class User { + final String firstName; + final String lastName; + + User({required this.firstName, required this.lastName}); + + factory User.fromJson(Map json) => _$UserFromJson(json); + Map toJson() => _$UserToJson(this); + } + ``` + + +#### Logging +* **Structured Logging:** Use the `log` function from `dart:developer` for + structured logging that integrates with Dart DevTools. + + ```dart + import 'dart:developer' as developer; + + // For simple messages + developer.log('User logged in successfully.'); + + // For structured error logging + try { + // ... code that might fail + } catch (e, s) { + developer.log( + 'Failed to fetch data', + name: 'myapp.network', + level: 1000, // SEVERE + error: e, + stackTrace: s, + ); + } + ``` diff --git a/docs/flutter-rules-testing-and-assets.md b/docs/flutter-rules-testing-and-assets.md new file mode 100644 index 0000000..15c1bdf --- /dev/null +++ b/docs/flutter-rules-testing-and-assets.md @@ -0,0 +1,154 @@ + + +# Code generation, testing and theming + +_Vendored Flutter/Dart AI rules. See [CLAUDE.md](../CLAUDE.md)._ + +### Code Generation +* **Build Runner:** If the project uses code generation, ensure that + `build_runner` is listed as a dev dependency in `pubspec.yaml`. +* **Code Generation Tasks:** Use `build_runner` for all code generation tasks, + such as for `json_serializable`. +* **Running Build Runner:** After modifying files that require code generation, + run the build command: + + ```shell + dart run build_runner build --delete-conflicting-outputs + ``` + +### Testing +* **Running Tests:** To run tests, use the `run_tests` tool if it is available, + otherwise use `flutter test`. +* **Unit Tests:** Use `package:test` for unit tests. +* **Widget Tests:** Use `package:flutter_test` for widget tests. +* **Integration Tests:** Use `package:integration_test` for integration tests. +* **Assertions:** Prefer using `package:checks` for more expressive and readable + assertions over the default `matchers`. + +#### Testing Best practices +* **Convention:** Follow the Arrange-Act-Assert (or Given-When-Then) pattern. +* **Unit Tests:** Write unit tests for domain logic, data layer, and state + management. +* **Widget Tests:** Write widget tests for UI components. +* **Integration Tests:** For broader application validation, use integration + tests to verify end-to-end user flows. +* **integration_test package:** Use the `integration_test` package from the + Flutter SDK for integration tests. Add it as a `dev_dependency` in + `pubspec.yaml` by specifying `sdk: flutter`. +* **Mocks:** Prefer fakes or stubs over mocks. If mocks are absolutely + necessary, use `mockito` or `mocktail` to create mocks for dependencies. While + code generation is common for state management (e.g., with `freezed`), try to + avoid it for mocks. +* **Coverage:** Aim for high test coverage. + +### Visual Design & Theming +* **UI Design:** Build beautiful and intuitive user interfaces that follow + modern design guidelines. +* **Responsiveness:** Ensure the app is mobile responsive and adapts to + different screen sizes, working perfectly on mobile and web. +* **Navigation:** If there are multiple pages for the user to interact with, + provide an intuitive and easy navigation bar or controls. +* **Typography:** Stress and emphasize font sizes to ease understanding, e.g., + hero text, section headlines, list headlines, keywords in paragraphs. +* **Background:** Keep backgrounds flat and simple — no noise texture or + gradients. Follow the `unified-design-system` skill (safe-design-rules + rule 17: simple-on-complex); let content and typography carry visual + interest instead of background texture. +* **Shadows:** Follow the `unified-design-system` skill's shadow policy: no + shadows at all in dark UI (differentiate elevation with lighter surface + fills, not shadows); in light UI, shadows are for floating/overlay + elements only (menus, dialogs), never for inline cards, and never mixed + with a border on the same surface. +* **Icons:** Incorporate icons to enhance the user’s understanding and the + logical navigation of the app. +* **Interactive Elements:** Buttons/controls signal state via color and fill + changes, not a "glow" shadow — one depth technique per surface, per + `unified-design-system`. + +#### Theming +* **Centralized Theme:** Define a centralized `ThemeData` object to ensure a + consistent application-wide style. +* **Light and Dark Themes:** Implement support for both light and dark themes, + ideal for a user-facing theme toggle (`ThemeMode.light`, `ThemeMode.dark`, + `ThemeMode.system`). +* **Color Scheme Generation:** Do NOT use `ColorScheme.fromSeed` for this + app's palette — it can't reproduce hand-picked values exactly. Build + `ThemeData` from explicit `ColorScheme.dark(...)`/`.light(...)` using the + frozen, identical-everywhere tokens in the `unified-design-system` skill + (`~/.claude/skills/unified-design-system/references/tokens.md`). + + ```dart + final ThemeData lightTheme = ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.deepPurple, + brightness: Brightness.light, + ), + // ... other theme properties + ); + ``` +* **Color Palette:** Include a wide range of color concentrations and hues in + the palette to create a vibrant and energetic look and feel. +* **Component Themes:** Use specific theme properties (e.g., `appBarTheme`, + `elevatedButtonTheme`) to customize the appearance of individual Material + components. +* **Custom Fonts:** For custom fonts, use the `google_fonts` package. Define a + `TextTheme` to apply fonts consistently. + + ```dart + // 1. Add the dependency + // flutter pub add google_fonts + + // 2. Define a TextTheme with a custom font + final TextTheme appTextTheme = TextTheme( + displayLarge: GoogleFonts.oswald(fontSize: 57, fontWeight: FontWeight.bold), + titleLarge: GoogleFonts.roboto(fontSize: 22, fontWeight: FontWeight.w500), + bodyMedium: GoogleFonts.openSans(fontSize: 14), + ); + ``` + +#### Assets and Images +* **Image Guidelines:** If images are needed, make them relevant and meaningful, + with appropriate size, layout, and licensing (e.g., freely available). Provide + placeholder images if real ones are not available. +* **Asset Declaration:** Declare all asset paths in your `pubspec.yaml` file. + + ```yaml + flutter: + uses-material-design: true + assets: + - assets/images/ + ``` + +* **Local Images:** Use `Image.asset` for local images from your asset + bundle. + + ```dart + Image.asset('assets/images/placeholder.png') + ``` +* **Network images:** Use NetworkImage for images loaded from the network. +* **Cached images:** For cached images, use NetworkImage a package like + `cached_network_image`. +* **Custom Icons:** Use `ImageIcon` to display an icon from an `ImageProvider`, + useful for custom icons not in the `Icons` class. +* **Network Images:** Use `Image.network` to display images from a URL, and + always include `loadingBuilder` and `errorBuilder` for a better user + experience. + + ```dart + // When using network images, always provide an errorBuilder. + Image.network( + 'https://picsum.photos/200/300', + loadingBuilder: (context, child, progress) { + if (progress == null) return child; + return const Center(child: CircularProgressIndicator()); + }, + errorBuilder: (context, error, stackTrace) { + return const Icon(Icons.error); + }, + ) + ``` diff --git a/docs/flutter-rules-ui.md b/docs/flutter-rules-ui.md new file mode 100644 index 0000000..60d41ba --- /dev/null +++ b/docs/flutter-rules-ui.md @@ -0,0 +1,201 @@ + + +# UI: theming, layout and overlays + +_Vendored Flutter/Dart AI rules. See [CLAUDE.md](../CLAUDE.md)._ + +### UI Theming and Styling Code + +* **Responsiveness:** Use `LayoutBuilder` or `MediaQuery` to create responsive + UIs. +* **Text:** Use `Theme.of(context).textTheme` for text styles. +* **Text Fields:** Configure `textCapitalization`, `keyboardType`, and + `placeholder`. + +### Material Theming Best Practices + +#### Embrace `ThemeData` and Material 3 + +* **Use `ColorScheme.fromSeed()`:** Use this to generate a complete, harmonious + color palette for both light and dark modes from a single seed color. +* **Define Light and Dark Themes:** Provide both `theme` and `darkTheme` to your + `MaterialApp` to support system brightness settings seamlessly. +* **Centralize Component Styles:** Customize specific component themes (e.g., + `elevatedButtonTheme`, `cardTheme`, `appBarTheme`) within `ThemeData` to + ensure consistency. +* **Dark/Light Mode and Theme Toggle:** Implement support for both light and + dark themes using `theme` and `darkTheme` properties of `MaterialApp`. The + `themeMode` property can be dynamically controlled (e.g., via a + `ChangeNotifierProvider`) to allow for toggling between `ThemeMode.light`, + `ThemeMode.dark`, or `ThemeMode.system`. + +```dart +// main.dart +MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.deepPurple, + brightness: Brightness.light, + ), + textTheme: const TextTheme( + displayLarge: TextStyle(fontSize: 57.0, fontWeight: FontWeight.bold), + bodyMedium: TextStyle(fontSize: 14.0, height: 1.4), + ), + ), + darkTheme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.deepPurple, + brightness: Brightness.dark, + ), + ), + home: const MyHomePage(), +); +``` + +#### Implement Design Tokens with `ThemeExtension` + +For custom styles that aren't part of the standard `ThemeData`, use +`ThemeExtension` to define reusable design tokens. + +* **Create a Custom Theme Extension:** Define a class that extends + `ThemeExtension` and include your custom properties. +* **Implement `copyWith` and `lerp`:** These methods are required for the + extension to work correctly with theme transitions. +* **Register in `ThemeData`:** Add your custom extension to the `extensions` + list in your `ThemeData`. +* **Access Tokens in Widgets:** Use `Theme.of(context).extension()!` + to access your custom tokens. + +```dart +// 1. Define the extension +@immutable +class MyColors extends ThemeExtension { + const MyColors({required this.success, required this.danger}); + + final Color? success; + final Color? danger; + + @override + ThemeExtension copyWith({Color? success, Color? danger}) { + return MyColors(success: success ?? this.success, danger: danger ?? this.danger); + } + + @override + ThemeExtension lerp(ThemeExtension? other, double t) { + if (other is! MyColors) return this; + return MyColors( + success: Color.lerp(success, other.success, t), + danger: Color.lerp(danger, other.danger, t), + ); + } +} + +// 2. Register it in ThemeData +theme: ThemeData( + extensions: const >[ + MyColors(success: Colors.green, danger: Colors.red), + ], +), + +// 3. Use it in a widget +Container( + color: Theme.of(context).extension()!.success, +) +``` + +#### Styling with `WidgetStateProperty` + +* **`WidgetStateProperty.resolveWith`:** Provide a function that receives a + `Set` and returns the appropriate value for the current state. +* **`WidgetStateProperty.all`:** A shorthand for when the value is the same for + all states. + +```dart +// Example: Creating a button style that changes color when pressed. +final ButtonStyle myButtonStyle = ButtonStyle( + backgroundColor: WidgetStateProperty.resolveWith( + (Set states) { + if (states.contains(WidgetState.pressed)) { + return Colors.green; // Color when pressed + } + return Colors.red; // Default color + }, + ), +); +``` + +### Layout Best Practices + +#### Building Flexible and Overflow-Safe Layouts + +##### For Rows and Columns + +* **`Expanded`:** Use to make a child widget fill the remaining available space + along the main axis. +* **`Flexible`:** Use when you want a widget to shrink to fit, but not + necessarily grow. Don't combine `Flexible` and `Expanded` in the same `Row` or + `Column`. +* **`Wrap`:** Use when you have a series of widgets that would overflow a `Row` + or `Column`, and you want them to move to the next line. + +##### For General Content + +* **`SingleChildScrollView`:** Use when your content is intrinsically larger + than the viewport, but is a fixed size. +* **`ListView` / `GridView`:** For long lists or grids of content, always use a + builder constructor (`.builder`). +* **`FittedBox`:** Use to scale or fit a single child widget within its parent. +* **`LayoutBuilder`:** Use for complex, responsive layouts to make decisions + based on the available space. + +#### Layering Widgets with Stack + +* **`Positioned`:** Use to precisely place a child within a `Stack` by anchoring it to the edges. +* **`Align`:** Use to position a child within a `Stack` using alignments like `Alignment.center`. + +#### Advanced Layout with Overlays + +* **`OverlayPortal`:** Use this widget to show UI elements (like custom + dropdowns or tooltips) "on top" of everything else. It manages the + `OverlayEntry` for you. + + ```dart + class MyDropdown extends StatefulWidget { + const MyDropdown({super.key}); + + @override + State createState() => _MyDropdownState(); + } + + class _MyDropdownState extends State { + final _controller = OverlayPortalController(); + + @override + Widget build(BuildContext context) { + return OverlayPortal( + controller: _controller, + overlayChildBuilder: (BuildContext context) { + return const Positioned( + top: 50, + left: 10, + child: Card( + child: Padding( + padding: EdgeInsets.all(8.0), + child: Text('I am an overlay!'), + ), + ), + ); + }, + child: ElevatedButton( + onPressed: _controller.toggle, + child: const Text('Toggle Overlay'), + ), + ); + } + } + ```